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.
114 lines
2.6 KiB
Vue
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>
|