Files
agent-ui/frontend/src/components/FloatingResponse.vue
josedario87 2133e2d057 refactor: Improve voice and response modals UX
- Center both modals with dark backdrop and blur effect
- Make voice modal larger (420px) with bigger record button
- Make response modal larger (540px) with bigger text (18px)
- Remove auto-dismiss from bubbles - manual dismiss only
- Add backdrop click to close response modal
- Remove unused bottom sheet code from FloatingVoice
- Add touch protection CSS to prevent text selection
- Clean up mobile-specific variables no longer needed
2026-02-14 05:07:27 -06:00

353 lines
7.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
import { ref, computed } from 'vue'
export interface ResponseMessage {
id: string
message: string
type: 'info' | 'success' | 'warning' | 'error'
timestamp: number
}
const messages = ref<ResponseMessage[]>([])
const isVisible = computed(() => messages.value.length > 0)
function addMessage(message: string, type: ResponseMessage['type'] = 'info') {
const id = `msg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
messages.value.push({
id,
message,
type,
timestamp: Date.now()
})
// No auto-dismiss - user must dismiss each message manually
return id
}
function removeMessage(id: string) {
const index = messages.value.findIndex(m => m.id === id)
if (index !== -1) {
messages.value.splice(index, 1)
}
}
function clearAll() {
messages.value = []
}
function getTypeIcon(type: ResponseMessage['type']) {
switch (type) {
case 'success': return '✓'
case 'warning': return '⚠'
case 'error': return '✕'
default: return ''
}
}
function getTypeColor(type: ResponseMessage['type']) {
switch (type) {
case 'success': return '#10b981'
case 'warning': return '#f59e0b'
case 'error': return '#ef4444'
default: return '#6366f1'
}
}
// Expose controls for MCP tools
defineExpose({
addMessage,
removeMessage,
clearAll,
getMessages: () => messages.value
})
</script>
<template>
<Teleport to="body">
<!-- Backdrop -->
<Transition name="backdrop-fade">
<div v-if="isVisible" class="response-backdrop" @click="clearAll"></div>
</Transition>
<Transition name="bubble-slide">
<div
v-if="isVisible"
class="floating-response"
>
<div class="response-glass">
<!-- Header -->
<div class="response-header">
<div class="header-left">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
</svg>
<span>Agent Response</span>
<span class="message-count">{{ messages.length }}</span>
</div>
<button class="close-btn" @click="clearAll" title="Dismiss all">
<svg width="10" height="10" viewBox="0 0 10 10">
<line x1="0" y1="0" x2="10" y2="10" stroke="currentColor" stroke-width="1.5"/>
<line x1="10" y1="0" x2="0" y2="10" stroke="currentColor" stroke-width="1.5"/>
</svg>
</button>
</div>
<!-- Messages -->
<div class="messages-container">
<TransitionGroup name="message">
<div
v-for="msg in messages"
:key="msg.id"
class="message-item"
:style="{ '--type-color': getTypeColor(msg.type) }"
>
<span class="type-icon" :class="msg.type">{{ getTypeIcon(msg.type) }}</span>
<span class="message-text">{{ msg.message }}</span>
<button class="dismiss-btn" @click="removeMessage(msg.id)" title="Dismiss">
<svg width="10" height="10" viewBox="0 0 10 10">
<line x1="0" y1="0" x2="10" y2="10" stroke="currentColor" stroke-width="1.5"/>
<line x1="10" y1="0" x2="0" y2="10" stroke="currentColor" stroke-width="1.5"/>
</svg>
</button>
</div>
</TransitionGroup>
</div>
</div>
</div>
</Transition>
</Teleport>
</template>
<style scoped>
/* Backdrop */
.response-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 10009;
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
}
.backdrop-fade-enter-active,
.backdrop-fade-leave-active {
transition: opacity 0.2s ease;
}
.backdrop-fade-enter-from,
.backdrop-fade-leave-to {
opacity: 0;
}
.floating-response {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 540px;
max-width: 92vw;
max-height: 80vh;
z-index: 10010;
}
.response-glass {
display: flex;
flex-direction: column;
max-height: 80vh;
background: rgba(200, 215, 235, 0.35);
backdrop-filter: blur(24px) saturate(1.6);
-webkit-backdrop-filter: blur(24px) saturate(1.6);
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.6);
box-shadow:
0 0 0 1px rgba(80, 120, 180, 0.25),
0 12px 40px rgba(0, 0, 0, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.6);
overflow: hidden;
}
.response-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: rgba(255, 255, 255, 0.25);
border-bottom: 1px solid rgba(255, 255, 255, 0.3);
user-select: none;
flex-shrink: 0;
}
.header-left {
display: flex;
align-items: center;
gap: 8px;
color: #222;
font: 600 13px/1 system-ui, sans-serif;
}
.header-left svg {
opacity: 0.8;
}
.message-count {
background: rgba(99, 102, 241, 0.8);
color: white;
font-size: 10px;
font-weight: 600;
padding: 2px 8px;
border-radius: 10px;
min-width: 20px;
text-align: center;
}
.close-btn {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.4);
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 6px;
color: #555;
cursor: pointer;
transition: all 0.15s ease;
}
.close-btn:hover {
background: linear-gradient(180deg, #e66 0%, #c33 100%);
border-color: #a22;
color: #fff;
}
.messages-container {
padding: 12px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 10px;
flex: 1;
min-height: 100px;
}
.message-item {
display: flex;
align-items: flex-start;
gap: 14px;
padding: 18px 20px;
background: rgba(255, 255, 255, 0.7);
border-radius: 12px;
border-left: 5px solid var(--type-color);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
transition: all 0.15s ease;
}
.message-item:hover {
background: rgba(255, 255, 255, 0.8);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
}
.type-icon {
flex-shrink: 0;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
font-size: 16px;
font-weight: 700;
color: white;
background: var(--type-color);
margin-top: 2px;
}
.message-text {
flex: 1;
font-size: 18px;
line-height: 1.6;
color: #111;
word-break: break-word;
font-weight: 500;
letter-spacing: -0.01em;
}
.dismiss-btn {
flex-shrink: 0;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.05);
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 6px;
color: #666;
cursor: pointer;
transition: all 0.15s ease;
/* Prevent text selection */
-webkit-user-select: none;
user-select: none;
-webkit-touch-callout: none;
touch-action: manipulation;
}
.dismiss-btn:hover {
background: linear-gradient(180deg, #e66 0%, #c33 100%);
border-color: #a22;
color: #fff;
}
/* Scrollbar */
.messages-container::-webkit-scrollbar {
width: 8px;
}
.messages-container::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.05);
border-radius: 4px;
}
.messages-container::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.2);
border-radius: 4px;
}
.messages-container::-webkit-scrollbar-thumb:hover {
background: rgba(0, 0, 0, 0.3);
}
/* Animations */
.bubble-slide-enter-active,
.bubble-slide-leave-active {
transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.bubble-slide-enter-from,
.bubble-slide-leave-to {
opacity: 0;
transform: translate(-50%, -50%) scale(0.9);
}
.message-enter-active {
transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.message-leave-active {
transition: all 0.2s ease;
}
.message-enter-from {
opacity: 0;
transform: translateY(-10px) scale(0.95);
}
.message-leave-to {
opacity: 0;
transform: scale(0.9);
}
</style>