Skip to content

Commit 38d0f2c

Browse files
authored
🤖 fix: show disabled voice button with HTTPS tooltip on insecure contexts (#838)
_Generated with mux_ When accessing mux over HTTP (non-localhost), `navigator.mediaDevices` is undefined due to browser security restrictions. Previously this caused a crash or hidden button. Now shows a disabled mic button with tooltip: "Voice input requires a secure connection. Use HTTPS or access via localhost." Changes: - Add `requiresSecureContext` flag to `useVoiceInput` hook - Show disabled button with educational tooltip instead of hiding/crashing - Prioritize HTTPS message over API key message (check HTTPS first)
1 parent 537020e commit 38d0f2c

File tree

3 files changed

+28
-6
lines changed

3 files changed

+28
-6
lines changed

‎src/browser/components/ChatInput/VoiceInputButton.tsx‎

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ interface VoiceInputButtonProps {
1414
state: VoiceInputState;
1515
isApiKeySet: boolean;
1616
shouldShowUI: boolean;
17+
requiresSecureContext: boolean;
1718
onToggle: () => void;
1819
disabled?: boolean;
1920
}
@@ -27,9 +28,17 @@ const STATE_CONFIG: Record<VoiceInputState, { label: string; colorClass: string
2728
export const VoiceInputButton: React.FC<VoiceInputButtonProps> = (props) => {
2829
if (!props.shouldShowUI) return null;
2930

30-
const needsApiKey = !props.isApiKeySet;
31-
const { label, colorClass } = needsApiKey
32-
? { label: "Voice input (requires OpenAI API key)", colorClass: "text-muted/50" }
31+
const needsHttps = props.requiresSecureContext;
32+
const needsApiKey = !needsHttps && !props.isApiKeySet;
33+
const isDisabledReason = needsHttps || needsApiKey;
34+
35+
const { label, colorClass } = isDisabledReason
36+
? {
37+
label: needsHttps
38+
? "Voice input (requires HTTPS)"
39+
: "Voice input (requires OpenAI API key)",
40+
colorClass: "text-muted/50",
41+
}
3342
: STATE_CONFIG[props.state];
3443

3544
const Icon = props.state === "transcribing" ? Loader2 : Mic;
@@ -40,7 +49,7 @@ export const VoiceInputButton: React.FC<VoiceInputButtonProps> = (props) => {
4049
<button
4150
type="button"
4251
onClick={props.onToggle}
43-
disabled={(props.disabled ?? false) || isTranscribing || needsApiKey}
52+
disabled={(props.disabled ?? false) || isTranscribing || isDisabledReason}
4453
aria-label={label}
4554
aria-pressed={props.state === "recording"}
4655
className={cn(
@@ -52,7 +61,13 @@ export const VoiceInputButton: React.FC<VoiceInputButtonProps> = (props) => {
5261
<Icon className={cn("h-4 w-4", isTranscribing && "animate-spin")} strokeWidth={1.5} />
5362
</button>
5463
<Tooltip className="tooltip" align="right">
55-
{needsApiKey ? (
64+
{needsHttps ? (
65+
<>
66+
Voice input requires a secure connection.
67+
<br />
68+
Use HTTPS or access via localhost.
69+
</>
70+
) : needsApiKey ? (
5671
<>
5772
Voice input requires OpenAI API key.
5873
<br />

‎src/browser/components/ChatInput/index.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1070,6 +1070,7 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
10701070
state={voiceInput.state}
10711071
isApiKeySet={voiceInput.isApiKeySet}
10721072
shouldShowUI={voiceInput.shouldShowUI}
1073+
requiresSecureContext={voiceInput.requiresSecureContext}
10731074
onToggle={voiceInput.toggle}
10741075
disabled={disabled || isSending}
10751076
/>

‎src/browser/hooks/useVoiceInput.ts‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ export interface UseVoiceInputResult {
2424
isApiKeySet: boolean;
2525
/** False on touch devices (they have native keyboard dictation) */
2626
shouldShowUI: boolean;
27+
/** True when running over HTTP (not localhost) - microphone requires secure context */
28+
requiresSecureContext: boolean;
2729
start: () => void;
2830
stop: (options?: { send?: boolean }) => void;
2931
cancel: () => void;
@@ -49,6 +51,8 @@ function hasTouchDictation(): boolean {
4951

5052
const HAS_TOUCH_DICTATION = hasTouchDictation();
5153
const HAS_MEDIA_RECORDER = typeof window !== "undefined" && typeof MediaRecorder !== "undefined";
54+
const HAS_GET_USER_MEDIA =
55+
typeof window !== "undefined" && typeof navigator.mediaDevices?.getUserMedia === "function";
5256

5357
// =============================================================================
5458
// Hook
@@ -131,6 +135,7 @@ export function useVoiceInput(options: UseVoiceInputOptions): UseVoiceInputResul
131135
// Guard: only start from idle state with valid configuration
132136
const canStart =
133137
HAS_MEDIA_RECORDER &&
138+
HAS_GET_USER_MEDIA &&
134139
!HAS_TOUCH_DICTATION &&
135140
state === "idle" &&
136141
callbacksRef.current.openAIKeySet;
@@ -237,9 +242,10 @@ export function useVoiceInput(options: UseVoiceInputOptions): UseVoiceInputResul
237242

238243
return {
239244
state,
240-
isSupported: HAS_MEDIA_RECORDER,
245+
isSupported: HAS_MEDIA_RECORDER && HAS_GET_USER_MEDIA,
241246
isApiKeySet: callbacksRef.current.openAIKeySet,
242247
shouldShowUI: HAS_MEDIA_RECORDER && !HAS_TOUCH_DICTATION,
248+
requiresSecureContext: HAS_MEDIA_RECORDER && !HAS_GET_USER_MEDIA,
243249
start: () => void start(),
244250
stop,
245251
cancel,

0 commit comments

Comments
 (0)