/** * Composant d'enregistrement audio pour les productions orales — * Sprint 4c-1, simplifié au Sprint 4c-3. * * Encapsule `useAudioRecorder` côté UI : timer montant MM:SS, indicateur * visuel d'enregistrement, garde-fou minimum 30 s, bouton de téléchargement * local de l'audio (le backend ne stocke aucun audio). * * Le streaming chunk-par-chunk a été retiré au Sprint 4c-3 : l'audio est * envoyé entier au backend après stop, le backend appelle Gemini batch pour * transcrire. `useAudioRecorder.subscribeChunks` reste exposé côté hook * pour un usage futur (ex. réactivation Deepgram live). */ import { useEffect } from 'react' import { Download, Mic, MicOff, Square } from 'lucide-react' import { Button } from '@/shared/ui/Button' import { formatTimer } from '../lib/simulationConfig' import { useAudioRecorder } from '../hooks/useAudioRecorder' import { RecordingTimeline } from './RecordingTimeline' import { RecordingWaveform } from './RecordingWaveform' interface Props { /** Durée minimale (s) avant que la soumission soit autorisée. */ minSeconds: number /** * Sprint 4b.3 — durée maximale recommandée (s). À l'atteinte, le hook * arrête automatiquement l'enregistrement et l'`onSubmit` est déclenché * via le chemin existant (status='stopped' → useEffect onSubmit). */ maxSeconds?: number /** Notification optionnelle quand `maxSeconds` est atteint. */ onMaxReached?: () => void /** Nom de fichier proposé au téléchargement local (sans extension). */ downloadFilename: string /** Appelé au clic « Arrêter et soumettre » avec le blob final + son MIME. */ onSubmit: (audioBlob: Blob, audioMimeType: string | null) => void onCancel: () => void /** Initialisé à true → l'utilisateur démarre l'enregistrement automatiquement * au mount. Sinon, un bouton « Démarrer » est affiché. */ autoStart?: boolean /** * Sprint 4c-3 — désactive les contrôles tant qu'une soumission est en * cours côté parent (transcription + correction backend ~30-60 s). */ disabled?: boolean } export function AudioRecorder({ minSeconds, maxSeconds, onMaxReached, downloadFilename, onSubmit, onCancel, autoStart = true, disabled = false, }: Props) { const recorder = useAudioRecorder({ maxSeconds, onMaxReached }) // Auto-start au mount si demandé. Pas de dépendance sur `recorder.start` // pour éviter les re-runs au changement d'identité de la fonction. useEffect(() => { if (!autoStart) return void recorder.start() // eslint-disable-next-line react-hooks/exhaustive-deps }, [autoStart]) const isRecording = recorder.status === 'recording' const isStopped = recorder.status === 'stopped' const remaining = Math.max(0, minSeconds - recorder.elapsedSeconds) const submitEnabled = isRecording && remaining === 0 function handleSubmitClick() { recorder.stop() } // Quand le recorder passe en 'stopped', on remonte le blob au parent. useEffect(() => { if (recorder.status === 'stopped' && recorder.audioBlob) { onSubmit(recorder.audioBlob, recorder.audioMimeType) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [recorder.status, recorder.audioBlob]) if (recorder.status === 'error') { return (
) } return (
{isRecording && (
)} {maxSeconds && (isRecording || isStopped) && (
)} {isRecording && remaining > 0 && (

Minimum 30 secondes requis ({remaining} s restantes).

)}
{isRecording && ( <> )} {recorder.status === 'idle' && ( )} {isStopped && ( )}
) }