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(() => {
|
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
|
||||||
|
|||||||
@@ -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 (только "слушаю" — для остальных текст сам говорит за себя) */}
|
||||||
|
|||||||
Reference in New Issue
Block a user