| |
|
|
| let classes = []; |
| let selectedFile = null; |
|
|
| const dropzone = document.getElementById("dropzone"); |
| const dropzoneEmpty = document.getElementById("dropzoneEmpty"); |
| const dropzonePreview = document.getElementById("dropzonePreview"); |
| const previewImage = document.getElementById("previewImage"); |
| const previewFileName = document.getElementById("previewFileName"); |
| const previewFileInfo = document.getElementById("previewFileInfo"); |
| const changeImageBtn = document.getElementById("changeImageBtn"); |
| const imageInput = document.getElementById("imageInput"); |
| const resizeCb = document.getElementById("resizeCb"); |
| const slidersEl = document.getElementById("sliders"); |
| const legendEl = document.getElementById("legend"); |
| const runBtn = document.getElementById("runBtn"); |
| const scanOverlay = document.getElementById("scanOverlay"); |
| const step1 = document.querySelector('.step[data-step="1"]'); |
| const step2 = document.querySelector('.step[data-step="2"]'); |
| const line1 = document.getElementById("line1"); |
| const stepEyebrow = document.getElementById("stepEyebrow"); |
|
|
| const toastStack = document.getElementById("toastStack"); |
|
|
| function showToast(message, type = "info") { |
| const el = document.createElement("div"); |
| el.className = `toast ${type}`; |
| el.textContent = message; |
| toastStack.appendChild(el); |
| setTimeout(() => el.remove(), 4000); |
| } |
|
|
| function formatBytes(bytes) { |
| if (bytes < 1024) return `${bytes} B`; |
| if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`; |
| return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; |
| } |
|
|
| function readImageDimensions(file) { |
| return new Promise((resolve) => { |
| const img = new Image(); |
| img.onload = () => { |
| resolve({ width: img.naturalWidth, height: img.naturalHeight }); |
| URL.revokeObjectURL(img.src); |
| }; |
| img.src = URL.createObjectURL(file); |
| }); |
| } |
|
|
| async function init() { |
| const res = await fetch("/api/classes"); |
| const data = await res.json(); |
| classes = data.classes; |
| renderSliders(); |
| renderLegend(); |
| } |
|
|
| function renderSliders() { |
| slidersEl.innerHTML = classes.map((c) => ` |
| <div class="slider-item"> |
| <label for="slider-${c.id}">${c.name}</label> |
| <input type="range" id="slider-${c.id}" min="0" max="1" step="0.01" value="${c.default_threshold}"> |
| <span id="slider-${c.id}-val" class="mono">${c.default_threshold}</span> |
| </div> |
| `).join(""); |
|
|
| classes.forEach((c) => { |
| const input = document.getElementById(`slider-${c.id}`); |
| const val = document.getElementById(`slider-${c.id}-val`); |
| input.addEventListener("input", () => { val.textContent = input.value; }); |
| }); |
| } |
|
|
| function renderLegend() { |
| legendEl.innerHTML = classes.map((c) => ` |
| <div class="legend-item"> |
| <span class="swatch" style="background: rgb(${c.color.join(",")})"></span> |
| <span>${c.name}</span> |
| </div> |
| `).join(""); |
| } |
|
|
| function getThresholds() { |
| const thresholds = {}; |
| classes.forEach((c) => { |
| thresholds[c.id] = parseFloat(document.getElementById(`slider-${c.id}`).value); |
| }); |
| return thresholds; |
| } |
|
|
| |
| async function setSelectedFile(file) { |
| if (!file || !file.type.startsWith("image/")) { |
| showToast("Please choose a valid image file.", "error"); |
| return; |
| } |
| selectedFile = file; |
| previewImage.src = URL.createObjectURL(file); |
| previewFileName.textContent = file.name; |
| previewFileInfo.textContent = formatBytes(file.size); |
| dropzoneEmpty.hidden = true; |
| dropzonePreview.hidden = false; |
| runBtn.disabled = false; |
|
|
| const { width, height } = await readImageDimensions(file); |
| previewFileInfo.textContent = `${width} Γ ${height} px Β· ${formatBytes(file.size)}`; |
| } |
|
|
| dropzone.addEventListener("click", (e) => { |
| if (e.target.closest(".dropzone-preview")) return; |
| imageInput.click(); |
| }); |
| dropzone.addEventListener("keydown", (e) => { |
| if (e.key === "Enter" || e.key === " ") { e.preventDefault(); imageInput.click(); } |
| }); |
| imageInput.addEventListener("change", () => setSelectedFile(imageInput.files[0])); |
|
|
| changeImageBtn.addEventListener("click", (e) => { |
| e.stopPropagation(); |
| imageInput.click(); |
| }); |
|
|
| ["dragenter", "dragover"].forEach((evt) => |
| dropzone.addEventListener(evt, (e) => { e.preventDefault(); dropzone.classList.add("dragover"); }) |
| ); |
| ["dragleave", "drop"].forEach((evt) => |
| dropzone.addEventListener(evt, (e) => { e.preventDefault(); dropzone.classList.remove("dragover"); }) |
| ); |
| dropzone.addEventListener("drop", (e) => { |
| const file = e.dataTransfer.files[0]; |
| if (file) setSelectedFile(file); |
| }); |
|
|
| |
| runBtn.addEventListener("click", async () => { |
| if (!selectedFile) { |
| showToast("Please choose an image first.", "error"); |
| return; |
| } |
|
|
| runBtn.disabled = true; |
| runBtn.textContent = "Analyzingβ¦"; |
| scanOverlay.classList.add("active"); |
| step1.classList.add("completed"); |
| step1.classList.remove("active"); |
| step2.classList.add("active"); |
| line1.classList.add("filled"); |
| stepEyebrow.innerHTML = `<span class="dot"></span> Step 2 of 3`; |
|
|
| try { |
| const formData = new FormData(); |
| formData.append("image", selectedFile); |
| formData.append("resize", resizeCb.checked); |
| formData.append("thresholds", JSON.stringify(getThresholds())); |
|
|
| const res = await fetch("/api/detect", { method: "POST", body: formData }); |
| if (!res.ok) throw new Error(`Request failed: ${res.status}`); |
| const data = await res.json(); |
|
|
| sessionStorage.setItem("cardentiq_result", JSON.stringify(data)); |
| window.location.href = "report.html"; |
| } catch (err) { |
| showToast(`Detection failed: ${err.message}`, "error"); |
| scanOverlay.classList.remove("active"); |
| step1.classList.remove("completed"); |
| step1.classList.add("active"); |
| step2.classList.remove("active"); |
| line1.classList.remove("filled"); |
| stepEyebrow.innerHTML = `<span class="dot"></span> Step 1 of 3`; |
| runBtn.disabled = false; |
| runBtn.textContent = "Analyze Image"; |
| } |
| }); |
|
|
| init(); |
|
|