Aynkhn's picture
Deploy FaceVision to Hugging Face Spaces
0fd3d47 verified
Raw History Blame Contribute Delete
5.74 kB
"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import { Card, CardHeader, CardTitle } from "@/components/ui";
import { useSettings } from "@/context/SettingsProvider";
import { useEmotionLogger } from "@/hooks/useEmotionLogger";
import { useFaceTracking } from "@/hooks/useFaceTracking";
import { useMediaStream } from "@/hooks/useMediaStream";
import { useTelemetryLog } from "@/hooks/useTelemetryLog";
import { formatDuration, formatTime } from "@/lib/format";
import { CameraFeed } from "./CameraFeed";
import { EmotionDisplay } from "./EmotionDisplay";
import { FaceOverlayCanvas } from "./FaceOverlayCanvas";
import { StatsPanel } from "./StatsPanel";
import { TelemetryConsole } from "./TelemetryConsole";
import { TrackingControls } from "./TrackingControls";
/**
* The tracking tab.
*
* Owns only what coordinates its children: whether tracking is on. Per-frame data
* never passes through here -- the canvas and the stats panel subscribe to it
* directly, which is what keeps this component from re-rendering sixty times a
* second along with everything below it.
*/
export function TrackingView() {
const { settings } = useSettings();
const [isTracking, setIsTracking] = useState(false);
const { videoRef, state: cameraState, error: cameraError, start, stop } = useMediaStream();
const { lines, push } = useTelemetryLog();
const { currentEmotion, faceDetected, fps, isLoaded, loadError, subscribe } = useFaceTracking(
videoRef,
isTracking && cameraState === "ready",
);
const { lastLog, logError, connectionStatus, queueCount } = useEmotionLogger(
currentEmotion,
isTracking && cameraState === "ready",
);
// Telemetry, driven by state changes rather than by eight separate effects in
// the root component.
const previousEmotion = useRef(currentEmotion);
useEffect(() => {
if (!isTracking) return;
if (previousEmotion.current === currentEmotion) return;
previousEmotion.current = currentEmotion;
push(`Expression changed to ${currentEmotion}`);
}, [currentEmotion, isTracking, push]);
useEffect(() => {
if (isLoaded) push("Face tracking model ready");
}, [isLoaded, push]);
useEffect(() => {
if (loadError) push(`Model error: ${loadError}`);
}, [loadError, push]);
useEffect(() => {
if (cameraError) push(`Camera error: ${cameraError}`);
}, [cameraError, push]);
useEffect(() => {
if (lastLog) push(`Logged ${lastLog.dominant_emotion} for ${lastLog.duration_seconds}s`);
}, [lastLog, push]);
useEffect(() => {
if (logError) push(`Sync issue: ${logError}`);
}, [logError, push]);
const handleStart = useCallback(async () => {
push("Starting camera");
await start();
setIsTracking(true);
}, [start, push]);
const handleStop = useCallback(() => {
setIsTracking(false);
// Releases the camera tracks, so the indicator light actually goes out.
stop();
push("Tracking stopped, camera released");
}, [stop, push]);
return (
<div className="grid gap-6 lg:grid-cols-[minmax(0,2fr)_minmax(0,1fr)]">
<div className="flex flex-col gap-4">
<CameraFeed
videoRef={videoRef}
state={cameraState}
error={cameraError}
onRetry={handleStart}
>
<FaceOverlayCanvas
videoRef={videoRef}
subscribe={subscribe}
active={isTracking && isLoaded}
emotion={currentEmotion}
faceDetected={faceDetected}
/>
{settings.showScannerGrid && isTracking && (
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 opacity-20"
style={{
backgroundImage:
"linear-gradient(rgba(96,165,250,.35) 1px, transparent 1px), linear-gradient(90deg, rgba(96,165,250,.35) 1px, transparent 1px)",
backgroundSize: "40px 40px",
}}
/>
)}
</CameraFeed>
<TrackingControls
isTracking={isTracking}
canStart={isLoaded && cameraState !== "requesting"}
starting={cameraState === "requesting"}
onStart={handleStart}
onStop={handleStop}
syncIntervalMs={settings.syncIntervalMs}
connectionStatus={connectionStatus}
queueCount={queueCount}
/>
{loadError && (
<p role="alert" className="text-danger text-sm">
{loadError}
</p>
)}
{/* Surfaced rather than silently dropped: the previous root component
destructured logError and never rendered it. */}
{logError && (
<p role="status" className="text-warning text-sm">
{logError}
</p>
)}
</div>
<div className="flex flex-col gap-4">
<EmotionDisplay emotion={currentEmotion} faceDetected={faceDetected} />
<StatsPanel subscribe={subscribe} fps={fps} />
<Card padding="sm">
<CardHeader className="mb-2">
<CardTitle className="text-sm">Last entry</CardTitle>
</CardHeader>
{lastLog ? (
<div className="flex flex-col gap-1">
<p className="text-content-primary text-sm font-medium">{lastLog.dominant_emotion}</p>
<p className="text-content-muted text-xs">
{formatDuration(lastLog.duration_seconds)} · {formatTime(lastLog.queued_at)}
</p>
</div>
) : (
<p className="text-content-muted text-xs">Nothing logged yet.</p>
)}
</Card>
{settings.showTerminal && <TelemetryConsole lines={lines} />}
</div>
</div>
);
}