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

27.2% Statements 34/125
100% Branches 0/0
0% Functions 0/1
27.2% Lines 34/125

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 124 125 1261x 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                                                                                                                                                                                        
import type { MutableRefObject } from 'react'
import { CONSTANT_IDS, EXPLORATION_DISPLAY } from './talkToNumber/explorationRegistry'
import { NARRATION_CONFIGS } from './constants/demos/narrationConfigs'
 
const DEMO_DISPLAY = EXPLORATION_DISPLAY
 
interface Narration {
  reset: () => void
  stop: () => void
  resume: (constantId: string) => void
  markTriggered: (constantId: string) => void
  isNarrating: MutableRefObject<boolean>
}
 
interface CreateHandleExploreConstantOptions {
  audioManager: { stop: () => void }
  voiceStateRef: MutableRefObject<string>
  pendingTourRef: MutableRefObject<string | null>
  voiceExplorationSegmentRef: MutableRefObject<number>
  narration: Narration
  cancelDemo: () => void
  exitTour: () => void
  startTour: () => void
  startDemo: (id: string) => void
  restoreDemo: (id: string, progress: number) => void
  sendSystemMessage: (msg: string, force?: boolean) => void
  onSpeedReset: () => void
  setTappedConstantId: (v: string | null) => void
  setTappedIntValue: (v: number | null) => void
  setHoveredValue: (v: number | null) => void
  tooltipHoveredRef: MutableRefObject<boolean>
}
 
export function createHandleExploreConstant({
  audioManager,
  voiceStateRef,
  pendingTourRef,
  voiceExplorationSegmentRef,
  narration,
  cancelDemo,
  exitTour,
  startTour,
  startDemo,
  restoreDemo,
  sendSystemMessage,
  onSpeedReset,
  setTappedConstantId,
  setTappedIntValue,
  setHoveredValue,
  tooltipHoveredRef,
}: CreateHandleExploreConstantOptions) {
  return (explorationId: string) => {
    console.log('[NumberLine] handleExploreConstant — exploration:', explorationId)
    audioManager.stop()

    // Route tour explorations to the prime tour handler
    if (!CONSTANT_IDS.has(explorationId)) {
      const onCall = voiceStateRef.current === 'active'
      if (onCall) {
        // Queue the tour to launch after the call ends — the agent will
        // explain the tour, say goodbye, and hang up on its own.
        console.log('[NumberLine] queuing tour for after hangup:', explorationId)
        pendingTourRef.current = explorationId
      } else {
        // Not on a call — start the tour immediately
        cancelDemo()
        setTappedConstantId(null)
        setTappedIntValue(null)
        setHoveredValue(null)
        tooltipHoveredRef.current = false
        startTour()
      }
      return
    }

    // Constant exploration path
    exitTour()
    narration.reset()
    // Reset speed UI to match (narration.reset() resets the ref + audio rate)
    onSpeedReset()

    const onCall = voiceStateRef.current === 'active'
    console.log(
      '[exploration] handleExploreConstant — onCall:',
      onCall,
      'explorationId:',
      explorationId
    )

    if (onCall) {
      // Start paused at progress 0 — narrator introduces first, then calls resume
      console.log('[exploration] starting PAUSED (restoreDemo + markTriggered)')
      restoreDemo(explorationId, 0)
      narration.markTriggered(explorationId) // suppress narration auto-start
    } else {
      startDemo(explorationId)
    }

    // If on a voice call, send companion instructions for the exploration
    if (onCall) {
      const cfg = NARRATION_CONFIGS[explorationId]
      const display = DEMO_DISPLAY[explorationId]
      if (cfg && display) {
        const visualDesc = display.visualDesc
          ? `\n\nWHAT THE ANIMATION SHOWS (for YOUR reference only — do NOT describe this to the child): ${display.visualDesc}`
          : ''

        // Companion rules are in the start_exploration tool output (not here)
        // so the model won't treat them as a "user message" to recite.
        // This message provides constant-specific context only.
        console.log('[exploration] sending intro system message + response.create')
        sendSystemMessage(
          `An animated exploration of ${display.symbol} (${display.name}) is ready to play.` +
            `${visualDesc}\n\n` +
            `Give the child a brief intro — why this constant is special to you personally. Match the child's energy level. ` +
            `Do NOT describe or preview what the animation will show. The visuals should be a surprise. ` +
            `Something like "Oh, I've been wanting to show you this!" or "This is one of my favorite things about living near ${display.symbol}." ` +
            `Keep it to 1-2 sentences, then call resume_exploration.`,
          true // prompt a response so the agent introduces the constant
        )
      }
      voiceExplorationSegmentRef.current = -1
    }
  }
}