mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
fix(web): clarify settings heading hierarchy (#1177)
This commit is contained in:
@@ -132,9 +132,9 @@ export function ComposerToolbarLayoutControl() {
|
||||
const selectedIndex = selectedItem ? selectedItems.indexOf(selectedItem) : -1
|
||||
|
||||
return (
|
||||
<div className="border-t border-[var(--app-divider)] px-3 py-3">
|
||||
<div className="mb-3">
|
||||
<h3 className="text-[var(--app-fg)]">{t('settings.chat.composerToolbar.title')}</h3>
|
||||
<div className="border-t border-[var(--app-divider)] py-3">
|
||||
<div className="mb-3 px-3">
|
||||
<h3 className="text-sm font-medium text-[var(--app-fg)]">{t('settings.chat.composerToolbar.title')}</h3>
|
||||
<p className="mt-0.5 text-xs text-[var(--app-hint)]">{t('settings.chat.composerToolbar.description')}</p>
|
||||
</div>
|
||||
<SettingsChoiceGroup
|
||||
@@ -152,13 +152,13 @@ export function ComposerToolbarLayoutControl() {
|
||||
|
||||
<div className="mt-3 flex items-center justify-between gap-3 px-3">
|
||||
<div>
|
||||
<h4 className="text-[var(--app-fg)]">{t('settings.chat.composerToolbar.order')}</h4>
|
||||
<h4 className="text-sm font-medium text-[var(--app-fg)]">{t('settings.chat.composerToolbar.order')}</h4>
|
||||
<p className="mt-0.5 text-xs text-[var(--app-hint)]">{t('settings.chat.composerToolbar.previewHint')}</p>
|
||||
</div>
|
||||
<button type="button" onClick={resetLayout} className="shrink-0 text-sm text-[var(--app-link)] hover:underline">{t('settings.chat.composerToolbar.reset')}</button>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 rounded-[24px] bg-[var(--app-composer-bg,var(--app-subtle-bg))] px-3 pb-2 pt-3 shadow-sm ring-1 ring-[var(--app-border)]">
|
||||
<div className="mx-3 mt-2 rounded-[24px] bg-[var(--app-composer-bg,var(--app-subtle-bg))] px-3 pb-2 pt-3 shadow-sm ring-1 ring-[var(--app-border)]">
|
||||
<div className="mb-2 px-1 text-sm text-[var(--app-hint)]">{t('misc.typeAMessage')}</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="min-w-0 flex-1 overflow-x-auto">
|
||||
@@ -174,7 +174,7 @@ export function ComposerToolbarLayoutControl() {
|
||||
</div>
|
||||
</div>
|
||||
{selectedItem && selectedIndex >= 0 ? (
|
||||
<div className="mt-2 flex items-center justify-between gap-2 rounded-lg bg-[var(--app-subtle-bg)] px-3 py-2 text-sm">
|
||||
<div className="mx-3 mt-2 flex items-center justify-between gap-2 rounded-lg bg-[var(--app-subtle-bg)] px-3 py-2 text-sm">
|
||||
<span className="min-w-0 truncate text-[var(--app-hint)]">{t(ITEM_LABEL_KEYS[selectedItem])}</span>
|
||||
<span className="flex shrink-0 items-center gap-1">
|
||||
<button
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
export function SettingsFieldLabel(props: { children: ReactNode; hidden?: boolean }) {
|
||||
return props.hidden ? null : <div className="mb-2 text-sm font-medium text-[var(--app-fg)]">{props.children}</div>
|
||||
}
|
||||
|
||||
export function ChevronRightIcon(props: { className?: string }) {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className={props.className} aria-hidden="true">
|
||||
@@ -16,12 +20,11 @@ export function CheckIcon(props: { className?: string }) {
|
||||
)
|
||||
}
|
||||
|
||||
export function SettingsPageContent(props: { title: string; description?: string; children: ReactNode }) {
|
||||
export function SettingsPageContent(props: { description?: string; children: ReactNode }) {
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-[720px] space-y-5 px-3 py-4 lg:px-6 lg:py-6">
|
||||
<div>
|
||||
<h1 tabIndex={-1} className="hidden text-xl font-semibold text-[var(--app-fg)] outline-none lg:block">{props.title}</h1>
|
||||
{props.description ? <p className="text-sm text-[var(--app-hint)] lg:mt-1">{props.description}</p> : null}
|
||||
{props.description ? <p className="text-sm text-[var(--app-hint)]">{props.description}</p> : null}
|
||||
</div>
|
||||
{props.children}
|
||||
</div>
|
||||
@@ -31,8 +34,8 @@ export function SettingsPageContent(props: { title: string; description?: string
|
||||
export function SettingsSection(props: { title?: string; description?: string; children: ReactNode }) {
|
||||
return (
|
||||
<section>
|
||||
{props.title ? <h2 className="mb-1.5 px-1 text-xs font-semibold uppercase tracking-wide text-[var(--app-hint)]">{props.title}</h2> : null}
|
||||
{props.description ? <p className="mb-2 px-1 text-sm text-[var(--app-hint)]">{props.description}</p> : null}
|
||||
{props.title ? <h2 className="mb-2 text-base font-semibold text-[var(--app-fg)]">{props.title}</h2> : null}
|
||||
{props.description ? <p className="mb-2 text-sm text-[var(--app-hint)]">{props.description}</p> : null}
|
||||
<div className="overflow-hidden rounded-xl border border-[var(--app-border)] bg-[var(--app-bg)] shadow-sm divide-y divide-[var(--app-divider)]">
|
||||
{props.children}
|
||||
</div>
|
||||
@@ -44,7 +47,7 @@ export function SettingsRow(props: { label: string; description?: string; traili
|
||||
return (
|
||||
<div className="flex min-h-12 items-center justify-between gap-3 px-3 py-3">
|
||||
<div className="min-w-0">
|
||||
<div className="text-[var(--app-fg)]">{props.label}</div>
|
||||
<div className="text-sm font-medium text-[var(--app-fg)]">{props.label}</div>
|
||||
{props.description ? <div className="mt-0.5 text-xs leading-snug text-[var(--app-hint)]">{props.description}</div> : null}
|
||||
{props.children}
|
||||
</div>
|
||||
@@ -67,6 +70,7 @@ export function SettingsSwitch(props: { label: string; description?: string; che
|
||||
|
||||
export function SettingsChoiceGroup<T extends string | number>(props: {
|
||||
label: string
|
||||
hideLabel?: boolean
|
||||
value: T
|
||||
options: ReadonlyArray<{ value: T; label: string; description?: string }>
|
||||
onChange: (value: T) => void
|
||||
@@ -75,7 +79,7 @@ export function SettingsChoiceGroup<T extends string | number>(props: {
|
||||
const columns = props.columns === 5 ? 'grid-cols-5' : props.columns === 4 ? 'grid-cols-2 sm:grid-cols-4' : 'grid-cols-2'
|
||||
return (
|
||||
<div className="px-3 py-3">
|
||||
<div className="mb-2 text-[var(--app-fg)]">{props.label}</div>
|
||||
<SettingsFieldLabel hidden={props.hideLabel}>{props.label}</SettingsFieldLabel>
|
||||
<div role="radiogroup" aria-label={props.label} className={`grid ${columns} gap-2`}>
|
||||
{props.options.map((option) => {
|
||||
const selected = props.value === option.value
|
||||
@@ -104,7 +108,7 @@ export function SettingsLinkRow(props: { label: string; value?: string; descript
|
||||
return (
|
||||
<button type="button" onClick={props.onClick} className="flex min-h-12 w-full items-center gap-3 px-3 py-3 text-left transition-colors hover:bg-[var(--app-subtle-bg)]">
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block text-[var(--app-fg)]">{props.label}</span>
|
||||
<span className="block text-sm font-medium text-[var(--app-fg)]">{props.label}</span>
|
||||
{props.description ? <span className="mt-0.5 block text-xs text-[var(--app-hint)]">{props.description}</span> : null}
|
||||
</span>
|
||||
{props.value ? <span className="max-w-[45%] truncate text-sm text-[var(--app-hint)]">{props.value}</span> : null}
|
||||
|
||||
@@ -69,7 +69,7 @@ export function VoiceRespondsControls(props: {
|
||||
|
||||
return (
|
||||
<div className="border-t border-[var(--app-divider)] px-3 py-3">
|
||||
<p className="mb-2 text-[var(--app-fg)]">{props.t('settings.voice.responseLength.label')}</p>
|
||||
<p className="mb-2 text-sm font-medium text-[var(--app-fg)]">{props.t('settings.voice.responseLength.label')}</p>
|
||||
<div className="flex gap-2">
|
||||
{RESPONSE_LENGTH_OPTIONS.map((opt) => (
|
||||
<button key={opt} type="button" onClick={() => setResponseLength(opt)}
|
||||
@@ -119,7 +119,7 @@ export function VoicePersonaControls(props: {
|
||||
<button type="button" onClick={() => setIdentityOpen((v) => !v)}
|
||||
className="flex w-full items-center justify-between border-t border-[var(--app-divider)] px-3 py-3 text-left transition-colors hover:bg-[var(--app-subtle-bg)]"
|
||||
aria-expanded={identityOpen}>
|
||||
<span className="text-[var(--app-fg)]">{props.t('settings.voice.identity.title')}</span>
|
||||
<span className="text-sm font-medium text-[var(--app-fg)]">{props.t('settings.voice.identity.title')}</span>
|
||||
<ChevronDownIcon className={`shrink-0 transition-transform ${identityOpen ? 'rotate-180' : ''}`} />
|
||||
</button>
|
||||
{identityOpen && (
|
||||
@@ -145,7 +145,7 @@ export function VoicePersonaControls(props: {
|
||||
<button type="button" onClick={() => setCharacterOpen((v) => !v)}
|
||||
className="flex w-full items-center justify-between border-t border-[var(--app-divider)] px-3 py-3 text-left transition-colors hover:bg-[var(--app-subtle-bg)]"
|
||||
aria-expanded={characterOpen}>
|
||||
<span className="text-[var(--app-fg)]">{props.t('settings.voice.character.promptTitle')}</span>
|
||||
<span className="text-sm font-medium text-[var(--app-fg)]">{props.t('settings.voice.character.promptTitle')}</span>
|
||||
<ChevronDownIcon className={`shrink-0 transition-transform ${characterOpen ? 'rotate-180' : ''}`} />
|
||||
</button>
|
||||
{characterOpen && (
|
||||
@@ -178,7 +178,7 @@ export function VoicePersonaControls(props: {
|
||||
<button type="button" onClick={() => setDeliveryOpen((v) => !v)}
|
||||
className="flex w-full items-center justify-between border-t border-[var(--app-divider)] px-3 py-3 text-left transition-colors hover:bg-[var(--app-subtle-bg)]"
|
||||
aria-expanded={deliveryOpen}>
|
||||
<span className="flex items-center gap-2 text-[var(--app-fg)]">
|
||||
<span className="flex items-center gap-2 text-sm font-medium text-[var(--app-fg)]">
|
||||
{props.t('settings.voice.character.title')}
|
||||
{!usingDefaults && (
|
||||
<span className="rounded-full bg-[var(--app-link)]/15 px-2 py-0.5 text-[10px] text-[var(--app-link)]">
|
||||
@@ -239,7 +239,7 @@ export function VoiceSoundsControls(props: {
|
||||
<button type="button" onClick={() => setTuningOpen((v) => !v)}
|
||||
className="flex w-full items-center justify-between border-t border-[var(--app-divider)] px-3 py-3 text-left transition-colors hover:bg-[var(--app-subtle-bg)]"
|
||||
aria-expanded={tuningOpen}>
|
||||
<span className="text-[var(--app-fg)]">{props.t('settings.voice.tuning.title')}</span>
|
||||
<span className="text-sm font-medium text-[var(--app-fg)]">{props.t('settings.voice.tuning.title')}</span>
|
||||
<ChevronDownIcon className={`shrink-0 transition-transform ${tuningOpen ? 'rotate-180' : ''}`} />
|
||||
</button>
|
||||
{tuningOpen && (
|
||||
@@ -341,7 +341,7 @@ export function VoiceDiagnosticsControls(props: {
|
||||
<button type="button" onClick={() => setFixturesOpen((v) => !v)}
|
||||
className="flex w-full items-center justify-between border-t border-[var(--app-divider)] px-3 py-3 text-left transition-colors hover:bg-[var(--app-subtle-bg)]"
|
||||
aria-expanded={fixturesOpen}>
|
||||
<span className="text-[var(--app-fg)]">{props.t('settings.voice.fixtures.title')}</span>
|
||||
<span className="text-sm font-medium text-[var(--app-fg)]">{props.t('settings.voice.fixtures.title')}</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"
|
||||
fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"
|
||||
className={`shrink-0 transition-transform ${fixturesOpen ? 'rotate-180' : ''}`}>
|
||||
|
||||
@@ -606,6 +606,7 @@ export default {
|
||||
'settings.display.typography': 'Typography',
|
||||
'settings.display.sessions': 'Session list',
|
||||
'settings.display.appearance': 'Appearance',
|
||||
'settings.display.appearanceMode': 'Appearance mode',
|
||||
'settings.display.appearance.system': 'Follow System',
|
||||
'settings.display.appearance.dark': 'Dark',
|
||||
'settings.display.appearance.oled': 'OLED Black',
|
||||
|
||||
@@ -610,6 +610,7 @@ export default {
|
||||
'settings.display.typography': '字体',
|
||||
'settings.display.sessions': '会话列表',
|
||||
'settings.display.appearance': '外观',
|
||||
'settings.display.appearanceMode': '显示模式',
|
||||
'settings.display.appearance.system': '跟随系统',
|
||||
'settings.display.appearance.dark': '深色',
|
||||
'settings.display.appearance.oled': 'OLED 纯黑',
|
||||
|
||||
@@ -5,7 +5,7 @@ import { SettingsPageContent, SettingsRow, SettingsSection } from '@/components/
|
||||
export default function SettingsAboutPage() {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<SettingsPageContent title={t('settings.about.title')} description={t('settings.about.description')}>
|
||||
<SettingsPageContent description={t('settings.about.description')}>
|
||||
<SettingsSection>
|
||||
<SettingsRow label={t('settings.about.website')} trailing={
|
||||
<a href="https://hapi.run" target="_blank" rel="noopener noreferrer" className="text-[var(--app-link)] hover:underline">hapi.run</a>
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
type ChatSurfaceColorPreference,
|
||||
type ChatSurfaceColorPreset,
|
||||
} from '@/hooks/useChatSurfaceColors'
|
||||
import { SettingsChoiceGroup, SettingsPageContent, SettingsSection } from '@/components/settings/SettingsPrimitives'
|
||||
import { SettingsChoiceGroup, SettingsFieldLabel, SettingsPageContent, SettingsSection } from '@/components/settings/SettingsPrimitives'
|
||||
import { ComposerToolbarLayoutControl } from '@/components/settings/ComposerToolbarLayoutControl'
|
||||
|
||||
function ChatSurfaceColorControl(props: {
|
||||
@@ -23,7 +23,7 @@ function ChatSurfaceColorControl(props: {
|
||||
const pickerValue = getChatSurfaceColorPickerValue(props.preference)
|
||||
return (
|
||||
<div className="px-3 py-3">
|
||||
<div className="mb-2 text-[var(--app-fg)]">{props.label}</div>
|
||||
<SettingsFieldLabel>{props.label}</SettingsFieldLabel>
|
||||
<div role="radiogroup" aria-label={props.label} className="grid grid-cols-2 gap-2 sm:grid-cols-4">
|
||||
{getChatSurfaceColorPresetOptions().map((option) => {
|
||||
const preference = toPresetChatSurfaceColorPreference(option.value)
|
||||
@@ -50,7 +50,7 @@ export default function SettingsChatPage() {
|
||||
const { terminalToolDisplayMode, setTerminalToolDisplayMode } = useTerminalToolDisplayMode()
|
||||
const { toolGroupBackground, userMessageBackground, setToolGroupBackground, setUserMessageBackground } = useChatSurfaceColors()
|
||||
return (
|
||||
<SettingsPageContent title={t('settings.chat.title')} description={t('settings.chat.description')}>
|
||||
<SettingsPageContent description={t('settings.chat.description')}>
|
||||
<SettingsSection title={t('settings.chat.input')}>
|
||||
<SettingsChoiceGroup
|
||||
label={t('settings.chat.enterBehavior')}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { getSessionListStatusModeOptions, useSessionListStatusMode } from '@/hoo
|
||||
import { useShowActiveSessionsOnly } from '@/hooks/useShowActiveSessionsOnly'
|
||||
import { MAX_SESSION_PREVIEW_LIMIT, MIN_SESSION_PREVIEW_LIMIT, normalizeSessionPreviewLimit, useSessionPreviewLimit } from '@/hooks/useSessionPreviewLimit'
|
||||
import { useThemeColors, type ThemeColorKeyId } from '@/hooks/useThemeColors'
|
||||
import { SettingsChoiceGroup, SettingsPageContent, SettingsRow, SettingsSection, SettingsSwitch } from '@/components/settings/SettingsPrimitives'
|
||||
import { SettingsChoiceGroup, SettingsFieldLabel, SettingsPageContent, SettingsRow, SettingsSection, SettingsSwitch } from '@/components/settings/SettingsPrimitives'
|
||||
|
||||
function MinusIcon() {
|
||||
return <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="h-4 w-4" aria-hidden="true"><path d="M5 12h14" /></svg>
|
||||
@@ -24,7 +24,7 @@ function ColorThemePicker() {
|
||||
|
||||
return (
|
||||
<div className="px-3 py-3">
|
||||
<div className="mb-2 text-[var(--app-fg)]">{t('settings.display.colorTheme')}</div>
|
||||
<SettingsFieldLabel>{t('settings.display.colorTheme')}</SettingsFieldLabel>
|
||||
<div role="radiogroup" aria-label={t('settings.display.colorTheme')} className="grid grid-cols-2 gap-2 sm:grid-cols-4">
|
||||
{getColorThemeOptions().map((option) => (
|
||||
<ColorThemeOption
|
||||
@@ -105,7 +105,7 @@ function ThemeColorControls() {
|
||||
<details open={hasAnyCustom} className="group">
|
||||
<summary className="flex cursor-pointer list-none items-center justify-between px-3 py-3 text-[var(--app-fg)] hover:bg-[var(--app-subtle-bg)]">
|
||||
<span>
|
||||
<span className="block">{t('settings.display.themeColors.title')}</span>
|
||||
<span className="block text-sm font-medium">{t('settings.display.themeColors.title')}</span>
|
||||
<span className="mt-0.5 block text-xs text-[var(--app-hint)]">{t('settings.display.themeColors.description')}</span>
|
||||
</span>
|
||||
<span className="ml-3 text-sm text-[var(--app-hint)]">{hasAnyCustom ? t('settings.voice.advanced.customizedBadge') : t('settings.display.themeColors.expand')}</span>
|
||||
@@ -137,10 +137,10 @@ export default function SettingsDisplayPage() {
|
||||
const { showActiveSessionsOnly, setShowActiveSessionsOnly } = useShowActiveSessionsOnly()
|
||||
|
||||
return (
|
||||
<SettingsPageContent title={t('settings.display.title')} description={t('settings.display.description')}>
|
||||
<SettingsPageContent description={t('settings.display.description')}>
|
||||
<SettingsSection title={t('settings.display.appearance')}>
|
||||
<SettingsChoiceGroup
|
||||
label={t('settings.display.appearance')}
|
||||
label={t('settings.display.appearanceMode')}
|
||||
value={appearance}
|
||||
columns={4}
|
||||
options={getAppearanceOptions().map((option) => ({ value: option.value, label: t(option.labelKey) }))}
|
||||
|
||||
@@ -9,9 +9,9 @@ const locales: ReadonlyArray<{ value: Locale; label: string }> = [
|
||||
export default function SettingsGeneralPage() {
|
||||
const { t, locale, setLocale } = useTranslation()
|
||||
return (
|
||||
<SettingsPageContent title={t('settings.general.title')} description={t('settings.general.description')}>
|
||||
<SettingsSection>
|
||||
<SettingsChoiceGroup label={t('settings.language.label')} value={locale} options={locales} onChange={setLocale} />
|
||||
<SettingsPageContent description={t('settings.general.description')}>
|
||||
<SettingsSection title={t('settings.language.label')}>
|
||||
<SettingsChoiceGroup hideLabel label={t('settings.language.label')} value={locale} options={locales} onChange={setLocale} />
|
||||
</SettingsSection>
|
||||
</SettingsPageContent>
|
||||
)
|
||||
|
||||
@@ -36,8 +36,8 @@ export default function SettingsLayout() {
|
||||
<BackIcon />
|
||||
</button>
|
||||
<div className="min-w-0 flex-1 font-semibold">
|
||||
<h1 className="truncate lg:hidden">{mobileTitle}</h1>
|
||||
<span className="hidden lg:inline">{t('settings.title')}</span>
|
||||
<h1 className="truncate text-lg lg:hidden">{mobileTitle}</h1>
|
||||
<h1 className="hidden text-lg lg:block">{t('settings.title')}</h1>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
@@ -47,7 +47,7 @@ export default function SettingsLayout() {
|
||||
<aside className="hidden w-56 shrink-0 border-r border-[var(--app-border)] lg:block">
|
||||
<SettingsNav activeId={category?.id ?? 'display'} />
|
||||
</aside>
|
||||
<main className="app-scroll-y min-w-0 flex-1">
|
||||
<main className="app-scroll-y min-w-0 flex-1 lg:[scrollbar-gutter:stable_both-edges]">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@ export default function SettingsVoiceAdvancedPage() {
|
||||
const { t } = useTranslation()
|
||||
const { backend } = useVoiceSettings()
|
||||
return (
|
||||
<SettingsPageContent title={t('settings.voice.advanced.title')} description={t('settings.voice.advanced.hint')}>
|
||||
<SettingsPageContent description={t('settings.voice.advanced.hint')}>
|
||||
<SettingsSection title={t('settings.voice.persona.title')}>
|
||||
<VoicePersonaControls t={t} voiceBackend={backend} />
|
||||
</SettingsSection>
|
||||
|
||||
@@ -14,7 +14,7 @@ export default function SettingsVoiceVoicesPage() {
|
||||
const { t } = useTranslation()
|
||||
const voice = useVoiceSettings()
|
||||
return (
|
||||
<SettingsPageContent title={t('settings.voice.voice')} description={t('settings.voice.voices.description')}>
|
||||
<SettingsPageContent description={t('settings.voice.voices.description')}>
|
||||
<SettingsSection>
|
||||
<div role="radiogroup" aria-label={t('settings.voice.voice')} className="divide-y divide-[var(--app-divider)]">
|
||||
<button type="button" role="radio" aria-checked={voice.voiceId === null} onClick={() => voice.setVoice(null)} className={`flex min-h-12 w-full items-center justify-between px-3 py-3 text-left ${voice.voiceId === null ? 'bg-[var(--app-subtle-bg)] text-[var(--app-link)]' : 'text-[var(--app-fg)] hover:bg-[var(--app-subtle-bg)]'}`}>
|
||||
|
||||
@@ -22,7 +22,7 @@ export default function SettingsVoicePage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsPageContent title={t('settings.voice.title')} description={t('settings.voice.description')}>
|
||||
<SettingsPageContent description={t('settings.voice.description')}>
|
||||
<SettingsSection title={t('settings.voice.connection.title')} description={t('settings.voice.group.hint')}>
|
||||
{voice.configuredBackends.length > 1 && voice.backend ? (
|
||||
<SettingsChoiceGroup
|
||||
@@ -33,7 +33,7 @@ export default function SettingsVoicePage() {
|
||||
/>
|
||||
) : null}
|
||||
<label className="flex min-h-12 items-center justify-between gap-3 px-3 py-3">
|
||||
<span className="text-[var(--app-fg)]">{t('settings.voice.language')}</span>
|
||||
<span className="text-sm font-medium text-[var(--app-fg)]">{t('settings.voice.language')}</span>
|
||||
<select
|
||||
value={voice.voiceLanguage ?? ''}
|
||||
onChange={(event) => {
|
||||
|
||||
Reference in New Issue
Block a user