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
This commit is contained in:
@@ -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({
|
||||
|
||||
<!-- Content -->
|
||||
<div class="content">
|
||||
<div class="transcript" :class="{ empty: !transcript && !interimTranscript }">
|
||||
<span class="final">{{ transcript }}</span>
|
||||
<div class="transcript" :class="{ empty: !animatedTranscript && !interimTranscript }">
|
||||
<span class="final">{{ animatedTranscript }}</span><span class="cursor" v-if="animatedTranscript && animatedTranscript.length < transcript.length">|</span>
|
||||
<span class="interim">{{ interimTranscript }}</span>
|
||||
<span v-if="!transcript && !interimTranscript" class="placeholder">
|
||||
<span v-if="!animatedTranscript && !interimTranscript" class="placeholder">
|
||||
Presiona el micrófono o mantén Ctrl+Space...
|
||||
</span>
|
||||
</div>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user