feat: Push-to-talk on voice FAB button
- Hold FAB to open panel and start recording immediately - Release to stop recording and send after 1s buffer - Orange pulsing animation when PTT active - PTT also works on record button inside modal - Added stopRecordingAndSend exposed method
This commit is contained in:
@@ -95,12 +95,14 @@ const showMicSelector = ref(false)
|
||||
// ============ MOBILE DETECTION & AUDIO FORMAT ============
|
||||
const isMobile = ref(false)
|
||||
const isAndroid = ref(false)
|
||||
const isMobilePTT = ref(false) // Mobile push-to-talk active
|
||||
const supportedMimeType = ref('audio/webm;codecs=opus')
|
||||
const sheetHeight = ref(45) // percentage of viewport for mobile
|
||||
const isDraggingSheet = ref(false)
|
||||
const sheetDragStart = ref({ y: 0, height: 0 })
|
||||
const keyboardHeight = ref(0)
|
||||
const snapPoints = [25, 45, 70] // collapsed, default, expanded
|
||||
let mobilePTTTimeout: number | null = null
|
||||
|
||||
function checkMobile() {
|
||||
const ua = navigator.userAgent
|
||||
@@ -537,10 +539,20 @@ function connectWhisperSocket() {
|
||||
}
|
||||
if (whisperSocket?.readyState === WebSocket.OPEN) return
|
||||
|
||||
console.log('[Voice] Connecting to Whisper server...')
|
||||
console.log('[Voice] Connecting to Whisper server at:', WHISPER_WS_URL)
|
||||
whisperSocket = new WebSocket(WHISPER_WS_URL)
|
||||
|
||||
// Connection timeout
|
||||
const connectionTimeout = setTimeout(() => {
|
||||
if (whisperSocket && whisperSocket.readyState !== WebSocket.OPEN) {
|
||||
console.error('[Voice] Whisper connection timeout (10s)')
|
||||
whisperSocket.close()
|
||||
whisperReady.value = false
|
||||
}
|
||||
}, 10000)
|
||||
|
||||
whisperSocket.onopen = () => {
|
||||
clearTimeout(connectionTimeout)
|
||||
console.log('[Voice] Whisper WebSocket connected')
|
||||
whisperReady.value = true
|
||||
}
|
||||
@@ -799,6 +811,75 @@ function stopRecording() {
|
||||
interimTranscript.value = ''
|
||||
}
|
||||
|
||||
// ============ MOBILE PUSH-TO-TALK ============
|
||||
let touchStarted = false
|
||||
|
||||
function handleRecClick() {
|
||||
// If touch just ended, ignore the click (prevents double-trigger)
|
||||
if (touchStarted) {
|
||||
touchStarted = false
|
||||
return
|
||||
}
|
||||
// Normal click behavior
|
||||
toggleRecording()
|
||||
}
|
||||
|
||||
function handleRecTouchStart(e: TouchEvent) {
|
||||
e.preventDefault()
|
||||
touchStarted = true
|
||||
|
||||
// Clear any pending timeout
|
||||
if (mobilePTTTimeout) {
|
||||
clearTimeout(mobilePTTTimeout)
|
||||
mobilePTTTimeout = null
|
||||
}
|
||||
|
||||
isMobilePTT.value = true
|
||||
isPushToTalk.value = true
|
||||
|
||||
// Start recording immediately
|
||||
if (!isRecording.value) {
|
||||
startRecording()
|
||||
}
|
||||
|
||||
console.log('[Voice] Mobile PTT started')
|
||||
}
|
||||
|
||||
function handleRecTouchEnd(e: TouchEvent) {
|
||||
e.preventDefault()
|
||||
|
||||
if (!isMobilePTT.value) return
|
||||
|
||||
console.log('[Voice] Mobile PTT released')
|
||||
|
||||
// Add 1s buffer before stopping (to capture trailing words)
|
||||
mobilePTTTimeout = window.setTimeout(() => {
|
||||
stopRecording()
|
||||
isMobilePTT.value = false
|
||||
|
||||
// Wait a moment for final transcription, then send
|
||||
setTimeout(() => {
|
||||
if (useWhisper.value) {
|
||||
// Whisper: wait for server response
|
||||
pendingWhisperSend = true
|
||||
console.log('[Voice] Waiting for Whisper transcription...')
|
||||
} else {
|
||||
// Web Speech API: send after short delay
|
||||
setTimeout(() => {
|
||||
if (transcript.value.trim()) {
|
||||
sendTranscriptAndClose()
|
||||
} else {
|
||||
isPushToTalk.value = false
|
||||
}
|
||||
}, 300)
|
||||
}
|
||||
}, 200)
|
||||
}, 1000)
|
||||
|
||||
// Reset touch flag after a short delay
|
||||
setTimeout(() => { touchStarted = false }, 100)
|
||||
}
|
||||
|
||||
function clearTranscript() {
|
||||
transcript.value = ''
|
||||
interimTranscript.value = ''
|
||||
@@ -1139,12 +1220,34 @@ watch(isOpen, (open) => {
|
||||
}
|
||||
})
|
||||
|
||||
// Stop recording and send (for external PTT control)
|
||||
function stopRecordingAndSend() {
|
||||
stopRecording()
|
||||
|
||||
// Wait a moment for final transcription, then send
|
||||
setTimeout(() => {
|
||||
if (useWhisper.value) {
|
||||
// Whisper: wait for server response
|
||||
pendingWhisperSend = true
|
||||
console.log('[Voice] Waiting for Whisper transcription...')
|
||||
} else {
|
||||
// Web Speech API: send after short delay
|
||||
setTimeout(() => {
|
||||
if (transcript.value.trim()) {
|
||||
sendTranscriptAndClose()
|
||||
}
|
||||
}, 300)
|
||||
}
|
||||
}, 200)
|
||||
}
|
||||
|
||||
// Expose methods
|
||||
defineExpose({
|
||||
open: () => { isOpen.value = true },
|
||||
close,
|
||||
startRecording,
|
||||
stopRecording,
|
||||
stopRecordingAndSend,
|
||||
getTranscript: () => transcript.value
|
||||
})
|
||||
</script>
|
||||
@@ -1264,7 +1367,7 @@ defineExpose({
|
||||
<span class="final">{{ animatedTranscript }}</span><span class="cursor" v-if="animatedTranscript && animatedTranscript.length < transcript.length">|</span>
|
||||
<span class="interim">{{ interimTranscript }}</span>
|
||||
<span v-if="!animatedTranscript && !interimTranscript" class="placeholder">
|
||||
Presiona el micrófono o mantén Ctrl+Space...
|
||||
{{ isMobile ? 'Mantén presionado el micrófono para grabar...' : 'Presiona el micrófono o mantén Ctrl+Space...' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -1275,9 +1378,12 @@ defineExpose({
|
||||
<div class="controls">
|
||||
<button
|
||||
class="rec-btn"
|
||||
:class="{ active: isRecording }"
|
||||
@click="toggleRecording"
|
||||
:title="isRecording ? 'Stop' : 'Record'"
|
||||
:class="{ active: isRecording, ptt: isMobilePTT }"
|
||||
@click="handleRecClick"
|
||||
@touchstart="handleRecTouchStart"
|
||||
@touchend="handleRecTouchEnd"
|
||||
@touchcancel="handleRecTouchEnd"
|
||||
:title="isRecording ? 'Stop' : (isMobile ? 'Mantén presionado para grabar' : 'Record')"
|
||||
>
|
||||
<svg v-if="!isRecording" width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/>
|
||||
@@ -1716,6 +1822,28 @@ defineExpose({
|
||||
animation: pulse 1s infinite;
|
||||
}
|
||||
|
||||
/* Mobile PTT - larger button with special style */
|
||||
.rec-btn.ptt {
|
||||
background: linear-gradient(180deg, #f97316 0%, #ea580c 100%);
|
||||
border-color: #c2410c;
|
||||
color: #fff;
|
||||
transform: scale(1.15);
|
||||
box-shadow: 0 0 20px rgba(249, 115, 22, 0.6);
|
||||
}
|
||||
|
||||
/* Make rec button bigger on mobile */
|
||||
@media (pointer: coarse) {
|
||||
.rec-btn {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
}
|
||||
|
||||
.rec-btn svg {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
height: 28px;
|
||||
padding: 0 10px;
|
||||
|
||||
Reference in New Issue
Block a user