/** * — source + cleanup settings for voice input. * * Writes the selection to application_state under `voice-transcription-prefs` * so the composer's `useVoiceDictation` hook picks it up on next record. The * legacy `provider` field is still written alongside `transcriptionMode` so * older clients continue to normalize safely. * * Provider status comes from `/_agent-native/voice-providers/status`, which * mirrors the server transcription route's key/env resolution. */ import { Switch } from "@agent-native/toolkit/design-system"; import { IconAlertCircle, IconCheck, IconChevronDown, IconChevronRight, IconExternalLink, IconLoader2, IconLockOpen, } from "@tabler/icons-react"; import React, { useCallback, useEffect, useState } from "react"; import { agentNativePath } from "../api-path.js"; import { openBuilderConnectPopup, useBuilderStatus, } from "./useBuilderStatus.js"; type TranscriptionMode = "mac-native" | "google-realtime" | "batch"; type Provider = | "auto" | "openai" | "builder-gemini" | "builder" | "browser" | "gemini" | "groq"; interface Prefs { transcriptionMode?: TranscriptionMode; provider?: Provider; instructions?: string; } interface SecretStatus { key: string; status: "set" | "unset" | "invalid"; } interface ProviderStatus { builder: boolean; gemini: boolean; openai: boolean; groq: boolean; googleRealtime?: boolean; browser: true; native?: true; } const PREFS_URL = agentNativePath( "/_agent-native/application-state/voice-transcription-prefs", ); const CLEANUP_PREFS_URL = agentNativePath( "/_agent-native/application-state/voice-cleanup-prefs", ); const SECRETS_URL = agentNativePath("/_agent-native/secrets"); const PROVIDER_STATUS_URL = agentNativePath( "/_agent-native/voice-providers/status", ); const DEFAULT_TRANSCRIPTION_MODE: TranscriptionMode = "batch"; const DEFAULT_BATCH_PROVIDER: Provider = "auto"; function isProvider(value: unknown): value is Provider { return ( value === "auto" || value === "openai" || value === "builder-gemini" || value === "builder" || value === "browser" || value === "gemini" || value === "groq" ); } function isTranscriptionMode(value: unknown): value is TranscriptionMode { return ( value === "mac-native" || value === "google-realtime" || value === "batch" ); } function normalizeProvider(value: unknown): Provider | null { if (!isProvider(value)) return null; return value === "builder" ? "builder-gemini" : value; } function legacyModeFromProvider(provider: Provider | null): TranscriptionMode { if (provider === "browser") return "mac-native"; return "batch"; } function providerForMode( mode: TranscriptionMode, currentProvider: Provider | null, ): Provider { if (mode === "mac-native") return "browser"; if (mode === "google-realtime") return "auto"; if (!currentProvider || currentProvider === "browser") { return DEFAULT_BATCH_PROVIDER; } return currentProvider; } function batchProvider(provider: Provider | null): Provider { if (!provider || provider === "browser") return DEFAULT_BATCH_PROVIDER; return provider; } export function VoiceTranscriptionSection() { const [transcriptionMode, setTranscriptionMode] = useState(null); const [provider, setProvider] = useState(DEFAULT_BATCH_PROVIDER); const [instructions, setInstructions] = useState(""); const [openAiConfigured, setOpenAiConfigured] = useState( null, ); const [geminiConfigured, setGeminiConfigured] = useState( null, ); const [groqConfigured, setGroqConfigured] = useState(null); const [googleRealtimeConfigured, setGoogleRealtimeConfigured] = useState< boolean | null >(null); const [saving, setSaving] = useState(false); const [saveError, setSaveError] = useState(null); const [showAdvanced, setShowAdvanced] = useState(false); const [cleanupEnabled, setCleanupEnabled] = useState(null); const { status: builderStatus } = useBuilderStatus(); const builderRealtimeReady = !!builderStatus?.privateKeyConfigured && !!builderStatus?.publicKeyConfigured; const googleRealtimeReady = !!googleRealtimeConfigured && builderRealtimeReady; // Read cleanup pref (default: true if Builder is connected). useEffect(() => { let cancelled = false; fetch(CLEANUP_PREFS_URL) .then((r) => (r.ok ? r.json() : null)) .then( ( body: | { enabled?: boolean } | { value?: { enabled?: boolean } } | null, ) => { if (cancelled) return; const stored = (body as { enabled?: boolean } | null)?.enabled ?? (body as { value?: { enabled?: boolean } } | null)?.value?.enabled; if (typeof stored === "boolean") setCleanupEnabled(stored); else setCleanupEnabled(null); // resolve once builderStatus arrives }, ) .catch(() => !cancelled && setCleanupEnabled(null)); return () => { cancelled = true; }; }, []); useEffect(() => { if (cleanupEnabled !== null) return; if (builderStatus?.configured !== undefined) { setCleanupEnabled(!!builderStatus.configured); } }, [builderStatus?.configured, cleanupEnabled]); const toggleCleanup = async (next: boolean) => { const previous = cleanupEnabled; setCleanupEnabled(next); try { const res = await fetch(CLEANUP_PREFS_URL, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ enabled: next }), }); if (!res.ok) { throw new Error(`HTTP ${res.status}`); } } catch { setCleanupEnabled(previous); } }; useEffect(() => { let cancelled = false; fetch(PREFS_URL) .then((r) => (r.ok ? r.json() : null)) .then((body: Prefs | { value?: Prefs } | null) => { if (cancelled) return; const value = (body as { value?: Prefs } | null)?.value ?? (body as Prefs | null); const p = normalizeProvider( (body as Prefs | null)?.provider ?? (body as { value?: Prefs } | null)?.value?.provider, ); const storedMode = isTranscriptionMode(value?.transcriptionMode) ? value.transcriptionMode : null; const mode = storedMode ?? (p ? legacyModeFromProvider(p) : DEFAULT_TRANSCRIPTION_MODE); const savedInstructions = (body as Prefs | null)?.instructions ?? (body as { value?: Prefs } | null)?.value?.instructions; setTranscriptionMode(mode); setProvider(providerForMode(mode, p)); if (typeof savedInstructions === "string") { setInstructions(savedInstructions); } }) .catch(() => { if (!cancelled) { setTranscriptionMode(DEFAULT_TRANSCRIPTION_MODE); setProvider(DEFAULT_BATCH_PROVIDER); } }); return () => { cancelled = true; }; }, []); useEffect(() => { let cancelled = false; fetch(PROVIDER_STATUS_URL) .then((r) => (r.ok ? r.json() : null)) .then((status: ProviderStatus | null) => { if (cancelled) return; if (status) { setOpenAiConfigured(status.openai); setGeminiConfigured(status.gemini); setGroqConfigured(status.groq); setGoogleRealtimeConfigured(!!status.googleRealtime); return; } return fetch(SECRETS_URL) .then((r) => (r.ok ? r.json() : [])) .then((list: SecretStatus[]) => { if (cancelled) return; const find = (key: string) => Array.isArray(list) ? list.find((s) => s.key === key) : null; setOpenAiConfigured(find("OPENAI_API_KEY")?.status === "set"); setGeminiConfigured(find("GEMINI_API_KEY")?.status === "set"); setGroqConfigured(find("GROQ_API_KEY")?.status === "set"); setGoogleRealtimeConfigured( find("GOOGLE_APPLICATION_CREDENTIALS")?.status === "set", ); }); }) .catch(() => { if (!cancelled) { setOpenAiConfigured(false); setGeminiConfigured(false); setGroqConfigured(false); setGoogleRealtimeConfigured(false); } }); return () => { cancelled = true; }; }, []); const persist = useCallback( async ( nextMode: TranscriptionMode, nextProvider: Provider, nextInstructions: string, previous: { transcriptionMode: TranscriptionMode | null; provider: Provider; instructions: string; }, ) => { setSaving(true); setSaveError(null); try { const res = await fetch(PREFS_URL, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ transcriptionMode: nextMode, provider: nextProvider, instructions: nextInstructions.trim(), }), }); if (!res.ok) { throw new Error(`HTTP ${res.status}`); } } catch (err) { // Revert the optimistic update so the UI matches server state. setTranscriptionMode(previous.transcriptionMode); setProvider(previous.provider); setInstructions(previous.instructions); setSaveError( `Couldn't save: ${(err as Error)?.message ?? "network error"}. Try again.`, ); } finally { setSaving(false); } }, [], ); const focusKey = (key: string) => { if (typeof window === "undefined") return; window.location.hash = `#secrets:${key}`; }; const chooseSource = (next: TranscriptionMode) => { if (next === transcriptionMode) return; if (next === "google-realtime" && !googleRealtimeReady) { setShowAdvanced(true); if (!googleRealtimeConfigured) { focusKey("GOOGLE_APPLICATION_CREDENTIALS"); } else if (!builderRealtimeReady) { openBuilderConnect(); } return; } const previous = { transcriptionMode, provider, instructions }; const nextProvider = providerForMode(next, provider); setTranscriptionMode(next); setProvider(nextProvider); void persist(next, nextProvider, instructions, previous); }; const openBuilderConnect = () => { openBuilderConnectPopup({ url: builderStatus?.cliAuthUrl ?? builderStatus?.connectUrl, source: "voice_transcription_settings", features: "noopener,noreferrer,width=600,height=700", }); }; const chooseBatchProvider = (next: Provider) => { const nextProvider = batchProvider(normalizeProvider(next)); if (transcriptionMode === "batch" && nextProvider === provider) return; const previous = { transcriptionMode, provider, instructions }; setTranscriptionMode("batch"); setProvider(nextProvider); void persist("batch", nextProvider, instructions, previous); }; const updateInstructions = (next: string) => { const previous = { transcriptionMode, provider, instructions }; setInstructions(next); if (transcriptionMode) { void persist(transcriptionMode, provider, next, previous); } }; if (transcriptionMode === null) { return (
Loading…
); } return (
Live transcription

Choose where real-time words come from. Batch still runs after recording stops.

chooseSource("mac-native")} title="Mac Native" subtitle="Free and fast in the macOS Tauri app. Web clients use the existing browser-native path when available." rightSlot={ Tauri default } /> chooseSource("google-realtime")} disabled={!googleRealtimeReady} title="Google Realtime" subtitle={ googleRealtimeReady ? "BYOK only for v1. Streams live partials and finals through Google Speech-to-Text." : googleRealtimeConfigured ? "Google credentials are set. Connect Builder completely to mint the managed realtime session." : "BYOK only for v1. Configure Google service account before selecting this source." } rightSlot={ googleRealtimeReady ? ( Ready ) : googleRealtimeConfigured ? ( ) : ( ) } /> chooseSource("batch")} title="Batch" subtitle="Universal fallback. Sends audio after recording stops through Builder Gemini, Gemini, Groq, then OpenAI." />
AI cleanup

