Cards control

This commit is contained in:
2026-05-02 19:52:48 +02:00
parent 6ddb120c94
commit 4e823bc403
7 changed files with 123 additions and 5 deletions

View File

@@ -180,6 +180,10 @@ def build_lesson_reply(db: Session, student_id: int, user_message: str, lesson_c
"content": (
"Tu reçois un contexte courant de leçon lié à la card affichée. "
"Tu dois parler uniquement de cette card, sauf si l'élève pose une question de clarification immédiate. "
"N'utilise que les nombres, mots de vocabulaire et actions explicitement autorisés dans le contexte de la card. "
"Si un nombre n'est pas affiché ou autorisé dans ce contexte, tu n'en parles pas. "
"Ne propose jamais 'un autre nombre' si le contexte ne donne pas cet autre nombre. "
"Si l'élève veut continuer mais que la card courante est terminée, mets `advance_lesson_step` à true. "
"N'utilise pas l'historique des séances précédentes pour choisir le thème de ta réponse. "
"Ta réponse visible doit être très courte: maximum 24 mots, 1 phrase d'explication "
"puis éventuellement 1 question courte. Évite les phrases longues avec plusieurs propositions. "

View File

@@ -8,6 +8,14 @@ Idée à enseigner :
- Le groupe `208` est le groupe des unités simples.
- Ce découpage aide à lire le nombre sans se perdre.
Nombre autorisé sur cette card :
- `345 208` uniquement.
Vocabulaire autorisé :
- groupe des milliers ;
- groupe des unités simples ;
- couper entre `345` et `208`.
Consigne orale courte pour Professeur TOP :
Explique que pour lire un grand nombre, on le sépare en deux paquets de trois chiffres : d'abord les milliers, puis les unités simples. Prends uniquement l'exemple affiché `345 208`. Demande ensuite à l'élève s'il comprend pourquoi on coupe entre `345` et `208`.
@@ -16,6 +24,7 @@ Limites :
- Ne donne pas les autres exemples de la fiche.
- Ne passe pas à la lecture complète du nombre sauf si l'élève demande ou semble prêt.
- Reste sur le thème : découper un nombre de 6 chiffres en deux groupes.
- N'utilise pas l'expression `groupe d'unité`; dis toujours `groupe des unités simples`.
Critère pour passer à la card suivante :
L'élève peut passer à la suite s'il dit clairement qu'il a compris, ou s'il explique avec ses mots que `345` représente les milliers et `208` les unités simples.

View File

@@ -7,12 +7,23 @@ Idée à enseigner :
- `425` est le groupe des unités simples.
- Le nombre se lit `sept-mille-quatre-cent-vingt-cinq`.
Nombre autorisé sur cette card :
- `7 425` uniquement.
Vocabulaire autorisé :
- milliers ;
- unités simples ;
- lire à voix haute ;
- `sept-mille`, puis `quatre-cent-vingt-cinq`.
Consigne orale courte pour Professeur TOP :
Demande à l'élève de lire `7 425` à voix haute. S'il hésite, aide-le en deux morceaux : `sept-mille`, puis `quatre-cent-vingt-cinq`.
Limites :
- Ne donne pas les deux autres exemples.
- Ne transforme pas encore en dictée.
- Ne propose pas de lire un autre nombre sur cette card.
- Si l'élève dit oui pour continuer après avoir travaillé `7 425`, passe à la card suivante au lieu de redonner `7 425`.
Critère pour passer à la card suivante :
L'élève peut passer à la suite s'il lit `7 425` correctement ou avec une petite correction guidée.

View File

@@ -7,12 +7,22 @@ Idée à enseigner :
- `090` se lit comme `90`, mais le zéro aide à garder la place.
- Le nombre se lit `trente-huit-mille-quatre-vingt-dix`.
Nombre autorisé sur cette card :
- `38 090` uniquement.
Vocabulaire autorisé :
- milliers ;
- zéro qui garde la place ;
- `090` se lit `quatre-vingt-dix`.
Consigne orale courte pour Professeur TOP :
Demande à l'élève de lire `38 090`. S'il oublie le zéro ou dit mal la fin, rappelle que `090` se lit `quatre-vingt-dix`.
Limites :
- Ne parle pas de tous les zéros en général.
- Reste sur le nombre affiché.
- Ne propose pas de lire un autre nombre sur cette card.
- Si l'élève veut continuer après `38 090`, passe à la card suivante au lieu de répéter `38 090`.
Critère pour passer à la card suivante :
L'élève peut passer à la suite s'il lit `38 090` en gardant bien `trente-huit-mille`.

View File

@@ -7,12 +7,21 @@ Idée à enseigner :
- `012` se lit comme `12`, mais les zéros gardent la structure du groupe.
- Le nombre se lit `cinq-cent-six-mille-douze`.
Nombre autorisé sur cette card :
- `506 012` uniquement.
Vocabulaire autorisé :
- milliers ;
- zéros qui gardent la structure ;
- `012` se lit `douze`.
Consigne orale courte pour Professeur TOP :
Demande à l'élève de lire `506 012`. S'il hésite, aide-le à dire `cinq-cent-six-mille`, puis `douze`.
Limites :
- Ne parle pas d'autres nombres.
- Ne donne pas une leçon complète sur les zéros.
- Ne propose pas de lire un autre nombre sur cette card.
Critère pour passer à la card suivante :
L'élève a réussi cette fiche s'il lit `506 012` correctement ou s'il corrige sa lecture après aide.

View File

@@ -217,6 +217,7 @@ function TutorApp({ currentUser, onLogout }) {
const [cardProgress, setCardProgress] = useState([])
const [sessionCardStates, setSessionCardStates] = useState({})
const [sessionLogs, setSessionLogs] = useState([])
const [selectedSessionLog, setSelectedSessionLog] = useState(null)
const [progressPanelOpen, setProgressPanelOpen] = useState(false)
const [settingsPanelOpen, setSettingsPanelOpen] = useState(false)
const mediaRecorderRef = useRef(null)
@@ -309,6 +310,7 @@ function TutorApp({ currentUser, onLogout }) {
setCardProgress([])
setSessionCardStates({})
setSessionLogs([])
setSelectedSessionLog(null)
if (selectedStudentId) {
loadProgress(selectedStudentId)
loadMessages(selectedStudentId)
@@ -520,6 +522,7 @@ function TutorApp({ currentUser, onLogout }) {
async function saveLessonSession() {
if (!selectedStudentId || !studentProgram) return
const currentCardState = getActiveCardState(40, false)
const conversation = messages.map((message) => ({
id: message.id,
role: message.role,
@@ -542,10 +545,11 @@ function TutorApp({ currentUser, onLogout }) {
lesson_id: studentProgram.id,
lesson_title: studentProgram.title,
conversation,
card_states: getMergedCardStates(),
card_states: getMergedCardStates(currentCardState),
}),
})
await loadCardProgress(selectedStudentId, studentProgram.id)
if (!isStudentUser) await loadSessionLogs(selectedStudentId)
}
async function endSession() {
@@ -697,12 +701,22 @@ function TutorApp({ currentUser, onLogout }) {
async function deleteSessionLog(logId) {
try {
await apiFetch(`/admin/session-logs/${logId}`, { method: 'DELETE' })
setSelectedSessionLog((current) => (current?.id === logId ? null : current))
await loadSessionLogs(selectedStudentId)
} catch (error) {
setErrorMessage(error.message || 'Impossible deffacer ce log.')
}
}
function parseLogJson(value) {
try {
const parsed = JSON.parse(value || '[]')
return Array.isArray(parsed) ? parsed : []
} catch {
return []
}
}
function goToPreviousLessonStep() {
if (activeSectionIndex > 0) {
setActiveSectionIndex((index) => index - 1)
@@ -764,7 +778,7 @@ function TutorApp({ currentUser, onLogout }) {
})
}
function getMergedCardStates() {
function getMergedCardStates(extraState = null) {
const fromProgress = cardProgress
.filter((item) => item.lesson_id === studentProgram?.id)
.map((item) => ({
@@ -780,6 +794,7 @@ function TutorApp({ currentUser, onLogout }) {
const merged = {}
for (const item of fromProgress) merged[item.section_key] = item
for (const item of Object.values(sessionCardStates)) merged[item.section_key] = item
if (extraState) merged[extraState.section_key] = extraState
return Object.values(merged)
}
@@ -1590,9 +1605,14 @@ function TutorApp({ currentUser, onLogout }) {
<strong>{log.lesson_title}</strong>
<span>{new Date(log.ended_at).toLocaleString('fr-FR')}</span>
</div>
<div className="admin-log-actions">
<button type="button" className="secondary-button" onClick={() => setSelectedSessionLog(log)}>
Voir
</button>
<button type="button" className="secondary-button" onClick={() => deleteSessionLog(log.id)}>
Effacer
</button>
</div>
</article>
))}
</div>
@@ -1602,6 +1622,32 @@ function TutorApp({ currentUser, onLogout }) {
</section>
)}
{selectedSessionLog && (
<section className="admin-preview">
<div className="admin-log-detail-head">
<h2>Détail de séance</h2>
<button type="button" className="secondary-button" onClick={() => setSelectedSessionLog(null)}>
Fermer
</button>
</div>
<div className="admin-log-detail">
<h3>Résultats des cards</h3>
{parseLogJson(selectedSessionLog.card_results).map((item) => (
<article key={item.section_key} className="admin-log-card">
<strong>{item.section_title}</strong>
<span>{Math.round(item.comprehension_score)}% · {item.validated ? 'validée' : 'vue/tentée'}</span>
</article>
))}
<h3>Conversation</h3>
{parseLogJson(selectedSessionLog.conversation).map((message, index) => (
<p key={`${message.id || index}`} className="admin-log-message">
<strong>{message.role === 'assistant' ? 'Professeur TOP' : 'Élève'}:</strong> {message.content}
</p>
))}
</div>
</section>
)}
{errorMessage && <p className="muted voice-status visible-status">{errorMessage}</p>}
</main>
</div>

View File

@@ -217,6 +217,35 @@ button:disabled { opacity: 0.5; cursor: not-allowed; }
color: #64748b;
font-size: 0.9rem;
}
.admin-log-actions {
display: flex;
gap: 0.5rem;
}
.admin-log-detail-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
}
.admin-log-detail {
display: flex;
flex-direction: column;
gap: 0.65rem;
border: 1px solid #e5e7eb;
border-radius: 12px;
background: #f8fafc;
padding: 1rem;
}
.admin-log-detail h3 {
margin: 0.6rem 0 0;
}
.admin-log-detail h3:first-child {
margin-top: 0;
}
.admin-log-message {
margin: 0;
line-height: 1.45;
}
.session-user {
margin-left: auto;
display: flex;