All files / web/src/components/toys/euclid/hooks useChatModeManager.ts

0% Statements 0/95
0% Branches 0/1
0% Functions 0/1
0% Lines 0/95

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,
  }
}