Files
agent-ui/frontend/src/App.vue
josedario87 f9b5ad3db6 feat: Push-to-talk on voice FAB button
- Hold FAB to open panel and start recording immediately
- Release to stop recording and send after 1s buffer
- Orange pulsing animation when PTT active
- PTT also works on record button inside modal
- Added stopRecordingAndSend exposed method
2026-02-14 04:51:50 -06:00

1207 lines
33 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, onMounted, onUnmounted, watch } from 'vue'
import { RouterView, useRoute, useRouter } from 'vue-router'
import StatusBar from './components/StatusBar.vue'
import Toolbar from './components/Toolbar.vue'
import ComponentsDropdown from './components/ComponentsDropdown.vue'
import ToolsDropdown from './components/ToolsDropdown.vue'
// ConnectionDropdown removed - replaced with debug console
import FloatingTerminal from './components/FloatingTerminal.vue'
import FloatingResponse from './components/FloatingResponse.vue'
import FloatingVoice from './components/FloatingVoice.vue'
import PwaInstallBanner from './components/PwaInstallBanner.vue'
import { initWebMCP, getWebMCP, startTokenPolling, stopTokenPolling, connectWithToken } from './services/webmcp'
import { endpoints } from './config/endpoints'
import { initToolRegistry, activatePageTools, initToolsOnRefresh } from './services/toolRegistry'
import { setTerminalControls } from './services/tools/handlers/terminalHandlers'
import { setResponseControls } from './services/tools/handlers/responseHandlers'
import { useCanvasStore } from './stores/canvas'
const route = useRoute()
const router = useRouter()
const showTerminal = ref(false)
const showVoice = ref(false)
const showDebugConsole = ref(false)
const debugLogs = ref<Array<{ type: string; message: string; time: string }>>([])
// Intercept console.log for debug panel
const originalConsoleLog = console.log
const originalConsoleWarn = console.warn
const originalConsoleError = console.error
function addDebugLog(type: string, args: any[]) {
const message = args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ')
const time = new Date().toLocaleTimeString()
debugLogs.value.push({ type, message, time })
// Keep only last 100 logs
if (debugLogs.value.length > 100) {
debugLogs.value.shift()
}
}
console.log = (...args) => {
originalConsoleLog.apply(console, args)
addDebugLog('log', args)
}
console.warn = (...args) => {
originalConsoleWarn.apply(console, args)
addDebugLog('warn', args)
}
console.error = (...args) => {
originalConsoleError.apply(console, args)
addDebugLog('error', args)
}
function copyDebugLogs() {
const text = debugLogs.value.map(l => `[${l.time}] [${l.type}] ${l.message}`).join('\n')
navigator.clipboard.writeText(text)
canvasStore.showNotification('Logs copied!', 'success')
}
function clearDebugLogs() {
debugLogs.value = []
}
const terminalRef = ref<InstanceType<typeof FloatingTerminal> | null>(null)
const responseRef = ref<InstanceType<typeof FloatingResponse> | null>(null)
const voiceRef = ref<InstanceType<typeof FloatingVoice> | null>(null)
const canvasStore = useCanvasStore()
// Voice FAB push-to-talk state
const voicePTTActive = ref(false)
let voiceTouchStarted = false
let voicePTTTimeout: number | null = null
// Claude status state (for FAB animations)
type ClaudeStatus = 'idle' | 'processing' | 'toolUse' | 'toolDone' | 'reading' | 'writing' | 'sessionStart' | 'subagentStart' | 'subagentStop' | 'notification' | 'permissionRequest' | 'thinking'
const claudeStatus = ref<ClaudeStatus>('idle')
const claudeTool = ref<string | null>(null)
const isProcessing = ref(false) // Main processing state (UserPromptSubmit → Stop)
const isReading = ref(false) // Reading files
const isWriting = ref(false) // Writing files
const hasSubagent = ref(false) // Subagent active
const awaitingPermission = ref(false) // Waiting for user permission (highest priority)
const showSessionStart = ref(false) // Session start animation (3s)
const showNotification = ref(false) // Notification pulse (2s)
const showToolFlash = ref(false) // Tool use flash (500ms)
let statusWs: WebSocket | null = null
let statusReconnectTimeout: number | null = null
let processingTimeout: number | null = null
let sessionStartTimeout: number | null = null
let notificationTimeout: number | null = null
let toolFlashTimeout: number | null = null
function hardRefresh() {
location.reload()
}
// Voice FAB push-to-talk handlers
function handleVoiceFabClick() {
// If touch just ended, ignore click
if (voiceTouchStarted) {
voiceTouchStarted = false
return
}
// Normal click: toggle panel
showVoice.value = !showVoice.value
}
function handleVoiceFabTouchStart(e: TouchEvent) {
e.preventDefault()
voiceTouchStarted = true
voicePTTActive.value = true
// Open panel and start recording
showVoice.value = true
// Wait a moment for panel to open, then start recording
setTimeout(() => {
voiceRef.value?.startRecording()
}, 100)
}
function handleVoiceFabTouchEnd(e: TouchEvent) {
e.preventDefault()
if (!voicePTTActive.value) return
// Add buffer before stopping
voicePTTTimeout = window.setTimeout(() => {
voiceRef.value?.stopRecordingAndSend()
voicePTTActive.value = false
}, 1000)
setTimeout(() => { voiceTouchStarted = false }, 100)
}
function connectStatusWs() {
if (statusWs?.readyState === WebSocket.OPEN) return
statusWs = new WebSocket(endpoints.claudeStatus)
statusWs.onopen = () => {
console.log('[App] Status WebSocket connected')
}
statusWs.onmessage = (event) => {
try {
const msg = JSON.parse(event.data)
if (msg.type === 'claude-status') {
const status = msg.status as ClaudeStatus
console.log('[App] Claude status:', status, msg.tool)
claudeStatus.value = status
claudeTool.value = msg.tool || null
// Handle different status types
switch (status) {
case 'processing':
case 'thinking':
isProcessing.value = true
// Auto-reset after 2 minutes (safety)
if (processingTimeout) clearTimeout(processingTimeout)
processingTimeout = window.setTimeout(() => {
isProcessing.value = false
}, 120000)
break
case 'idle':
isProcessing.value = false
isReading.value = false
isWriting.value = false
awaitingPermission.value = false
if (processingTimeout) clearTimeout(processingTimeout)
break
case 'permissionRequest':
awaitingPermission.value = true
break
case 'reading':
isReading.value = true
triggerToolFlash()
break
case 'writing':
isWriting.value = true
triggerToolFlash()
break
case 'toolUse':
triggerToolFlash()
break
case 'toolDone':
isReading.value = false
isWriting.value = false
awaitingPermission.value = false
break
case 'sessionStart':
showSessionStart.value = true
if (sessionStartTimeout) clearTimeout(sessionStartTimeout)
sessionStartTimeout = window.setTimeout(() => {
showSessionStart.value = false
}, 3000)
break
case 'subagentStart':
hasSubagent.value = true
break
case 'subagentStop':
hasSubagent.value = false
break
case 'notification':
showNotification.value = true
if (notificationTimeout) clearTimeout(notificationTimeout)
notificationTimeout = window.setTimeout(() => {
showNotification.value = false
}, 2000)
break
}
}
} catch { /* ignore non-JSON messages */ }
}
statusWs.onclose = () => {
isProcessing.value = false
statusReconnectTimeout = window.setTimeout(connectStatusWs, 2000)
}
}
function triggerToolFlash() {
showToolFlash.value = true
if (toolFlashTimeout) clearTimeout(toolFlashTimeout)
toolFlashTimeout = window.setTimeout(() => {
showToolFlash.value = false
}, 500)
}
type PageName = 'home' | 'canvas' | 'components' | 'themes' | 'projects' | 'project-canvas' | 'database' | 'source' | 'terminal' | 'tools'
onMounted(async () => {
// Connect to WebSocket for Claude status updates
connectStatusWs()
// Initialize WebMCP connection
await initWebMCP()
// Initialize tool registry with router
initToolRegistry(router)
// Initialize tools for current page (handles refresh)
const currentPage = (route.name as string) || 'canvas'
initToolsOnRefresh(currentPage as PageName)
// Setup terminal controls for MCP tools
setTerminalControls({
open: (x?: number, y?: number) => {
if (terminalRef.value) {
terminalRef.value.open(x, y)
} else {
showTerminal.value = true
}
},
close: () => {
if (terminalRef.value) {
terminalRef.value.close()
} else {
showTerminal.value = false
}
},
toggle: () => {
if (terminalRef.value) {
terminalRef.value.toggle()
} else {
showTerminal.value = !showTerminal.value
}
},
move: (x: number, y: number) => {
terminalRef.value?.move(x, y)
},
resize: (w: number, h: number) => {
terminalRef.value?.resize(w, h)
},
getState: () => {
if (terminalRef.value) {
return terminalRef.value.getState()
}
return { isOpen: showTerminal.value, position: { x: 0, y: 0 }, size: { w: 580, h: 360 } }
}
})
// Setup response controls for MCP tools
setResponseControls({
addMessage: (message: string, type?: 'info' | 'success' | 'warning' | 'error') => {
if (responseRef.value) {
return responseRef.value.addMessage(message, type)
}
return ''
},
removeMessage: (id: string) => {
responseRef.value?.removeMessage(id)
},
clearAll: () => {
responseRef.value?.clearAll()
},
getMessages: () => {
return responseRef.value?.getMessages() || []
},
move: (x: number, y: number) => {
responseRef.value?.move(x, y)
}
})
// Start polling for token if not connected
const webmcp = getWebMCP()
if (!webmcp?.isConnected) {
startTokenPolling(async (token) => {
console.log('[App] Token received, connecting...')
const success = await connectWithToken(token)
if (success) {
canvasStore.showNotification('WebMCP connected!', 'success')
}
})
}
})
onUnmounted(() => {
stopTokenPolling()
if (statusReconnectTimeout) clearTimeout(statusReconnectTimeout)
if (processingTimeout) clearTimeout(processingTimeout)
if (sessionStartTimeout) clearTimeout(sessionStartTimeout)
if (notificationTimeout) clearTimeout(notificationTimeout)
if (toolFlashTimeout) clearTimeout(toolFlashTimeout)
statusWs?.close()
})
// Watch for route changes and update tools
watch(() => route.name, (newPage) => {
if (newPage) {
activatePageTools(newPage as PageName)
}
})
</script>
<template>
<div class="app-container">
<header class="app-header">
<div class="header-left">
<h1 class="logo">Agent UI</h1>
<button class="debug-btn" :class="{ active: showDebugConsole }" @click="showDebugConsole = !showDebugConsole" title="Debug Console">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/>
</svg>
<span v-if="debugLogs.length" class="log-count">{{ debugLogs.length }}</span>
</button>
<ComponentsDropdown />
<ToolsDropdown />
<PwaInstallBanner />
</div>
<button class="refresh-btn" @click="hardRefresh" title="Hard refresh (Ctrl+F5)">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/>
<path d="M21 3v5h-5"/>
</svg>
</button>
<StatusBar />
</header>
<main class="app-main">
<Toolbar />
<RouterView v-slot="{ Component }">
<Transition name="page" mode="out-in">
<component :is="Component" />
</Transition>
</RouterView>
</main>
<!-- Floating Terminal Toggle Button (Claude Life FAB) -->
<button
class="terminal-fab"
:class="{
active: showTerminal,
processing: isProcessing,
reading: isReading,
writing: isWriting,
subagent: hasSubagent,
permission: awaitingPermission,
'session-start': showSessionStart,
notification: showNotification,
'tool-flash': showToolFlash,
'sheet-open': showTerminal || showVoice
}"
@click="showTerminal = !showTerminal"
:title="awaitingPermission ? `Permiso requerido: ${claudeTool || 'herramienta'}` : isProcessing ? `Claude: ${claudeTool || 'processing'}` : 'Toggle Terminal'"
>
<!-- Subagent orbital ring -->
<svg v-if="hasSubagent" class="orbital-ring" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="45" fill="none" stroke="currentColor" stroke-width="2" stroke-dasharray="20 10" />
</svg>
<!-- Session start ripples -->
<div v-if="showSessionStart" class="session-ripples">
<span></span>
<span></span>
<span></span>
</div>
<!-- Permission request icon (alert/hand) - highest priority -->
<svg v-if="awaitingPermission" class="permission-icon" xmlns="http://www.w3.org/2000/svg" width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
<line x1="12" y1="9" x2="12" y2="13"/>
<line x1="12" y1="17" x2="12.01" y2="17"/>
</svg>
<!-- Processing animation (three dots) -->
<div v-else-if="isProcessing && !isReading && !isWriting" class="thinking-dots">
<span></span>
<span></span>
<span></span>
</div>
<!-- Reading icon (eye) -->
<svg v-else-if="isReading" class="status-icon" xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
<circle cx="12" cy="12" r="3"/>
</svg>
<!-- Writing icon (pencil) -->
<svg v-else-if="isWriting" class="status-icon" xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 19l7-7 3 3-7 7-3-3z"/>
<path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/>
<path d="M2 2l7.586 7.586"/>
<circle cx="11" cy="11" r="2"/>
</svg>
<!-- Normal icons -->
<template v-else>
<!-- Terminal closed: Nucleo atom icon -->
<svg v-if="!showTerminal" class="nucleo-icon" width="28" height="28" viewBox="0 0 24 24" fill="none">
<!-- Core nucleus -->
<circle cx="12" cy="12" r="3.5" fill="white" opacity="0.95"/>
<!-- Orbital rings -->
<ellipse cx="12" cy="12" rx="8" ry="3.5" stroke="white" stroke-width="1.3" fill="none" opacity="0.7" transform="rotate(-30 12 12)"/>
<ellipse cx="12" cy="12" rx="8" ry="3.5" stroke="white" stroke-width="1.3" fill="none" opacity="0.5" transform="rotate(30 12 12)"/>
<ellipse cx="12" cy="12" rx="8" ry="3.5" stroke="white" stroke-width="1.3" fill="none" opacity="0.6" transform="rotate(90 12 12)"/>
<!-- Electrons -->
<circle cx="12" cy="4" r="1.8" fill="white" opacity="0.9"/>
<circle cx="19" cy="14" r="1.8" fill="white" opacity="0.7"/>
<circle cx="5" cy="14" r="1.8" fill="white" opacity="0.8"/>
</svg>
<!-- Terminal open: Minimize chevron with connection indicator -->
<template v-else>
<span class="connection-dot"></span>
<svg class="minimize-icon" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</template>
</template>
</button>
<!-- Voice FAB Button -->
<button
class="voice-fab"
:class="{ active: showVoice, 'sheet-open': showTerminal || showVoice, 'ptt-active': voicePTTActive }"
@click="handleVoiceFabClick"
@touchstart="handleVoiceFabTouchStart"
@touchend="handleVoiceFabTouchEnd"
@touchcancel="handleVoiceFabTouchEnd"
title="Voice Input (mantén presionado para PTT)"
>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" 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>
</button>
<!-- Floating Terminal -->
<FloatingTerminal ref="terminalRef" v-model="showTerminal" />
<!-- Floating Response (Agent UI messages) -->
<FloatingResponse ref="responseRef" />
<!-- Floating Voice Input -->
<FloatingVoice ref="voiceRef" v-model="showVoice" />
<!-- Debug Console Panel -->
<Teleport to="body">
<Transition name="debug-slide">
<div v-if="showDebugConsole" class="debug-console">
<div class="debug-header">
<span>Debug Console ({{ debugLogs.length }})</span>
<div class="debug-actions">
<button @click="copyDebugLogs" title="Copy all">Copy</button>
<button @click="clearDebugLogs" title="Clear">Clear</button>
<button @click="showDebugConsole = false" title="Close">×</button>
</div>
</div>
<div class="debug-logs">
<div
v-for="(log, i) in debugLogs"
:key="i"
class="debug-log"
:class="log.type"
@click="navigator.clipboard.writeText(log.message)"
>
<span class="log-time">{{ log.time }}</span>
<span class="log-type">{{ log.type }}</span>
<span class="log-msg">{{ log.message }}</span>
</div>
<div v-if="!debugLogs.length" class="debug-empty">No logs yet</div>
</div>
</div>
</Transition>
</Teleport>
</div>
</template>
<style scoped>
.app-container {
display: flex;
flex-direction: column;
height: 100vh;
height: 100dvh;
background: var(--bg-primary);
}
.app-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem 1.5rem;
padding-top: calc(0.75rem + env(safe-area-inset-top, 0px));
padding-left: calc(1.5rem + env(safe-area-inset-left, 0px));
padding-right: calc(1.5rem + env(safe-area-inset-right, 0px));
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
flex-shrink: 0;
}
.header-left {
display: flex;
align-items: center;
gap: 1.5rem;
}
.logo {
font-size: 1.25rem;
font-weight: 600;
color: var(--text-primary);
margin: 0;
}
.refresh-btn {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
padding: 0;
background: transparent;
border: 1px solid var(--border-color);
border-radius: 6px;
color: var(--text-secondary);
cursor: pointer;
transition: all 0.15s ease;
}
.refresh-btn:hover {
background: var(--bg-hover);
color: var(--accent);
border-color: var(--accent);
}
.refresh-btn:active {
transform: rotate(180deg);
}
.app-main {
display: flex;
flex: 1;
overflow: hidden;
}
/* Page transitions */
.page-enter-active,
.page-leave-active {
transition: opacity 0.2s ease, transform 0.2s ease;
}
.page-enter-from {
opacity: 0;
transform: translateX(20px);
}
.page-leave-to {
opacity: 0;
transform: translateX(-20px);
}
/* Terminal FAB - Claude Life */
.terminal-fab {
position: fixed;
bottom: 20px;
right: 20px;
width: 58px;
height: 58px;
border-radius: 18px;
background: linear-gradient(145deg, #7c3aed 0%, #6366f1 50%, #8b5cf6 100%);
color: white;
border: 2px solid rgba(255, 255, 255, 0.15);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow:
0 4px 15px rgba(124, 58, 237, 0.4),
0 8px 30px rgba(99, 102, 241, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.2);
transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
z-index: 9998;
overflow: visible;
backdrop-filter: blur(10px);
}
.terminal-fab::before {
content: '';
position: absolute;
inset: -3px;
border-radius: 21px;
background: linear-gradient(145deg, rgba(139, 92, 246, 0.5), rgba(99, 102, 241, 0.2));
z-index: -1;
opacity: 0;
transition: opacity 0.3s ease;
}
.terminal-fab:hover {
transform: translateY(-3px) scale(1.05);
box-shadow:
0 8px 25px rgba(124, 58, 237, 0.5),
0 15px 40px rgba(99, 102, 241, 0.35),
inset 0 1px 0 rgba(255, 255, 255, 0.25);
}
.terminal-fab:hover::before {
opacity: 1;
}
/* Nucleo atom icon animation */
.nucleo-icon {
animation: nucleo-orbit 8s linear infinite;
filter: drop-shadow(0 0 6px rgba(255, 255, 255, 0.6));
}
.nucleo-icon ellipse {
transform-origin: center;
}
/* Terminal active - Connected state */
.terminal-fab.active {
background: linear-gradient(145deg, #6366f1 0%, #4f46e5 50%, #7c3aed 100%);
border-color: rgba(16, 185, 129, 0.5);
box-shadow:
0 4px 15px rgba(99, 102, 241, 0.4),
0 8px 30px rgba(79, 70, 229, 0.3),
0 0 20px rgba(16, 185, 129, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.2);
transform: none;
}
.terminal-fab.active:hover {
transform: translateY(-2px);
box-shadow:
0 6px 20px rgba(99, 102, 241, 0.5),
0 12px 35px rgba(79, 70, 229, 0.35),
0 0 25px rgba(16, 185, 129, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.25);
}
/* Connection indicator dot */
.connection-dot {
position: absolute;
top: 8px;
right: 8px;
width: 10px;
height: 10px;
background: #10b981;
border-radius: 50%;
border: 2px solid rgba(255, 255, 255, 0.9);
box-shadow: 0 0 8px rgba(16, 185, 129, 0.8);
animation: connection-pulse 2s ease-in-out infinite;
}
/* Minimize icon */
.minimize-icon {
opacity: 0.9;
transition: transform 0.2s ease;
}
.terminal-fab.active:hover .minimize-icon {
transform: translateY(2px);
}
/* Processing state (UserPromptSubmit → Stop) - Orange pulsing */
.terminal-fab.processing {
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%) !important;
box-shadow: 0 8px 24px rgba(245, 158, 11, 0.4) !important;
animation: processing-pulse 2s ease-in-out infinite !important;
transform: rotate(0deg) !important;
}
.terminal-fab.processing:hover {
box-shadow: 0 12px 32px rgba(245, 158, 11, 0.5) !important;
transform: scale(1.05) !important;
}
/* Reading state - Cyan scanning */
.terminal-fab.reading {
background: linear-gradient(135deg, #06b6d4 0%, #0891b2 100%) !important;
box-shadow: 0 8px 24px rgba(6, 182, 212, 0.4) !important;
animation: reading-scan 1.5s ease-in-out infinite !important;
}
/* Writing state - Green pulse */
.terminal-fab.writing {
background: linear-gradient(135deg, #10b981 0%, #059669 100%) !important;
box-shadow: 0 8px 24px rgba(16, 185, 129, 0.4) !important;
animation: writing-pulse 0.8s ease-in-out infinite !important;
}
/* Subagent active - Purple with orbital ring */
.terminal-fab.subagent {
box-shadow: 0 8px 24px rgba(139, 92, 246, 0.5) !important;
}
/* Session start - Green wake-up effect */
.terminal-fab.session-start {
animation: session-wake 3s ease-out forwards !important;
}
/* Notification - Yellow bounce */
.terminal-fab.notification {
animation: notification-bounce 0.5s ease-in-out 4 !important;
}
/* Permission Request - HIGHEST PRIORITY - Red pulsing alert */
.terminal-fab.permission {
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%) !important;
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7) !important;
animation: permission-pulse 1s ease-in-out infinite !important;
transform: rotate(0deg) scale(1.1) !important;
z-index: 10000 !important;
}
.terminal-fab.permission:hover {
transform: scale(1.15) !important;
}
/* Permission icon animation */
.permission-icon {
animation: permission-shake 0.5s ease-in-out infinite;
filter: drop-shadow(0 0 8px rgba(255, 255, 255, 0.8));
}
/* Tool flash - Quick white flash */
.terminal-fab.tool-flash::after {
content: '';
position: absolute;
inset: -4px;
border-radius: 20px;
background: rgba(255, 255, 255, 0.4);
animation: tool-flash 0.5s ease-out forwards;
pointer-events: none;
}
/* Thinking dots animation */
.thinking-dots {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
}
.thinking-dots span {
width: 6px;
height: 6px;
background: white;
border-radius: 50%;
animation: thinking-dot 1.4s ease-in-out infinite;
}
.thinking-dots span:nth-child(1) { animation-delay: 0s; }
.thinking-dots span:nth-child(2) { animation-delay: 0.2s; }
.thinking-dots span:nth-child(3) { animation-delay: 0.4s; }
/* Status icons */
.status-icon {
animation: icon-breathe 1s ease-in-out infinite;
}
/* Orbital ring for subagent */
.orbital-ring {
position: absolute;
width: 80px;
height: 80px;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
animation: orbit-spin 3s linear infinite;
color: rgba(139, 92, 246, 0.8);
pointer-events: none;
}
/* Session start ripples */
.session-ripples {
position: absolute;
inset: 0;
pointer-events: none;
}
.session-ripples span {
position: absolute;
inset: -10px;
border: 2px solid rgba(16, 185, 129, 0.6);
border-radius: 20px;
animation: ripple-expand 3s ease-out forwards;
}
.session-ripples span:nth-child(1) { animation-delay: 0s; }
.session-ripples span:nth-child(2) { animation-delay: 0.5s; }
.session-ripples span:nth-child(3) { animation-delay: 1s; }
/* Keyframes */
@keyframes thinking-dot {
0%, 80%, 100% { transform: scale(0.6); opacity: 0.5; }
40% { transform: scale(1); opacity: 1; }
}
@keyframes processing-pulse {
0%, 100% { box-shadow: 0 8px 24px rgba(245, 158, 11, 0.4); }
50% { box-shadow: 0 8px 40px rgba(245, 158, 11, 0.7); }
}
@keyframes reading-scan {
0%, 100% {
box-shadow: 0 8px 24px rgba(6, 182, 212, 0.4);
transform: rotate(0deg);
}
25% { transform: rotate(-2deg); }
75% { transform: rotate(2deg); }
50% {
box-shadow: 0 8px 40px rgba(6, 182, 212, 0.7);
}
}
@keyframes writing-pulse {
0%, 100% {
transform: scale(1);
box-shadow: 0 8px 24px rgba(16, 185, 129, 0.4);
}
50% {
transform: scale(1.05);
box-shadow: 0 8px 32px rgba(16, 185, 129, 0.6);
}
}
@keyframes session-wake {
0% {
transform: scale(0.8);
opacity: 0.5;
box-shadow: 0 0 0 rgba(16, 185, 129, 0);
}
30% {
transform: scale(1.15);
box-shadow: 0 0 60px rgba(16, 185, 129, 0.6);
}
60% { transform: scale(0.95); }
100% {
transform: scale(1);
opacity: 1;
box-shadow: 0 8px 24px rgba(99, 102, 241, 0.4);
}
}
@keyframes notification-bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-8px); }
}
@keyframes tool-flash {
0% { opacity: 1; transform: scale(1); }
100% { opacity: 0; transform: scale(1.3); }
}
@keyframes icon-breathe {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
@keyframes orbit-spin {
from { transform: translate(-50%, -50%) rotate(0deg); }
to { transform: translate(-50%, -50%) rotate(360deg); }
}
@keyframes ripple-expand {
0% {
transform: scale(1);
opacity: 1;
}
100% {
transform: scale(2);
opacity: 0;
}
}
@keyframes permission-pulse {
0%, 100% {
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7), 0 8px 24px rgba(239, 68, 68, 0.5);
transform: scale(1.1);
}
50% {
box-shadow: 0 0 0 15px rgba(239, 68, 68, 0), 0 8px 40px rgba(239, 68, 68, 0.8);
transform: scale(1.15);
}
}
@keyframes permission-shake {
0%, 100% { transform: rotate(0deg); }
25% { transform: rotate(-5deg); }
75% { transform: rotate(5deg); }
}
@keyframes nucleo-orbit {
0% {
transform: rotate(0deg);
filter: drop-shadow(0 0 6px rgba(255, 255, 255, 0.6));
}
50% {
filter: drop-shadow(0 0 10px rgba(255, 255, 255, 0.9));
}
100% {
transform: rotate(360deg);
filter: drop-shadow(0 0 6px rgba(255, 255, 255, 0.6));
}
}
@keyframes connection-pulse {
0%, 100% {
box-shadow: 0 0 8px rgba(16, 185, 129, 0.8);
transform: scale(1);
}
50% {
box-shadow: 0 0 12px rgba(16, 185, 129, 1), 0 0 20px rgba(16, 185, 129, 0.4);
transform: scale(1.1);
}
}
/* Voice FAB */
.voice-fab {
position: fixed;
bottom: 20px;
left: 20px;
width: 48px;
height: 48px;
border-radius: 50%;
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
color: white;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 6px 20px rgba(16, 185, 129, 0.4);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
z-index: 9998;
}
.voice-fab:hover {
transform: scale(1.08);
box-shadow: 0 8px 24px rgba(16, 185, 129, 0.5);
}
.voice-fab.active {
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
box-shadow: 0 6px 20px rgba(239, 68, 68, 0.4);
}
.voice-fab.active:hover {
box-shadow: 0 8px 24px rgba(239, 68, 68, 0.5);
}
/* Voice FAB PTT active - recording in progress */
.voice-fab.ptt-active {
background: linear-gradient(135deg, #f97316 0%, #ea580c 100%);
box-shadow: 0 0 30px rgba(249, 115, 22, 0.7);
transform: scale(1.15);
animation: ptt-pulse 0.5s ease-in-out infinite;
}
@keyframes ptt-pulse {
0%, 100% { box-shadow: 0 0 30px rgba(249, 115, 22, 0.7); }
50% { box-shadow: 0 0 50px rgba(249, 115, 22, 0.9); }
}
@media (max-width: 768px) {
.terminal-fab {
bottom: 16px;
right: 16px;
width: 54px;
height: 54px;
border-radius: 16px;
}
.terminal-fab::before {
border-radius: 19px;
}
.connection-dot {
width: 8px;
height: 8px;
top: 6px;
right: 6px;
}
.orbital-ring {
width: 70px;
height: 70px;
}
.voice-fab {
bottom: 16px;
left: 16px;
width: 44px;
height: 44px;
}
}
/* Mobile: FABs above bottom sheets */
@media (max-width: 1024px) and (pointer: coarse) {
.terminal-fab,
.voice-fab {
z-index: 10001;
transition: bottom 0.25s cubic-bezier(0.4, 0, 0.2, 1), transform 0.3s ease;
}
.terminal-fab.sheet-open,
.voice-fab.sheet-open {
bottom: calc(10vh + 16px);
}
}
/* Debug Console Button */
.debug-btn {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
background: rgba(100, 100, 100, 0.2);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 6px;
color: #888;
font-size: 11px;
cursor: pointer;
transition: all 0.2s;
}
.debug-btn:hover {
background: rgba(100, 100, 100, 0.3);
color: #aaa;
}
.debug-btn.active {
background: rgba(59, 130, 246, 0.3);
border-color: rgba(59, 130, 246, 0.5);
color: #60a5fa;
}
.log-count {
background: #ef4444;
color: white;
font-size: 9px;
padding: 1px 5px;
border-radius: 10px;
min-width: 16px;
text-align: center;
}
/* Debug Console Panel */
.debug-console {
position: fixed;
top: 50px;
left: 10px;
right: 10px;
bottom: 80px;
background: rgba(20, 20, 25, 0.98);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
z-index: 10002;
display: flex;
flex-direction: column;
backdrop-filter: blur(10px);
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
}
.debug-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 14px;
background: rgba(255, 255, 255, 0.05);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px 12px 0 0;
color: #ddd;
font-size: 12px;
font-weight: 600;
}
.debug-actions {
display: flex;
gap: 8px;
}
.debug-actions button {
padding: 4px 10px;
background: rgba(255, 255, 255, 0.1);
border: none;
border-radius: 4px;
color: #aaa;
font-size: 11px;
cursor: pointer;
}
.debug-actions button:hover {
background: rgba(255, 255, 255, 0.2);
color: #fff;
}
.debug-logs {
flex: 1;
overflow-y: auto;
padding: 8px;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 11px;
}
.debug-log {
display: flex;
gap: 8px;
padding: 6px 8px;
border-radius: 4px;
cursor: pointer;
border-bottom: 1px solid rgba(255, 255, 255, 0.03);
}
.debug-log:hover {
background: rgba(255, 255, 255, 0.05);
}
.debug-log:active {
background: rgba(59, 130, 246, 0.2);
}
.log-time {
color: #666;
flex-shrink: 0;
}
.log-type {
width: 40px;
flex-shrink: 0;
font-weight: 600;
}
.debug-log.log .log-type { color: #888; }
.debug-log.warn .log-type { color: #f59e0b; }
.debug-log.error .log-type { color: #ef4444; }
.log-msg {
color: #ccc;
word-break: break-all;
}
.debug-log.warn .log-msg { color: #fcd34d; }
.debug-log.error .log-msg { color: #fca5a5; }
.debug-empty {
color: #666;
text-align: center;
padding: 40px;
}
/* Debug slide animation */
.debug-slide-enter-active,
.debug-slide-leave-active {
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
.debug-slide-enter-from,
.debug-slide-leave-to {
opacity: 0;
transform: translateY(-20px);
}
</style>