GharScan / static /app.js
Ritvik Shrivastava
minor changes
f907011
Raw
History Blame Contribute Delete
9.44 kB
/**
* app.js β€” GharScan frontend logic
*
* Uses @gradio/client to connect to the gr.Server backend.
* Handles: camera capture, image upload, API call, report rendering.
*/
import { Client, handle_file } from
"https://cdn.jsdelivr.net/npm/@gradio/client@1.5.0/dist/index.min.js";
// ── State ──────────────────────────────────────────────────────────────────────
let selectedFile = null;
let selectedLang = "en";
let gradioClient = null;
const SEVERITY_COLORS = {
1: "#22c55e",
2: "#84cc16",
3: "#f59e0b",
4: "#ef4444",
5: "#991b1b",
};
// ── Gradio client init (lazy, connects on first analysis) ─────────────────────
async function getClient() {
if (!gradioClient) {
gradioClient = await Client.connect(window.location.origin);
}
return gradioClient;
}
// ── Language toggle ────────────────────────────────────────────────────────────
window.setLang = function(lang) {
selectedLang = lang;
document.querySelectorAll(".lang-btn").forEach(btn => {
btn.classList.toggle("active", btn.dataset.lang === lang);
});
};
// ── Image input handlers ───────────────────────────────────────────────────────
function bindInputs() {
document.getElementById("cameraInput").addEventListener("change", handleFileChange);
document.getElementById("uploadInput").addEventListener("change", handleFileChange);
// Drag-and-drop on capture card
const dropZone = document.getElementById("dropZone");
dropZone.addEventListener("dragover", e => { e.preventDefault(); dropZone.style.borderColor = "#3b82f6"; });
dropZone.addEventListener("dragleave", () => { dropZone.style.borderColor = ""; });
dropZone.addEventListener("drop", e => {
e.preventDefault();
dropZone.style.borderColor = "";
const file = e.dataTransfer.files[0];
if (file && file.type.startsWith("image/")) setSelectedFile(file);
});
}
function handleFileChange(e) {
const file = e.target.files[0];
if (file) setSelectedFile(file);
}
function setSelectedFile(file) {
selectedFile = file;
// Show preview
const reader = new FileReader();
reader.onload = ev => {
const img = document.getElementById("previewImg");
img.src = ev.target.result;
img.classList.remove("hidden");
document.getElementById("capturePlaceholder").classList.add("hidden");
};
reader.readAsDataURL(file);
// Enable analyse button
document.getElementById("analyzeBtn").disabled = false;
}
// ── Main analysis flow ─────────────────────────────────────────────────────────
window.runAnalysis = async function() {
if (!selectedFile) return;
showLoading();
try {
const client = await getClient();
// Simulate step progression in loading UI
progressLoadingSteps();
const result = await client.predict("/analyze_defect", {
image_path: handle_file(selectedFile),
language: selectedLang,
});
const report = result.data[0]; // API returns dict as first element
if (report.analysis_ok === false) {
showError(report.description || "Analysis failed. Please retake the photo.");
} else {
renderReport(report);
}
} catch (err) {
console.error("GharScan API error:", err);
// Cold-start timeout is common on ZeroGPU β€” show friendly message
const msg = err.message?.includes("timeout")
? "The model is warming up (first request takes ~30s). Please try again in a moment."
: `Analysis failed: ${err.message}`;
showError(msg);
}
};
// ── Report rendering ───────────────────────────────────────────────────────────
function renderReport(r) {
// ── Structural banners ──
const structBanner = document.getElementById("structuralBanner");
const safeBanner = document.getElementById("safeBanner");
if (r.is_structural) {
structBanner.classList.remove("hidden");
safeBanner.classList.add("hidden");
document.getElementById("structuralReasoning").textContent = r.structural_reasoning || "";
} else {
safeBanner.classList.remove("hidden");
structBanner.classList.add("hidden");
}
// ── Defect label ──
document.getElementById("defectLabel").textContent = r.defect_display || r.defect_type;
document.getElementById("defectTypeSub").textContent = r.defect_type?.replace(/_/g, " ").toUpperCase();
// ── Severity chip ──
const chip = document.getElementById("severityChip");
const color = SEVERITY_COLORS[r.severity] || "#6b7280";
chip.style.borderColor = color;
document.getElementById("severityNum").textContent = r.severity;
document.getElementById("severityNum").style.color = color;
document.getElementById("severityWord").textContent = r.severity_label || "";
// ── Severity meter animation ──
const fill = document.getElementById("severityFill");
const pct = ((r.severity / 5) * 100).toFixed(1);
fill.style.width = pct + "%";
fill.style.background = color;
// ── Text rows ──
document.getElementById("defectDescription").textContent = r.description || "";
document.getElementById("primaryCause").textContent = r.primary_cause || "";
document.getElementById("immediateAction").textContent = r.immediate_action || "";
document.getElementById("urgencyDisplay").textContent = r.urgency_display || "";
// ── Cost block ──
const costRange = document.getElementById("costRange");
// Strip the leading β‚Ή if present (it's already in the HTML)
costRange.textContent = (r.cost_range_inr || "").replace(/^β‚Ή\s*/, "");
document.getElementById("professionalDisplay").textContent = r.professional_display || "";
// ── Monsoon risk ──
document.getElementById("monsoonWarning").classList.toggle("hidden", !r.monsoon_risk);
// ── Liability banner β€” ALWAYS shown for severity >= 4 (Watch-Out 2) ──
const liabBanner = document.getElementById("liabilityBanner");
if (r.show_liability_banner && r.liability_text) {
document.getElementById("liabilityText").textContent = r.liability_text;
liabBanner.classList.remove("hidden");
} else {
liabBanner.classList.add("hidden");
}
// ── Defect-specific disclaimer ──
const discCard = document.getElementById("disclaimerCard");
if (r.disclaimer) {
document.getElementById("disclaimerText").textContent = r.disclaimer;
discCard.classList.remove("hidden");
} else {
discCard.classList.add("hidden");
}
showSection("reportSection");
}
// ── UI state helpers ───────────────────────────────────────────────────────────
function showLoading() {
showSection("loadingSection");
// Reset step states
["step1","step2","step3"].forEach(id => {
const el = document.getElementById(id);
el.classList.remove("active","done");
});
document.getElementById("step1").classList.add("active");
}
function progressLoadingSteps() {
const steps = ["step1","step2","step3"];
let i = 0;
const interval = setInterval(() => {
if (i > 0) document.getElementById(steps[i-1]).classList.replace("active","done");
if (i < steps.length) {
document.getElementById(steps[i]).classList.add("active");
const labels = [
"Step 1 of 3: Classifying defect type",
"Step 2 of 3: Assessing severity",
"Step 3 of 3: Calculating cost estimate",
];
document.getElementById("loadingSub").textContent = labels[i];
}
i++;
if (i >= steps.length + 1) clearInterval(interval);
}, 4000); // Advance every 4s (inference ~12-15s total)
}
function showError(msg) {
document.getElementById("errorMsg").textContent = msg;
showSection("errorSection");
}
function showSection(id) {
["captureSection","loadingSection","reportSection","errorSection"].forEach(s => {
document.getElementById(s)?.classList.toggle("hidden", s !== id);
});
}
window.resetToCapture = function() {
selectedFile = null;
document.getElementById("previewImg").src = "";
document.getElementById("previewImg").classList.add("hidden");
document.getElementById("capturePlaceholder").classList.remove("hidden");
document.getElementById("analyzeBtn").disabled = true;
document.getElementById("cameraInput").value = "";
document.getElementById("uploadInput").value = "";
showSection("captureSection");
};
// ── Boot ───────────────────────────────────────────────────────────────────────
document.addEventListener("DOMContentLoaded", () => {
bindInputs();
// Warm up the Gradio client connection early (reduces first-click latency)
getClient().catch(() => {});
});