From f45e45ca8f374fd05d6e40e82db3b8bbbcad8ec7 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2026 23:12:45 +0000 Subject: [PATCH] fix: hide voice features in browsers without Speech Recognition support Add Brave browser detection to pwaUtils.ts since Brave exposes the SpeechRecognition API but silently blocks it from returning results, causing false positive feature detection. - Add `Is.Browser.Brave` detection via `navigator.brave` property - Update `browserSpeechRecognitionCapability()` to mark Brave as unsupported with a clear warning message - Gate Call button (both mobile and desktop) on speech recognition capability since Call fundamentally requires voice input - CallWizard already displays capability warnings, so Brave users navigating directly to /call will see a clear explanation Closes #1061 Co-authored-by: Enrico Ros --- src/apps/chat/components/composer/Composer.tsx | 7 ++++--- .../components/speechrecognition/useSpeechRecognition.ts | 8 ++++++-- src/common/util/pwaUtils.ts | 1 + 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/apps/chat/components/composer/Composer.tsx b/src/apps/chat/components/composer/Composer.tsx index df5ffa9f5..7b51702b8 100644 --- a/src/apps/chat/components/composer/Composer.tsx +++ b/src/apps/chat/components/composer/Composer.tsx @@ -658,6 +658,7 @@ export function Composer(props: { const showChatInReferenceTo = !!inReferenceTo?.length; const showChatExtras = isText && !showChatInReferenceTo && !assistantAbortible && composerQuickButton !== 'off'; + const speechMayWork = browserSpeechRecognitionCapability().mayWork; const sendButtonVariant: VariantProp = (isAppend || (isMobile && isTextBeam)) ? 'outlined' : 'solid'; @@ -964,7 +965,7 @@ export function Composer(props: { {/* [mobile] bottom-corner secondary button */} {isMobile && (showChatExtras - ? (composerQuickButton === 'call' + ? (composerQuickButton === 'call' && speechMayWork ? : ) : isDraw @@ -1055,8 +1056,8 @@ export function Composer(props: { {/* [desktop] secondary bottom-buttons (aligned to bottom for now, and mutually exclusive) */} {isDesktop && - {/* [desktop] Call secondary button */} - {showChatExtras && } + {/* [desktop] Call secondary button - hidden when speech recognition is not available */} + {showChatExtras && speechMayWork && } {/* [desktop] Draw Options secondary button */} {isDraw && } diff --git a/src/common/components/speechrecognition/useSpeechRecognition.ts b/src/common/components/speechrecognition/useSpeechRecognition.ts index ca556fa19..54a0336a4 100644 --- a/src/common/components/speechrecognition/useSpeechRecognition.ts +++ b/src/common/components/speechrecognition/useSpeechRecognition.ts @@ -18,13 +18,17 @@ let cachedCapability: CapabilityBrowserSpeechRecognition | null = null; export const browserSpeechRecognitionCapability = (): CapabilityBrowserSpeechRecognition => { if (!cachedCapability) { - const isApiAvailable = !!getSpeechRecognitionClass(); + const isBraveBlocked = Is.Browser.Brave; // Brave exposes the API but silently blocks results + const isApiAvailable = !!getSpeechRecognitionClass() && !isBraveBlocked; const isDeviceNotSupported = false; cachedCapability = { mayWork: isApiAvailable && !isDeviceNotSupported, isApiAvailable, isDeviceNotSupported, - warnings: Is.OS.iOS ? ['Not tested on this browser/device.'] : [], + warnings: [ + ...Is.OS.iOS ? ['Not tested on this browser/device.'] : [], + ...isBraveBlocked ? ['Speech recognition is not supported in Brave. Please use Chrome, Edge, or Safari.'] : [], + ], }; } return cachedCapability; diff --git a/src/common/util/pwaUtils.ts b/src/common/util/pwaUtils.ts index c5af2f490..176ede929 100644 --- a/src/common/util/pwaUtils.ts +++ b/src/common/util/pwaUtils.ts @@ -11,6 +11,7 @@ const _safeUA = isBrowser ? window.navigator?.userAgent.toLowerCase() || '' : '' export const Is = { Desktop: !/mobile|android|iphone|ipad|ipod/.test(_safeUA), Browser: { + Brave: isBrowser && !!(navigator as any).brave, Chrome: _safeUA.includes('chrome') || _safeUA.includes('crios'), get Safari() { return _safeUA.includes('safari') && !this.Chrome && !_safeUA.includes('chromium');