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

Upload images or use the browser camera for organized, real-time manufacturing inspection.

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

{mode === "upload" ? "Image-based inspection" : "Camera-based live detection"}

{inspectionResult ? ( ) : null}
{mode === "upload" ? (
{imagePreview ? (
Selected inspection
) : null}
Uploaded inspections are saved as reports automatically.
Fast mode is optimized for real-time monitoring. Turn on deep analysis only when you want the slower cloud GenAI recommendation layer.
) : (

Camera Controls

{!cameraReady ? ( ) : ( <> )}

Live Status

{cameraStatus}

{lastInspectedAt ? `Last inspection: ${formatDateTime(lastInspectedAt)}` : "Waiting for the first live inspection result."}

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.
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.
)} {error ? (
{error}
) : null}

Inspection Result

Annotated output and operator guidance

{inspectionResult?.annotated_image ? (
Annotated inspection result
) : (
Run an inspection to see the AI overlay, severity callouts, and decision banner.
)} {inspectionResult ? (
{formatDateTime(inspectionResult.timestamp)}

{inspectionResult.recommendation}

{inspectionResult.agent_provider} {inspectionResult.agent_model && inspectionResult.agent_model !== "fallback" ? `ยท ${inspectionResult.agent_model}` : ""}

{inspectionResult.summary_text || "Decision support guidance is not available for this inspection yet."}

{(inspectionResult.defects || []).length ? ( (inspectionResult.defects || []).map((defect, index) => (

{titleCase(defect.type)}

Confidence {defect.confidence ?? "--"} | Area ratio {defect.area_ratio} | Length {defect.length}

)) ) : (
The latest frame is clear. The system still inspected the image and returned a PASS result with no actionable defects detected.
)}
) : null}

Live Monitoring Notes

What improves real-time stability

Mount or position the camera so the steel surface is visible in a stable top view.
Keep the surface centered and avoid hands, faces, tools, or other objects crossing into the frame.
Use deep AI analysis for manual review, not for every live frame, when you want the fastest monitoring loop.
) }