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