From 220d5955686715ccf1c55809761201a57fb6abcc Mon Sep 17 00:00:00 2001 From: josedario87 Date: Fri, 20 Feb 2026 12:12:53 -0600 Subject: [PATCH] feat: voice mic, pixel life layer, enhanced transcript-debug UX VoiceMicButton component, PixelLife aquatic layer, improved UserMessageBubble with voice display, AgentBadge terminal switcher, ChatContainer voice integration, FloatingTranscriptDebug ocean life enhancements, and terminal registry support. Remove traefik config. --- frontend/src/App.vue | 31 +- frontend/src/components/FloatingTerminal.vue | 14 +- .../components/FloatingTranscriptDebug.vue | 469 +++++++++++++++++- .../transcript-debug/AgentBadge.vue | 141 +++++- .../transcript-debug/ChatContainer.vue | 209 ++++++++ .../components/transcript-debug/UserInput.vue | 48 +- .../transcript-debug/UserMessageBubble.vue | 268 +++++++++- .../transcript-debug/VoiceMicButton.vue | 191 +++++++ .../aquaticBackground/AquaticBackground.vue | 2 + .../aquaticBackground/layers/PixelLife.vue | 356 +++++++++++++ .../aquaticBackground/layers/index.ts | 1 + .../src/components/transcript-debug/index.ts | 1 + .../transcript-debug/useTranscriptDebug.ts | 385 ++++++++++++-- .../src/composables/useEphemeralTerminal.ts | 46 +- frontend/src/pages/TranscriptDebugPage.vue | 32 +- frontend/src/styles/main.css | 9 +- frontend/src/types/transcript-debug.ts | 13 + server/services/terminal.ts | 115 +++++ traefik/agent-ui.yml | 248 --------- 19 files changed, 2221 insertions(+), 358 deletions(-) create mode 100644 frontend/src/components/transcript-debug/VoiceMicButton.vue create mode 100644 frontend/src/components/transcript-debug/aquaticBackground/layers/PixelLife.vue delete mode 100644 traefik/agent-ui.yml diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 1cdbd88..13502f7 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -5,6 +5,7 @@ import Toolbar from './components/Toolbar.vue' import TorchButton from './components/TorchButton.vue' import FloatingTerminal from './components/FloatingTerminal.vue' import FloatingResponse from './components/FloatingResponse.vue' +import { initWhisperSocket } from './services/whisperSocket' import FloatingVoice from './components/FloatingVoice.vue' import FloatingTranscriptDebug from './components/FloatingTranscriptDebug.vue' import AgentBar from './components/AgentBar.vue' @@ -70,6 +71,8 @@ function clearDebugLogs() { const terminalRef = ref | null>(null) const responseRef = ref | null>(null) const voiceRef = ref | null>(null) +const transcriptDebugRef = ref | null>(null) +const mousePos = ref({ x: 0, y: 0 }) const canvasStore = useCanvasStore() const projectCanvasStore = useProjectCanvasStore() const { totalPending, modalVisible, connect: connectApproval, disconnect: disconnectApproval, fetchPending: fetchApprovalPending } = useGlobalApproval() @@ -104,6 +107,21 @@ function hardRefresh() { location.reload() } +function trackMouse(e: MouseEvent) { + mousePos.value = { x: e.clientX, y: e.clientY } +} + +function handleGlobalKeydown(e: KeyboardEvent) { + if (e.ctrlKey && e.key === 'e') { + e.preventDefault() + if (transcriptDebugRef.value) { + transcriptDebugRef.value.openAtCursor(mousePos.value.x, mousePos.value.y) + } else { + showTranscriptDebug.value = !showTranscriptDebug.value + } + } +} + // Voice FAB push-to-talk handlers function handleVoiceFabClick() { // If touch just ended, ignore click @@ -273,6 +291,9 @@ onMounted(async () => { connectApproval() fetchApprovalPending() + // Initialize Whisper WebSocket connection early + initWhisperSocket() + // Fire torch connection early (don't await yet) const torchReady = initTorch() @@ -342,6 +363,12 @@ onMounted(async () => { } }) + // Track mouse for Ctrl+E cursor-based opening + document.addEventListener('mousemove', trackMouse) + + // Global keyboard shortcut: Ctrl+E toggles Transcript Debug + document.addEventListener('keydown', handleGlobalKeydown) + // Detect virtual keyboard on mobile if (window.visualViewport) { const initialHeight = window.visualViewport.height @@ -357,6 +384,8 @@ onMounted(async () => { }) onUnmounted(() => { + document.removeEventListener('mousemove', trackMouse) + document.removeEventListener('keydown', handleGlobalKeydown) destroyTorch() disconnectApproval() if (statusReconnectTimeout) clearTimeout(statusReconnectTimeout) @@ -577,7 +606,7 @@ watch(() => route.name, (newPage) => { - + diff --git a/frontend/src/components/FloatingTerminal.vue b/frontend/src/components/FloatingTerminal.vue index 305baed..f69a848 100644 --- a/frontend/src/components/FloatingTerminal.vue +++ b/frontend/src/components/FloatingTerminal.vue @@ -85,13 +85,6 @@ const renderer = useTerminalRenderer({ } }, onKeyEvent: (e) => { - // Ctrl+E: Toggle terminal - if (e.ctrlKey && e.key === 'e') { - e.preventDefault() - toggleTerminal() - return false - } - // Ctrl+V: Paste from clipboard if (e.ctrlKey && e.key === 'v' && e.type === 'keydown') { e.preventDefault() @@ -216,11 +209,8 @@ function toggleTerminal() { } } -function handleKeydown(e: KeyboardEvent) { - if (e.ctrlKey && e.key === 'e') { - e.preventDefault() - toggleTerminal() - } +function handleKeydown(_e: KeyboardEvent) { + // Reserved for future terminal shortcuts } function startDrag(e: MouseEvent | TouchEvent) { diff --git a/frontend/src/components/FloatingTranscriptDebug.vue b/frontend/src/components/FloatingTranscriptDebug.vue index 302a992..db5eb99 100644 --- a/frontend/src/components/FloatingTranscriptDebug.vue +++ b/frontend/src/components/FloatingTranscriptDebug.vue @@ -1,6 +1,7 @@ @@ -420,8 +531,49 @@ onBeforeUnmount(() => {
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
- +
+
@@ -84,6 +125,23 @@ onBeforeUnmount(() => document.removeEventListener('mousedown', onClickOutside)) color: #86efac; } +.term-count { + font-size: 8px; + font-weight: 700; + font-family: 'Courier New', monospace; + color: rgba(165, 180, 252, 0.6); + background: rgba(99, 102, 241, 0.2); + padding: 0 3px; + min-width: 12px; + text-align: center; + line-height: 12px; +} + +.connected .term-count { + color: rgba(134, 239, 172, 0.6); + background: rgba(34, 197, 94, 0.15); +} + .caret { color: rgba(165, 180, 252, 0.5); transition: transform 0.2s ease; @@ -101,13 +159,16 @@ onBeforeUnmount(() => document.removeEventListener('mousedown', onClickOutside)) position: absolute; top: calc(100% + 4px); left: 0; - min-width: 120px; + min-width: 180px; + max-width: 260px; + max-height: 280px; + overflow-y: auto; background: rgba(8, 8, 18, 0.95); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border: 1px solid rgba(99, 102, 241, 0.2); z-index: 100; - padding: 4px 0; + padding: 3px 0; } .dropdown-item { @@ -116,12 +177,69 @@ onBeforeUnmount(() => document.removeEventListener('mousedown', onClickOutside)) font-family: 'Courier New', monospace; color: rgba(255, 255, 255, 0.5); letter-spacing: 0.5px; - text-transform: uppercase; } -.dropdown-item.todo { +.dropdown-item.empty { color: rgba(255, 255, 255, 0.25); font-style: italic; + text-align: center; +} + +.dropdown-item.terminal-item { + display: flex; + align-items: center; + gap: 6px; + padding: 5px 8px; + cursor: pointer; + transition: background 0.1s; +} + +.dropdown-item.terminal-item:hover { + background: rgba(99, 102, 241, 0.12); +} + +.dropdown-item.terminal-item.active { + background: rgba(99, 102, 241, 0.18); + color: rgba(255, 255, 255, 0.8); +} + +.state-dot { + width: 5px; + height: 5px; + flex-shrink: 0; + border-radius: 0; +} + +.terminal-label { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 9px; +} + +.close-btn { + display: flex; + align-items: center; + justify-content: center; + width: 14px; + height: 14px; + flex-shrink: 0; + background: transparent; + border: none; + color: rgba(255, 255, 255, 0.2); + cursor: pointer; + opacity: 0; + transition: opacity 0.15s, color 0.15s, background 0.15s; +} + +.dropdown-item.terminal-item:hover .close-btn { + opacity: 1; +} + +.close-btn:hover { + color: #fca5a5; + background: rgba(239, 68, 68, 0.2); } /* Transition */ @@ -138,4 +256,17 @@ onBeforeUnmount(() => document.removeEventListener('mousedown', onClickOutside)) opacity: 0; transform: translateY(-4px); } + +/* Scrollbar */ +.dropdown::-webkit-scrollbar { + width: 4px; +} + +.dropdown::-webkit-scrollbar-track { + background: transparent; +} + +.dropdown::-webkit-scrollbar-thumb { + background: rgba(99, 102, 241, 0.2); +} diff --git a/frontend/src/components/transcript-debug/ChatContainer.vue b/frontend/src/components/transcript-debug/ChatContainer.vue index 27e2798..6fab41b 100644 --- a/frontend/src/components/transcript-debug/ChatContainer.vue +++ b/frontend/src/components/transcript-debug/ChatContainer.vue @@ -27,6 +27,16 @@ const props = defineProps<{ sessions?: { id: string; firstUserMessage?: string }[] selectedSessionId?: string | null sessionsLoading?: boolean + voiceMode?: 'web' | 'whisper' + whisperStatus?: 'offline' | 'loading' | 'ready' + audioDevices?: MediaDeviceInfo[] + selectedDeviceId?: string + isRecording?: boolean + voiceTranscript?: string + lastAudioUrl?: string + isPlayingAudio?: boolean + overlayOpacity?: number + inputMaxLines?: number }>() const emit = defineEmits<{ @@ -34,6 +44,13 @@ const emit = defineEmits<{ switchAgent: [agent: AgentName] selectSession: [sessionId: string] createSession: [] + startRecording: [] + stopRecording: [] + setVoiceMode: [mode: 'web' | 'whisper'] + selectMicrophone: [deviceId: string] + playLastAudio: [] + 'update:overlayOpacity': [value: number] + 'update:inputMaxLines': [value: number] }>() const scrollContainer = ref(null) @@ -286,6 +303,71 @@ function formatDuration(start: string, end: string): string { :terminal="terminal ?? null" /> +
+ + + + + +
+
+ + + {{ Math.round((overlayOpacity ?? 0.55) * 100) }}% +
+
+ + + {{ inputMaxLines ?? 6 }} +