mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-26 05:15:48 +08:00
fix(voice): harden recording and backend behavior
Redirects could bypass the backend URL guard, and TTS playback waited for full buffering. Recording could overlap or finish after teardown. Controls also ignored backend readiness. Explicit formats and config-aware cache keys prevent stale audio after settings change.
This commit is contained in:
@@ -1,11 +1,39 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { authenticatedFetch } from '../../../utils/api';
|
||||
import { VOICE_CONFIG_SYNC_EVENT, voiceConfigHeaders } from '../../../hooks/useVoiceConfig';
|
||||
|
||||
// Voice UI is gated on the `voiceEnabled` UI preference (toggled in Quick Settings /
|
||||
// the Settings modal). This is a lightweight read-only view of that preference so the
|
||||
// mic/speak controls can hide themselves, kept in sync via the same events
|
||||
// useUiPreferences emits. No server probe.
|
||||
// the Settings modal) and a configured voice backend.
|
||||
const STORAGE_KEY = 'uiPreferences';
|
||||
const SYNC_EVENT = 'ui-preferences:sync';
|
||||
const healthCache = new Map<string, boolean>();
|
||||
const healthRequests = new Map<string, Promise<boolean>>();
|
||||
|
||||
function checkVoiceHealth(): Promise<boolean> {
|
||||
const baseUrl = voiceConfigHeaders()['x-voice-base-url'];
|
||||
const signature = baseUrl || '';
|
||||
if (healthCache.has(signature)) return Promise.resolve(healthCache.get(signature) ?? false);
|
||||
const pending = healthRequests.get(signature);
|
||||
if (pending) return pending;
|
||||
const request = authenticatedFetch('/api/voice/health', {
|
||||
headers: baseUrl ? { 'x-voice-base-url': baseUrl } : {},
|
||||
})
|
||||
.then(async (response) => {
|
||||
if (!response.ok) throw new Error(`Voice health check failed (${response.status})`);
|
||||
const data = await response.json();
|
||||
return data?.configured === true;
|
||||
})
|
||||
.then((available) => {
|
||||
healthCache.set(signature, available);
|
||||
return available;
|
||||
})
|
||||
.finally(() => {
|
||||
healthRequests.delete(signature);
|
||||
});
|
||||
healthRequests.set(signature, request);
|
||||
return request;
|
||||
}
|
||||
|
||||
function readVoiceEnabled(): boolean {
|
||||
try {
|
||||
@@ -22,6 +50,7 @@ export function useVoiceAvailable(): boolean {
|
||||
const [enabled, setEnabled] = useState<boolean>(() =>
|
||||
typeof window === 'undefined' ? false : readVoiceEnabled(),
|
||||
);
|
||||
const [available, setAvailable] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const update = () => setEnabled(readVoiceEnabled());
|
||||
@@ -33,5 +62,31 @@ export function useVoiceAvailable(): boolean {
|
||||
};
|
||||
}, []);
|
||||
|
||||
return enabled;
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
let requestId = 0;
|
||||
|
||||
const check = async () => {
|
||||
if (!enabled) {
|
||||
setAvailable(false);
|
||||
return;
|
||||
}
|
||||
const id = ++requestId;
|
||||
try {
|
||||
const result = await checkVoiceHealth();
|
||||
if (active && id === requestId) setAvailable(result);
|
||||
} catch {
|
||||
if (active && id === requestId) setAvailable(false);
|
||||
}
|
||||
};
|
||||
|
||||
void check();
|
||||
window.addEventListener(VOICE_CONFIG_SYNC_EVENT, check);
|
||||
return () => {
|
||||
active = false;
|
||||
window.removeEventListener(VOICE_CONFIG_SYNC_EVENT, check);
|
||||
};
|
||||
}, [enabled]);
|
||||
|
||||
return enabled && available;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user