'use client'; import React, { useEffect, useRef, useState } from 'react'; import { HARCore } from '@/lib/pose/HARCore'; // Import from the official tasks-vision package import { PoseLandmarker, FilesetResolver, DrawingUtils } from '@mediapipe/tasks-vision'; import { ArrowLeft, Activity, ShieldAlert, Ban, CheckCircle, Edit3, Trash2, MousePointerClick, BellRing } from 'lucide-react'; import Link from 'next/link'; import { AuthProvider, useAuth } from '@/lib/auth'; export default function MonitorPageWrap() { return ( ); } function MonitorPage() { const videoRef = useRef(null); const canvasRef = useRef(null); const [isLoading, setIsLoading] = useState(true); // State const [stats, setStats] = useState({ status: 'Initializing...', confidence: 0 }); // Safety Zone State (Normalized 0-1) const [safetyZone, setSafetyZone] = useState<{x: number, y: number, w: number, h: number} | null>(null); const [isEditingZone, setIsEditingZone] = useState(false); const [isDrawing, setIsDrawing] = useState(false); const [startPoint, setStartPoint] = useState<{x: number, y: number} | null>(null); const zoneRef = useRef<{x: number, y: number, w: number, h: number} | null>(null); // Alarm State const [alarmTriggered, setAlarmTriggered] = useState(false); const fallStartTimeRef = useRef(null); const [timeToAlarm, setTimeToAlarm] = useState(null); // Sync ref for loop access useEffect(() => { zoneRef.current = safetyZone; }, [safetyZone]); // Refs const harRef = useRef(null); const landmarkerRef = useRef(null); const requestRef = useRef(null); useEffect(() => { let isMounted = true; async function init() { try { // 1. Init Core const core = new HARCore(); harRef.current = core; // 2. Init Vision const vision = await FilesetResolver.forVisionTasks( "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.14/wasm" ); if (!isMounted) return; const landmarker = await PoseLandmarker.createFromOptions(vision, { baseOptions: { modelAssetPath: `https://storage.googleapis.com/mediapipe-models/pose_landmarker/pose_landmarker_full/float16/1/pose_landmarker_full.task`, delegate: "GPU" }, runningMode: "VIDEO", numPoses: 1 }); landmarkerRef.current = landmarker; // 3. Init Camera if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { const stream = await navigator.mediaDevices.getUserMedia({ video: { width: 640, height: 480 } }); if (videoRef.current) { videoRef.current.srcObject = stream; await videoRef.current.play(); setIsLoading(false); requestRef.current = requestAnimationFrame(predictWebcam); } } } catch (e) { console.error("Init Error:", e); setIsLoading(false); } } init(); return () => { isMounted = false; if (requestRef.current) cancelAnimationFrame(requestRef.current); if (videoRef.current && videoRef.current.srcObject) { (videoRef.current.srcObject as MediaStream).getTracks().forEach(t => t.stop()); } }; }, []); // Logging // Logging const { user } = useAuth(); const userRef = useRef(user); // Keep userRef synced useEffect(() => { userRef.current = user; }, [user]); const lastLogRef = useRef(Date.now()); const alarmLoggedRef = useRef(false); const sendLog = async (data: any) => { if (!userRef.current) return; try { await fetch('/api/logs', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-user-id': userRef.current.id }, body: JSON.stringify(data) }); } catch (e) { console.error("Log failed", e); } }; // Loop const lastVideoTimeRef = useRef(-1); const predictWebcam = async () => { const video = videoRef.current; const canvas = canvasRef.current; const landmarker = landmarkerRef.current; const har = harRef.current; if (video && canvas && landmarker && har) { let startTimeMs = performance.now(); if (lastVideoTimeRef.current !== video.currentTime && video.videoWidth > 0 && video.videoHeight > 0) { lastVideoTimeRef.current = video.currentTime; const result = landmarker.detectForVideo(video, startTimeMs); // Draw const ctx = canvas.getContext('2d'); if (ctx) { ctx.save(); ctx.clearRect(0, 0, canvas.width, canvas.height); // Mirror ctx.scale(-1, 1); ctx.translate(-canvas.width, 0); // Draw Video Frame ctx.drawImage(video, 0, 0, canvas.width, canvas.height); if (result.landmarks) { const drawingUtils = new DrawingUtils(ctx); for (const lm of result.landmarks) { drawingUtils.drawLandmarks(lm, { radius: 1, color: '#00FF00' }); drawingUtils.drawConnectors(lm, PoseLandmarker.POSE_CONNECTIONS, { color: '#00FF00', lineWidth: 2 }); } } ctx.restore(); } // Process Logic if (result.landmarks && result.landmarks.length > 0) { const lm = result.landmarks[0]; // 1. Run HAR first (Always detect status) const res = await har.process(lm as any); if (res) { // 2. Check Safety Zone let isUnsafe = false; if (zoneRef.current) { const z = zoneRef.current; const inZone = (p: {x:number, y:number}) => p.x >= z.x && p.x <= (z.x + z.w) && p.y >= z.y && p.y <= (z.y + z.h); let outsideCount = 0; for (const point of lm) { // Convert to Screen Coords (Mirrored) const screenPoint = { x: 1 - point.x, y: point.y }; if (!inZone(screenPoint)) { outsideCount++; } } // Threshold: > 70% of points outside triggers Unsafe if ((outsideCount / lm.length) > 0.7) { isUnsafe = true; } } // Update Status setStats({ status: res.status, confidence: res.confidence || 0 }); // 3. Check Alarm Condition let currentAlarmState = false; if (res.status === 'Fall Detected' && isUnsafe) { const now = Date.now(); if (!fallStartTimeRef.current) { fallStartTimeRef.current = now; } const elapsed = now - fallStartTimeRef.current; if (elapsed > 10000) { // Trigger Alarm setAlarmTriggered(true); currentAlarmState = true; } else { // Update countdown for UI setTimeToAlarm(Math.ceil((10000 - elapsed) / 1000)); } } else { // Reset fallStartTimeRef.current = null; setTimeToAlarm(null); } // 4. Logging Logic const now = Date.now(); // A. Check for Alarm Log (Immediate) // If alarm just triggered (transition) or is buzzing if (currentAlarmState && !alarmLoggedRef.current) { sendLog({ status: 'ALARM: Fall Outside Zone', confidence: '1.0', details: { reason: 'Fall detected outside safe zone > 10s' } }); alarmLoggedRef.current = true; // Prevent spamming per frame } // B. Periodic Log (Every 1 min) if (now - lastLogRef.current > 60000) { sendLog({ status: res.status, confidence: String(res.confidence), details: { isUnsafe, zoneConfigured: !!zoneRef.current } }); lastLogRef.current = now; } } } } } requestRef.current = requestAnimationFrame(predictWebcam); }; // Drawing Handlers const handleMouseDown = (e: React.MouseEvent) => { if (!isEditingZone || !canvasRef.current) return; const rect = canvasRef.current.getBoundingClientRect(); const x = (e.clientX - rect.left) / rect.width; const y = (e.clientY - rect.top) / rect.height; setIsDrawing(true); setStartPoint({x, y}); setSafetyZone({x, y, w: 0, h: 0}); }; const handleMouseMove = (e: React.MouseEvent) => { if (!isDrawing || !startPoint || !canvasRef.current) return; const rect = canvasRef.current.getBoundingClientRect(); const currentX = (e.clientX - rect.left) / rect.width; const currentY = (e.clientY - rect.top) / rect.height; const w = Math.abs(currentX - startPoint.x); const h = Math.abs(currentY - startPoint.y); const x = Math.min(currentX, startPoint.x); const y = Math.min(currentY, startPoint.y); setSafetyZone({x, y, w, h}); }; const handleMouseUp = () => { setIsDrawing(false); }; return ( LIVE.MONITOR System Active {/* Main Camera View */} {isLoading && Loading AI Engine (WASM)...} {/* Interaction Layer */} {/* Render Safety Zone using HTML Overlay */} {safetyZone && ( SAFE ZONE )} {/* Status Overlay */} {stats.status === 'Fall Detected' && } {stats.status} {/* Danger Countdown */} {timeToAlarm !== null && !alarmTriggered && ( {timeToAlarm} Zone Violation Detected )} {/* ALARM TRIGGERED */} {alarmTriggered && ( EMERGENCY Fall Detected Outside Safe Zone { setAlarmTriggered(false); fallStartTimeRef.current = null; alarmLoggedRef.current = false; }} className="bg-white text-red-600 px-10 py-4 rounded-full font-black text-xl hover:scale-105 transition-all shadow-xl uppercase border-4 border-red-800 pointer-events-auto" > DISMISS ALARM )} {isEditingZone && ( Mode: Draw Safety Zone )} {/* Sidebar Controls */} Safety Controls setIsEditingZone(!isEditingZone)} className={`w-full flex items-center gap-3 px-4 py-3 rounded-xl transition-all font-medium text-sm ${ isEditingZone ? 'bg-yellow-50 text-yellow-700 border border-yellow-200 shadow-inner' : 'bg-zinc-50 text-zinc-700 hover:bg-zinc-100 border border-zinc-100' }`} > {isEditingZone ? 'Done Editing' : 'Edit Safe Zone'} {safetyZone && ( setSafetyZone(null)} className="w-full flex items-center gap-3 px-4 py-3 rounded-xl bg-red-50 text-red-600 hover:bg-red-100 border border-red-100 transition-all font-medium text-sm" > Clear Zone )} {safetyZone ? ( Zone Active Alarm triggers if a fall is detected OUTSIDE this zone for > 10 seconds. ) : ( No Zone Click "Edit Safe Zone" and draw a box on the camera to define the safe area. )} Stats Current State {stats.status} AI Confidence {(stats.confidence * 100).toFixed(1)}% ); }
Fall Detected Outside Safe Zone
Alarm triggers if a fall is detected OUTSIDE this zone for > 10 seconds.
Click "Edit Safe Zone" and draw a box on the camera to define the safe area.