import { useState, useRef } from "react" import api from "../lib/api" const G = "linear-gradient(135deg, #1d4ed8 0%, #0891b2 100%)" const MIN_SECONDS = 20 const MAX_SECONDS = 60 const ROUND_CONFIG = [ { type: "scripted", badge: "Calm baseline", description: "Read each sentence aloud at a comfortable, natural pace.", tip: "Speak clearly — this establishes your baseline voice sample.", content: [ "I usually start my mornings by checking my messages and planning what I need to get done.", "In meetings, I try to listen carefully before sharing my perspective on the topic.", "One thing I've noticed about myself is that I tend to think through problems out loud.", "I find it easier to explain complex ideas when I break them down step by step.", "At the end of the day, I like to reflect on what went well and what I could improve.", ], }, { type: "freeform", badge: "Natural conversation", description: "Talk freely about the prompt below. No script — just speak as you normally would.", tip: "Pretend you're catching up with a friend. Natural pace, natural words.", content: "Tell me about something you did or experienced recently — a conversation, a meeting, a trip, or just something that happened. Explain it as you'd tell a friend, with as much detail as you like.", }, { type: "expressive", badge: "Expressive speech", description: "Share a genuine opinion or give advice. Let your natural energy come through.", tip: "This captures your voice when engaged or passionate — the most important style for accurate recognition.", content: "Talk about something you strongly believe in, disagree with, or find exciting. It could be advice you'd give someone, an opinion you hold, or a recommendation. Don't hold back — speak the way you do when you actually care about what you're saying.", }, ] export default function EnrollView({ onEnrolled, onSkip }) { const [state, setState] = useState("idle") const [round, setRound] = useState(1) const [blobs, setBlobs] = useState([]) const [elapsed, setElapsed] = useState(0) const [error, setError] = useState("") const mediaRef = useRef(null) const chunksRef = useRef([]) const timerRef = useRef(null) const roundConfig = ROUND_CONFIG[round - 1] const startRecording = async () => { setError("") try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }) const recorder = new MediaRecorder(stream) mediaRef.current = { recorder, stream } chunksRef.current = [] recorder.ondataavailable = (e) => { if (e.data.size > 0) chunksRef.current.push(e.data) } recorder.onstop = () => { stream.getTracks().forEach(t => t.stop()) clearInterval(timerRef.current) const blob = new Blob(chunksRef.current, { type: "audio/webm" }) setBlobs(prev => [...prev, blob]) setState("round_done") } recorder.start() setState("recording") setElapsed(0) timerRef.current = setInterval(() => { setElapsed(prev => { if (prev + 1 >= MAX_SECONDS) { stopRecording(); return MAX_SECONDS } return prev + 1 }) }, 1000) } catch { setError("Microphone access denied. Please allow microphone access and try again.") } } const stopRecording = () => { const { recorder } = mediaRef.current || {} if (recorder && recorder.state !== "inactive") recorder.stop() clearInterval(timerRef.current) } const continueToNext = () => { setRound(r => r + 1); setElapsed(0); setState("idle") } const submitAll = async (allBlobs) => { setState("uploading") try { const form = new FormData() allBlobs.forEach((blob, i) => form.append(`audio${i + 1}`, blob, `enrollment_${i + 1}.webm`)) await api.post("/api/enroll", form, { headers: { "Content-Type": "multipart/form-data" } }) setState("complete") } catch (e) { setError(e.response?.data?.detail || "Enrollment failed. Please try again.") setState("idle"); setRound(1); setBlobs([]) } } const handleRoundDone = () => round < 3 ? continueToNext() : submitAll(blobs) if (state === "complete") { return (
We recorded 3 rounds of your voice to build a robust voiceprint.
You'll now be automatically recognized in future conversations.
Round {round} of 3 — {roundConfig.description}
{sentence}
{roundConfig.content}
)}{roundConfig.tip}
Click Start recording, then read the sentences above clearly.
> )} {state === "recording" && ( <>{canStop ? "✓ You can stop now" : `Keep speaking… ${MIN_SECONDS - elapsed}s more minimum`}
> )} {state === "round_done" && ( <>Round {round} done!
{round < 3 ? `${3 - round} more round${3 - round > 1 ? "s" : ""} to go.` : "All 3 rounds complete — submitting your voiceprint…"}
> )} {state === "uploading" && ( <>Processing all 3 rounds…
> )}