diff --git a/frontend/src/App.vue b/frontend/src/App.vue
index 0db3609..86ff228 100644
--- a/frontend/src/App.vue
+++ b/frontend/src/App.vue
@@ -3,7 +3,6 @@ import { onMounted, onUnmounted, ref, watch } from "vue";
import { useRouter } from "vue-router";
import AppHeader from "@/components/AppHeader.vue";
import ToastNotification from "@/components/ToastNotification.vue";
-import VoiceOverlay from "@/components/VoiceOverlay.vue";
import { useTheme } from "@/composables/useTheme";
import { useShortcuts } from "@/composables/useShortcuts";
import { useAuthStore } from "@/stores/auth";
@@ -121,10 +120,6 @@ function onGlobalKeydown(e: KeyboardEvent) {
router.push("/chat");
}
break;
- case " ":
- e.preventDefault();
- document.dispatchEvent(new CustomEvent("voice:ptt-toggle"));
- break;
}
}
@@ -274,10 +269,6 @@ onUnmounted(() => {
Enter
New line
-
- Space
- Tap to speak (voice, when enabled)
-
@@ -287,7 +278,6 @@ onUnmounted(() => {
-
diff --git a/frontend/src/components/ChatInputBar.vue b/frontend/src/components/ChatInputBar.vue
index 218961e..35862ad 100644
--- a/frontend/src/components/ChatInputBar.vue
+++ b/frontend/src/components/ChatInputBar.vue
@@ -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'"
>