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; }