Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 | import { useState, useEffect, useRef } from 'react' import type { ChatCallState, ChatMessage } from '@/lib/character/types' import { generateId } from '@/lib/character/useCharacterChat' import type { UseGeometryVoiceReturn } from '../agent/useGeometryVoice' interface UseChatModeManagerOptions { euclidVoice: UseGeometryVoiceReturn euclidCallVisible: boolean hecklerPreDialRef: React.MutableRefObject<boolean> addMessage: (msg: ChatMessage) => void } export function useChatModeManager({ euclidVoice, euclidCallVisible, hecklerPreDialRef, addMessage, }: UseChatModeManagerOptions) { // Chat mode: closed (hidden), docked (in proof column), floating (old quad popup) const [chatMode, setChatMode] = useState<'closed' | 'docked' | 'floating'>('closed') const [mobileDockedExpanded, setMobileDockedExpanded] = useState(false) // Floating chat animation (only when mode === 'floating') const [chatMounted, setChatMounted] = useState(false) const [chatExpanded, setChatExpanded] = useState(false) useEffect(() => { if (chatMode === 'floating') { setChatMounted(true) const raf = requestAnimationFrame(() => { requestAnimationFrame(() => setChatExpanded(true)) }) return () => cancelAnimationFrame(raf) } else { setChatExpanded(false) if (chatMounted) { const timer = setTimeout(() => setChatMounted(false), 250) return () => clearTimeout(timer) } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [chatMode]) // Auto-open docked chat when a visible call starts (skip heckler pre-dial) useEffect(() => { if (euclidVoice.state === 'active' && chatMode === 'closed') { setChatMode('docked') } // Also open on normal ringing (non-heckler pre-dial) if (euclidVoice.state === 'ringing' && !hecklerPreDialRef.current && chatMode === 'closed') { setChatMode('docked') } }, [euclidVoice.state, chatMode]) // Build chatCallState from euclidVoice const chatCallState: ChatCallState | undefined = euclidCallVisible ? { state: euclidVoice.state as ChatCallState['state'], timeRemaining: euclidVoice.timeRemaining, isSpeaking: euclidVoice.isSpeaking, isThinking: euclidVoice.isThinking, thinkingLabel: 'Consulting scrolls', error: euclidVoice.error, errorCode: euclidVoice.errorCode, onHangUp: euclidVoice.hangUp, onRetry: euclidVoice.dial, } : undefined // Inject "Call ended" event when transitioning from active → ending const prevVoiceStateRef = useRef(euclidVoice.state) useEffect(() => { const prev = prevVoiceStateRef.current prevVoiceStateRef.current = euclidVoice.state if (prev === 'active' && euclidVoice.state === 'ending') { addMessage({ id: generateId(), role: 'user' as const, content: 'Call ended', timestamp: Date.now(), isEvent: true, }) } }, [euclidVoice.state, addMessage]) return { chatMode, setChatMode, mobileDockedExpanded, setMobileDockedExpanded, chatMounted, chatExpanded, chatCallState, } } |