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

0% Statements 0/81
0% Branches 0/1
0% Functions 0/1
0% Lines 0/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 82                                                                                                                                                                   
/**
 * 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
    )
  }
}