- 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
353 lines
7.9 KiB
Vue
353 lines
7.9 KiB
Vue
<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>
|