- 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
1207 lines
33 KiB
Vue
1207 lines
33 KiB
Vue
<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>
|