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

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

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 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123                                                                                                                                                                                                                                                     
'use client'

import { useCallback, useEffect, useRef } from 'react'
import { useTTS } from '@/hooks/useTTS'
import { useAudioManager } from '@/hooks/useAudioManager'
import type { ExplorationNarration } from '../types'

// Stable tone constants
const INSTRUCTION_TONE =
  'Patiently guiding a young child through a geometry construction. Clear, steady, encouraging.'
const CELEBRATION_TONE =
  'Warmly congratulating a child on completing a geometric proof. Genuinely happy and impressed.'
const EXPLORATION_TONE =
  'Warmly encouraging a young child to explore and discover geometry. Curious, playful, genuinely excited.'

interface UseEuclidAudioHelpOptions {
  /** The current step instruction text */
  instruction: string
  /** Whether the entire construction is complete */
  isComplete: boolean
  /** Custom celebration message (defaults to generic) */
  celebrationText?: string
  /** Proposition-specific exploration narration (intro + per-point tips) */
  explorationNarration?: ExplorationNarration
  /**
   * Override the enabled state from the global audio manager.
   * When provided, this value takes precedence over the manager's isEnabled.
   * Useful when the canvas manages its own audio state (e.g. disableAudio prop).
   */
  enabledOverride?: boolean
}

export function useEuclidAudioHelp({
  instruction,
  isComplete,
  celebrationText = 'Construction complete! Well done!',
  explorationNarration,
  enabledOverride,
}: UseEuclidAudioHelpOptions) {
  const { isEnabled: globalIsEnabled, stop } = useAudioManager()
  const isEnabled = enabledOverride !== undefined ? enabledOverride : globalIsEnabled

  const sayInstruction = useTTS(instruction, { tone: INSTRUCTION_TONE })
  const sayCelebration = useTTS(isComplete ? celebrationText : '', { tone: CELEBRATION_TONE })

  // Register exploration intro for pre-caching (only when narration exists)
  const sayExplorationIntro = useTTS(explorationNarration?.introSpeech ?? '', {
    tone: EXPLORATION_TONE,
  })

  // Speaker for dynamic per-point tips (tone set via config, text provided at call time)
  const sayPointTip = useTTS('', { tone: EXPLORATION_TONE })

  // Auto-play when instruction changes
  const prevInstructionRef = useRef('')
  useEffect(() => {
    if (!isEnabled || !instruction || instruction === prevInstructionRef.current) return
    prevInstructionRef.current = instruction
    sayInstruction()
  }, [isEnabled, instruction, sayInstruction])

  // Auto-play celebration or exploration intro on completion
  const playedCelebrationRef = useRef(false)
  const playedExplorationRef = useRef(false)
  useEffect(() => {
    if (!isEnabled || !isComplete) return

    if (explorationNarration && !playedExplorationRef.current) {
      playedExplorationRef.current = true
      playedCelebrationRef.current = true // suppress generic celebration
      sayExplorationIntro()
    } else if (!playedCelebrationRef.current && celebrationText) {
      playedCelebrationRef.current = true
      sayCelebration()
    }
  }, [isEnabled, isComplete, sayCelebration, sayExplorationIntro, explorationNarration])

  // Reset exploration state when rewinding (isComplete goes false)
  useEffect(() => {
    if (!isComplete) {
      playedExplorationRef.current = false
      playedCelebrationRef.current = false
      narratedPointsRef.current = new Set()
      playedBreakdownRef.current = false
    }
  }, [isComplete])

  // Track which points have already had their tip narrated
  const narratedPointsRef = useRef<Set<string>>(new Set())

  const handleDragStart = useCallback(
    (pointId: string) => {
      if (!isEnabled || !explorationNarration) return
      if (narratedPointsRef.current.has(pointId)) return

      const tip = explorationNarration.pointTips.find((t) => t.pointId === pointId)
      if (!tip) return

      narratedPointsRef.current.add(pointId)
      sayPointTip({ say: { en: tip.speech } })
    },
    [isEnabled, explorationNarration, sayPointTip]
  )

  // One-shot breakdown narration when the construction falls apart during drag
  const playedBreakdownRef = useRef(false)

  const handleConstructionBreakdown = useCallback(() => {
    if (!isEnabled || !explorationNarration?.breakdownTip) return
    if (playedBreakdownRef.current) return

    playedBreakdownRef.current = true
    sayPointTip({ say: { en: explorationNarration.breakdownTip } })
  }, [isEnabled, explorationNarration, sayPointTip])

  // Stop audio on unmount
  useEffect(() => {
    return () => stop()
  }, [stop])

  return { replay: sayInstruction, handleDragStart, handleConstructionBreakdown }
}