feat(voice): кнопка X в overlay закрывает прослушивание
All checks were successful
Deploy / deploy (push) Successful in 2m13s

В overlay появляется крестик в правом верхнем углу. Тап = эмитит
voice-cancel → VoiceController прерывает активный VAD-захват и сам
overlay закрывается. Wake-word, если был активен, продолжает слушать
в фоне.
This commit is contained in:
Cosmo
2026-04-27 10:25:21 +00:00
parent 0ea9fad144
commit 9583c84e27
2 changed files with 42 additions and 0 deletions

View File

@@ -68,7 +68,23 @@ export default function VoiceController() {
useEffect(() => { useEffect(() => {
console.log('[VoiceController] mounted, state=idle, ждём тап на микрофон') 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 () => { return () => {
window.removeEventListener('voice-cancel', onCancel)
try { vadRef.current?.destroy?.() } catch {} try { vadRef.current?.destroy?.() } catch {}
try { wakeRef.current?.stop?.() } catch {} try { wakeRef.current?.stop?.() } catch {}
vadRef.current = null vadRef.current = null

View File

@@ -1,6 +1,7 @@
'use client' 'use client'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { motion, AnimatePresence } from 'framer-motion' import { motion, AnimatePresence } from 'framer-motion'
import { X } from 'lucide-react'
type VoiceState = 'idle' | 'wake' | 'listening' | 'command' | 'response' | 'error' type VoiceState = 'idle' | 'wake' | 'listening' | 'command' | 'response' | 'error'
type Agent = 'cosmo' | 'lusya' type Agent = 'cosmo' | 'lusya'
@@ -206,6 +207,31 @@ export default function VoiceOverlay() {
pointerEvents: 'none', 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} /> <SiriOrb core={colors.core} halo={colors.halo} state={state} />
{/* Subtle status (только "слушаю" — для остальных текст сам говорит за себя) */} {/* Subtle status (только "слушаю" — для остальных текст сам говорит за себя) */}