From 730dbfaf7bf87a4649807d5422e4e5af9534aeaf Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Tue, 14 Apr 2026 22:46:37 -0400 Subject: [PATCH] feat(voice): pulse mic button with live amplitude MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mic button now scales and glows proportional to the silence detector's RMS amplitude instead of sitting static. Gives obvious feedback that audio is actually being picked up — silence still breathes via a 0.1 floor, loud input caps at ~1.18x scale so it doesn't punch through the input bar. Co-Authored-By: Claude Opus 4.6 --- frontend/src/components/ChatInputBar.vue | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/ChatInputBar.vue b/frontend/src/components/ChatInputBar.vue index c8742c1..3527969 100644 --- a/frontend/src/components/ChatInputBar.vue +++ b/frontend/src/components/ChatInputBar.vue @@ -114,6 +114,18 @@ const transcribingVoice = ref(false) const recorder = useVoiceRecorder() const silenceDetector = useSilenceDetector() +// Live mic pulse. `silenceDetector.amplitude` is 0..1 RMS; we floor at 0.1 +// so the button still breathes on silence and cap the scale growth so loud +// input doesn't punch through the input bar. +const micStyle = computed(() => { + if (!recorder.recording.value) return {} + const pulse = 0.1 + Math.min(silenceDetector.amplitude.value, 1) * 0.9 + return { + transform: `scale(${1 + pulse * 0.18})`, + boxShadow: `0 0 ${6 + pulse * 14}px ${2 + pulse * 4}px rgba(239, 68, 68, ${0.2 + pulse * 0.3})`, + } +}) + async function toggleVoice() { if (transcribingVoice.value) return if (recorder.recording.value) { @@ -235,6 +247,7 @@ defineExpose({ focus, prefill }) v-if="voiceEnabled" class="btn-icon btn-mic" :class="{ 'mic-recording': recorder.recording.value, 'mic-transcribing': transcribingVoice }" + :style="micStyle" @click.prevent="toggleVoice" :disabled="transcribingVoice || !store.chatReady" :title="recorder.recording.value ? 'Click to stop (or wait for silence)' : 'Click to speak'" @@ -343,7 +356,14 @@ defineExpose({ focus, prefill }) .btn-icon:hover { opacity: 1; } .btn-icon:disabled { opacity: 0.3; cursor: default; } -.btn-mic.mic-recording { opacity: 1; color: #ef4444; } +.btn-mic.mic-recording { + opacity: 1; + color: #ef4444; + border-radius: 50%; + /* Smooth between amplitude samples (~100ms) so the pulse feels continuous + rather than stepping. */ + transition: transform 0.12s ease-out, box-shadow 0.12s ease-out; +} .btn-mic.mic-transcribing { opacity: 0.5; } .note-picker-wrapper { position: relative; }