// ---------- Tabs ----------
document.querySelectorAll(".tab").forEach((btn) => {
btn.addEventListener("click", () => {
document.querySelectorAll(".tab").forEach((b) => b.classList.remove("active"));
document.querySelectorAll(".pane").forEach((p) => p.classList.remove("active"));
btn.classList.add("active");
document.getElementById(btn.dataset.target).classList.add("active");
});
});
// ---------- Helpers ----------
function show(el) { el.classList.remove("hidden"); }
function hide(el) { el.classList.add("hidden"); }
function bindDropzone(zoneEl, inputEl, previewEl) {
inputEl.addEventListener("change", () => {
const file = inputEl.files && inputEl.files[0];
if (!file) {
zoneEl.classList.remove("has-file");
previewEl.removeAttribute("src");
return;
}
const reader = new FileReader();
reader.onload = (e) => {
previewEl.src = e.target.result;
zoneEl.classList.add("has-file");
};
reader.readAsDataURL(file);
});
["dragenter", "dragover"].forEach((ev) => {
zoneEl.addEventListener(ev, (e) => {
e.preventDefault();
zoneEl.classList.add("dragover");
});
});
["dragleave", "drop"].forEach((ev) => {
zoneEl.addEventListener(ev, (e) => {
e.preventDefault();
zoneEl.classList.remove("dragover");
});
});
zoneEl.addEventListener("drop", (e) => {
const file = e.dataTransfer.files && e.dataTransfer.files[0];
if (file) {
const dt = new DataTransfer();
dt.items.add(file);
inputEl.files = dt.files;
inputEl.dispatchEvent(new Event("change"));
}
});
}
// ---------- Detection ----------
const detectForm = document.getElementById("detectForm");
const detectFile = document.getElementById("detectFile");
const detectDrop = document.getElementById("detectDrop");
const detectPreview = document.getElementById("detectPreview");
const detectBtn = document.getElementById("detectBtn");
const detectLoader = document.getElementById("detectLoader");
const detectError = document.getElementById("detectError");
const detectEmpty = document.getElementById("detectEmpty");
const detectResult = document.getElementById("detectResult");
const detectCount = document.getElementById("detectCount");
const detectServerMs = document.getElementById("detectServerMs");
const detectTotalMs = document.getElementById("detectTotalMs");
const detectBackend = document.getElementById("detectBackend");
const detectImg = document.getElementById("detectImg");
const detectOverlay = document.getElementById("detectOverlay");
const detectActiveFace = document.getElementById("detectActiveFace");
const detectFacesLabel = document.getElementById("detectFacesLabel");
const detectNoFace = document.getElementById("detectNoFace");
const detectImgWrap = document.getElementById("detectImgWrap");
const detectFaceNav = document.getElementById("detectFaceNav");
const detectPrev = document.getElementById("detectPrev");
const detectNext = document.getElementById("detectNext");
const detectSelectedIdx = document.getElementById("detectSelectedIdx");
const detectTotal = document.getElementById("detectTotal");
let detectState = null;
bindDropzone(detectDrop, detectFile, detectPreview);
detectForm.addEventListener("submit", async (e) => {
e.preventDefault();
if (!detectFile.files[0]) return;
hide(detectError);
hide(detectResult);
hide(detectEmpty);
show(detectLoader);
detectBtn.disabled = true;
const fd = new FormData(detectForm);
const t0 = performance.now();
try {
const res = await fetch("/api/detect", { method: "POST", body: fd });
const data = await res.json();
const totalMs = Math.round(performance.now() - t0);
if (!res.ok) throw new Error(data.error || "Request failed");
detectServerMs.textContent = data.processing_ms != null ? data.processing_ms + " ms" : "—";
detectTotalMs.textContent = totalMs + " ms";
detectBackend.textContent = data.backend || "—";
detectCount.textContent = data.face_count;
detectImg.src = data.image;
if (data.faces.length === 0) {
detectFacesLabel.classList.add("hidden");
detectFaceNav.classList.add("hidden");
detectActiveFace.classList.add("hidden");
detectImgWrap.classList.add("hidden");
detectNoFace.classList.remove("hidden");
detectOverlay.innerHTML = "";
detectState = null;
} else {
detectFacesLabel.classList.remove("hidden");
detectImgWrap.classList.remove("hidden");
detectNoFace.classList.add("hidden");
detectState = { data, selectedIdx: 0 };
renderDetectSelection(0);
}
show(detectResult);
} catch (err) {
detectError.textContent = err.message;
show(detectError);
show(detectEmpty);
} finally {
hide(detectLoader);
detectBtn.disabled = false;
}
});
// ---------- Compare ----------
const compareForm = document.getElementById("compareForm");
const compareFile1 = document.getElementById("compareFile1");
const compareFile2 = document.getElementById("compareFile2");
const compareDrop1 = document.getElementById("compareDrop1");
const compareDrop2 = document.getElementById("compareDrop2");
const comparePreview1 = document.getElementById("comparePreview1");
const comparePreview2 = document.getElementById("comparePreview2");
const compareBtn = document.getElementById("compareBtn");
const compareLoader = document.getElementById("compareLoader");
const compareError = document.getElementById("compareError");
const compareResult = document.getElementById("compareResult");
const compareTiming = document.getElementById("compareTiming");
const compareServerMs = document.getElementById("compareServerMs");
const compareTotalMs = document.getElementById("compareTotalMs");
const compareBackend = document.getElementById("compareBackend");
const compareVerdict = document.getElementById("compareVerdict");
const cmpSimilarity = document.getElementById("cmpSimilarity");
const metricsGrid = document.getElementById("metricsGrid");
const cmpCount1 = document.getElementById("cmpCount1");
const cmpCount2 = document.getElementById("cmpCount2");
const cmpImg1 = document.getElementById("cmpImg1");
const cmpImg2 = document.getElementById("cmpImg2");
const cmpOverlay1 = document.getElementById("cmpOverlay1");
const cmpOverlay2 = document.getElementById("cmpOverlay2");
const cmpCrop1 = document.getElementById("cmpCrop1");
const cmpCrop2 = document.getElementById("cmpCrop2");
const cmpIdx1 = document.getElementById("cmpIdx1");
const cmpIdx2 = document.getElementById("cmpIdx2");
const cmpFaceNav = document.getElementById("cmpFaceNav");
const cmpPrev = document.getElementById("cmpPrev");
const cmpNext = document.getElementById("cmpNext");
const cmpSelectedIdx = document.getElementById("cmpSelectedIdx");
const cmpTotal = document.getElementById("cmpTotal");
let cmpState = null;
const SVG_NS = "http://www.w3.org/2000/svg";
function renderOverlay(svgEl, w, h, boxes, activeIdx, withDataIdx) {
svgEl.setAttribute("viewBox", `0 0 ${w} ${h}`);
const baseStroke = Math.max(2, Math.min(w, h) * 0.005);
const labelH = Math.max(20, Math.min(w, h) * 0.035);
const labelW = labelH * 1.6;
const fontSize = labelH * 0.62;
svgEl.innerHTML = boxes.map((b, i) => {
const [x1, y1, x2, y2] = b;
const bw = x2 - x1, bh = y2 - y1;
const active = i === activeIdx;
const color = active ? "#22c55e" : "#ef4444";
const lblY = Math.max(0, y1 - labelH - 2);
const dataAttr = withDataIdx ? `data-face-idx="${i}"` : "";
return `