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:
2026-02-15 19:33:29 -06:00
parent ffceb2efc2
commit 68edc01d44
8 changed files with 1741 additions and 1013 deletions

View File

@@ -0,0 +1,185 @@
<script setup lang="ts">
import type { Agent, ConversationEntry } from '../../types/agent'
defineProps<{
agent: Agent
}>()
const mockEntries: ConversationEntry[] = [
{
id: '1',
role: 'user',
content: 'Revisa el módulo de autenticación y encuentra los errores de validación de tokens.',
timestamp: '14:32',
method: 'voice'
},
{
id: '2',
role: 'agent',
content: 'Encontré 3 issues en el validador de tokens JWT: expiración no verificada en refresh, falta sanitización del header Authorization, y el middleware no maneja tokens revocados.',
timestamp: '14:33',
method: 'text'
},
{
id: '3',
role: 'user',
content: 'Corrige los tres problemas y agrega tests unitarios para cada caso.',
timestamp: '14:35',
method: 'text'
},
{
id: '4',
role: 'agent',
content: 'Corregidos los 3 issues. Se agregaron 8 tests unitarios cubriendo cada caso: token expirado, header malformado, token revocado, y sus variantes edge-case. Todos los tests pasan.',
timestamp: '14:38',
method: 'text'
},
{
id: '5',
role: 'user',
content: 'Ahora implementa un sistema de rate limiting para la API de login con un máximo de 5 intentos por minuto.',
timestamp: '15:01',
method: 'voice'
},
{
id: '6',
role: 'agent',
content: 'Rate limiter implementado usando sliding window en Redis. Configurado a 5 intentos/minuto por IP. Retorna HTTP 429 con header Retry-After cuando se excede el límite.',
timestamp: '15:04',
method: 'text'
}
]
</script>
<template>
<div class="conversation-history">
<div class="history-header">
<span class="history-title">Historial</span>
<span class="history-count">{{ mockEntries.length }}</span>
</div>
<div class="history-list">
<div
v-for="entry in mockEntries"
:key="entry.id"
class="history-entry"
:class="entry.role"
>
<div class="entry-meta">
<span class="role-badge" :class="entry.role">
{{ entry.role === 'user' ? 'Tú' : agent.uiConfig?.shortLabel || 'AG' }}
</span>
<span class="entry-time">{{ entry.timestamp }}</span>
<!-- Mic icon for voice entries -->
<svg v-if="entry.method === 'voice'" class="method-icon" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/>
<path d="M19 10v2a7 7 0 0 1-14 0v-2"/>
<line x1="12" y1="19" x2="12" y2="23"/>
<line x1="8" y1="23" x2="16" y2="23"/>
</svg>
</div>
<div class="entry-content">{{ entry.content }}</div>
</div>
</div>
</div>
</template>
<style scoped>
.conversation-history {
margin-top: 8px;
animation: slide-in 0.2s ease-out;
}
.history-header {
display: flex;
align-items: center;
gap: 8px;
padding-bottom: 8px;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
margin-bottom: 8px;
}
.history-title {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: rgba(255, 255, 255, 0.4);
}
.history-count {
font-size: 10px;
font-weight: 700;
color: rgba(255, 255, 255, 0.3);
background: rgba(255, 255, 255, 0.06);
padding: 1px 6px;
border-radius: 8px;
}
.history-list {
max-height: 300px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 8px;
scrollbar-width: thin;
scrollbar-color: rgba(255, 255, 255, 0.1) transparent;
}
.history-entry {
padding: 8px 10px;
border-radius: 8px;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.04);
}
.history-entry.agent {
background: rgba(139, 92, 246, 0.06);
border-color: rgba(139, 92, 246, 0.1);
}
.entry-meta {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.role-badge {
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.3px;
padding: 1px 6px;
border-radius: 4px;
}
.role-badge.user {
color: rgba(59, 130, 246, 0.9);
background: rgba(59, 130, 246, 0.12);
}
.role-badge.agent {
color: rgba(139, 92, 246, 0.9);
background: rgba(139, 92, 246, 0.12);
}
.entry-time {
font-size: 10px;
color: rgba(255, 255, 255, 0.25);
}
.method-icon {
color: rgba(255, 255, 255, 0.25);
}
.entry-content {
font-size: 12px;
line-height: 1.5;
color: rgba(255, 255, 255, 0.7);
}
@keyframes slide-in {
from { opacity: 0; transform: translateY(-8px); }
to { opacity: 1; transform: translateY(0); }
}
</style>