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.
This commit is contained in:
113
frontend/src/components/agent/TranscriptCard.vue
Normal file
113
frontend/src/components/agent/TranscriptCard.vue
Normal file
@@ -0,0 +1,113 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user