import { cancelEnvironmentSpeechModelDownload, downloadEnvironmentSpeechModel, getEnvironmentSpeechModels, getEnvironmentSpeechStatus, linkEnvironmentSpeechModels, removeEnvironmentSpeechModel, selectEnvironmentSpeechModel, updateEnvironmentSpeechAcceleration, updateEnvironmentSpeechModelUnloadTimeout, } from "@t3tools/client-runtime/voice-input"; import type { PreparedConnection } from "@t3tools/client-runtime/connection"; import { openRouterIcon } from "@t3tools/client-runtime/provider-clients"; import { AuthFilesystemReadScope, AuthSettingsWriteScope, CLOUD_SPEECH_LABELS, CloudSpeechProvider, DEFAULT_SPEECH_POST_PROCESSING_PROMPT, type EnvironmentId, type EnvironmentSpeechModel, type EnvironmentSpeechStatus, type ProjectId, ProviderDriverKind, type SpeechAcceleration, type SpeechBenchmarkTier, type SpeechCustomWords, type SpeechModelUnloadTimeout, type VoiceShortcutMode, } from "@t3tools/contracts"; import { createModelSelection } from "@t3tools/shared/model"; import { resolveSpeechPostProcessingModelSelection } from "@t3tools/shared/serverSettings"; import { cloudSpeechLanguages } from "@t3tools/shared/speech"; import { AudioLinesIcon, CheckIcon, DownloadIcon, LanguagesIcon, KeyRoundIcon, MonitorIcon, MicOffIcon, MicIcon, RefreshCwIcon, StarIcon, SlidersHorizontalIcon, Trash2Icon, UnlinkIcon, XIcon, } from "lucide-react"; import { useEffect, useRef, useState, type ReactNode } from "react"; import { useStableCallback } from "~/hooks/useStableCallback"; import { listBrowserMicrophones, type BrowserMicrophone } from "../../speech/browserMicrophones"; import { preferredSpeechLanguages } from "../../speech/preferredSpeechLanguages"; import { chooseAudioDestination, chooseTranscriptCleanup, useCleanupGrant, } from "../../speech/voiceConsent"; import { updateClientSettings, useClientSettings } from "../../hooks/useSettings"; import { runEnvironmentSpeech } from "../../speech/runEnvironmentSpeech"; import { getCustomModelOptionsByInstance, resolveAppModelSelectionState, } from "../../modelSelection"; import { useTextGenerationTargets } from "../../textGenerators"; import { useEnvironmentProviders, useEnvironments } from "../../state/environments"; import { useEnvironmentScope } from "../../state/session"; import { EMPTY_SERVER_PROVIDERS } from "../../state/server"; import { useEnvironmentSpeechInput, useVoiceTranscriptionDestination, } from "../../speech/useEnvironmentSpeechInput"; import { ComposerSpeechRecordingPill } from "../chat/ComposerSpeechButton"; import { ProviderModelPicker } from "../chat/ProviderModelPicker"; import { ProviderInstanceIcon } from "../chat/ProviderInstanceIcon"; import { ProviderPackageIcon } from "../chat/ProviderPackageIcon"; import { TraitsPicker } from "../chat/TraitsPicker"; import { VoiceInputSetup } from "../chat/VoiceInputSetup"; import { Badge } from "../ui/badge"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; import { Button } from "../ui/button"; import { Input } from "../ui/input"; import { InputGroup, InputGroupAddon, InputGroupInput } from "../ui/input-group"; import { Menu, MenuCheckboxItem, MenuGroup, MenuGroupLabel, MenuPopup, MenuRadioGroup, MenuRadioItem, MenuRadioItemIndicator, MenuSeparator, MenuTrigger, } from "../ui/menu"; import { Select, SelectGroup, SelectGroupLabel, SelectItem, SelectPopup, SelectTrigger, SelectValue, } from "../ui/select"; import { Switch } from "../ui/switch"; import { Textarea } from "../ui/textarea"; import { toastManager } from "../ui/toast"; import { MicrophoneTest } from "./MicrophoneTest"; import { CloudVoiceConfiguration } from "./CloudVoiceConfiguration"; import { LanguageRow, ModelBenchmarks, orderSpeechLanguages } from "./SpeechModelComparison"; import { languageLabel, SpeechLanguageSelect } from "./SpeechLanguageSelect"; import { filterSpeechModels, type SpeechModelSort } from "./speechModelBrowser"; import { AdvancedSettings } from "./AdvancedSettings"; import { SETTINGS_PICKER_TRIGGER_CLASSNAME, SettingsPageContainer, SettingsRow, SettingsSearchTarget, SettingsSection, } from "./settingsLayout"; import { searchableSetting } from "./settingsSearch"; import { useOptionalSettingsScope, useSettingsScope } from "./SettingsScopeContext"; import { useScopedModelDisabledReason } from "./useScopedModelAvailability"; import { useScopedSettings, useUpdateScopedSettings } from "./useScopedSettings"; const OFF = "off"; function CloudSpeechLogo({ provider }: { provider: CloudSpeechProvider }) { if (provider === "openrouter") return ; return ( ); } const SYSTEM_DEFAULT = "system-default"; const formatSize = (bytes: number) => `${Math.round(bytes / 1024 / 1024)} MB`; const SHORTCUT_MODE_LABELS: Record = { auto: "Auto: press to toggle, hold to record", hold: "Hold: record while held", toggle: "Toggle: press to start, press again to stop", }; const UNLOAD_LABELS: Record = { never: "Never", immediately: "Immediately", min_2: "After 2 minutes", min_5: "After 5 minutes", min_10: "After 10 minutes", min_15: "After 15 minutes", hour_1: "After 1 hour", }; const normalizeTerm = (value: string) => value.replace(/\s+/g, " ").trim().normalize("NFC"); const spellingsOf = (words: SpeechCustomWords) => new Set( words.flatMap(({ term, aliases }) => [term, ...aliases].map((spelling) => spelling.toLocaleLowerCase()), ), ); type ModelCardProps = { readonly model: EnvironmentSpeechModel; readonly comparisonLanguages: readonly string[]; readonly benchmarkTier: SpeechBenchmarkTier; readonly busy: boolean; readonly canCancel: boolean; readonly onDownload: () => void; readonly onSelect: () => void; readonly onCancel: () => void; readonly onDelete: () => void; readonly testControls?: ReactNode; readonly children?: ReactNode; }; function ModelBadge(props: { icon: ReactNode; label: string; tooltip: string }) { return ( }> {props.icon} {props.label} {props.tooltip} ); } function ModelCard(props: ModelCardProps) { const { model } = props; const downloading = model.state === "downloading" || model.state === "verifying"; const progress = model.downloaded === undefined ? 0 : (model.downloaded / model.size) * 100; return (
{model.name} {model.recommended ? ( } label="Recommended" tooltip="A good default pick for the languages it covers." /> ) : null} {model.supportsStreaming ? ( } label="Live preview" tooltip="Shows words in your draft as you speak, before the recording finishes." /> ) : null} {model.supportsTranslation ? ( } label="Translate" tooltip="Can write English text from speech in another language. Turn on Translate to English to use it." /> ) : null}

