Skip to content

Commit c39570a

Browse files
committed
fix: show disabled voice button with tooltip when HTTPS required
navigator.mediaDevices is undefined in non-secure contexts (HTTP on non-localhost). Now shows a disabled mic button with tooltip explaining HTTPS is required, rather than hiding it entirely or crashing.
1 parent 2820a98 commit c39570a

File tree

3 files changed

+26
-6
lines changed

3 files changed

+26
-6
lines changed

src/browser/components/ChatInput/VoiceInputButton.tsx

Lines changed: 18 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,15 @@ 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 ? "Voice input (requires HTTPS)" : "Voice input (requires OpenAI API key)",
38+
colorClass: "text-muted/50",
39+
}
3340
: STATE_CONFIG[props.state];
3441

3542
const Icon = props.state === "transcribing" ? Loader2 : Mic;
@@ -40,7 +47,7 @@ export const VoiceInputButton: React.FC<VoiceInputButtonProps> = (props) => {
4047
<button
4148
type="button"
4249
onClick={props.onToggle}
43-
disabled={(props.disabled ?? false) || isTranscribing || needsApiKey}
50+
disabled={(props.disabled ?? false) || isTranscribing || isDisabledReason}
4451
aria-label={label}
4552
aria-pressed={props.state === "recording"}
4653
className={cn(
@@ -52,7 +59,13 @@ export const VoiceInputButton: React.FC<VoiceInputButtonProps> = (props) => {
5259
<Icon className={cn("h-4 w-4", isTranscribing && "animate-spin")} strokeWidth={1.5} />
5360
</button>
5461
<Tooltip className="tooltip" align="right">
55-
{needsApiKey ? (
62+
{needsHttps ? (
63+
<>
64+
Voice input requires a secure connection.
65+
<br />
66+
Use HTTPS or access via localhost.
67+
</>
68+
) : needsApiKey ? (
5669
<>
5770
Voice input requires OpenAI API key.
5871
<br />

src/browser/components/ChatInput/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1066,6 +1066,7 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
10661066
state={voiceInput.state}
10671067
isApiKeySet={voiceInput.isApiKeySet}
10681068
shouldShowUI={voiceInput.shouldShowUI}
1069+
requiresSecureContext={voiceInput.requiresSecureContext}
10691070
onToggle={voiceInput.toggle}
10701071
disabled={disabled || isSending}
10711072
/>

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)