From 3016d909a6be29dd1dd7146dbad627d4ec53df80 Mon Sep 17 00:00:00 2001 From: Hermann_Kitio Date: Tue, 30 Jun 2026 22:53:57 +0300 Subject: [PATCH] =?UTF-8?q?feat(t1-live):=20T1=20Live=20frontend=20?= =?UTF-8?q?=E2=80=94=20Sprint=207b?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add T1 state machine (8 states, presenting ⇄ interrupted) - Add useT1LiveSession (WS /t1/live, uplink gate by ref, no context msg) - Add T1PreparationPage, T1DialoguePage, T1SpeakingIndicator - Add EO_T1_LIVE card in TaskSelector gated via oral_t2_live - Extract shared t1Questionnaire.ts for batch/live DRY - Remove T1LiveQuestionnairePage + T1LiveContext (post patch 7a) - Simplified flow: card → preparation → dialogue - FTD-44 frozen (cross-feature audio hooks, Sprint 7.5) - FTD-45/46 frozen (Gemini relance quality + transcription) - Tests: 301/301 green --- docs/CHANGELOG.md | 29 ++ docs/ROADMAP.md | 2 +- docs/TECH_DEBT.md | 47 +- src/app/router.tsx | 8 + .../simulations/components/TaskSelector.tsx | 43 +- .../simulations/lib/t1Questionnaire.ts | 78 +++ .../simulations/pages/QuestionnaireT1Page.tsx | 67 +-- .../simulations/pages/SimulationEOPage.tsx | 1 + .../components/T1SpeakingIndicator.tsx | 114 +++++ .../t1-live/hooks/useT1LiveSession.ts | 453 ++++++++++++++++++ src/features/t1-live/pages/T1DialoguePage.tsx | 237 +++++++++ .../t1-live/pages/T1PreparationPage.tsx | 122 +++++ .../state/__tests__/t1-machine.test.ts | 127 +++++ src/features/t1-live/state/t1-machine.ts | 125 +++++ 14 files changed, 1385 insertions(+), 68 deletions(-) create mode 100644 src/features/simulations/lib/t1Questionnaire.ts create mode 100644 src/features/t1-live/components/T1SpeakingIndicator.tsx create mode 100644 src/features/t1-live/hooks/useT1LiveSession.ts create mode 100644 src/features/t1-live/pages/T1DialoguePage.tsx create mode 100644 src/features/t1-live/pages/T1PreparationPage.tsx create mode 100644 src/features/t1-live/state/__tests__/t1-machine.test.ts create mode 100644 src/features/t1-live/state/t1-machine.ts diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 914a643..8349eaa 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -29,6 +29,35 @@ Chaque entrée suit ce format : --- +## [Unreleased] — 2026-06-30 — Sprint 7b — Frontend T1 Live (monologue + interruption non déterministe) + +### Added + +- Machine d'état T1 (`features/t1-live/state/t1-machine.ts`) — 8 états purs (`idle`, `preparing`, `connecting`, `presenting`, `interrupted`, `processing`, `ended`, `error`). Le cœur est la transition `interrupted ⇄ presenting` (interruption examinateur puis reprise candidat). +23 tests. +- `useT1LiveSession` (`features/t1-live/hooks/useT1LiveSession.ts`) — orchestrateur du dialogue T1, calqué sur `useT2LiveSession` (discipline « Voie A »). WS `wss://${API_URL}/t1/live?token=` (PAS de `&sujet=` — T1 n'est pas subject-based). Aucun VAD micro (T1 = monologue) ; l'uplink micro est coupé/rétabli pendant une interruption via un **ref** (`uplinkMutedRef`), jamais via `setState` (leçon Voie A). Réagit aux signaux applicatifs `{type:'interruption_start'}` / `{type:'interruption_end'}`. Timer dur 180 s. Close codes 1000/4001/4003/4005/4006. +- `T1PreparationPage` + `T1DialoguePage` (`features/t1-live/pages/`) — parcours préparation → dialogue (3:00) ; écran terminal « Télécharger l'audio » + « Voir le rapport » (`/rapport/:id`). L'UI ne suppose JAMAIS qu'une relance suit (interruption non déterministe). +- `T1SpeakingIndicator` (`features/t1-live/components/`) — indicateur de prise de parole (amplitude micro réelle en `presenting`, animation décorative en `interrupted`). +- Carte `EO_T1_LIVE` dans `TaskSelector` (discriminateur `live?: 'T1' | 'T2'`, label « Tâche 1 — Live ») gatée Premium via `hasAccess(plan, 'oral_t2_live')` (TD-24 — pas de nouvelle permission, le gate couvre T1 et T2 Live) + prop `onT1LiveSelect`. `SimulationEOPage` câble `onT1LiveSelect → /simulation/eo/t1/live/preparation`. +- `features/simulations/lib/t1Questionnaire.ts` — définition partagée du questionnaire T1 (FIELDS + schéma zod + `EMPTY_REPONSES`), réutilisée par le batch `QuestionnaireT1Page`. + +### Changed + +- `useT1LiveSession` aligné sur le **Patch 7a backend** : plus d'envoi du message `{type:'context'}`, plus d'option `reponses`, la session audio démarre directement sur `ws.onopen` (WS_OPENED → presenting). +- Parcours T1 Live simplifié : carte `EO_T1_LIVE` → préparation → dialogue (plus d'étape questionnaire intermédiaire). +- `t1-machine` : commentaire et test nettoyés (mapping close **4004** retiré → 4006), cohérent avec la suppression du contexte côté backend. + +### Removed + +- `T1LiveQuestionnairePage` et `T1LiveContext` (post-Patch 7a) — le backend n'exige plus de message `context` ni de réponses pré-remplies ; ces écrans/état deviennent sans objet. + +### Notes + +- **FTD-44 gelée** (§3bis TECH_DEBT) — les trois hooks audio génériques sont empruntés à `features/t2-live/hooks/` (violation FSD inter-features assumée et tracée, sites marqués `// TODO(FTD-44)`), réactivée au Sprint 7.5 (factorisation Sprint 7). +- WebSocket / AudioContext non matérialisables en jsdom → validation manuelle ; la logique pure de transition est couverte par `t1-machine.test.ts`. +- Bugs amont observés au test manuel, hors contrôle frontend : **FTD-45** (relances Gemini hors-sujet, extension TD-23) et **FTD-46** (transcription Gemini Live hasardeuse). + +--- + ## [Unreleased] — 2026-06-29 — Sprint 6e — T2 Live « Voie A » (mix audio temps réel) ### Added diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 1556dc0..bbd7e7d 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -164,7 +164,7 @@ ## Sprint 7 — T1 Live (interruption aléatoire) - **7a (backend) ✅** : extension du proxy WebSocket Gemini Live (`gemini-3.1-flash-live-preview`, ws brut, pas de SDK) au mode T1 — system prompt « examinateur », décision d'interruption probabiliste, génération de la question de relance sur transcription partielle (DeepSeek). Réutilise l'infra T2 Live. Scoring EO 5 critères × /4. Phonologie live = 0 (TD-08, gelé). Contraintes héritées : pas de `speechConfig`. Livré : commits `868bd09` (code) + `3722e2a` (docs) ; dettes tracées TD-23/24/25 (cf. `TECH_DEBT-backend.md`). -- **7b (frontend)** : UI T1 Live réutilisant ws-client + audio worklet + state machine T2 ; phase préparation ; gestion interruption / reprise du flux audio dans la state machine ; gating Premium. +- **7b (frontend) ✅** : UI T1 Live — machine d'état T1 (8 états, `interrupted ⇄ presenting`), `useT1LiveSession` (WS `/t1/live`, sans message `context` post-Patch 7a, uplink coupé par ref pendant interruption), `T1PreparationPage` / `T1DialoguePage` / `T1SpeakingIndicator`, carte `EO_T1_LIVE` gatée Premium (`oral_t2_live`). Parcours simplifié carte → prépa → dialogue. `T1LiveQuestionnairePage` + `T1LiveContext` retirés. Réutilise les hooks audio T2 (FTD-44 gelée). **Bugs amont observés au test manuel** (hors contrôle frontend) : **FTD-45** (relances Gemini hors-sujet, extension TD-23) et **FTD-46** (transcription Gemini Live hasardeuse). ## Sprint 7.5 — Clean diff --git a/docs/TECH_DEBT.md b/docs/TECH_DEBT.md index c2ec53e..64d65b5 100644 --- a/docs/TECH_DEBT.md +++ b/docs/TECH_DEBT.md @@ -1,6 +1,6 @@ # TECH_DEBT.md — Expria Frontend -> **Document de référence — Version 1.28** +> **Document de référence — Version 1.30** > Ce document recense les décisions techniques prises par pragmatisme qui devront être revisitées, les stubs temporaires, et les fonctionnalités reportées. > À mettre à jour après chaque session de développement. > @@ -456,6 +456,49 @@ Frontend : --- +### FTD-44 — Hooks audio génériques empruntés à `features/t2-live/` (T1 Live) + +**Priorité :** 🟡 Important +**Statut :** Gelé — réactivé au Sprint 7.5 (« factorisation Sprint 7 ») +**Estimation de session :** 0,5 jour +**Description :** Le flux T1 Live (Sprint 7b) importe directement les trois hooks audio génériques de `features/t2-live/hooks/` (`useAudioCapture`, `useAudioPlayback`, `useAudioRecording`) — une violation assumée de la frontière inter-features FSD (un feature ne devrait pas importer un sibling). Décision prise pour NE PAS toucher aux fichiers T2 (pipeline audio validé à l'oreille, intouchable jusqu'à factorisation). Sites d'import marqués `// TODO(FTD-44)` dans `features/t1-live/hooks/useT1LiveSession.ts`. + +**À faire :** relocaliser les trois hooks (génériques par nature : aucune logique T2 spécifique) vers `shared/lib/audio/`, puis migrer les imports T2 ET T1 vers ce chemin partagé. Validation à l'oreille obligatoire après déplacement (T2 + T1). + +**Condition de résolution :** Sprint 7.5 (factorisation Sprint 7), une fois les flux T1 et T2 Live stabilisés. + +--- + +### FTD-45 — Relances Gemini T1 Live hors-sujet (extension TD-23) + +**Priorité :** 🟡 Important +**Statut :** Gelé — dépend de l'amont (Gemini / backend TD-23), hors contrôle frontend +**Estimation de session :** à évaluer (chantier backend/prompt) +**Description :** En T1 Live, l'examinateur (Gemini) formule ses relances à partir de son contexte audio interne. Au test manuel, certaines relances partent **hors-sujet** (sans rapport avec ce que le candidat vient de dire). Extension de la dette backend **TD-23** : en VAD manuel, `inputTranscription` candidat n'est flushé qu'à `activityEnd`, donc le modèle relance sans transcription token-par-token fiable. + +**Impact actuel :** dégrade le réalisme de l'entretien T1 ; non bloquant pour la livraison 7b (le flux fonctionne, l'évaluation finale reste correcte). + +**À faire :** ré-évaluer côté backend/prompt (formulation de la consigne de relance, fenêtre de contexte) une fois la transcription incrémentale repensée (Sprint 7e / TD-23). + +**Condition de résolution :** après traitement de TD-23 (transcription live) — non actionnable côté frontend seul. + +--- + +### FTD-46 — Transcription Gemini Live hasardeuse (qualité audio→texte) + +**Priorité :** 🟡 Important +**Statut :** Gelé — dépend de l'amont (qualité Gemini Live), hors contrôle frontend +**Estimation de session :** à évaluer +**Description :** La transcription produite par Gemini Live (`input/outputTranscription`) est de qualité **inégale** : mots manqués, segments approximatifs. Observé au test manuel T1 Live (et applicable au T2 Live). Affecte la fidélité du transcript utilisé pour l'évaluation et bloquera l'affichage live (Sprint 7e). + +**Impact actuel :** qualité du transcript variable ; non bloquant pour 7b (l'évaluation 5 critères reste exploitable). + +**À faire :** suivre l'évolution du modèle Gemini Live ; évaluer un post-traitement ou une source de transcription alternative si la qualité reste insuffisante au Sprint 7e. + +**Condition de résolution :** amélioration amont (modèle) ou décision d'architecture transcription au Sprint 7e. + +--- + ## 4. Tests à renforcer > FTD-09 gelée au Sprint 5.5 (2026-04-26) — voir §3bis Backlog gelé. @@ -555,3 +598,5 @@ Frontend : | 1.26 | 2026-04-26 | Sprint 5e (clean Sprint 5 Billing) — Ajout FTD-42 🟡 (modal prorata Standard→Premium avec montant exact — divergence PARCOURS_UTILISATEURS §3, actuellement Customer Portal natif sans preview in-app) et FTD-43 🟢 (race condition webhook post-redirect Stripe — `usePlan()` peut retourner ancien plan brièvement). **21 FTD actives — cap 15 dépassé de 6. Résorption FTD critique au Sprint 5.5 avant Sprint 6.** | | 1.27 | 2026-04-26 | Sprint 5.5 Clean — FTD-09, FTD-33, FTD-42 gelées. FTD-35 fermée (subsumée par FTD-41). FTD-14, FTD-38, FTD-39 résolues. **14 FTD actives** (cap 15 respecté). | | 1.28 | 2026-04-26 | Sprint 6c — FTD-09 et FTD-33 résolues (dégelées → fermées). **14 FTD actives** (inchangé — les gelées ne comptaient pas dans le cap). | +| 1.29 | 2026-06-30 | Sprint 7b (T1 Live) — Ajout FTD-44 🟡 **gelée** (hooks audio génériques empruntés à `features/t2-live/`, réactivée au Sprint 7.5). **14 FTD actives** (inchangé — entrée gelée, ne compte pas dans le cap, même mécanique que FTD-06). | +| 1.30 | 2026-06-30 | Sprint 7b (T1 Live, finalisation) — Ajout FTD-45 🟡 **gelée** (relances Gemini hors-sujet, extension TD-23) et FTD-46 🟡 **gelée** (transcription Gemini Live hasardeuse). Bugs amont observés au test manuel, hors contrôle frontend. **14 FTD actives** (inchangé — entrées gelées, ne comptent pas dans le cap). | diff --git a/src/app/router.tsx b/src/app/router.tsx index 5fcabcb..54ec63c 100644 --- a/src/app/router.tsx +++ b/src/app/router.tsx @@ -24,6 +24,8 @@ import { T2LiveProvider } from '@/features/t2-live/state/T2LiveContext' import { T2SujetsPage } from '@/features/t2-live/pages/T2SujetsPage' import { T2PreparationPage } from '@/features/t2-live/pages/T2PreparationPage' import { T2DialoguePage } from '@/features/t2-live/pages/T2DialoguePage' +import { T1PreparationPage } from '@/features/t1-live/pages/T1PreparationPage' +import { T1DialoguePage } from '@/features/t1-live/pages/T1DialoguePage' import { AppLayout } from './AppLayout' const DesignSystemPage = import.meta.env.DEV @@ -105,6 +107,12 @@ export function AppRouter() { } /> + {/* Sprint 7b — T1 Live (Premium) : prépa 2min → dialogue 3:00 avec + interruption non déterministe de l'examinateur. Entrée directe depuis + la carte TaskSelector (plus de questionnaire — Patch 7a backend). */} + } /> + } /> + {/* Autres sections — Sprint 4+ */} } /> } /> diff --git a/src/features/simulations/components/TaskSelector.tsx b/src/features/simulations/components/TaskSelector.tsx index a27db91..2b8d58e 100644 --- a/src/features/simulations/components/TaskSelector.tsx +++ b/src/features/simulations/components/TaskSelector.tsx @@ -33,14 +33,22 @@ interface Props { * reste verrouillée. */ onT2LiveSelect?: () => void + /** + * Sprint 7b — handler dédié pour la carte EO_T1_LIVE. Même gating Premium + * que T2 Live (`oral_t2_live`, TD-24 — pas de permission distincte). La + * production EO_T1 est créée en backend en fin de session, pas au clic. + */ + onT1LiveSelect?: () => void } interface TaskCard { key: string - tache: Tache | null // null = carte verrouillée (EO_T2 Live) + tache: Tache | null // null = carte non sélectionnable directement (Live → handler dédié) label: string sublabel: string lockLabel?: string + /** Sprint 7b — carte « Live » (T1 ou T2) : gating Premium via hasAccess. */ + live?: 'T1' | 'T2' } const EE_CARDS: readonly TaskCard[] = [ @@ -52,12 +60,21 @@ const EE_CARDS: readonly TaskCard[] = [ const EO_CARDS: readonly TaskCard[] = [ { key: 'EO_T1', tache: 'EO_T1', label: 'Expression Orale', sublabel: 'Entretien' }, { key: 'EO_T3', tache: 'EO_T3', label: 'Expression Orale', sublabel: 'Point de vue' }, + { + key: 'EO_T1_LIVE', + tache: null, + label: 'Expression Orale', + sublabel: 'Tâche 1 — Live', + lockLabel: 'Exclusivité Premium', + live: 'T1', + }, { key: 'EO_T2_LIVE', tache: 'EO_T2_LIVE', label: 'Expression Orale', sublabel: 'Tâche 2 — Live', lockLabel: 'Exclusivité Premium', + live: 'T2', }, ] @@ -68,10 +85,13 @@ export function TaskSelector({ isLoading, onSelect, onT2LiveSelect, + onT1LiveSelect, }: Props) { const simulationCheck = canSimulate(plan, simulationsUsed) const quotaBlocked = !simulationCheck.allowed const cards = type === 'EE' ? EE_CARDS : EO_CARDS + // TD-24 : T1 Live ET T2 Live partagent la même permission `oral_t2_live`. + const t1LiveUnlocked = hasAccess(plan, 'oral_t2_live') && Boolean(onT1LiveSelect) const t2LiveUnlocked = hasAccess(plan, 'oral_t2_live') && Boolean(onT2LiveSelect) return ( @@ -98,16 +118,21 @@ export function TaskSelector({
{cards.map((card) => { - const isT2Live = card.tache === 'EO_T2_LIVE' - // T2 Live ne consomme pas le quota Free (plan Premium uniquement, illimité). - // Verrouillage : T2 Live → hasAccess(plan, 'oral_t2_live') ; autres → quota. - const locked = isT2Live ? !t2LiveUnlocked : card.tache === null || quotaBlocked + const isT1Live = card.live === 'T1' + const isT2Live = card.live === 'T2' + // Live ne consomme pas le quota Free (plan Premium uniquement, illimité). + // Verrouillage : Live → hasAccess(plan, 'oral_t2_live') ; autres → quota. + const locked = isT1Live + ? !t1LiveUnlocked + : isT2Live + ? !t2LiveUnlocked + : card.tache === null || quotaBlocked const abbrev = card.tache ? card.tache.split('_')[0] : 'EO' if (locked) { return ( - {(card.tache === null || isT2Live) && ( + {(card.tache === null || isT1Live || isT2Live) && (