RavindranadhM's picture
Simplify deployed UX and remove surface gate flow
c36c5e5 verified
Raw
History Blame Contribute Delete
23.5 kB
import { useCallback, useEffect, useRef, useState } from "react"
import MetricCard from "../components/MetricCard"
import StatusBadge from "../components/StatusBadge"
import { inspectFrame, inspectImage } from "../lib/api"
import { formatDateTime, titleCase } from "../lib/formatters"
import { getDecisionTone } from "../lib/status"
function fileToDataUrl(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = () => resolve(reader.result)
reader.onerror = () => reject(new Error("Unable to read the selected image"))
reader.readAsDataURL(file)
})
}
function playAlertTone() {
const AudioContextRef = window.AudioContext || window.webkitAudioContext
if (!AudioContextRef) return
const audioContext = new AudioContextRef()
const oscillator = audioContext.createOscillator()
const gainNode = audioContext.createGain()
oscillator.type = "triangle"
oscillator.frequency.setValueAtTime(880, audioContext.currentTime)
gainNode.gain.setValueAtTime(0.0001, audioContext.currentTime)
gainNode.gain.exponentialRampToValueAtTime(0.08, audioContext.currentTime + 0.02)
gainNode.gain.exponentialRampToValueAtTime(0.0001, audioContext.currentTime + 0.28)
oscillator.connect(gainNode)
gainNode.connect(audioContext.destination)
oscillator.start()
oscillator.stop(audioContext.currentTime + 0.3)
}
export default function Live() {
const [mode, setMode] = useState("upload")
const [selectedFile, setSelectedFile] = useState(null)
const [imagePreview, setImagePreview] = useState("")
const [inspectionResult, setInspectionResult] = useState(null)
const [isInspecting, setIsInspecting] = useState(false)
const [error, setError] = useState("")
const [cameraReady, setCameraReady] = useState(false)
const [streaming, setStreaming] = useState(false)
const [autoInspect, setAutoInspect] = useState(true)
const [persistLive, setPersistLive] = useState(false)
const [deepAnalysisEnabled, setDeepAnalysisEnabled] = useState(false)
const [alertsEnabled, setAlertsEnabled] = useState(true)
const [cameraStatus, setCameraStatus] = useState("Camera offline")
const [lastInspectedAt, setLastInspectedAt] = useState("")
const videoRef = useRef(null)
const canvasRef = useRef(null)
const streamRef = useRef(null)
const requestInFlight = useRef(false)
const lastAlertDecision = useRef("")
async function handleImageSelection(event) {
const file = event.target.files?.[0]
if (!file) return
setError("")
setSelectedFile(file)
try {
const previewUrl = await fileToDataUrl(file)
setImagePreview(previewUrl)
} catch (selectionError) {
setError(selectionError.message)
}
}
async function handleImageInspection() {
if (!selectedFile) {
setError("Select an image to run inspection.")
return
}
setIsInspecting(true)
setError("")
try {
const imageBase64 = await fileToDataUrl(selectedFile)
const result = await inspectImage({
image_base64: imageBase64,
filename: selectedFile.name,
source: "upload",
persist: true,
llm_mode: deepAnalysisEnabled ? "always" : "off",
})
setInspectionResult(result)
setLastInspectedAt(result.timestamp)
if (alertsEnabled && result.decision === "FAIL" && lastAlertDecision.current !== "FAIL") {
playAlertTone()
}
lastAlertDecision.current = result.decision
} catch (inspectionError) {
setError(inspectionError.message)
} finally {
setIsInspecting(false)
}
}
async function startCamera() {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: {
facingMode: "environment",
width: { ideal: 1280 },
height: { ideal: 720 },
},
audio: false,
})
streamRef.current = stream
if (videoRef.current) {
videoRef.current.srcObject = stream
await videoRef.current.play()
}
setCameraReady(true)
setStreaming(true)
setCameraStatus("Camera active and scanning live frames")
setError("")
} catch {
setError("Camera access was blocked or unavailable.")
}
}
function stopCamera() {
if (streamRef.current) {
streamRef.current.getTracks().forEach((track) => track.stop())
streamRef.current = null
}
if (videoRef.current) {
videoRef.current.srcObject = null
}
requestInFlight.current = false
setStreaming(false)
setCameraReady(false)
setCameraStatus("Camera offline")
}
const captureAndInspectFrame = useCallback(async ({ persist = false } = {}) => {
if (!videoRef.current || !canvasRef.current || requestInFlight.current) return
if (videoRef.current.readyState < 2) return
const canvas = canvasRef.current
const videoWidth = videoRef.current.videoWidth || 1280
const videoHeight = videoRef.current.videoHeight || 720
const targetWidth = Math.min(768, videoWidth)
const targetHeight = Math.round((videoHeight / videoWidth) * targetWidth)
canvas.width = targetWidth
canvas.height = targetHeight
const context = canvas.getContext("2d")
if (!context) {
setError("Unable to capture the current camera frame.")
return
}
context.drawImage(videoRef.current, 0, 0, targetWidth, targetHeight)
requestInFlight.current = true
setIsInspecting(true)
setCameraStatus("Inspecting live frame")
setError("")
try {
const result = await inspectFrame({
image_base64: canvas.toDataURL("image/jpeg", 0.82),
source: "camera",
persist,
llm_mode: deepAnalysisEnabled ? "always" : "off",
})
setInspectionResult(result)
setLastInspectedAt(result.timestamp)
setCameraStatus(
result.total_defects > 0
? `Detected ${result.total_defects} defect${result.total_defects > 1 ? "s" : ""} in the latest frame`
: "No actionable defects detected in the latest frame"
)
if (alertsEnabled && result.decision === "FAIL" && lastAlertDecision.current !== "FAIL") {
playAlertTone()
}
lastAlertDecision.current = result.decision
} catch (inspectionError) {
setError(inspectionError.message)
setCameraStatus("Camera active, waiting for a successful inspection response")
} finally {
requestInFlight.current = false
setIsInspecting(false)
}
}, [alertsEnabled, deepAnalysisEnabled])
useEffect(() => {
return () => {
stopCamera()
}
}, [])
useEffect(() => {
if (mode !== "camera" || !streaming || !cameraReady || !autoInspect) return undefined
const interval = window.setInterval(() => {
captureAndInspectFrame({ persist: persistLive })
}, 2000)
return () => {
window.clearInterval(interval)
}
}, [autoInspect, cameraReady, captureAndInspectFrame, mode, persistLive, streaming])
useEffect(() => {
if (!cameraReady || mode !== "camera") return
const timer = window.setTimeout(() => {
captureAndInspectFrame({ persist: persistLive })
}, 500)
return () => {
window.clearTimeout(timer)
}
}, [cameraReady, captureAndInspectFrame, mode, persistLive])
useEffect(() => {
if (mode === "camera") {
setSelectedFile(null)
setImagePreview("")
return
}
setStreaming(false)
setCameraStatus("Camera offline")
}, [mode])
return (
<div className="space-y-6 pb-10">
<section className="surface-card p-6 md:p-8">
<div className="grid gap-6 xl:grid-cols-[1.25fr_0.75fr]">
<div>
<p className="eyebrow">Live Monitoring Page</p>
<h2 className="hero-title mt-3">Upload images or use the browser camera for organized, real-time manufacturing inspection.</h2>
<p className="body-copy mt-5 max-w-3xl">
Use the live workspace to inspect saved images, scan frames from the browser camera, and review defect decisions with optional AI recommendations.
</p>
<div className="mt-6 flex flex-wrap gap-3">
<button
type="button"
className={mode === "upload" ? "primary-btn" : "secondary-btn"}
onClick={() => setMode("upload")}
>
Upload images
</button>
<button
type="button"
className={mode === "camera" ? "primary-btn" : "secondary-btn"}
onClick={() => setMode("camera")}
>
Live camera feed
</button>
</div>
</div>
<div className="surface-card p-6">
<p className="eyebrow">Camera Setup</p>
<p className="mt-4 text-sm leading-7 text-slate-300">
For reliable live detection, keep the steel surface clearly visible in a stable top view. Reduce background clutter, avoid steep camera angles, and keep lighting even across the surface.
</p>
</div>
</div>
</section>
<section className="grid gap-6 xl:grid-cols-[1.05fr_0.95fr]">
<div className="surface-card p-6">
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<p className="eyebrow">Detection Workspace</p>
<h3 className="section-title mt-2">
{mode === "upload" ? "Image-based inspection" : "Camera-based live detection"}
</h3>
</div>
{inspectionResult ? (
<StatusBadge label={inspectionResult.decision} tone={getDecisionTone(inspectionResult.decision)} pulse={inspectionResult.decision === "FAIL"} />
) : null}
</div>
{mode === "upload" ? (
<div className="mt-6 space-y-5">
<label className="flex min-h-[240px] cursor-pointer flex-col items-center justify-center rounded-[28px] border border-dashed border-white/15 bg-white/5 px-6 py-8 text-center transition hover:border-cyan-300/40 hover:bg-cyan-400/[0.08]">
<input type="file" accept="image/*" className="hidden" onChange={handleImageSelection} />
<p className="text-lg font-medium text-slate-100">Drop or choose a steel surface image</p>
<p className="mt-3 max-w-xl text-sm leading-7 text-slate-400">
Use this mode to inspect saved surface samples, log a full report, and broadcast the result to the dashboard and history views.
</p>
</label>
{imagePreview ? (
<div className="overflow-hidden rounded-[28px] border border-white/10 bg-slate-950/60">
<img src={imagePreview} alt="Selected inspection" className="max-h-[420px] w-full object-cover" />
</div>
) : null}
<div className="flex flex-wrap gap-3">
<button type="button" className="primary-btn" onClick={handleImageInspection} disabled={isInspecting}>
{isInspecting ? "Inspecting image..." : "Run image inspection"}
</button>
<div className="rounded-full border border-white/10 bg-white/5 px-4 py-3 text-sm text-slate-300">
Uploaded inspections are saved as reports automatically.
</div>
</div>
<label className="flex items-center justify-between rounded-3xl border border-white/10 bg-white/5 px-4 py-3 text-sm text-slate-300">
<span>Enable GenAI deep analysis (slower response)</span>
<input
type="checkbox"
checked={deepAnalysisEnabled}
onChange={(event) => setDeepAnalysisEnabled(event.target.checked)}
className="h-5 w-5 accent-cyan-300"
/>
</label>
<div className="rounded-3xl border border-amber-300/20 bg-amber-400/10 px-4 py-4 text-sm leading-7 text-amber-50">
Fast mode is optimized for real-time monitoring. Turn on deep analysis only when you want the slower cloud GenAI recommendation layer.
</div>
</div>
) : (
<div className="mt-6 space-y-5">
<div className="grid gap-4 lg:grid-cols-2">
<div className="overflow-hidden rounded-[28px] border border-white/10 bg-slate-950/60">
<video ref={videoRef} className="aspect-video w-full object-cover" muted playsInline />
</div>
<div className="rounded-[28px] border border-white/10 bg-white/5 p-5">
<p className="eyebrow">Camera Controls</p>
<div className="mt-4 flex flex-wrap gap-3">
{!cameraReady ? (
<button type="button" className="primary-btn" onClick={startCamera}>
Enable camera
</button>
) : (
<>
<button
type="button"
className={streaming ? "secondary-btn" : "primary-btn"}
onClick={() => setStreaming((current) => !current)}
>
{streaming ? "Pause live inspection" : "Start live inspection"}
</button>
<button
type="button"
className="secondary-btn"
onClick={() => captureAndInspectFrame({ persist: persistLive })}
>
Inspect current frame
</button>
<button type="button" className="secondary-btn" onClick={stopCamera}>
Stop camera
</button>
</>
)}
</div>
<div className="mt-5 space-y-3">
<label className="flex items-center justify-between rounded-3xl border border-white/10 bg-white/5 px-4 py-3 text-sm text-slate-300">
<span>Auto inspect every few seconds</span>
<input
type="checkbox"
checked={autoInspect}
onChange={(event) => setAutoInspect(event.target.checked)}
className="h-5 w-5 accent-cyan-300"
/>
</label>
<label className="flex items-center justify-between rounded-3xl border border-white/10 bg-white/5 px-4 py-3 text-sm text-slate-300">
<span>Log live detections to reports and database</span>
<input
type="checkbox"
checked={persistLive}
onChange={(event) => setPersistLive(event.target.checked)}
className="h-5 w-5 accent-cyan-300"
/>
</label>
<label className="flex items-center justify-between rounded-3xl border border-white/10 bg-white/5 px-4 py-3 text-sm text-slate-300">
<span>Enable GenAI deep analysis (slower response)</span>
<input
type="checkbox"
checked={deepAnalysisEnabled}
onChange={(event) => setDeepAnalysisEnabled(event.target.checked)}
className="h-5 w-5 accent-cyan-300"
/>
</label>
<label className="flex items-center justify-between rounded-3xl border border-white/10 bg-white/5 px-4 py-3 text-sm text-slate-300">
<span>Play alert tone on FAIL states</span>
<input
type="checkbox"
checked={alertsEnabled}
onChange={(event) => setAlertsEnabled(event.target.checked)}
className="h-5 w-5 accent-cyan-300"
/>
</label>
</div>
<div className="mt-5 rounded-3xl border border-cyan-300/20 bg-cyan-400/10 px-4 py-4 text-sm text-cyan-50">
<p className="font-medium uppercase tracking-[0.2em] text-cyan-100/80">Live Status</p>
<p className="mt-2 leading-7">{cameraStatus}</p>
<p className="mt-2 text-cyan-100/80">
{lastInspectedAt ? `Last inspection: ${formatDateTime(lastInspectedAt)}` : "Waiting for the first live inspection result."}
</p>
</div>
<div className="rounded-3xl border border-white/10 bg-white/5 px-4 py-4 text-sm leading-7 text-slate-300">
Calibrate the camera so the top view of the steel surface fills most of the frame. This gives the detector a cleaner visual field and improves live inspection consistency.
</div>
<div className="rounded-3xl border border-amber-300/20 bg-amber-400/10 px-4 py-4 text-sm leading-7 text-amber-50">
Keep deep analysis off during continuous monitoring for the fastest response. Turn it on only when you want a slower GenAI disposition note for a specific frame.
</div>
</div>
</div>
<canvas ref={canvasRef} className="hidden" />
</div>
)}
{error ? (
<div className="mt-5 rounded-3xl border border-rose-300/30 bg-rose-400/10 px-4 py-4 text-sm text-rose-100">
{error}
</div>
) : null}
</div>
<div className="space-y-6">
<div className="surface-card p-6">
<p className="eyebrow">Inspection Result</p>
<h3 className="section-title mt-2">Annotated output and operator guidance</h3>
{inspectionResult?.annotated_image ? (
<div className="mt-5 overflow-hidden rounded-[28px] border border-white/10 bg-slate-950/60">
<img
src={inspectionResult.annotated_image}
alt="Annotated inspection result"
className="max-h-[420px] w-full object-cover"
/>
</div>
) : (
<div className="mt-5 rounded-[28px] border border-dashed border-white/15 bg-white/5 p-6 text-sm leading-7 text-slate-400">
Run an inspection to see the AI overlay, severity callouts, and decision banner.
</div>
)}
{inspectionResult ? (
<div className="mt-5 space-y-4">
<div className="flex flex-wrap items-center gap-3">
<StatusBadge label={inspectionResult.decision} tone={getDecisionTone(inspectionResult.decision)} pulse={inspectionResult.decision === "FAIL"} />
<div className="rounded-full border border-white/10 bg-white/5 px-4 py-2 text-sm text-slate-300">
{formatDateTime(inspectionResult.timestamp)}
</div>
</div>
<p className="text-sm leading-7 text-slate-300">{inspectionResult.recommendation}</p>
<div className="rounded-[28px] border border-cyan-300/20 bg-cyan-400/10 p-5">
<div className="flex flex-wrap items-center gap-3">
<StatusBadge
label={
inspectionResult.agent_mode === "llm"
? "Decision Copilot"
: "Rules Engine"
}
tone={
inspectionResult.agent_mode === "llm"
? "info"
: "warning"
}
/>
<div className="rounded-full border border-white/10 bg-white/5 px-4 py-2 text-sm text-slate-200">
{inspectionResult.agent_provider} {inspectionResult.agent_model && inspectionResult.agent_model !== "fallback" ? `· ${inspectionResult.agent_model}` : ""}
</div>
</div>
<p className="mt-4 text-sm leading-7 text-cyan-50/95">
{inspectionResult.summary_text || "Decision support guidance is not available for this inspection yet."}
</p>
</div>
<div className="grid gap-4 sm:grid-cols-3">
<MetricCard label="Minor" value={inspectionResult.minor} detail="Low severity anomalies" tone="success" />
<MetricCard label="Moderate" value={inspectionResult.moderate} detail="Review threshold" tone="warning" />
<MetricCard label="Critical" value={inspectionResult.critical} detail="Immediate action" tone="danger" />
</div>
<div className="space-y-3">
{(inspectionResult.defects || []).length ? (
(inspectionResult.defects || []).map((defect, index) => (
<div key={`${defect.type}-${index}`} className="rounded-3xl border border-white/10 bg-white/5 px-4 py-4">
<div className="flex flex-wrap items-center justify-between gap-3">
<p className="text-base font-medium text-slate-50">{titleCase(defect.type)}</p>
<StatusBadge label={defect.severity} tone={getDecisionTone(defect.decision)} />
</div>
<p className="mt-3 text-sm leading-7 text-slate-300">
Confidence {defect.confidence ?? "--"} | Area ratio {defect.area_ratio} | Length {defect.length}
</p>
</div>
))
) : (
<div className="rounded-3xl border border-emerald-300/20 bg-emerald-400/10 px-4 py-4 text-sm leading-7 text-emerald-50">
The latest frame is clear. The system still inspected the image and returned a PASS result with no actionable defects detected.
</div>
)}
</div>
</div>
) : null}
</div>
<div className="surface-card p-6">
<p className="eyebrow">Live Monitoring Notes</p>
<h3 className="section-title mt-2">What improves real-time stability</h3>
<div className="mt-5 grid gap-4 text-sm leading-7 text-slate-300">
<div className="rounded-3xl border border-white/10 bg-white/5 p-4">
Mount or position the camera so the steel surface is visible in a stable top view.
</div>
<div className="rounded-3xl border border-white/10 bg-white/5 p-4">
Keep the surface centered and avoid hands, faces, tools, or other objects crossing into the frame.
</div>
<div className="rounded-3xl border border-white/10 bg-white/5 p-4">
Use deep AI analysis for manual review, not for every live frame, when you want the fastest monitoring loop.
</div>
</div>
</div>
</div>
</section>
</div>
)
}