import { CameraPreview } from "@/components/CameraPreview"; import { CameraSelect } from "@/components/CameraSelect"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { readStoredCameraIndex, useCameras, writeStoredCameraIndex, } from "@/hooks/useCameras"; import { apiPath } from "@/lib/api"; import { callMcpTool } from "@/lib/mcpTools"; import { Activity, ListOrdered, ScanFace, ShieldCheck, Trash2, } from "lucide-react"; import { useEffect, useState } from "react"; type SafetyPayload = { status?: string; face_tool_opt_in?: boolean; snapshot?: { actions_last_60s?: number; max_actions_per_minute?: number; kill_switch?: boolean; dry_run?: boolean; }; env?: Record; hitl?: { safe_window_until?: number }; }; export function Biometrics() { const { cameras, loading, error, refresh } = useCameras(); const [cameraIndex, setCameraIndex] = useState(0); const [safety, setSafety] = useState(null); const [safetyErr, setSafetyErr] = useState(""); const [toolNames] = useState([]); const [faceBusy, setFaceBusy] = useState< null | "list" | "capture" | "delete" >(null); const [faceResult, setFaceResult] = useState(null); const [faceErr, setFaceErr] = useState(""); const [deleteName, setDeleteName] = useState(""); useEffect(() => { if (cameras.length === 0) return; const stored = readStoredCameraIndex(); const pick = stored !== null && cameras.some((c) => c.index === stored) ? stored : cameras[0].index; setCameraIndex(pick); }, [cameras]); useEffect(() => { let cancelled = false; fetch(apiPath("/api/v1/safety/status")) .then(async (r) => { const j = (await r.json()) as SafetyPayload; if (cancelled) return; if (!r.ok || j.status !== "success") { setSafety(null); setSafetyErr(`HTTP ${r.status}`); return; } setSafetyErr(""); setSafety(j); }) .catch((e) => { if (!cancelled) { setSafety(null); setSafetyErr(String(e)); } }); return () => { cancelled = true; }; }, []); const handleCameraChange = (index: number) => { setCameraIndex(index); writeStoredCameraIndex(index); }; const hitlActive = safety?.hitl?.safe_window_until != null && safety.hitl.safe_window_until > Date.now() / 1000; const faceRegistered = toolNames.includes("automation_face"); const runFace = async (op: "list" | "capture" | "delete") => { setFaceErr(""); setFaceResult(null); setFaceBusy(op); try { const args: Record = op === "list" ? { operation: "list" } : op === "capture" ? { operation: "capture", camera_index: cameraIndex } : { operation: "delete", name: deleteName.trim() }; if (op === "delete" && !deleteName.trim()) { setFaceErr("Enter a profile name to delete."); setFaceBusy(null); return; } const res = await callMcpTool("automation_face", args); if (!res.ok) { setFaceErr(res.error); return; } setFaceResult(res.raw.result); const inner = res.raw.result as | { status?: string; error?: string } | undefined; if (inner && typeof inner === "object" && inner.status === "error") { setFaceErr(inner.error ?? "automation_face returned status error"); } } catch (e) { setFaceErr(e instanceof Error ? e.message : String(e)); } finally { setFaceBusy(null); } }; return (

Biometrics

Opt-in feature only —{" "} automation_face is not registered unless you enable it (see Help / docs/SAFETY.md §5). Camera selection matches the REST/OpenCV list for{" "} camera_index. The live preview uses the browser's{" "} video device order— if preview and OpenCV disagree, fix the default camera in Windows Settings.

Camera device void refresh()} value={cameraIndex} onChange={handleCameraChange} id="biometrics-camera" label="Capture device" />

OpenCV index {cameraIndex}{" "} for automation_face — see{" "} docs/SAFETY.md §5.

Live preview Browser Server safety GET /api/v1/safety/status {safetyErr &&

{safetyErr}

} {!safety && !safetyErr && (

Loading…

)} {safety && ( <>
Face tool registered {safety.face_tool_opt_in ? "opt-in ON" : "opt-in OFF"}
Kill switch {safety.snapshot?.kill_switch ? "ACTIVE" : "off"}
Dry run {safety.snapshot?.dry_run ? "on" : "off"}
HITL (human-in-the-loop) window {hitlActive ? "open" : "closed"}

Mutations (60s): {safety.snapshot?.actions_last_60s ?? "—"} / cap {safety.snapshot?.max_actions_per_minute ?? "—"}

)}
Server: automation_face POST /api/v1/tools/call {!faceRegistered && (

automation_face is not registered. Set{" "} PYWINAUTO_MCP_ENABLE_FACE=1 , install the face{" "} extra, restart the server, then refresh this page.

)} {faceRegistered && (

Runs on the host (OpenCV + face_recognition). Uses the selected{" "} camera_index for capture. See docs/SAFETY.md §5.

)}
setDeleteName(e.target.value)} placeholder="exact name" />
{faceErr &&

{faceErr}

} {faceResult !== null && (
							{JSON.stringify(faceResult, null, 2)}
						
)}
); }