feat(voice): кнопка X в overlay закрывает прослушивание
All checks were successful
Deploy / deploy (push) Successful in 2m13s
All checks were successful
Deploy / deploy (push) Successful in 2m13s
В overlay появляется крестик в правом верхнем углу. Тап = эмитит voice-cancel → VoiceController прерывает активный VAD-захват и сам overlay закрывается. Wake-word, если был активен, продолжает слушать в фоне.
This commit is contained in:
@@ -68,7 +68,23 @@ export default function VoiceController() {
|
||||
|
||||
useEffect(() => {
|
||||
console.log('[VoiceController] mounted, state=idle, ждём тап на микрофон')
|
||||
|
||||
// Кнопка X в overlay шлёт voice-cancel → прерываем активную запись фразы,
|
||||
// но wake-word оставляем слушать в фоне (если он включён).
|
||||
const onCancel = () => {
|
||||
console.log('[voice] cancel — прерываю запись')
|
||||
try { vadRef.current?.pause?.() } catch {}
|
||||
try { vadRef.current?.destroy?.() } catch {}
|
||||
vadRef.current = null
|
||||
busyRef.current = false
|
||||
try { wakeRef.current?.resume?.() } catch {}
|
||||
setState((s) => (wakeRef.current ? 'listening' : 'idle'))
|
||||
emitLocal('idle', AGENT)
|
||||
}
|
||||
window.addEventListener('voice-cancel', onCancel)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('voice-cancel', onCancel)
|
||||
try { vadRef.current?.destroy?.() } catch {}
|
||||
try { wakeRef.current?.stop?.() } catch {}
|
||||
vadRef.current = null
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { X } from 'lucide-react'
|
||||
|
||||
type VoiceState = 'idle' | 'wake' | 'listening' | 'command' | 'response' | 'error'
|
||||
type Agent = 'cosmo' | 'lusya'
|
||||
@@ -206,6 +207,31 @@ export default function VoiceOverlay() {
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
>
|
||||
{/* Закрыть прослушивание. Просим VoiceController прервать активный
|
||||
VAD/recording через voice-cancel, и сами закрываем UI. */}
|
||||
<button
|
||||
onClick={() => {
|
||||
window.dispatchEvent(new CustomEvent('voice-cancel'))
|
||||
clearDismiss()
|
||||
stopAudio()
|
||||
setState('idle')
|
||||
}}
|
||||
aria-label="Закрыть"
|
||||
style={{
|
||||
position: 'absolute', top: 24, right: 24,
|
||||
width: 56, height: 56, borderRadius: '50%',
|
||||
border: 'none', cursor: 'pointer',
|
||||
background: 'rgba(255,255,255,0.08)',
|
||||
color: 'rgba(255,255,255,0.85)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
backdropFilter: 'blur(16px)',
|
||||
WebkitBackdropFilter: 'blur(16px)' as any,
|
||||
pointerEvents: 'auto',
|
||||
}}
|
||||
>
|
||||
<X size={26} />
|
||||
</button>
|
||||
|
||||
<SiriOrb core={colors.core} halo={colors.halo} state={state} />
|
||||
|
||||
{/* Subtle status (только "слушаю" — для остальных текст сам говорит за себя) */}
|
||||
|
||||
Reference in New Issue
Block a user