Skip to content

Commit f8168ad

Browse files
refactor(frontend): improve voice status overlay and scroll button visibility
1 parent c14403a commit f8168ad

3 files changed

Lines changed: 52 additions & 50 deletions

File tree

frontend/src/components/message/PromptInput.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1241,7 +1241,7 @@ return (
12411241

12421242
</div>
12431243
<div className="flex items-center gap-1.5 md:gap-2 flex-shrink-0">
1244-
{isMobile && showScrollButton ? (
1244+
{isMobile && showScrollButton && !showVoiceFeedback ? (
12451245
<button
12461246
onClick={onScrollToBottom}
12471247
className="px-4 py-2 rounded-lg bg-black hover:bg-zinc-900 text-white transition-all duration-200 active:scale-95 shadow-md flex items-center justify-center min-w-[52px] border border-zinc-700"
@@ -1286,7 +1286,7 @@ return (
12861286
{sttEnabled && sttSupported && (
12871287
renderVoiceButton('desktop')
12881288
)}
1289-
{isMobile && !showScrollButton && sttEnabled && sttSupported && !hasPendingPermissionForSession && (
1289+
{isMobile && sttEnabled && sttSupported && !hasPendingPermissionForSession && (!showScrollButton || showVoiceFeedback) && (
12901290
renderVoiceButton('mobile')
12911291
)}
12921292
<button

frontend/src/components/message/VoiceStatusOverlay.tsx

Lines changed: 30 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,31 +8,36 @@ interface VoiceStatusOverlayProps {
88
state: VoiceStatusOverlayState | null
99
}
1010

11+
function WaveformBars() {
12+
return (
13+
<div className="flex items-end gap-[3px] h-8">
14+
{[0, 1, 2, 3, 4].map((i) => (
15+
<div
16+
key={i}
17+
className="w-1.5 rounded-full bg-white"
18+
style={{
19+
height: '100%',
20+
animation: `waveBar 0.9s ease-in-out infinite`,
21+
animationDelay: `${i * 0.12}s`,
22+
}}
23+
/>
24+
))}
25+
<style>{`
26+
@keyframes waveBar {
27+
0%, 100% { transform: scaleY(0.2); }
28+
50% { transform: scaleY(1); }
29+
}
30+
`}</style>
31+
</div>
32+
)
33+
}
34+
1135
export function VoiceStatusOverlay({ show, label, state }: VoiceStatusOverlayProps) {
1236
if (!show || !label || !state) {
1337
return null
1438
}
1539

1640
const isLoading = state === 'starting' || state === 'processing' || state === 'sending'
17-
const showLoadingText = state !== 'starting'
18-
const topLabel = state === 'readyToSend'
19-
? 'Release'
20-
: state === 'starting'
21-
? 'Starting'
22-
: state === 'processing'
23-
? 'Transcribe'
24-
: state === 'sending'
25-
? 'Sending'
26-
: 'Swipe'
27-
const bottomLabel = state === 'starting'
28-
? 'Mic'
29-
: state === 'processing'
30-
? 'Speech'
31-
: state === 'sending'
32-
? 'Prompt'
33-
: state === 'readyToSend'
34-
? 'Send'
35-
: 'Send'
3641
const actionWords = state === 'readyToSend'
3742
? ['Release', 'To', 'Send']
3843
: ['Swipe', 'To', 'Send']
@@ -47,12 +52,11 @@ export function VoiceStatusOverlay({ show, label, state }: VoiceStatusOverlayPro
4752
<div className="absolute inset-x-1 top-1 h-10 rounded-full bg-white/20 blur-sm" />
4853
<div className="relative flex flex-1 flex-col items-center justify-center gap-1">
4954
{isLoading ? (
50-
<>
55+
state === 'processing' ? (
56+
<WaveformBars />
57+
) : (
5158
<LoaderCircle className="h-6 w-6 animate-spin" />
52-
{showLoadingText && (
53-
<span className="text-[10px] font-bold uppercase leading-none tracking-wide">{topLabel}</span>
54-
)}
55-
</>
59+
)
5660
) : (
5761
<>
5862
<ArrowUp className="h-8 w-8 animate-bounce" />
@@ -64,11 +68,9 @@ export function VoiceStatusOverlay({ show, label, state }: VoiceStatusOverlayPro
6468
</>
6569
)}
6670
</div>
67-
{isLoading && showLoadingText ? (
68-
<span className="relative text-[10px] font-bold uppercase leading-none tracking-wide">{bottomLabel}</span>
69-
) : !isLoading ? (
71+
{!isLoading && (
7072
<X className="relative h-4 w-4" />
71-
) : null}
73+
)}
7274
</div>
7375
</div>
7476
)

frontend/src/hooks/useSTT.ts

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,26 @@ export function useSTT(userId = 'default') {
187187
}
188188
}, [isEnabled, isExternalProvider, setupAudioRecorder])
189189

190+
const clearStartupTimeout = useCallback(() => {
191+
if (startupTimeoutRef.current) {
192+
clearTimeout(startupTimeoutRef.current)
193+
startupTimeoutRef.current = null
194+
}
195+
}, [])
196+
197+
const abortAndResetOnTimeout = useCallback(() => {
198+
if (isExternalProvider && audioRecorder.current) {
199+
audioRecorder.current.abort()
200+
} else {
201+
recognizer.current.abort()
202+
}
203+
setIsRecording(false)
204+
setIsProcessing(false)
205+
setState('idle')
206+
setIsError(true)
207+
setError('Microphone start timed out')
208+
}, [isExternalProvider])
209+
190210
const startRecording = useCallback(async (): Promise<boolean> => {
191211
if (!isSupported) {
192212
setError('Speech recognition is not supported in this browser')
@@ -328,26 +348,6 @@ export function useSTT(userId = 'default') {
328348
setInterimTranscript('')
329349
}, [])
330350

331-
const clearStartupTimeout = useCallback(() => {
332-
if (startupTimeoutRef.current) {
333-
clearTimeout(startupTimeoutRef.current)
334-
startupTimeoutRef.current = null
335-
}
336-
}, [])
337-
338-
const abortAndResetOnTimeout = useCallback(() => {
339-
if (isExternalProvider && audioRecorder.current) {
340-
audioRecorder.current.abort()
341-
} else {
342-
recognizer.current.abort()
343-
}
344-
setIsRecording(false)
345-
setIsProcessing(false)
346-
setState('idle')
347-
setIsError(true)
348-
setError('Microphone start timed out')
349-
}, [isExternalProvider])
350-
351351
useEffect(() => {
352352
return () => {
353353
if (errorTimeoutRef.current) clearTimeout(errorTimeoutRef.current)

0 commit comments

Comments
 (0)