import { Trans } from "@lingui/react/macro"; import type { VoiceInfo } from "@rakazo/contracts"; import { Button } from "@rakazo/ui-web"; import { useEffect, useRef, useState } from "react"; import { defaultOnboardingVoiceId, offlineOnboardingVoices, onboardingVoicesFromList, } from "../../lib/onboarding-voices"; import { rpc } from "../../lib/rpc"; import type { UiLocale } from "../../lib/ui-locale"; /** Must match packages/adapters KokoroVoiceProvider placeholder. */ export const KOKORO_LOCAL_TOKEN = "local-kokoro"; type OnboardingVoiceStepProps = { uiLocale: UiLocale; selectedVoiceId: string; onSelectedVoiceIdChange: (voiceId: string) => void; onBack: () => void; onSkip: () => void; onContinue: () => void; }; export function OnboardingVoiceStep({ uiLocale, selectedVoiceId, onSelectedVoiceIdChange, onBack, onSkip, onContinue, }: OnboardingVoiceStepProps) { const [voiceOptions, setVoiceOptions] = useState([]); const [voiceLoading, setVoiceLoading] = useState(true); const previewTask = useRef>(Promise.resolve()); useEffect(() => { let cancelled = false; setVoiceLoading(true); const task = (async () => { try { await rpc.voice.connect({ provider: "kokoro", apiKey: KOKORO_LOCAL_TOKEN }); const listed = await rpc.voice.voices({ provider: "kokoro" }); if (cancelled) return; const ordered = onboardingVoicesFromList(listed, uiLocale); setVoiceOptions(ordered); const nextId = defaultOnboardingVoiceId(uiLocale, ordered); if (nextId) onSelectedVoiceIdChange(nextId); } catch { if (!cancelled) { const fallback = offlineOnboardingVoices(uiLocale); setVoiceOptions(fallback); const nextId = defaultOnboardingVoiceId(uiLocale, fallback); if (nextId) onSelectedVoiceIdChange(nextId); } } finally { if (!cancelled) setVoiceLoading(false); } })(); previewTask.current = task.then( () => undefined, () => undefined, ); return () => { cancelled = true; }; }, [onSelectedVoiceIdChange, uiLocale]); async function skipVoice() { await previewTask.current; // Listing voices persists a local credential. Skip must drop it so Voice // still opens settings instead of a call the user never chose. await rpc.voice.disconnect({ provider: "kokoro" }).catch(() => undefined); onSkip(); } return (

Pick a voice

How should your Oo sound when reading replies aloud?

{voiceLoading ? (

Loading voices…

) : voiceOptions.length > 0 ? (
{voiceOptions.map((voice) => ( ))}
) : (

Voice is not available yet. You can turn it on later in settings.

)}
); }