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
This commit is contained in:
@@ -38,11 +38,16 @@ const WS_URL = `ws://${window.location.hostname}:4103`
|
|||||||
let socket: WebSocket | null = null
|
let socket: WebSocket | null = null
|
||||||
const connected = ref(false)
|
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(() => {
|
const displayText = computed(() => {
|
||||||
if (interimTranscript.value) {
|
if (interimTranscript.value) {
|
||||||
return transcript.value + ' ' + 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(() => {
|
const containerStyle = computed(() => {
|
||||||
@@ -146,12 +151,13 @@ function clearTranscript() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function connectSocket() {
|
function connectSocket() {
|
||||||
|
console.log('[Voice] connectSocket called, current socket:', socket?.readyState)
|
||||||
if (socket && socket.readyState === WebSocket.OPEN) return
|
if (socket && socket.readyState === WebSocket.OPEN) return
|
||||||
|
|
||||||
socket = new WebSocket(WS_URL)
|
socket = new WebSocket(WS_URL)
|
||||||
|
|
||||||
socket.onopen = () => {
|
socket.onopen = () => {
|
||||||
console.log('[Voice] WebSocket connected')
|
console.log('[Voice] WebSocket connected, readyState:', socket?.readyState)
|
||||||
connected.value = true
|
connected.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,16 +252,115 @@ function stopDrag() {
|
|||||||
document.removeEventListener('mouseup', 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(() => {
|
onMounted(() => {
|
||||||
recognition = initRecognition()
|
recognition = initRecognition()
|
||||||
|
document.addEventListener('keydown', handleKeyDown)
|
||||||
|
document.addEventListener('keyup', handleKeyUp)
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
stopRecording()
|
stopRecording()
|
||||||
recognition = null
|
recognition = null
|
||||||
disconnectSocket()
|
disconnectSocket()
|
||||||
|
document.removeEventListener('keydown', handleKeyDown)
|
||||||
|
document.removeEventListener('keyup', handleKeyUp)
|
||||||
document.removeEventListener('mousemove', onDrag)
|
document.removeEventListener('mousemove', onDrag)
|
||||||
document.removeEventListener('mouseup', stopDrag)
|
document.removeEventListener('mouseup', stopDrag)
|
||||||
|
if (holdTimeout) clearTimeout(holdTimeout)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Connect when panel opens, disconnect when closes
|
// Connect when panel opens, disconnect when closes
|
||||||
@@ -298,7 +403,7 @@ defineExpose({
|
|||||||
<line x1="8" y1="23" x2="16" y2="23"/>
|
<line x1="8" y1="23" x2="16" y2="23"/>
|
||||||
</svg>
|
</svg>
|
||||||
<span>Voice</span>
|
<span>Voice</span>
|
||||||
<i class="dot" :class="{ recording: isRecording }"></i>
|
<i class="dot" :class="{ recording: isRecording, ptt: isPushToTalk }"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="window-controls">
|
<div class="window-controls">
|
||||||
<button class="x" @click="close" title="Close">
|
<button class="x" @click="close" title="Close">
|
||||||
@@ -316,7 +421,7 @@ defineExpose({
|
|||||||
<span class="final">{{ transcript }}</span>
|
<span class="final">{{ transcript }}</span>
|
||||||
<span class="interim">{{ interimTranscript }}</span>
|
<span class="interim">{{ interimTranscript }}</span>
|
||||||
<span v-if="!transcript && !interimTranscript" class="placeholder">
|
<span v-if="!transcript && !interimTranscript" class="placeholder">
|
||||||
Presiona el micrófono para comenzar...
|
Presiona el micrófono o mantén Ctrl+S...
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -431,6 +536,11 @@ defineExpose({
|
|||||||
animation: pulse 0.8s infinite;
|
animation: pulse 0.8s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dot.ptt {
|
||||||
|
background: #f90;
|
||||||
|
box-shadow: 0 0 6px #f90;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
0%, 100% { opacity: 1; }
|
0%, 100% { opacity: 1; }
|
||||||
50% { opacity: 0.5; }
|
50% { opacity: 0.5; }
|
||||||
|
|||||||
Reference in New Issue
Block a user