Spaces:
Running
Running
| const state = { | |
| selectedSample: null, | |
| uploadedFile: null, | |
| lastResponse: null, | |
| busy: false, | |
| }; | |
| const dropZone = document.getElementById("drop-zone"); | |
| const fileInput = document.getElementById("file-input"); | |
| const browseBtn = document.getElementById("browse-btn"); | |
| const sampleGrid = document.getElementById("sample-grid"); | |
| const analyzeBtn = document.getElementById("analyze-btn"); | |
| const loader = document.getElementById("loader"); | |
| const resultPanel = document.getElementById("result-panel"); | |
| const previewWrapper = document.getElementById("preview-wrapper"); | |
| const previewImage = document.getElementById("preview-image"); | |
| const annotatedImage = document.getElementById("annotated-image"); | |
| const reportText = document.getElementById("report-text"); | |
| const findingsGrid = document.getElementById("findings-grid"); | |
| const insightsList = document.getElementById("insights-list"); | |
| const metricsGrid = document.getElementById("metrics-grid"); | |
| const metricsCard = document.getElementById("metrics-card"); | |
| const timeline = document.getElementById("timeline"); | |
| const statusChip = document.getElementById("status-chip"); | |
| const errorBanner = document.getElementById("error-banner"); | |
| const downloadBtn = document.getElementById("download-btn"); | |
| const viewJsonBtn = document.getElementById("view-json-btn"); | |
| const refreshSamples = document.getElementById("refresh-samples"); | |
| const jsonDialog = document.getElementById("json-dialog"); | |
| browseBtn.addEventListener("click", () => fileInput.click()); | |
| dropZone.addEventListener("dragover", (event) => { | |
| event.preventDefault(); | |
| dropZone.classList.add("toggle-on"); | |
| }); | |
| dropZone.addEventListener("dragleave", () => dropZone.classList.remove("toggle-on")); | |
| dropZone.addEventListener("drop", (event) => { | |
| event.preventDefault(); | |
| dropZone.classList.remove("toggle-on"); | |
| const [file] = event.dataTransfer.files || []; | |
| if (file) { | |
| handleFileSelection(file); | |
| } | |
| }); | |
| fileInput.addEventListener("change", (event) => { | |
| const [file] = event.target.files || []; | |
| if (file) { | |
| handleFileSelection(file); | |
| } | |
| }); | |
| refreshSamples.addEventListener("click", loadSamples); | |
| viewJsonBtn.addEventListener("click", () => { | |
| if (!state.lastResponse) return; | |
| jsonDialog.innerHTML = ` | |
| <div class="flex items-center justify-between"> | |
| <h3 class="text-base font-semibold">Raw Response</h3> | |
| <button class="rounded-lg bg-slate-800 px-3 py-1 text-xs text-slate-300" id="close-json">Close</button> | |
| </div> | |
| <pre class="mt-4 max-h-[60vh] overflow-y-auto rounded-xl bg-slate-950/80 p-4 text-xs text-slate-200">${JSON.stringify(state.lastResponse, null, 2)}</pre> | |
| `; | |
| jsonDialog.showModal(); | |
| document.getElementById("close-json")?.addEventListener("click", () => jsonDialog.close()); | |
| }); | |
| downloadBtn.addEventListener("click", () => { | |
| if (!state.lastResponse) return; | |
| const blob = new Blob([state.lastResponse.report_text], { type: "text/plain" }); | |
| const url = URL.createObjectURL(blob); | |
| const link = document.createElement("a"); | |
| link.href = url; | |
| const name = state.lastResponse.source_id || "xray-report"; | |
| link.download = `${name}-report.txt`; | |
| link.click(); | |
| URL.revokeObjectURL(url); | |
| }); | |
| analyzeBtn.addEventListener("click", async () => { | |
| if (state.busy) return; | |
| if (!state.uploadedFile && !state.selectedSample) { | |
| showError("Provide an image via upload or sample gallery."); | |
| return; | |
| } | |
| state.busy = true; | |
| errorBanner.classList.add("hidden"); | |
| loader.classList.remove("hidden"); | |
| statusChip.textContent = "Processing"; | |
| statusChip.classList.add("border-sky-400", "text-sky-200"); | |
| try { | |
| const payload = await buildPayload(); | |
| const response = await fetch("https://satyam-singh-rmmm-backend.hf.space/api/v1/report", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify(payload), | |
| }); | |
| if (!response.ok) { | |
| throw new Error(`Server responded with ${response.status}`); | |
| } | |
| const data = await response.json(); | |
| state.lastResponse = data; | |
| renderResults(data); | |
| statusChip.textContent = "Ready"; | |
| } catch (error) { | |
| console.error(error); | |
| showError("Unable to generate report. Please retry."); | |
| statusChip.textContent = "Error"; | |
| } finally { | |
| loader.classList.add("hidden"); | |
| state.busy = false; | |
| } | |
| }); | |
| function showError(message) { | |
| errorBanner.textContent = message; | |
| errorBanner.classList.remove("hidden"); | |
| setTimeout(() => errorBanner.classList.add("hidden"), 6000); | |
| } | |
| async function buildPayload() { | |
| if (state.uploadedFile) { | |
| const base64 = await fileToDataUrl(state.uploadedFile); | |
| return { | |
| image_base64: base64, | |
| filename: state.uploadedFile.name, | |
| }; | |
| } | |
| return { | |
| image_base64: state.selectedSample.image_base64, | |
| source_id: state.selectedSample.id, | |
| filename: `${state.selectedSample.id}.png`, | |
| }; | |
| } | |
| function handleFileSelection(file) { | |
| state.uploadedFile = file; | |
| state.selectedSample = null; | |
| previewFile(file); | |
| renderSampleSelection(); | |
| } | |
| async function previewFile(file) { | |
| const dataUrl = await fileToDataUrl(file); | |
| previewWrapper.classList.remove("hidden"); | |
| previewImage.src = dataUrl; | |
| previewImage.classList.add("opacity-0"); | |
| requestAnimationFrame(() => previewImage.classList.remove("opacity-0")); | |
| } | |
| function renderSampleSelection() { | |
| const cards = sampleGrid.querySelectorAll("[data-sample-id]"); | |
| cards.forEach((card) => card.classList.remove("ring-2", "ring-sky-400")); | |
| if (state.selectedSample) { | |
| const activeCard = sampleGrid.querySelector(`[data-sample-id="${state.selectedSample.id}"]`); | |
| activeCard?.classList.add("ring-2", "ring-sky-400"); | |
| } | |
| } | |
| function renderResults(data) { | |
| const now = new Date(data.created_at).toLocaleString(); | |
| resultPanel.classList.remove("hidden"); | |
| reportText.textContent = data.report_text; | |
| annotatedImage.src = data.annotated_image; | |
| insightsList.innerHTML = ""; | |
| data.insights.forEach((insight, index) => { | |
| const li = document.createElement("li"); | |
| li.innerHTML = `<span class="text-sky-300">${index + 1}.</span> ${insight}`; | |
| insightsList.appendChild(li); | |
| }); | |
| findingsGrid.innerHTML = ""; | |
| Object.entries(data.findings).forEach(([term, info]) => { | |
| const card = document.createElement("div"); | |
| card.className = "rounded-xl border border-slate-700/70 bg-slate-900/60 p-4 transition hover:border-sky-500/70"; | |
| card.innerHTML = ` | |
| <div class="flex items-center justify-between"> | |
| <p class="text-sm font-semibold text-slate-200">${term.replaceAll("_", " ")}</p> | |
| <span class="rounded-full border border-sky-400/50 bg-sky-400/10 px-2 py-0.5 text-[11px] uppercase tracking-wide text-sky-200">${info.severity}</span> | |
| </div> | |
| <p class="mt-2 text-xs text-slate-400">${info.definition}</p> | |
| `; | |
| findingsGrid.appendChild(card); | |
| }); | |
| if (data.metrics) { | |
| metricsCard.classList.remove("hidden"); | |
| metricsGrid.innerHTML = ""; | |
| Object.entries(data.metrics).forEach(([key, value]) => { | |
| const term = key.toUpperCase().replaceAll("_", " "); | |
| metricsGrid.insertAdjacentHTML( | |
| "beforeend", | |
| `<div class="rounded-xl border border-slate-700/70 bg-slate-950/60 p-3"><dt class="text-xs text-slate-400">${term}</dt><dd class="text-lg font-semibold text-slate-100">${value}</dd></div>` | |
| ); | |
| }); | |
| } else { | |
| metricsCard.classList.add("hidden"); | |
| } | |
| timeline.innerHTML = ""; | |
| data.status_chain.forEach((step, index) => { | |
| const item = document.createElement("li"); | |
| item.className = "rounded-xl border border-slate-800/80 bg-slate-950/40 p-3"; | |
| item.innerHTML = ` | |
| <p class="text-xs uppercase tracking-wide text-slate-400">Step ${index + 1}</p> | |
| <p class="text-sm font-semibold text-slate-200">${step.title}</p> | |
| <p class="text-xs text-slate-400">${step.detail}</p> | |
| `; | |
| timeline.appendChild(item); | |
| }); | |
| statusChip.textContent = `Updated ${now}`; | |
| } | |
| async function fileToDataUrl(file) { | |
| return new Promise((resolve, reject) => { | |
| const reader = new FileReader(); | |
| reader.onload = () => resolve(reader.result); | |
| reader.onerror = reject; | |
| reader.readAsDataURL(file); | |
| }); | |
| } | |
| async function loadSamples() { | |
| sampleGrid.innerHTML = '<div class="col-span-full flex items-center justify-center py-8 text-sm text-slate-500">Loading samples...</div>'; | |
| try { | |
| const response = await fetch("https://satyam-singh-rmmm-backend.hf.space/api/v1/samples"); | |
| if (!response.ok) throw new Error("Failed to load samples"); | |
| const data = await response.json(); | |
| sampleGrid.innerHTML = ""; | |
| data.items.forEach((item) => { | |
| const card = document.createElement("button"); | |
| card.type = "button"; | |
| card.dataset.sampleId = item.id; | |
| card.className = "group overflow-hidden rounded-2xl border border-slate-800/70 bg-slate-900/40 text-left transition hover:border-sky-500/70 hover:bg-slate-900/70"; | |
| card.innerHTML = ` | |
| <div class="aspect-square overflow-hidden"> | |
| <img src="${item.image_base64}" alt="${item.title}" class="h-full w-full object-cover transition duration-500 group-hover:scale-105" /> | |
| </div> | |
| <div class="p-3"> | |
| <p class="text-xs uppercase tracking-wide text-slate-400">Sample</p> | |
| <p class="mt-1 text-sm font-semibold text-slate-100">${item.title}</p> | |
| </div> | |
| `; | |
| card.addEventListener("click", () => { | |
| state.selectedSample = item; | |
| state.uploadedFile = null; | |
| previewWrapper.classList.remove("hidden"); | |
| previewImage.src = item.image_base64; | |
| renderSampleSelection(); | |
| }); | |
| sampleGrid.appendChild(card); | |
| }); | |
| renderSampleSelection(); | |
| } catch (error) { | |
| console.error(error); | |
| sampleGrid.innerHTML = '<div class="col-span-full rounded-xl border border-rose-500/40 bg-rose-500/10 p-4 text-sm text-rose-200">Unable to load samples.</div>'; | |
| } | |
| } | |
| loadSamples(); | |