Files
agent-ui/frontend/src/components/agent/TranscriptCard.vue
josedario87 68edc01d44 refactor: Split AgentBar into modular components with PromptBar chat flow
Extract 1226-line monolithic AgentBar.vue into focused components:
- types/agent.ts: shared types (Agent, AgentStatusState, ClaudeStatus, ConversationEntry)
- agent/FloatBubble.vue: bubble with all status/ejecutor animations, hold detection, recording audio bars
- agent/PromptBar.vue: floating panel with chat conversation, transcript, history
- agent/ChatInput.vue: reusable input row (text, mic, send, history buttons)
- agent/TranscriptCard.vue: typewriter transcription simulation
- agent/ResponseCard.vue: thinking dots + mock response
- agent/ConversationHistory.vue: scrollable mock history entries
- AgentBar.vue: thin orchestrator (~290 lines) keeping WebSocket + status logic

New interaction: click bubble opens PromptBar in text mode, hold opens in
recording mode with audio bar animation on the bubble. Spring enter/blur
exit animations on PromptBar. Text submit shows chat bubbles with mock
agent responses.
2026-02-15 19:33:29 -06:00

114 lines
2.6 KiB
Vue

<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from 'vue'
const props = withDefaults(defineProps<{
typeSpeed?: number
}>(), {
typeSpeed: 30
})
const emit = defineEmits<{
done: [text: string]
}>()
const PLACEHOLDER_TEXT = 'Necesito que revises el componente de autenticación en el módulo de usuarios. ' +
'Hay un problema con la validación de tokens JWT cuando el usuario tiene sesiones múltiples activas. ' +
'El token se invalida correctamente en el servidor pero el cliente sigue usando el token anterior ' +
'hasta que expira naturalmente. Quiero que implementes una verificación en tiempo real usando WebSocket ' +
'para notificar al cliente cuando su token ha sido revocado desde otra sesión.'
const displayedText = ref('')
let intervalId: number | null = null
let charIndex = 0
onMounted(() => {
intervalId = window.setInterval(() => {
if (charIndex < PLACEHOLDER_TEXT.length) {
displayedText.value += PLACEHOLDER_TEXT[charIndex]
charIndex++
} else {
if (intervalId) clearInterval(intervalId)
intervalId = null
emit('done', displayedText.value)
}
}, props.typeSpeed)
})
onBeforeUnmount(() => {
if (intervalId) clearInterval(intervalId)
})
</script>
<template>
<div class="transcript-card">
<div class="transcript-header">
<span class="rec-dot"></span>
<span class="rec-label">Transcribiendo...</span>
</div>
<div class="transcript-body">
<span class="transcript-text">{{ displayedText }}</span>
<span class="blink-cursor">|</span>
</div>
</div>
</template>
<style scoped>
.transcript-card {
background: rgba(239, 68, 68, 0.08);
border: 1px solid rgba(239, 68, 68, 0.2);
border-radius: 10px;
padding: 12px;
margin-top: 8px;
}
.transcript-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
.rec-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #ef4444;
box-shadow: 0 0 8px #ef4444;
animation: rec-pulse 1s ease-in-out infinite;
}
.rec-label {
font-size: 11px;
font-weight: 600;
color: rgba(239, 68, 68, 0.9);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.transcript-body {
font-size: 13px;
line-height: 1.6;
color: rgba(255, 255, 255, 0.85);
}
.transcript-text {
white-space: pre-wrap;
}
.blink-cursor {
color: rgba(255, 255, 255, 0.7);
animation: cursor-blink 0.8s step-end infinite;
font-weight: 300;
}
@keyframes rec-pulse {
0%, 100% { opacity: 1; box-shadow: 0 0 8px #ef4444; }
50% { opacity: 0.4; box-shadow: 0 0 16px #ef4444; }
}
@keyframes cursor-blink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0; }
}
</style>