All files / web/src/components/tutorial/CoachBar CoachBar.tsx

61.01% Statements 36/59
15.38% Branches 2/13
50% Functions 1/2
61.01% Lines 36/59

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 601x 1x 1x 1x 1x 1x 1x 1x 1x 1x 160x 160x 160x 160x 160x 160x 160x 160x 160x 160x 160x 160x 160x     160x 160x 160x             160x 160x                 160x 160x 160x 160x               160x 160x 160x 160x  
'use client'
 
import { useEffect, useRef } from 'react'
import { useTranslations } from 'next-intl'
import { useAudioManager } from '@/hooks/useAudioManager'
import { useTTS } from '@/hooks/useTTS'
import type { PedagogicalSegment } from '../DecompositionWithReasons'
import { useTutorialUI } from '../TutorialUIContext'
 
export function CoachBar() {
  const ui = useTutorialUI()
  const t = useTranslations('tutorial.coachBar')
  const { isEnabled: audioHelpEnabled } = useAudioManager()
  const seg: PedagogicalSegment | null = ui.activeSegment
  const lastSummaryRef = useRef<string>('')
 
  // Read coach hint aloud via TTS voice chain when it changes
  // Uses hash-based clip ID so each unique summary gets its own mp3
  const summary = seg?.readable?.summary ?? ''
  const sayCoachHint = useTTS(summary ? { say: { en: summary }, tone: 'tutorial-instruction' } : '')
 
  useEffect(() => {
    if (!audioHelpEnabled || !summary || summary === lastSummaryRef.current) return
    lastSummaryRef.current = summary
    sayCoachHint()
  }, [audioHelpEnabled, summary, sayCoachHint])
 
  if (!ui.showCoachBar || !seg || !seg.readable?.summary) return null

  const r = seg.readable

  return (
    <aside className="coachbar" role="status" aria-live="polite" data-test-id="coachbar">
      <div className="coachbar__row">
        <div className="coachbar__title">{r.title ?? t('titleFallback')}</div>
        {ui.canHideCoachBar && (
          <button
            type="button"
            className="coachbar__hide"
            onClick={() => ui.setShowCoachBar(false)}
            aria-label={t('hideAria')}
          >

          </button>
        )}
      </div>
      <p className="coachbar__summary">{r.summary}</p>
      {(r.chips?.length ?? 0) > 0 && (
        <div className="coachbar__chips">
          {r.chips.slice(0, 2).map((c, i) => (
            <span key={i} className="coachbar__chip">
              {c.label}: {c.value}
            </span>
          ))}
        </div>
      )}
    </aside>
  )
}