From 81183569999b99def401ac55c12149ed3246e096 Mon Sep 17 00:00:00 2001 From: josedario87 Date: Fri, 13 Feb 2026 20:24:57 -0600 Subject: [PATCH] feat: Add FloatingVoice component for voice-to-text input - Add FloatingVoice component with Web Speech API transcription - Each component has its own independent WebSocket session - Voice panel connects on open, disconnects on close - Sends transcribed text to Claude Code with Enter key --- frontend/src/App.vue | 61 ++ frontend/src/components/FloatingTerminal.vue | 9 +- frontend/src/components/FloatingVoice.vue | 583 +++++++++++++++++++ 3 files changed, 652 insertions(+), 1 deletion(-) create mode 100644 frontend/src/components/FloatingVoice.vue diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 1d8b079..46f988a 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -8,6 +8,7 @@ import ToolsDropdown from './components/ToolsDropdown.vue' import ConnectionDropdown from './components/ConnectionDropdown.vue' 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 { initToolRegistry, activatePageTools, initToolsOnRefresh } from './services/toolRegistry' @@ -18,6 +19,7 @@ import { useCanvasStore } from './stores/canvas' const route = useRoute() const router = useRouter() const showTerminal = ref(false) +const showVoice = ref(false) const terminalRef = ref | null>(null) const responseRef = ref | null>(null) const canvasStore = useCanvasStore() @@ -157,11 +159,29 @@ watch(() => route.name, (newPage) => { + + + + + + @@ -257,6 +277,40 @@ watch(() => route.name, (newPage) => { box-shadow: 0 12px 32px rgba(239, 68, 68, 0.5); } +/* 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); +} + @media (max-width: 768px) { .terminal-fab { bottom: 16px; @@ -269,5 +323,12 @@ watch(() => route.name, (newPage) => { opacity: 0; pointer-events: none; } + + .voice-fab { + bottom: 16px; + left: 16px; + width: 44px; + height: 44px; + } } diff --git a/frontend/src/components/FloatingTerminal.vue b/frontend/src/components/FloatingTerminal.vue index b2aecb7..128ca07 100644 --- a/frontend/src/components/FloatingTerminal.vue +++ b/frontend/src/components/FloatingTerminal.vue @@ -436,7 +436,14 @@ defineExpose({ isOpen: isOpen.value, position: position.value, size: size.value - }) + }), + sendInput: (text: string) => { + if (socket && socket.readyState === WebSocket.OPEN) { + socket.send(JSON.stringify({ type: 'input', data: text + '\r' })) + return true + } + return false + } }) diff --git a/frontend/src/components/FloatingVoice.vue b/frontend/src/components/FloatingVoice.vue new file mode 100644 index 0000000..d448c24 --- /dev/null +++ b/frontend/src/components/FloatingVoice.vue @@ -0,0 +1,583 @@ + + + + +