{model.description}

{model.linkedFrom ? (

Using your file at {model.linkedFrom}

) : null} 1 ? [`${model.languages.length} languages`] : model.benchmark ? [] : [languageLabel(model.languages[0] ?? "")]), formatSize(model.size), ]} /> {downloading ? (

{model.state === "verifying" ? "Verifying download…" : `${Math.round(progress)}% downloaded`}

) : null}
{props.testControls} {downloading ? ( ) : model.state === "downloadable" ? ( ) : model.active ? ( ) : ( )} {model.state === "installed" ? ( ) : null}
{props.children}
); } function DictionaryEditor(props: { readonly label: string; readonly words: SpeechCustomWords; readonly disabled?: boolean; readonly onChange: (words: SpeechCustomWords) => void; }) { const { words, label, disabled = false, onChange } = props; const [termDraft, setTermDraft] = useState(""); const [aliasDraft, setAliasDraft] = useState<{ term: string; value: string } | null>(null); const spellings = spellingsOf(words); const seen = new Set(spellings); const newTerms = termDraft .split(/[,\n]/) .map(normalizeTerm) .filter((term) => { const key = term.toLocaleLowerCase(); if (!key || seen.has(key)) return false; seen.add(key); return true; }); const canAddTerm = !disabled && newTerms.length > 0 && newTerms.every((term) => term.length <= 50) && words.length + newTerms.length <= 100; const addTerm = () => { if (!canAddTerm) return; onChange([...words, ...newTerms.map((term) => ({ term, aliases: [] }))]); setTermDraft(""); }; const addAlias = () => { if (!aliasDraft) return; const value = normalizeTerm(aliasDraft.value); if (!value || value.length > 50 || spellings.has(value.toLocaleLowerCase())) return; onChange( words.map((entry) => entry.term === aliasDraft.term && entry.aliases.length < 8 ? { term: entry.term, aliases: [...entry.aliases, value] } : entry, ), ); setAliasDraft(null); }; return ( <>