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;