All files / web/src/components/toys/number-line syncVoiceNarration.ts

45.67% Statements 37/81
100% Branches 0/0
0% Functions 0/1
45.67% Lines 37/81

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 821x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x                                                                                          
/**
 * Feed narration segment updates to the voice agent during active calls.
 *
 * Uses the sequencer's actual segment index (not revealProgress) to gate
 * cues — prevents premature firing when animation finishes a segment
 * before the voice agent does.
 *
 * Extracted from NumberLine.tsx draw().
 */
 
import type { DemoNarrationConfig } from './constants/demos/useConstantDemoNarration'
import {
  EXPLORATION_DISPLAY,
  EXPLORATION_RECOMMENDATIONS,
} from './talkToNumber/explorationRegistry'
 
export interface VoiceNarrationSyncState {
  voiceState: string
  demoPhase: string
  constantId: string | null
  revealProgress: number
  isNarrating: boolean
  segmentIndex: number
  lastReportedSegment: number
}
 
export interface VoiceNarrationSyncActions {
  sendSystemMessage: (message: string, promptResponse: boolean) => void
  setNarrationPlaying: (playing: boolean) => void
  updateLastReportedSegment: (index: number) => void
}
 
/**
 * Sync narration state to voice agent. Call once per draw frame.
 * Returns the new lastReportedSegment value.
 */
export function syncVoiceNarration(
  state: VoiceNarrationSyncState,
  actions: VoiceNarrationSyncActions,
  narrationConfig: DemoNarrationConfig | undefined
): void {
  if (state.voiceState !== 'active') return
  if (state.demoPhase === 'idle' || !state.constantId) return
  if (!narrationConfig) return

  const segIdx = state.isNarrating ? state.segmentIndex : -1

  // Send context-only cue when the sequencer advances to a new segment
  if (segIdx >= 0 && segIdx !== state.lastReportedSegment) {
    actions.updateLastReportedSegment(segIdx)
    const seg = narrationConfig.segments[segIdx]
    actions.sendSystemMessage(
      `[Narration playing — DO NOT speak this. The narrator is saying: "${seg.ttsText}"]`,
      false
    )
  }

  // Notify when exploration completes
  if (state.revealProgress >= 1 && state.lastReportedSegment !== narrationConfig.segments.length) {
    actions.updateLastReportedSegment(narrationConfig.segments.length)
    actions.setNarrationPlaying(false)
    const display = EXPLORATION_DISPLAY[state.constantId]
    const recs = EXPLORATION_RECOMMENDATIONS[state.constantId] ?? []
    const recText =
      recs.length > 0
        ? ` If the child seems into it, casually suggest one of these: ${recs
            .map((r) => {
              const d = EXPLORATION_DISPLAY[r.id]
              return `${d?.name ?? r.id} (${r.reason})`
            })
            .join(
              '; '
            )}. Don't list them all — just pick whichever feels most natural for the conversation and mention it casually.`
        : ''
    actions.sendSystemMessage(
      `[The ${display?.name ?? state.constantId} exploration finished. Ask the child what they thought — ` +
        `brief check-in, then move on.${recText}]`,
      true
    )
  }
}