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({