profileShare

rasmusjy / voicetask

Read-only snapshot

No repository description.

main default branch 105 files Expires Sep 13, 2026, 9:06 AM

Commit

Redesign push-to-talk as a circular mic button

commit f45afe7

1 changed file with +24 and −6

modified client/src/components/PushToTalkButton.tsx +24 −6
@@ -4,14 +4,16 @@import { createPushToTalkRecorder } from '../audio'
4 4 interface PushToTalkButtonProps {
5 5 disabled?: boolean
6 6 onRecorded: (blob: Blob) => void
7 + onRecordingChange?: (recording: boolean) => void
8 + onLevel?: (level: number) => void
7 9 }
8 10
9 11 function isTypingTarget(target: EventTarget | null): boolean {
10 12 if (!(target instanceof HTMLElement)) return false
11 13 return target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable
12 14 }
13 15
14 -export function PushToTalkButton({ disabled, onRecorded }: PushToTalkButtonProps) {
16 +export function PushToTalkButton({ disabled, onRecorded, onRecordingChange, onLevel }: PushToTalkButtonProps) {
15 17 const [recording, setRecording] = useState(false)
16 18 const [error, setError] = useState<string | null>(null)
17 19 const recorderRef = useRef(createPushToTalkRecorder())
@@ -22,25 +24,27 @@export function PushToTalkButton({ disabled, onRecorded }: PushToTalkButtonProps
22 24 activeRef.current = true
23 25 setError(null)
24 26 try {
25 - await recorderRef.current.start()
27 + await recorderRef.current.start(onLevel)
26 28 setRecording(true)
29 + onRecordingChange?.(true)
27 30 } catch (err) {
28 31 activeRef.current = false
29 32 setError(err instanceof Error ? err.message : 'could not access the microphone')
30 33 }
31 - }, [disabled])
34 + }, [disabled, onLevel, onRecordingChange])
32 35
33 36 const stopRecording = useCallback(async () => {
34 37 if (!activeRef.current) return
35 38 activeRef.current = false
36 39 setRecording(false)
40 + onRecordingChange?.(false)
37 41 try {
38 42 const blob = await recorderRef.current.stop()
39 43 onRecorded(blob)
40 44 } catch (err) {
41 45 setError(err instanceof Error ? err.message : 'recording failed')
42 46 }
43 - }, [onRecorded])
47 + }, [onRecorded, onRecordingChange])
44 48
45 49 useEffect(() => {
46 50 function handleKeyDown(event: KeyboardEvent) {
@@ -67,16 +71,30 @@export function PushToTalkButton({ disabled, onRecorded }: PushToTalkButtonProps
67 71 <div className="push-to-talk">
68 72 <button
69 73 type="button"
70 - className={`record-button ${recording ? 'recording' : ''}`}
74 + className={`mic-button ${recording ? 'recording' : ''}`}
71 75 disabled={disabled}
76 + aria-label={recording ? 'Release to send' : 'Hold to talk'}
72 77 onMouseDown={() => void startRecording()}
73 78 onMouseUp={() => void stopRecording()}
74 79 onMouseLeave={() => {
75 80 if (recording) void stopRecording()
76 81 }}
82 + onTouchStart={(e) => {
83 + e.preventDefault()
84 + void startRecording()
85 + }}
86 + onTouchEnd={(e) => {
87 + e.preventDefault()
88 + void stopRecording()
89 + }}
77 90 >
78 - {recording ? 'Recording… release to send' : 'Hold to talk (or press space)'}
91 + <svg viewBox="0 0 24 24" width="26" height="26" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round">
92 + <rect x="9" y="3" width="6" height="11" rx="3" />
93 + <path d="M5 11a7 7 0 0 0 14 0" />
94 + <path d="M12 18v3" />
95 + </svg>
79 96 </button>
97 + <p className="mic-hint">{recording ? 'release to send' : 'hold to talk · or press space'}</p>
80 98 {error && <p className="error">{error}</p>}
81 99 </div>
82 100 )