From 29259e04e0f7fddd9954d93888d2d5ac34e48839 Mon Sep 17 00:00:00 2001 From: Aaron Nelson Date: Sun, 26 Apr 2026 13:00:16 -0400 Subject: [PATCH] =?UTF-8?q?Voice=20input=20=E2=80=94=20toggle=20mode,=20iO?= =?UTF-8?q?S=20touch=20support,=20MIME=20type=20detection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/MessageInput.tsx | 135 +++++++++++++++++++++++------------- 1 file changed, 88 insertions(+), 47 deletions(-) diff --git a/components/MessageInput.tsx b/components/MessageInput.tsx index bea6fec..283dd98 100644 --- a/components/MessageInput.tsx +++ b/components/MessageInput.tsx @@ -9,9 +9,11 @@ export default function MessageInput() { const { currentId, setCurrentId, addMessage, setIsLoading, isLoading, setConversations } = useStore(); const [text, setText] = useState(''); const [recording, setRecording] = useState(false); + const [transcribing, setTranscribing] = useState(false); const textareaRef = useRef(null); const mediaRecorderRef = useRef(null); const chunksRef = useRef([]); + const streamRef = useRef(null); useEffect(() => { textareaRef.current?.focus(); @@ -49,16 +51,15 @@ export default function MessageInput() { try { const data = await api.sendMessage(message, convId); setCurrentId(data.conversation_id); - const assistantMsg: Message = { + addMessage({ role: 'assistant', content: data.response, sources: data.sources || [], timestamp: new Date().toISOString(), - }; - addMessage(assistantMsg); + }); const updated = await api.getConversations(); setConversations(updated); - } catch (e) { + } catch { addMessage({ role: 'assistant', content: 'Error — please try again.', @@ -71,34 +72,57 @@ export default function MessageInput() { } } - async function startRecording() { - try { - const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); - const mr = new MediaRecorder(stream); - chunksRef.current = []; - mr.ondataavailable = e => chunksRef.current.push(e.data); - mr.onstop = async () => { - stream.getTracks().forEach(t => t.stop()); - const blob = new Blob(chunksRef.current, { type: 'audio/webm' }); - try { - const { text: transcript } = await api.transcribe(blob); - setText(prev => prev ? prev + ' ' + transcript : transcript); - textareaRef.current?.focus(); - } catch { - console.error('Transcription failed'); - } - }; - mr.start(); - mediaRecorderRef.current = mr; - setRecording(true); - } catch { - alert('Microphone access denied'); - } - } + async function toggleRecording() { + if (recording) { + // Stop recording + mediaRecorderRef.current?.stop(); + setRecording(false); + } else { + // Start recording + try { + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + streamRef.current = stream; + + // Pick supported mime type + const mimeType = MediaRecorder.isTypeSupported('audio/webm') + ? 'audio/webm' + : MediaRecorder.isTypeSupported('audio/mp4') + ? 'audio/mp4' + : 'audio/ogg'; - function stopRecording() { - mediaRecorderRef.current?.stop(); - setRecording(false); + const mr = new MediaRecorder(stream, { mimeType }); + chunksRef.current = []; + + mr.ondataavailable = e => { + if (e.data.size > 0) chunksRef.current.push(e.data); + }; + + mr.onstop = async () => { + streamRef.current?.getTracks().forEach(t => t.stop()); + if (chunksRef.current.length === 0) return; + + setTranscribing(true); + try { + const blob = new Blob(chunksRef.current, { type: mimeType }); + const { text: transcript } = await api.transcribe(blob); + if (transcript.trim()) { + setText(prev => prev ? prev + ' ' + transcript.trim() : transcript.trim()); + setTimeout(() => autoResize(), 0); + } + } catch (e) { + console.error('Transcription failed', e); + } finally { + setTranscribing(false); + } + }; + + mr.start(1000); // collect data every second + mediaRecorderRef.current = mr; + setRecording(true); + } catch { + alert('Microphone access denied'); + } + } } function handleKeyDown(e: React.KeyboardEvent) { @@ -108,10 +132,13 @@ export default function MessageInput() { } } + const micColor = recording ? '#e8f5ed' : transcribing ? '#e8f5ed' : 'var(--text3)'; + const micBg = recording ? '#a32d2d' : transcribing ? 'var(--accent)' : 'var(--bg3)'; + return (
- {/* Voice button */} + {/* Mic button — tap to start, tap to stop */} {/* Text input */} @@ -152,7 +192,7 @@ export default function MessageInput() { value={text} onChange={e => { setText(e.target.value); autoResize(); }} onKeyDown={handleKeyDown} - placeholder="Ask anything..." + placeholder={recording ? 'Recording... tap mic to stop' : transcribing ? 'Transcribing...' : 'Ask anything...'} rows={1} className="w-full block resize-none outline-none bg-transparent px-3 py-3 leading-relaxed min-w-0" style={{ @@ -167,9 +207,9 @@ export default function MessageInput() { {/* Send button */}