refactor(voice): integrate click-to-toggle mic into ChatInputBar; remove VoiceOverlay

This commit is contained in:
2026-04-07 13:17:25 -04:00
parent 39e554d938
commit 7f37cee49f
3 changed files with 28 additions and 591 deletions
+28 -12
View File
@@ -2,6 +2,7 @@
import { ref, computed, nextTick } from 'vue'
import { apiGet, transcribeAudio } from '@/api/client'
import { useVoiceRecorder } from '@/composables/useVoiceRecorder'
import { useSilenceDetector } from '@/composables/useSilenceDetector'
import { useChatStore } from '@/stores/chat'
import { useSettingsStore } from '@/stores/settings'
import type { Note } from '@/types/note'
@@ -107,21 +108,39 @@ function removeAttachedNote() {
attachedNote.value = null
}
// ── PTT ───────────────────────────────────────────────────────────────────────
// ── Voice (click-to-toggle + silence detection) ─────────────────────────────
const transcribingVoice = ref(false)
const recorder = useVoiceRecorder()
const silenceDetector = useSilenceDetector()
async function startPtt() {
if (!voiceEnabled.value || recorder.recording.value) return
await recorder.startRecording()
async function toggleVoice() {
if (transcribingVoice.value) return
if (recorder.recording.value) {
await stopRecording()
} else {
await startRecording()
}
}
async function stopPtt() {
async function startRecording() {
if (!voiceEnabled.value || recorder.recording.value) return
await recorder.startRecording()
if (recorder.error.value) return
if (recorder.stream.value) {
silenceDetector.start(recorder.stream.value, () => stopRecording())
}
}
async function stopRecording() {
silenceDetector.stop()
if (!recorder.recording.value) return
transcribingVoice.value = true
try {
const blob = await recorder.stopRecording()
const { transcript } = await transcribeAudio(blob)
// Pass last assistant message as context to reduce STT mishearings
const msgs = store.currentConversation?.messages ?? []
const lastAssistant = [...msgs].reverse().find(m => m.role === 'assistant')?.content
const { transcript } = await transcribeAudio(blob, lastAssistant)
if (transcript.trim()) {
messageInput.value = transcript.trim()
await nextTick()
@@ -208,13 +227,10 @@ defineExpose({ focus, prefill })
v-if="voiceEnabled && recorder.isSupported"
class="btn-icon btn-mic"
:class="{ 'mic-recording': recorder.recording.value, 'mic-transcribing': transcribingVoice }"
@mousedown.prevent="startPtt"
@mouseup.prevent="stopPtt"
@touchstart.prevent="startPtt"
@touchend.prevent="stopPtt"
@click.prevent="toggleVoice"
:disabled="transcribingVoice || !store.chatReady"
:title="recorder.recording.value ? 'Release to send' : 'Hold to speak'"
aria-label="Push to talk"
:title="recorder.recording.value ? 'Click to stop (or wait for silence)' : 'Click to speak'"
:aria-label="recorder.recording.value ? 'Stop recording' : 'Start recording'"
>
<svg v-if="!transcribingVoice" width="17" height="17" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm-1-9c0-.55.45-1 1-1s1 .45 1 1v6c0 .55-.45 1-1 1s-1-.45-1-1V5zm6 6c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z"/>