chore: hide voice button when no voice backend configured (not deployed)

This commit is contained in:
2026-08-02 20:28:10 +08:00
parent 42106c28b2
commit 2ff4ef0854
5 changed files with 31 additions and 17 deletions
+10 -2
View File
@@ -15,8 +15,8 @@ describe('listConfiguredVoiceBackends', () => {
expect(backends).toEqual(['elevenlabs', 'gemini-live', 'qwen-realtime']) expect(backends).toEqual(['elevenlabs', 'gemini-live', 'qwen-realtime'])
}) })
test('falls back to elevenlabs when no keys configured', () => { test('returns empty when no keys configured', () => {
expect(listConfiguredVoiceBackends({})).toEqual(['elevenlabs']) expect(listConfiguredVoiceBackends({})).toEqual([])
}) })
}) })
@@ -37,6 +37,10 @@ describe('resolveHubVoiceBackend', () => {
}) })
expect(backend).toBe('elevenlabs') expect(backend).toBe('elevenlabs')
}) })
test('returns null when no backends configured', () => {
expect(resolveHubVoiceBackend({})).toBeNull()
})
}) })
describe('resolveEffectiveVoiceBackend', () => { describe('resolveEffectiveVoiceBackend', () => {
@@ -50,4 +54,8 @@ describe('resolveEffectiveVoiceBackend', () => {
expect(resolveEffectiveVoiceBackend(configured, 'gemini-live', null)).toBe('gemini-live') expect(resolveEffectiveVoiceBackend(configured, 'gemini-live', null)).toBe('gemini-live')
expect(resolveEffectiveVoiceBackend(configured, 'gemini-live', 'qwen-realtime')).toBe('gemini-live') expect(resolveEffectiveVoiceBackend(configured, 'gemini-live', 'qwen-realtime')).toBe('gemini-live')
}) })
test('returns null when no backends configured', () => {
expect(resolveEffectiveVoiceBackend([], null, null)).toBeNull()
})
}) })
+14 -8
View File
@@ -260,25 +260,31 @@ export function listConfiguredVoiceBackends(env: VoiceBackendEnv): VoiceBackendT
if (env.DASHSCOPE_API_KEY?.trim() || env.QWEN_API_KEY?.trim()) { if (env.DASHSCOPE_API_KEY?.trim() || env.QWEN_API_KEY?.trim()) {
backends.push('qwen-realtime') backends.push('qwen-realtime')
} }
return backends.length > 0 ? backends : [DEFAULT_VOICE_BACKEND] return backends
} }
/** Hub default from VOICE_BACKEND when configured, else first available backend. */ /** Hub default from VOICE_BACKEND when configured, else first available backend. null when none configured. */
export function resolveHubVoiceBackend(env: VoiceBackendEnv): VoiceBackendType { export function resolveHubVoiceBackend(env: VoiceBackendEnv): VoiceBackendType | null {
const configured = listConfiguredVoiceBackends(env) const configured = listConfiguredVoiceBackends(env)
if (configured.length === 0) {
return null
}
const raw = env.VOICE_BACKEND const raw = env.VOICE_BACKEND
const fromEnv = VOICE_BACKEND_VALUES.includes(raw as VoiceBackendType) const fromEnv = VOICE_BACKEND_VALUES.includes(raw as VoiceBackendType)
? (raw as VoiceBackendType) ? (raw as VoiceBackendType)
: DEFAULT_VOICE_BACKEND : DEFAULT_VOICE_BACKEND
return configured.includes(fromEnv) ? fromEnv : (configured[0] ?? DEFAULT_VOICE_BACKEND) return configured.includes(fromEnv) ? fromEnv : configured[0]!
} }
/** User preference wins when valid; otherwise hub default. */ /** User preference wins when valid; otherwise hub default. */
export function resolveEffectiveVoiceBackend( export function resolveEffectiveVoiceBackend(
configured: readonly VoiceBackendType[], configured: readonly VoiceBackendType[],
hubDefault: VoiceBackendType, hubDefault: VoiceBackendType | null,
storedPreference: string | null | undefined storedPreference: string | null | undefined
): VoiceBackendType { ): VoiceBackendType | null {
if (configured.length === 0) {
return null
}
if ( if (
storedPreference storedPreference
&& VOICE_BACKEND_VALUES.includes(storedPreference as VoiceBackendType) && VOICE_BACKEND_VALUES.includes(storedPreference as VoiceBackendType)
@@ -286,10 +292,10 @@ export function resolveEffectiveVoiceBackend(
) { ) {
return storedPreference as VoiceBackendType return storedPreference as VoiceBackendType
} }
if (configured.includes(hubDefault)) { if (hubDefault && configured.includes(hubDefault)) {
return hubDefault return hubDefault
} }
return configured[0] ?? hubDefault return configured[0] ?? null
} }
export const GEMINI_LIVE_MODEL = 'gemini-2.5-flash-native-audio-latest' export const GEMINI_LIVE_MODEL = 'gemini-2.5-flash-native-audio-latest'
+4 -4
View File
@@ -205,7 +205,7 @@ export async function fetchQwenToken(api: ApiClient): Promise<QwenTokenResponse>
export interface VoiceBackendResponse { export interface VoiceBackendResponse {
/** Hub default (VOICE_BACKEND env, validated against configured backends). */ /** Hub default (VOICE_BACKEND env, validated against configured backends). */
backend: VoiceBackendType backend: VoiceBackendType | null
/** Backends with API keys configured on the hub. */ /** Backends with API keys configured on the hub. */
backends: VoiceBackendType[] backends: VoiceBackendType[]
} }
@@ -229,12 +229,12 @@ function isVoiceBackendType(value: string): value is VoiceBackendType {
export async function fetchVoiceBackend(api: ApiClient): Promise<VoiceBackendResponse> { export async function fetchVoiceBackend(api: ApiClient): Promise<VoiceBackendResponse> {
const result = await api.fetchVoiceBackend() const result = await api.fetchVoiceBackend()
const { backend } = result const { backend } = result
if (!isVoiceBackendType(backend)) { if (backend !== null && !isVoiceBackendType(backend)) {
throw new Error(`Unrecognised voice backend: ${backend}`) throw new Error(`Unrecognised voice backend: ${backend}`)
} }
const rawBackends = Array.isArray(result.backends) ? result.backends : [backend] const rawBackends = Array.isArray(result.backends) ? result.backends : backend !== null ? [backend] : []
const backends = rawBackends.filter(isVoiceBackendType) const backends = rawBackends.filter(isVoiceBackendType)
if (backends.length === 0) { if (backend !== null && backends.length === 0) {
backends.push(backend) backends.push(backend)
} }
return { backend, backends } return { backend, backends }
+2 -2
View File
@@ -58,8 +58,8 @@ export function writeStoredVoiceBackendPreference(backend: VoiceBackendType): vo
export function resolveSelectedVoiceBackend( export function resolveSelectedVoiceBackend(
configured: readonly VoiceBackendType[], configured: readonly VoiceBackendType[],
hubDefault: VoiceBackendType hubDefault: VoiceBackendType | null
): VoiceBackendType { ): VoiceBackendType | null {
return resolveEffectiveVoiceBackend( return resolveEffectiveVoiceBackend(
configured, configured,
hubDefault, hubDefault,
+1 -1
View File
@@ -31,7 +31,7 @@ export function useVoiceSettings() {
setConfiguredBackends(response.backends) setConfiguredBackends(response.backends)
const selected = resolveSelectedVoiceBackend(response.backends, response.backend) const selected = resolveSelectedVoiceBackend(response.backends, response.backend)
setBackendState(selected) setBackendState(selected)
setVoiceIdState(readStoredVoiceSelection(selected)) setVoiceIdState(selected ? readStoredVoiceSelection(selected) : null)
}).catch(() => { }).catch(() => {
if (cancelled) return if (cancelled) return
setConfiguredBackends(['elevenlabs']) setConfiguredBackends(['elevenlabs'])