itsluckysharma01's picture
Clean deployment
4ed7d03
// ─── Video Analysis – NETRA ─────────────────────────────────────────────────
let selectedFile = null;
let availableModels = {};
let selectedModels = [];
let currentAnalysisId = null;
let currentJobId = null;
let pollTimer = null;
let seenHighRiskCount = 0; // track alerts already shown in banner
let bannerDismissTimer = null;
// ─── Init ────────────────────────────────────────────────────────────────────
document.addEventListener("DOMContentLoaded", async () => {
await loadAvailableModels();
await loadSelectedModels();
wireDropzone();
});
// ─── Model selection ─────────────────────────────────────────────────────────
async function loadAvailableModels() {
try {
const data = await (await fetch("/api/available-models")).json();
availableModels = data.models || {};
renderModelSelection();
} catch (e) {
showToast("Failed to load models", "error");
}
}
async function loadSelectedModels() {
try {
const data = await (await fetch("/api/get-selected-models")).json();
selectedModels = data.selected_models || [];
updateModelCheckboxes();
} catch (e) {
/* silent */
}
}
function renderModelSelection() {
const grid = document.getElementById("models-grid");
if (!grid) return;
// Empty selectedModels means no saved preference β†’ default all checked
const allByDefault = selectedModels.length === 0;
const cards = Object.entries(availableModels).map(
([key, info]) => `
<div class="model-card">
<label class="model-checkbox-label">
<input type="checkbox" class="model-checkbox" data-model-id="${key}"
${allByDefault || selectedModels.includes(key) ? "checked" : ""}>
<div class="checkbox-content">
<strong>${info.name}</strong>
<p>${info.description}</p>
<span class="model-badge">${key}</span>
</div>
</label>
</div>`,
);
grid.innerHTML = cards.length ? cards.join("") : "<p>No models available</p>";
}
function updateModelCheckboxes() {
// Empty selectedModels = no saved preference β†’ keep all checked
if (selectedModels.length === 0) return;
document.querySelectorAll(".model-checkbox").forEach((cb) => {
cb.checked = selectedModels.includes(cb.dataset.modelId);
});
}
function toggleModelPanel() {
const c = document.getElementById("model-panel-content");
const i = document.getElementById("model-toggle-icon");
if (!c) return;
c.classList.toggle("open");
i.textContent = c.classList.contains("open") ? "β–²" : "β–Ό";
}
function selectAllModels() {
document
.querySelectorAll(".model-checkbox")
.forEach((cb) => (cb.checked = true));
}
function deselectAllModels() {
document
.querySelectorAll(".model-checkbox")
.forEach((cb) => (cb.checked = false));
}
async function applyModelSelection() {
const selected = Array.from(
document.querySelectorAll(".model-checkbox:checked"),
).map((cb) => cb.dataset.modelId);
try {
const data = await (
await fetch("/api/set-models", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ models: selected }),
})
).json();
if (data.success) {
selectedModels = selected;
showToast(`Applied ${selected.length || "all"} model(s)`, "success");
} else {
showToast("Failed to apply models", "error");
}
} catch (e) {
showToast("Error applying models", "error");
}
}
// ─── File handling ───────────────────────────────────────────────────────────
function handleFileSelect(event) {
const file = event.target.files[0];
if (!file) return;
const validExts = /\.(mp4|avi|mov|mkv)$/i;
if (!validExts.test(file.name)) {
showToast("Please select a valid video file (MP4, AVI, MOV, MKV)", "error");
return;
}
if (file.size > 500 * 1024 * 1024) {
showToast("File size must be under 500 MB", "error");
return;
}
selectedFile = file;
document.getElementById("file-name").textContent = file.name;
document.getElementById("file-size").textContent = fmtSize(file.size);
document.getElementById("upload-box").style.display = "none";
document.getElementById("file-info").style.display = "flex";
}
function cancelFile() {
selectedFile = null;
document.getElementById("video-file").value = "";
document.getElementById("file-info").style.display = "none";
document.getElementById("upload-box").style.display = "flex";
}
function wireDropzone() {
const box = document.getElementById("upload-box");
if (!box) return;
box.addEventListener("dragover", (e) => {
e.preventDefault();
box.style.borderColor = "var(--blue-600)";
});
box.addEventListener("dragleave", () => {
box.style.borderColor = "";
});
box.addEventListener("drop", (e) => {
e.preventDefault();
box.style.borderColor = "";
if (e.dataTransfer.files.length) {
const input = document.getElementById("video-file");
input.files = e.dataTransfer.files;
handleFileSelect({ target: input });
}
});
}
function fmtSize(bytes) {
const units = ["B", "KB", "MB", "GB"];
let i = 0;
while (bytes >= 1024 && i < units.length - 1) {
bytes /= 1024;
i++;
}
return `${bytes.toFixed(1)} ${units[i]}`;
}
// ─── Upload & start analysis ──────────────────────────────────────────────────
async function uploadAndAnalyze() {
if (!selectedFile) {
showToast("Select a video first", "error");
return;
}
// Apply currently checked models β€” fall back to all available if none checked
let checked = Array.from(
document.querySelectorAll(".model-checkbox:checked"),
).map((cb) => cb.dataset.modelId);
if (checked.length === 0) {
checked = Object.keys(availableModels);
if (checked.length === 0) {
showToast("No detection models available", "error");
return;
}
showToast("Using all available detection models", "info");
}
await fetch("/api/set-models", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ models: checked }),
});
// Show upload progress UI
document.getElementById("file-info").style.display = "none";
document.getElementById("upload-progress").style.display = "block";
document.getElementById("results-section").style.display = "none";
resetLivePanel();
const formData = new FormData();
formData.append("video", selectedFile);
return new Promise((resolve) => {
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener("progress", (e) => {
if (e.lengthComputable) {
const pct = Math.round((e.loaded / e.total) * 100);
document.getElementById("upload-fill").style.width = pct + "%";
document.getElementById("upload-text").textContent =
`Uploading… ${pct}%`;
}
});
xhr.upload.addEventListener("load", () => {
document.getElementById("upload-text").textContent =
"Upload complete β€” queuing analysis…";
});
xhr.addEventListener("load", () => {
document.getElementById("upload-progress").style.display = "none";
if (xhr.status === 200) {
const resp = JSON.parse(xhr.responseText);
if (resp.success) {
currentJobId = resp.job_id;
seenHighRiskCount = 0;
showRealtimePanel();
startPolling(resp.job_id);
showToast("Video uploaded β€” analysis started", "success");
} else {
showToast("Error: " + (resp.message || "Unknown error"), "error");
showUploadBox();
}
} else {
showToast("Upload failed β€” please try again", "error");
showUploadBox();
}
resolve();
});
xhr.addEventListener("error", () => {
document.getElementById("upload-progress").style.display = "none";
showToast("Network error during upload", "error");
showUploadBox();
resolve();
});
xhr.open("POST", "/api/start-video-analysis");
xhr.send(formData);
});
}
// ─── Polling ─────────────────────────────────────────────────────────────────
function startPolling(jobId) {
stopPolling();
pollTimer = setInterval(() => pollJob(jobId), 1500);
}
function stopPolling() {
if (pollTimer) {
clearInterval(pollTimer);
pollTimer = null;
}
}
async function pollJob(jobId) {
try {
const data = await (await fetch(`/api/analysis-progress/${jobId}`)).json();
if (!data.success) return;
updateRealtimePanel(data);
// Show new high-risk alerts in banner + feed
const newHighRisk = (data.high_risk_alerts || []).slice(seenHighRiskCount);
newHighRisk.forEach((a) => {
triggerAlertBanner(a);
addFeedItem(a, true);
});
seenHighRiskCount = (data.high_risk_alerts || []).length;
// Check for weapon detection alerts and trigger them prominently
const weaponAlerts = (data.alerts || []).filter(a =>
a.type && a.type.toLowerCase().includes('weapon')
);
weaponAlerts.forEach((a) => {
// Upgrade weapon alerts to trigger banner if they aren't already
if (!newHighRisk.some(hr => hr.type === a.type)) {
triggerAlertBanner({
severity: a.severity || 'HIGH',
frame: a.frame || 0,
message: a.message || `πŸ”« WEAPON DETECTED`,
type: a.type || 'weapon_detected'
});
addFeedItem(a, true);
}
});
// Add any new alerts to feed
const allAlerts = data.alerts || [];
const alreadyShown = seenHighRiskCount; // high-risk already handled
allAlerts
.filter((a) => !["HIGH", "CRITICAL"].includes(a.severity))
.slice(-5) // only show last 5 non-critical to avoid flooding
.forEach((a) => addFeedItem(a, false));
if (data.status === "done") {
stopPolling();
hideRealtimePanel();
currentAnalysisId = data.analysis_id;
showToast("Analysis complete!", "success");
displayResults(data.results);
if (data.analysis_id) {
setTimeout(() => {
loadAnalysisHistory(data.analysis_id);
loadDetectionRecords();
document.getElementById("reanalyze-btn").style.display =
"inline-block";
}, 400);
}
} else if (data.status === "error") {
stopPolling();
hideRealtimePanel();
showToast("Analysis error: " + (data.error || "Unknown"), "error");
showUploadBox();
}
} catch (e) {
console.error("Poll error:", e);
}
}
// ─── Real-time panel helpers ──────────────────────────────────────────────────
function showRealtimePanel() {
document.getElementById("realtime-panel").style.display = "block";
document.getElementById("upload-box").style.display = "none";
}
function hideRealtimePanel() {
document.getElementById("realtime-panel").style.display = "none";
}
function resetLivePanel() {
document.getElementById("rt-progress-fill").style.width = "0%";
document.getElementById("rt-frame-info").textContent = "Preparing…";
document.getElementById("rt-detections").textContent = "0";
document.getElementById("rt-alerts").textContent = "0";
document.getElementById("rt-high-risk").textContent = "0";
document.getElementById("live-alerts-feed").innerHTML =
'<p style="color:var(--slate-500);font-size:0.85rem;padding:0.3rem;">No alerts yet β€” all clear</p>';
// Reset processing video panel
document.getElementById("processing-bar-fill").style.width = "0%";
document.getElementById("processing-frame-count").textContent =
"Frame 0 of 0";
document.getElementById("proc-detections").textContent = "0";
document.getElementById("proc-alerts").textContent = "0";
document.getElementById("proc-high-risk").textContent = "0";
}
function updateRealtimePanel(data) {
const pct = data.progress || 0;
const cur = data.current_frame || 0;
const tot = data.total_frames || 0;
// Update main progress bar
document.getElementById("rt-progress-fill").style.width = pct + "%";
// Update frame info
document.getElementById("rt-frame-info").textContent =
tot > 0
? `Frame ${cur.toLocaleString()} of ${tot.toLocaleString()} (${pct}%)`
: `Processing… ${pct}%`;
// Update main stats
document.getElementById("rt-detections").textContent = (
data.detection_count || 0
).toLocaleString();
document.getElementById("rt-alerts").textContent = (
data.alert_count || 0
).toLocaleString();
document.getElementById("rt-high-risk").textContent = (
data.high_risk_alerts || []
).length.toString();
// Update processing video panel with live stats
document.getElementById("processing-bar-fill").style.width = pct + "%";
document.getElementById("processing-frame-count").textContent =
tot > 0
? `Frame ${cur.toLocaleString()} of ${tot.toLocaleString()}`
: "Frame 0 of 0";
document.getElementById("proc-detections").textContent = (
data.detection_count || 0
).toLocaleString();
document.getElementById("proc-alerts").textContent = (
data.alert_count || 0
).toLocaleString();
document.getElementById("proc-high-risk").textContent = (
data.high_risk_alerts || []
).length.toString();
}
const feedShown = new Set();
function addFeedItem(alert, isHighRisk) {
const key = `${alert.frame}-${alert.type}-${alert.severity}`;
if (feedShown.has(key)) return;
feedShown.add(key);
const feed = document.getElementById("live-alerts-feed");
const placeholder = feed.querySelector("p");
if (placeholder) placeholder.remove();
const sev = (alert.severity || "low").toLowerCase();
const div = document.createElement("div");
div.className = `feed-item ${sev}`;
div.innerHTML = `
<span class="feed-badge ${sev}">${alert.severity}</span>
<span><strong>Frame ${alert.frame}</strong> β€” ${alert.message || alert.type}</span>`;
feed.insertBefore(div, feed.firstChild);
// Keep feed manageable
while (feed.children.length > 20) feed.removeChild(feed.lastChild);
}
// ─── Alert banner ─────────────────────────────────────────────────────────────
function triggerAlertBanner(alert) {
const banner = document.getElementById("alert-banner");
const text = document.getElementById("alert-banner-text");
text.textContent = `🚨 ${alert.severity} ALERT β€” Frame ${alert.frame}: ${alert.message || alert.type}`;
banner.style.display = "block";
// Play browser notification sound if available
try {
new Audio("data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAA==")
.play()
.catch(() => {});
} catch (e) {}
clearTimeout(bannerDismissTimer);
bannerDismissTimer = setTimeout(dismissBanner, 6000);
}
function dismissBanner() {
document.getElementById("alert-banner").style.display = "none";
}
// ─── Display results ─────────────────────────────────────────────────────────
function displayResults(results) {
// Hide the processing overlay when results are ready
const processingOverlay = document.getElementById("processing-overlay");
if (processingOverlay) {
processingOverlay.style.opacity = "0";
processingOverlay.style.visibility = "hidden";
processingOverlay.style.transition =
"opacity 0.5s ease, visibility 0.5s ease";
}
document.getElementById("results-section").style.display = "block";
// Summary cards
document.getElementById("res-total-frames").textContent = (
results.total_frames || 0
).toLocaleString();
document.getElementById("res-analyzed").textContent = (
results.processed_frames || 0
).toLocaleString();
document.getElementById("res-detections").textContent = (
results.summary?.total_detections || 0
).toLocaleString();
document.getElementById("res-alerts").textContent = (
results.summary?.total_alerts || 0
).toLocaleString();
document.getElementById("res-emergency").textContent = (
results.summary?.emergency_frames_count ??
results.emergency_frames?.length ??
0
).toString();
// Processed video
const mediaEl = document.getElementById("media-results");
const cards = [];
if (results.preview_url) {
cards.push(`
<div class="detection-item">
<strong>Preview Frame</strong>
<div style="margin-top:0.8rem;">
<img src="${results.preview_url}" alt="Preview" style="max-width:100%;border-radius:10px;">
</div>
</div>`);
}
if (results.output_url) {
// Determine MIME type based on file extension
const url = results.output_url;
let mimeType = "video/mp4";
if (url.endsWith(".avi")) {
// Include codec info for better browser compatibility
mimeType = 'video/x-msvideo; codecs="xvid"';
} else if (url.endsWith(".mkv")) {
mimeType = "video/x-matroska";
} else if (url.endsWith(".mov")) {
mimeType = "video/quicktime";
}
cards.push(`
<div class="detection-item">
<strong>Processed Video with Detection Overlays</strong>
<div style="margin-top:0.8rem;">
<video controls style="width:100%;border-radius:10px;background:#000;">
<source src="${results.output_url}" type="${mimeType}">
Your browser does not support video playback.
</video>
</div>
<p style="margin-top:0.5rem;font-size:0.9rem;">
<a href="${results.output_url}" download>πŸ“₯ Download processed video</a>
</p>
</div>`);
}
mediaEl.innerHTML =
cards.join("") ||
"<p style='color:var(--slate-600);'>No processed video available.</p>";
// Emergency frames
const ef = results.emergency_frames || [];
const efSection = document.getElementById("emergency-section");
if (ef.length > 0) {
efSection.style.display = "block";
document.getElementById("emergency-gallery").innerHTML = ef
.map((f) => {
const color =
f.alert_type === "CRITICAL"
? "var(--danger)"
: f.alert_type === "WEAPON"
? "var(--warn)"
: "var(--info)";
return `
<div style="position:relative;border-radius:8px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,0.15);cursor:pointer;"
onclick="viewEmergencyFrame('${f.filename}','${f.alert_type}')">
<img src="/processed/emergency_frames/${f.filename}" alt="Emergency"
style="width:100%;height:140px;object-fit:cover;display:block;"
onerror="this.style.display='none'">
<div style="position:absolute;bottom:0;left:0;right:0;background:linear-gradient(to top,rgba(0,0,0,0.8),transparent);padding:0.5rem;color:white;font-size:0.75rem;">
<div style="font-weight:700;">🚨 ${f.alert_type}</div>
<div style="opacity:0.8;">Frame ${f.frame_number}</div>
</div>
<div style="position:absolute;top:4px;right:4px;background:${color};color:white;padding:2px 7px;border-radius:4px;font-size:0.7rem;font-weight:700;">EMERGENCY</div>
</div>`;
})
.join("");
} else {
efSection.style.display = "none";
}
// Populate insights panel with key insights
populateInsightsPanel(results);
// Scroll to results
document
.getElementById("results-section")
.scrollIntoView({ behavior: "smooth" });
}
// ─── Populate Insights Panel ──────────────────────────────────────────────────
function populateInsightsPanel(results) {
const insightsPanel = document.getElementById("insights-panel");
const insights = [];
// Total detections insight
const totalDets = results.summary?.total_detections || 0;
insights.push({
icon: "πŸ”",
label: "Total Detections",
value: totalDets,
class: totalDets > 0 ? "" : "info",
desc: totalDets > 0 ? `Found ${totalDets} objects` : "No objects detected",
});
// Alerts insight
const totalAlerts = results.summary?.total_alerts || 0;
insights.push({
icon: "🚨",
label: "Alerts",
value: totalAlerts,
class: totalAlerts > 0 ? "alert" : "",
desc: totalAlerts > 0 ? `${totalAlerts} alert(s) generated` : "No alerts",
});
// Weapons detected
const dets = results.detections || [];
const weaponCount = dets.filter((d) =>
(d.class || "").toLowerCase().includes("weapon"),
).length;
// TRIGGER WEAPON ALERT if weapons detected
if (weaponCount > 0) {
const weaponAlerts = (results.alerts || []).filter(a =>
a.type && a.type.toLowerCase().includes('weapon')
);
if (weaponAlerts.length > 0) {
// Trigger the first weapon alert as banner
triggerAlertBanner({
severity: weaponAlerts[0].severity || 'HIGH',
frame: weaponAlerts[0].frame || 0,
message: weaponAlerts[0].message || `${weaponCount} weapon(s) detected in video`,
type: weaponAlerts[0].type || 'weapon_detected'
});
} else {
// Even without alert details, trigger a warning for weapons
triggerAlertBanner({
severity: 'HIGH',
frame: 0,
message: `πŸ”« WEAPON DETECTED β€” ${weaponCount} weapon(s) found in video`,
type: 'weapon_detected'
});
}
}
insights.push({
icon: "πŸ”«",
label: "Weapons",
value: weaponCount,
class: weaponCount > 0 ? "alert" : "",
desc:
weaponCount > 0
? `${weaponCount} weapon(s) detected β€” ⚠️ ALERT TRIGGERED`
: "No weapons detected",
});
// People detected
const personCount = dets.filter((d) =>
(d.class || "").toLowerCase().includes("person"),
).length;
insights.push({
icon: "πŸ‘€",
label: "People",
value: personCount,
class: "",
desc:
personCount > 0
? `${personCount} person(ies) detected`
: "No people detected",
});
// High-risk frames
const summaries = results.frame_summaries || [];
const highRiskFrames = summaries.filter(
(f) => f.alert_state === "CRITICAL" || f.alert_state === "HIGH",
).length;
insights.push({
icon: "⚠️",
label: "High-Risk Frames",
value: highRiskFrames,
class: highRiskFrames > 0 ? "warning" : "",
desc:
highRiskFrames > 0
? `${highRiskFrames} frame(s) flagged`
: "No high-risk frames",
});
// Emergency frames
const emergencyFrames = results.emergency_frames?.length || 0;
insights.push({
icon: "⚑",
label: "Emergency Frames",
value: emergencyFrames,
class: emergencyFrames > 0 ? "alert" : "",
desc:
emergencyFrames > 0
? `${emergencyFrames} critical frame(s)`
: "No emergencies",
});
// Generate HTML
insightsPanel.innerHTML = insights
.map(
(insight) => `
<div class="insight-card ${insight.class}">
<div class="insight-icon">${insight.icon}</div>
<div class="insight-label">${insight.label}</div>
<div class="insight-value">${insight.value}</div>
<div class="insight-desc">${insight.desc}</div>
</div>
`,
)
.join("");
}
// ─── Emergency frame viewer ───────────────────────────────────────────────────
function viewEmergencyFrame(filename, alertType) {
const url = `/processed/emergency_frames/${filename}`;
new Modal(
"🚨 Emergency Frame",
`
<div style="text-align:center;">
<img src="${url}" alt="Emergency" style="max-width:100%;max-height:480px;border-radius:8px;margin-bottom:1rem;">
<div style="background:#fff5f5;border:2px solid var(--danger);padding:1rem;border-radius:8px;">
<p style="color:var(--danger);font-weight:700;">🚨 ${alertType} β€” High Priority Detection</p>
</div>
<div style="margin-top:1rem;">
<a href="${url}" target="_blank" class="btn btn-primary">πŸ” Full Size</a>
</div>
</div>`,
{ footer: false, width: "640px" },
).show();
}
// ─── Detection records from DB ────────────────────────────────────────────────
async function loadDetectionRecords() {
try {
const data = await (await fetch("/api/detection-history?limit=20")).json();
if (!data.success) return;
const el = document.getElementById("detection-records");
const records = (data.data || []).filter((d) => d.type === "detection");
if (records.length === 0) {
el.innerHTML =
'<p style="color:var(--slate-600);">No detection records saved yet.</p>';
return;
}
el.innerHTML = records
.map((r) => {
const ts = new Date(r.detected_at).toLocaleString();
const src =
r.details?.source === "video_analysis" ? "Video" : "Live Camera";
return `
<div class="det-record">
<img class="det-thumb" src="/api/detection-image/${r.image_filename}"
alt="Detection" onerror="this.style.display='none'">
<div class="det-info">
<div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:0.3rem;">
<strong>${r.detection_type.toUpperCase()}</strong>
<span class="badge-sev ${r.alert_level}">${r.alert_level}</span>
<span style="font-size:0.75rem;color:var(--slate-500);">${src}</span>
</div>
<div style="color:var(--slate-700);">Confidence: ${(r.confidence * 100).toFixed(1)}%</div>
<div style="color:var(--slate-500);font-size:0.8rem;">${ts}</div>
${r.details?.message ? `<div style="color:var(--slate-700);margin-top:0.2rem;font-size:0.8rem;">${r.details.message}</div>` : ""}
</div>
<a href="/api/detection-image/${r.image_filename}" download
class="btn btn-secondary" style="padding:0.3rem 0.6rem;font-size:0.8rem;flex-shrink:0;">⬇️</a>
</div>`;
})
.join("");
} catch (e) {
console.error("loadDetectionRecords error:", e);
}
}
// ─── Analysis history ─────────────────────────────────────────────────────────
async function loadAnalysisHistory(currentId) {
try {
const data = await (
await fetch("/api/video-analysis-list?limit=10")
).json();
if (!data.success) return;
const el = document.getElementById("analysis-history");
const analyses = data.data || [];
if (analyses.length === 0) {
el.innerHTML = "<p>No previous analyses.</p>";
return;
}
el.innerHTML = analyses
.map((a) => {
const isCur = a.id === currentId;
const ts = new Date(a.created_at).toLocaleString();
return `
<div class="history-item" style="${isCur ? "border-color:var(--blue-600);background:var(--blue-50);" : ""}">
${
a.preview_image_url
? `<img class="history-thumb" src="${a.preview_image_url}" onerror="this.style.display='none'">`
: `<div class="history-thumb" style="display:flex;align-items:center;justify-content:center;font-size:1.8rem;">🎬</div>`
}
<div class="history-meta" style="flex:1;min-width:0;">
<div style="font-weight:600;word-break:break-all;">${a.original_filename || "video"}${isCur ? " (current)" : ""}</div>
<div style="font-size:0.8rem;color:var(--slate-600);margin-top:0.2rem;">${ts}</div>
<div class="history-badges">
<span class="hbadge">🎬 ${a.total_frames} frames</span>
<span class="hbadge">πŸ” ${a.detection_count} detections</span>
<span class="hbadge ${a.alert_count > 0 ? "red" : ""}">🚨 ${a.alert_count} alerts</span>
${a.emergency_frames_count > 0 ? `<span class="hbadge red">⚑ ${a.emergency_frames_count} emergency</span>` : ""}
</div>
</div>
<button class="btn btn-secondary" onclick="loadAnalysisDetail(${a.id})"
style="white-space:nowrap;flex-shrink:0;padding:0.4rem 0.8rem;font-size:0.85rem;">
View
</button>
</div>`;
})
.join("");
} catch (e) {
console.error("loadAnalysisHistory error:", e);
}
}
async function loadAnalysisDetail(id) {
try {
const data = await (
await fetch(`/api/video-analysis-history/${id}`)
).json();
if (!data.success) {
showToast("Failed to load details", "error");
return;
}
const a = data.data;
currentAnalysisId = a.id;
displayResults({
total_frames: a.total_frames,
processed_frames: a.processed_frames,
output_url: a.processed_video_url,
output_mime: "video/mp4",
preview_url: a.preview_image_url,
detections: a.detections,
alerts: [],
frame_summaries: a.frame_summaries,
emergency_frames: a.emergency_frames,
summary: {
total_detections: a.detection_count,
total_alerts: a.alert_count,
emergency_frames_count: (a.emergency_frames || []).length,
},
});
document.getElementById("reanalyze-btn").style.display = "inline-block";
loadAnalysisHistory(id);
showToast("Analysis details loaded", "success");
} catch (e) {
showToast("Error loading details", "error");
}
}
// ─── Re-analyze ───────────────────────────────────────────────────────────────
async function reanalyzeVideo() {
if (!currentAnalysisId) {
showToast("No analysis to re-run. Upload a video first.", "info");
return;
}
if (!confirm("Re-analyze this video with the currently selected models?"))
return;
document.getElementById("results-section").style.display = "none";
resetLivePanel();
showRealtimePanel();
seenHighRiskCount = 0;
try {
const data = await (
await fetch(`/api/reanalyze-video/${currentAnalysisId}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
})
).json();
if (data.success) {
currentAnalysisId = data.analysis_id;
hideRealtimePanel();
displayResults(data.results);
loadAnalysisHistory(currentAnalysisId);
loadDetectionRecords();
showToast("Re-analysis complete", "success");
} else {
hideRealtimePanel();
showToast("Re-analysis failed: " + (data.error || "Unknown"), "error");
}
} catch (e) {
hideRealtimePanel();
showToast("Re-analysis error", "error");
}
}
// ─── UI helpers ───────────────────────────────────────────────────────────────
function startNewAnalysis() {
stopPolling();
selectedFile = null;
currentJobId = null;
currentAnalysisId = null;
seenHighRiskCount = 0;
feedShown.clear();
dismissBanner();
document.getElementById("video-file").value = "";
document.getElementById("file-info").style.display = "none";
document.getElementById("upload-progress").style.display = "none";
document.getElementById("results-section").style.display = "none";
hideRealtimePanel();
showUploadBox();
showToast("Ready for new analysis", "info");
}
function showUploadBox() {
document.getElementById("upload-box").style.display = "flex";
document.getElementById("file-info").style.display = "none";
}
function showToast(message, type = "info") {
const c = document.getElementById("toast-container");
if (!c) return;
const t = document.createElement("div");
t.className = `toast ${type}`;
t.textContent = message;
c.appendChild(t);
setTimeout(() => t.remove(), 3000);
}