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