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 (

Voice enrolled

We recorded 3 rounds of your voice to build a robust voiceprint.
You'll now be automatically recognized in future conversations.

) } const progressPct = Math.min((elapsed / MAX_SECONDS) * 100, 100) const canStop = elapsed >= MIN_SECONDS return (
{/* Header */}

Train your voice

{[1, 2, 3].map(r => (
))}

Round {round} of 3 — {roundConfig.description}

{/* Round content */}
Round {round} {roundConfig.badge}
{roundConfig.type === "scripted" ? ( roundConfig.content.map((sentence, i) => (
{i + 1}

{sentence}

)) ) : (

{roundConfig.content}

)}

{roundConfig.tip}

{/* Recording area */}
{state === "idle" && ( <>
🎤

Click Start recording, then read the sentences above clearly.

)} {state === "recording" && ( <>
🔴
{elapsed}s

{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…

)}
{error && (
⚠️ {error}
)} {state === "idle" && ( )} {state === "recording" && ( )} {state === "round_done" && ( )} {(state === "idle" || state === "recording") && ( )}
) }