diff --git a/frontend/src/components/FloatingVoice.vue b/frontend/src/components/FloatingVoice.vue index 8952a1a..e9cdd00 100644 --- a/frontend/src/components/FloatingVoice.vue +++ b/frontend/src/components/FloatingVoice.vue @@ -23,6 +23,11 @@ const transcript = ref('') const interimTranscript = ref('') const error = ref('') +// Typing animation state +const animatedTranscript = ref('') +let typingTimeout: number | null = null +let lastAnimatedLength = 0 + // Position and drag state const position = ref({ x: 0, y: 0 }) const hasCustomPosition = ref(false) @@ -401,6 +406,12 @@ function stopRecording() { function clearTranscript() { transcript.value = '' interimTranscript.value = '' + animatedTranscript.value = '' + lastAnimatedLength = 0 + if (typingTimeout) { + clearTimeout(typingTimeout) + typingTimeout = null + } } function connectSocket() { @@ -613,6 +624,46 @@ function sendTranscriptAndClose() { typeChar() } +// Typing animation effect +function animateTyping(targetText: string) { + // Clear any pending animation + if (typingTimeout) { + clearTimeout(typingTimeout) + typingTimeout = null + } + + // If new text is shorter, just set it (user cleared or correction) + if (targetText.length < animatedTranscript.value.length) { + animatedTranscript.value = targetText + lastAnimatedLength = targetText.length + return + } + + // Start from where we left off + const startIndex = lastAnimatedLength + + // Type remaining characters one by one + function typeNext(index: number) { + if (index <= targetText.length) { + animatedTranscript.value = targetText.substring(0, index) + lastAnimatedLength = index + + if (index < targetText.length) { + // Faster typing speed: 15-25ms per character + const delay = 15 + Math.random() * 10 + typingTimeout = window.setTimeout(() => typeNext(index + 1), delay) + } + } + } + + typeNext(startIndex) +} + +// Watch transcript changes for typing animation +watch(transcript, (newVal) => { + animateTyping(newVal) +}) + onMounted(async () => { recognition = initRecognition() // Use capture phase to intercept before terminal or other elements @@ -632,6 +683,7 @@ onBeforeUnmount(() => { disconnectSocket() disconnectWhisperSocket() if (chunkInterval) clearInterval(chunkInterval) + if (typingTimeout) clearTimeout(typingTimeout) if (mediaStream) { mediaStream.getTracks().forEach(track => track.stop()) } @@ -711,10 +763,10 @@ defineExpose({