From 306aade623fcc498cf6880809abed1271544702e Mon Sep 17 00:00:00 2001 From: josedario87 Date: Fri, 13 Feb 2026 21:28:44 -0600 Subject: [PATCH] feat: Add push-to-talk keyboard shortcut (Ctrl+S) to FloatingVoice - Hold Ctrl+S for 500ms to start recording - Release to stop recording and send to terminal - Shows PTT indicator when using keyboard shortcut --- frontend/src/components/FloatingVoice.vue | 118 +++++++++++++++++++++- 1 file changed, 114 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/FloatingVoice.vue b/frontend/src/components/FloatingVoice.vue index d448c24..a067cb2 100644 --- a/frontend/src/components/FloatingVoice.vue +++ b/frontend/src/components/FloatingVoice.vue @@ -38,11 +38,16 @@ const WS_URL = `ws://${window.location.hostname}:4103` let socket: WebSocket | null = null const connected = ref(false) +// Push-to-talk state (Ctrl+S) +let keyDownTime = 0 +let holdTimeout: number | null = null +const isPushToTalk = ref(false) + const displayText = computed(() => { if (interimTranscript.value) { return transcript.value + ' ' + interimTranscript.value } - return transcript.value || 'Presiona el micrófono para comenzar...' + return transcript.value || 'Presiona el micrófono o mantén Ctrl+S...' }) const containerStyle = computed(() => { @@ -146,12 +151,13 @@ function clearTranscript() { } function connectSocket() { + console.log('[Voice] connectSocket called, current socket:', socket?.readyState) if (socket && socket.readyState === WebSocket.OPEN) return socket = new WebSocket(WS_URL) socket.onopen = () => { - console.log('[Voice] WebSocket connected') + console.log('[Voice] WebSocket connected, readyState:', socket?.readyState) connected.value = true } @@ -246,16 +252,115 @@ function stopDrag() { document.removeEventListener('mouseup', stopDrag) } +// Keyboard shortcut handlers (Ctrl+S) +function handleKeyDown(e: KeyboardEvent) { + if (e.ctrlKey && e.key.toLowerCase() === 's') { + e.preventDefault() + + // Ignore if already holding + if (keyDownTime > 0) return + + keyDownTime = Date.now() + + // Open panel and connect if not open + if (!isOpen.value) { + isOpen.value = true + } + + // Start recording after 500ms hold + holdTimeout = window.setTimeout(() => { + if (keyDownTime > 0 && !isRecording.value) { + isPushToTalk.value = true + startRecording() + } + }, 500) + } +} + +function handleKeyUp(e: KeyboardEvent) { + // Only react to 's' key release, ignore Control + if (e.key.toLowerCase() === 's' && keyDownTime > 0) { + console.log('[Voice] Key S released, isPushToTalk:', isPushToTalk.value, 'isRecording:', isRecording.value) + + if (holdTimeout) { + clearTimeout(holdTimeout) + holdTimeout = null + } + + // If was push-to-talk recording, stop and send after 500ms + if (isPushToTalk.value && isRecording.value) { + console.log('[Voice] Stopping recording, will send in 500ms') + stopRecording() + setTimeout(() => { + console.log('[Voice] Sending transcript:', transcript.value.trim()) + console.log('[Voice] Socket state:', socket?.readyState) + if (transcript.value.trim()) { + sendTranscriptAndClose() + } else { + // No transcript, just close + isPushToTalk.value = false + close() + } + }, 500) + } + + keyDownTime = 0 + } +} + +// Send and close for push-to-talk mode +function sendTranscriptAndClose() { + console.log('[Voice] sendTranscriptAndClose called') + const text = transcript.value.trim() + if (!text) { + console.log('[Voice] No text, closing') + isPushToTalk.value = false + close() + return + } + + console.log('[Voice] Text to send:', text) + console.log('[Voice] Socket:', socket, 'readyState:', socket?.readyState) + + if (!socket || socket.readyState !== WebSocket.OPEN) { + canvasStore.showNotification('Not connected to terminal', 'error') + isPushToTalk.value = false + return + } + + // Send text character by character then Enter + const chars = (text + '\r').split('') + let i = 0 + const typeChar = () => { + if (i < chars.length && socket && socket.readyState === WebSocket.OPEN) { + socket.send(JSON.stringify({ type: 'input', data: chars[i] })) + i++ + setTimeout(typeChar, 15) + } else if (i >= chars.length) { + canvasStore.showNotification('Voice message sent', 'success') + clearTranscript() + isPushToTalk.value = false + close() + } + } + typeChar() +} + onMounted(() => { recognition = initRecognition() + document.addEventListener('keydown', handleKeyDown) + document.addEventListener('keyup', handleKeyUp) }) onBeforeUnmount(() => { stopRecording() recognition = null disconnectSocket() + document.removeEventListener('keydown', handleKeyDown) + document.removeEventListener('keyup', handleKeyUp) document.removeEventListener('mousemove', onDrag) document.removeEventListener('mouseup', stopDrag) + if (holdTimeout) clearTimeout(holdTimeout) }) // Connect when panel opens, disconnect when closes @@ -298,7 +403,7 @@ defineExpose({ Voice - +
@@ -431,6 +536,11 @@ defineExpose({ animation: pulse 0.8s infinite; } +.dot.ptt { + background: #f90; + box-shadow: 0 0 6px #f90; +} + @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; }