Polish punctuation, casing, filler words, titles, and summaries after capture. Builder Gemini is tried first; BYOK Gemini is the fallback.

{cleanupEnabled && ( {builderStatus?.configured ? "Builder ready" : geminiConfigured ? "Gemini key set" : "Needs key"} )}
{showAdvanced && (
chooseSource("google-realtime")} disabled={!googleRealtimeReady} title="Google Speech-to-Text service account" subtitle={ googleRealtimeConfigured ? "Service-account JSON is set. Connect Builder to mint the managed realtime WebSocket session." : "Service-account JSON for the dedicated realtime WebSocket to Google StreamingRecognize." } rightSlot={ googleRealtimeConfigured === null ? null : googleRealtimeReady ? ( Ready ) : googleRealtimeConfigured ? ( ) : ( ) } /> chooseBatchProvider("auto")} title="Automatic batch fallback" subtitle="Keep the current Clips fallback chain: Builder Gemini, Gemini, Groq, then OpenAI." /> chooseBatchProvider("builder-gemini")} disabled={!builderStatus?.configured} title="Builder.io Connect" subtitle={ builderStatus?.configured ? "Use Builder-hosted Gemini Flash-Lite for batch transcription and cleanup." : "One-click connect for Gemini Flash-Lite cleanup and batch transcription. No Google key needed." } rightSlot={ builderStatus?.configured ? ( Connected ) : ( ) } /> chooseBatchProvider("gemini")} title="Google Gemini" subtitle="BYOK Gemini for AI cleanup and optional strict batch transcription." rightSlot={ geminiConfigured === null ? null : geminiConfigured ? ( Key set ) : ( ) } /> chooseBatchProvider("openai")} title="OpenAI Whisper" subtitle="Batch Whisper provider. Requires an OpenAI API key." rightSlot={ openAiConfigured === null ? null : openAiConfigured ? ( Key set ) : ( ) } /> chooseBatchProvider("groq")} title="Groq Whisper" subtitle="Fast Whisper batch provider. Requires a Groq API key." rightSlot={ groqConfigured === null ? null : groqConfigured ? ( Key set ) : ( ) } />
)}
{(cleanupEnabled || transcriptionMode === "batch") && (