/** * 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(() => {}); });