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 (
Live Monitoring Page
Use the live workspace to inspect saved images, scan frames from the browser camera, and review defect decisions with optional AI recommendations.
Camera Setup
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.
Detection Workspace
Camera Controls
Live Status
{cameraStatus}
{lastInspectedAt ? `Last inspection: ${formatDateTime(lastInspectedAt)}` : "Waiting for the first live inspection result."}
Inspection Result
{inspectionResult.recommendation}
{inspectionResult.summary_text || "Decision support guidance is not available for this inspection yet."}
{titleCase(defect.type)}
Confidence {defect.confidence ?? "--"} | Area ratio {defect.area_ratio} | Length {defect.length}
Live Monitoring Notes