From 9f1e10b8d516280fefac8d3193fb99b23d6a9a26 Mon Sep 17 00:00:00 2001 From: josedario87 Date: Sat, 14 Feb 2026 00:28:26 -0600 Subject: [PATCH] feat: Add typing animation to voice transcription - Text appears letter by letter (15-25ms per character) - Blinking cursor shows while text is animating - Animation continues from last position for new chunks - Smooth visual feedback for transcription progress --- frontend/src/components/FloatingVoice.vue | 69 ++++++++++++++++++++++- 1 file changed, 66 insertions(+), 3 deletions(-) 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({
-
- {{ transcript }} +
+ {{ animatedTranscript }}| {{ interimTranscript }} - + Presiona el micrófono o mantén Ctrl+Space...
@@ -938,6 +990,17 @@ defineExpose({ font-style: italic; } +.transcript .cursor { + color: #4a9; + font-weight: bold; + animation: blink 0.6s infinite; +} + +@keyframes blink { + 0%, 50% { opacity: 1; } + 51%, 100% { opacity: 0; } +} + .transcript .placeholder { color: #888; }