Spaces:
Sleeping
Sleeping
File size: 4,366 Bytes
375924d ce51e88 375924d 690c106 375924d 948a968 375924d ce51e88 375924d fe675d0 375924d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 | import { useState, useCallback, useEffect, useRef } from "react";
import type { Persona, Affect, ChatMessage, LatencyLog } from "./types";
import { resetSession, checkHealth } from "./lib/api";
import { useWebcam } from "./hooks/useWebcam";
import { useSensing } from "./hooks/useSensing";
import { PersonaSelector } from "./components/PersonaSelector";
import { ChatPanel } from "./components/ChatPanel";
import { WebcamSensing } from "./components/WebcamSensing";
import { SensingStatus } from "./components/SensingStatus";
import { LatencyMetrics } from "./components/LatencyMetrics";
import "./App.css";
function App() {
const [persona, setPersona] = useState<Persona | null>(null);
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [latency, setLatency] = useState<LatencyLog | null>(null);
const [webcamEnabled, setWebcamEnabled] = useState(false);
const [affectOverride, setAffectOverride] = useState<Affect | null>(null);
const [backendReady, setBackendReady] = useState(false);
const healthPoll = useRef<ReturnType<typeof setInterval>>(undefined);
useEffect(() => {
async function poll() {
const ready = await checkHealth();
if (ready) {
setBackendReady(true);
clearInterval(healthPoll.current);
}
}
poll();
healthPoll.current = setInterval(poll, 2000);
return () => clearInterval(healthPoll.current);
}, []);
const {
sensing,
ready,
initError,
init,
processFrame,
clearAirWrittenText,
clearHeadSignal,
resetCalibration,
} = useSensing();
const onFrame = useCallback(
(video: HTMLVideoElement, timestamp: number) => {
processFrame(video, timestamp);
},
[processFrame]
);
const { videoRef, active, error } = useWebcam({
enabled: webcamEnabled && ready,
onFrame,
});
async function handleWebcamToggle() {
if (!webcamEnabled) {
const ok = await init();
if (ok) setWebcamEnabled(true);
} else {
setWebcamEnabled(false);
resetCalibration();
}
}
async function handlePersonaSelect(p: Persona) {
setPersona(p);
setMessages([]);
setLatency(null);
try {
await resetSession(p.id);
} catch {
// Session reset failed — non-critical, continue with fresh UI state
}
}
return (
<div className="app-layout">
<aside className="sidebar">
<h1 className="app-title">
<img src="/favicon.svg" alt="" className="app-logo" />
AAC Chatbot
</h1>
<PersonaSelector
selected={persona?.id ?? null}
onSelect={handlePersonaSelect}
/>
<div className="sidebar-section">
<label className="toggle-label">
<input
type="checkbox"
checked={webcamEnabled}
onChange={handleWebcamToggle}
/>
Enable webcam
</label>
<WebcamSensing videoRef={videoRef} active={active} error={error || initError} />
<SensingStatus sensing={sensing} webcamActive={active} />
</div>
<div className="sidebar-section">
<label htmlFor="affect-override">Affect override</label>
<select
id="affect-override"
value={affectOverride ?? "auto"}
onChange={(e) =>
setAffectOverride(
e.target.value === "auto" ? null : (e.target.value as Affect)
)
}
>
<option value="auto">Auto (webcam)</option>
<option value="HAPPY">HAPPY</option>
<option value="FRUSTRATED">FRUSTRATED</option>
<option value="NEUTRAL">NEUTRAL</option>
<option value="SURPRISED">SURPRISED</option>
</select>
</div>
<LatencyMetrics latency={latency} />
</aside>
<main className="main-content">
<ChatPanel
userId={persona?.id ?? null}
personaName={persona?.name ?? ""}
sensing={sensing}
affectOverride={affectOverride}
onAirTextConsumed={clearAirWrittenText}
onHeadSignalConsumed={clearHeadSignal}
messages={messages}
setMessages={setMessages}
onLatency={setLatency}
backendReady={backendReady}
/>
</main>
</div>
);
}
export default App;
|