Skip to content

Commit 9be195d

Browse files
authored
Merge pull request #15 from VapiAI/sri/add-mute-button
Add mute button to voice widget
2 parents c433880 + ff29213 commit 9be195d

File tree

5 files changed

+74
-4
lines changed

5 files changed

+74
-4
lines changed

src/components/VapiWidget.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,7 +386,9 @@ const VapiWidget: React.FC<VapiWidgetProps> = ({
386386
isCallActive={vapi.voice.isCallActive}
387387
connectionStatus={vapi.voice.connectionStatus}
388388
isAvailable={vapi.voice.isAvailable}
389+
isMuted={vapi.voice.isMuted}
389390
onToggleCall={handleToggleCall}
391+
onToggleMute={vapi.voice.toggleMute}
390392
startButtonText={effectiveStartButtonText}
391393
endButtonText={effectiveEndButtonText}
392394
colors={colors}
@@ -417,9 +419,11 @@ const VapiWidget: React.FC<VapiWidgetProps> = ({
417419
connectionStatus={vapi.voice.connectionStatus}
418420
isChatAvailable={vapi.chat.isAvailable}
419421
isVoiceAvailable={vapi.voice.isAvailable}
422+
isMuted={vapi.voice.isMuted}
420423
onInputChange={handleChatInputChange}
421424
onSendMessage={handleSendMessage}
422425
onToggleCall={handleToggleCall}
426+
onToggleMute={vapi.voice.toggleMute}
423427
colors={colors}
424428
styles={styles}
425429
inputRef={inputRef}

src/components/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,9 @@ export interface VoiceControlsProps {
185185
isCallActive: boolean;
186186
connectionStatus: 'disconnected' | 'connecting' | 'connected';
187187
isAvailable: boolean;
188+
isMuted: boolean;
188189
onToggleCall: () => void;
190+
onToggleMute: () => void;
189191
startButtonText: string;
190192
endButtonText: string;
191193
colors: ColorScheme;
@@ -208,9 +210,11 @@ export interface HybridControlsProps {
208210
connectionStatus: 'disconnected' | 'connecting' | 'connected';
209211
isChatAvailable: boolean;
210212
isVoiceAvailable: boolean;
213+
isMuted: boolean;
211214
onInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
212215
onSendMessage: () => void;
213216
onToggleCall: () => void;
217+
onToggleMute: () => void;
214218
colors: ColorScheme;
215219
styles: StyleConfig;
216220
inputRef?: React.RefObject<HTMLInputElement>;

src/components/widget/controls/HybridControls.tsx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import React from 'react';
22
import {
33
PaperPlaneTiltIcon,
44
MicrophoneIcon,
5+
MicrophoneSlashIcon,
56
StopIcon,
7+
WaveformIcon,
68
} from '@phosphor-icons/react';
79
import { HybridControlsProps } from '../../types';
810

@@ -12,9 +14,11 @@ const HybridControls: React.FC<HybridControlsProps> = ({
1214
connectionStatus,
1315
isChatAvailable,
1416
isVoiceAvailable,
17+
isMuted,
1518
onInputChange,
1619
onSendMessage,
1720
onToggleCall,
21+
onToggleMute,
1822
colors,
1923
styles,
2024
inputRef,
@@ -66,6 +70,23 @@ const HybridControls: React.FC<HybridControlsProps> = ({
6670
>
6771
<PaperPlaneTiltIcon size={20} weight="fill" />
6872
</button>
73+
{isCallActive && connectionStatus === 'connected' && (
74+
<button
75+
onClick={onToggleMute}
76+
className="h-10 w-10 flex items-center justify-center rounded-lg transition-all hover:opacity-90 active:scale-95"
77+
style={{
78+
backgroundColor: isMuted ? '#ef4444' : colors.accentColor,
79+
color: colors.ctaButtonTextColor || 'white',
80+
}}
81+
title={isMuted ? 'Unmute microphone' : 'Mute microphone'}
82+
>
83+
{isMuted ? (
84+
<MicrophoneSlashIcon size={20} weight="fill" />
85+
) : (
86+
<MicrophoneIcon size={20} weight="fill" />
87+
)}
88+
</button>
89+
)}
6990
<button
7091
onClick={onToggleCall}
7192
disabled={!isVoiceAvailable && !isCallActive}
@@ -91,7 +112,7 @@ const HybridControls: React.FC<HybridControlsProps> = ({
91112
) : isCallActive ? (
92113
<StopIcon size={20} weight="fill" />
93114
) : (
94-
<MicrophoneIcon size={20} weight="fill" />
115+
<WaveformIcon size={20} weight="bold" />
95116
)}
96117
</button>
97118
</div>

src/components/widget/controls/VoiceControls.tsx

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,41 @@
11
import React from 'react';
2-
import { MicrophoneIcon, StopIcon } from '@phosphor-icons/react';
2+
import {
3+
MicrophoneIcon,
4+
StopIcon,
5+
MicrophoneSlashIcon,
6+
WaveformIcon,
7+
} from '@phosphor-icons/react';
38
import { VoiceControlsProps } from '../../types';
49

510
const VoiceControls: React.FC<VoiceControlsProps> = ({
611
isCallActive,
712
connectionStatus,
813
isAvailable,
14+
isMuted,
915
onToggleCall,
16+
onToggleMute,
1017
startButtonText,
1118
endButtonText,
1219
colors,
1320
}) => (
14-
<div className="flex items-center justify-center">
21+
<div className="flex items-center justify-center space-x-2">
22+
{isCallActive && connectionStatus === 'connected' && (
23+
<button
24+
onClick={onToggleMute}
25+
className="h-12 w-12 flex items-center justify-center rounded-full transition-all hover:opacity-90 active:scale-95"
26+
style={{
27+
backgroundColor: isMuted ? '#ef4444' : colors.accentColor,
28+
color: colors.ctaButtonTextColor || 'white',
29+
}}
30+
title={isMuted ? 'Unmute microphone' : 'Mute microphone'}
31+
>
32+
{isMuted ? (
33+
<MicrophoneSlashIcon size={20} weight="fill" />
34+
) : (
35+
<MicrophoneIcon size={20} weight="fill" />
36+
)}
37+
</button>
38+
)}
1539
<button
1640
onClick={onToggleCall}
1741
disabled={!isAvailable && !isCallActive}
@@ -37,7 +61,7 @@ const VoiceControls: React.FC<VoiceControlsProps> = ({
3761
</>
3862
) : (
3963
<>
40-
<MicrophoneIcon size={16} weight="fill" />
64+
<WaveformIcon size={16} weight="bold" />
4165
<span>{startButtonText}</span>
4266
</>
4367
)}

src/hooks/useVapiCall.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ export interface VapiCallState {
66
isSpeaking: boolean;
77
volumeLevel: number;
88
connectionStatus: 'disconnected' | 'connecting' | 'connected';
9+
isMuted: boolean;
910
}
1011

1112
export interface VapiCallHandlers {
1213
startCall: () => Promise<void>;
1314
endCall: () => Promise<void>;
1415
toggleCall: () => Promise<void>;
16+
toggleMute: () => void;
1517
}
1618

1719
export interface UseVapiCallOptions {
@@ -47,6 +49,7 @@ export const useVapiCall = ({
4749

4850
const [isCallActive, setIsCallActive] = useState(false);
4951
const [isSpeaking, setIsSpeaking] = useState(false);
52+
const [isMuted, setIsMuted] = useState(false);
5053
const [volumeLevel, setVolumeLevel] = useState(0);
5154
const [connectionStatus, setConnectionStatus] = useState<
5255
'disconnected' | 'connecting' | 'connected'
@@ -86,6 +89,7 @@ export const useVapiCall = ({
8689
setConnectionStatus('disconnected');
8790
setVolumeLevel(0);
8891
setIsSpeaking(false);
92+
setIsMuted(false);
8993
callbacksRef.current.onCallEnd?.();
9094
};
9195

@@ -185,15 +189,28 @@ export const useVapiCall = ({
185189
}
186190
}, [isCallActive, startCall, endCall]);
187191

192+
const toggleMute = useCallback(() => {
193+
if (!vapi || !isCallActive) {
194+
console.log('Cannot toggle mute: no vapi instance or call not active');
195+
return;
196+
}
197+
198+
const newMutedState = !isMuted;
199+
vapi.setMuted(newMutedState);
200+
setIsMuted(newMutedState);
201+
}, [vapi, isCallActive, isMuted]);
202+
188203
return {
189204
// State
190205
isCallActive,
191206
isSpeaking,
192207
volumeLevel,
193208
connectionStatus,
209+
isMuted,
194210
// Handlers
195211
startCall,
196212
endCall,
197213
toggleCall,
214+
toggleMute,
198215
};
199216
};

0 commit comments

Comments
 (0)