bouton fin de séance

This commit is contained in:
2026-04-25 21:17:43 +02:00
parent 96a566fa09
commit baa75807e2
3 changed files with 131 additions and 16 deletions

View File

@@ -28,7 +28,12 @@ Ce projet est un POC de professeur virtuel pour enfants, avec:
- Les reponses aux mini-tests sont maintenant aussi enregistrees dans `messages` avec le role `user`.
- Le frontend recharge l'historique quand un eleve est selectionne, mais ne met plus automatiquement la derniere ancienne phrase au centre.
- Au demarrage d'une nouvelle seance, Professeur TOP doit faire une courte reprise de l'historique puis annoncer le cours du jour.
- L'interface de seance est maintenant en hauteur fixe `100vh`: pas de scroll global, cours central au milieu, historique transforme en journal compact.
- L'interface de seance est maintenant en hauteur fixe `100vh`: pas de scroll global, cours central au milieu, pas de conversation visible.
- La conversation reste stockee en base pour analyse, mais l'ecran de cours remplace la consigne/reponse courante au lieu d'empiler des bulles.
- Les reponses de mini-test utilisent la meme zone de reponse eleve en bas; le texte du prof reste au-dessus et pourra accueillir une image sous le texte.
- Cote eleve, le bouton principal bascule entre `Demarrer la seance` et `Fin de la seance`.
- Cote eleve, la colonne gauche affiche les themes du jour: une card lecon avec pourcentage local, une card exercice avec statut.
- Le bilan complet de progression reste reserve aux vues prof/admin.
- Le vrai `docker-compose.yml` de production n'est pas dans ce repo. Il est situe un niveau au-dessus sur le serveur.
- La conf nginx reelle route:
- `/api/` vers `tutor-backend:8000`

View File

@@ -183,6 +183,9 @@ function TutorApp({ currentUser, onLogout }) {
})
const [messages, setMessages] = useState([])
const [currentInstruction, setCurrentInstruction] = useState('')
const [sessionActive, setSessionActive] = useState(false)
const [currentFocusSkill, setCurrentFocusSkill] = useState(null)
const [exerciseStatus, setExerciseStatus] = useState('not-started')
const [input, setInput] = useState('')
const [progress, setProgress] = useState([])
const [assessment, setAssessment] = useState(null)
@@ -232,6 +235,21 @@ function TutorApp({ currentUser, onLogout }) {
return Math.round(total / progress.length)
}, [progress])
const lessonFocus = useMemo(() => {
if (currentFocusSkill) return currentFocusSkill
if (!progress.length) return null
return [...progress].sort((a, b) => a.mastery_score - b.mastery_score)[0]
}, [currentFocusSkill, progress])
const lessonPercent = lessonFocus ? Math.round(lessonFocus.mastery_score) : averageScore
const exerciseLabel = {
'not-started': 'Pas encore lance',
'in-progress': 'En cours',
resolved: 'Resolu',
'needs-review': 'A revoir',
}[exerciseStatus]
useEffect(() => {
loadStudents()
}, [currentUser])
@@ -260,6 +278,11 @@ function TutorApp({ currentUser, onLogout }) {
useEffect(() => {
setCurrentInstruction('')
setSessionActive(false)
setAssessment(null)
setAssessmentAnswer('')
setCurrentFocusSkill(null)
setExerciseStatus('not-started')
if (selectedStudentId) {
loadProgress(selectedStudentId)
loadMessages(selectedStudentId)
@@ -368,6 +391,9 @@ function TutorApp({ currentUser, onLogout }) {
const data = await apiFetch(`/session/start?student_id=${selectedStudentId}`, { method: 'POST' })
appendMessage('assistant', data.reply)
setCurrentInstruction(data.reply)
setSessionActive(true)
setExerciseStatus('not-started')
setCurrentFocusSkill((current) => current || lessonFocus)
speak(data.reply)
await loadProgress(selectedStudentId)
} catch (error) {
@@ -397,6 +423,15 @@ function TutorApp({ currentUser, onLogout }) {
setAssessment(data)
setAssessmentAnswer('')
setCurrentInstruction(data.question)
setSessionActive(true)
setExerciseStatus('in-progress')
setCurrentFocusSkill(
progress.find((item) => item.code === data.skill_code) || {
code: data.skill_code,
label: data.skill_label,
mastery_score: lessonPercent,
}
)
} catch (error) {
setErrorMessage(error.message || 'Impossible de charger le mini-test.')
}
@@ -419,6 +454,7 @@ function TutorApp({ currentUser, onLogout }) {
appendMessage('user', `Reponse au mini-test: ${assessmentAnswer}`)
appendMessage('assistant', data.feedback)
setCurrentInstruction(data.feedback)
setExerciseStatus(data.correct ? 'resolved' : 'needs-review')
speak(data.feedback)
setAssessment(null)
setAssessmentAnswer('')
@@ -441,6 +477,24 @@ function TutorApp({ currentUser, onLogout }) {
setSpeaking(false)
}
function endSession() {
stopSpeechPlayback()
deactivateAutoListening(false)
setSessionActive(false)
setCurrentInstruction('')
setAssessment(null)
setAssessmentAnswer('')
setInput('')
}
async function toggleSession() {
if (sessionActive) {
endSession()
return
}
await startSession()
}
async function loadMessages(studentId) {
try {
setErrorMessage('')
@@ -805,7 +859,7 @@ function TutorApp({ currentUser, onLogout }) {
}
return (
<div className="layout">
<div className={`layout ${isStudentUser ? 'student-layout' : 'admin-layout'}`}>
<aside className="sidebar card">
<h2>{isStudentUser ? 'Ma seance' : 'Eleves'}</h2>
{!isStudentUser && (
@@ -858,7 +912,33 @@ function TutorApp({ currentUser, onLogout }) {
<div className="session-summary">
<strong>{selectedStudent.first_name}</strong>
<span>{selectedStudent.grade}</span>
<span>Score moyen: {averageScore}%</span>
<span>{sessionActive ? 'Seance en cours' : 'Pret pour la seance'}</span>
</div>
)}
{isStudentUser && (
<div className="today-panel">
<button onClick={toggleSession} disabled={!selectedStudentId}>
{sessionActive ? 'Fin de la seance' : 'Demarrer la seance'}
</button>
<h3>Themes du jour</h3>
<div className="today-card lesson-card">
<div className="today-card-head">
<strong>Lecon</strong>
<span>{lessonPercent}% acquis</span>
</div>
<p>{lessonFocus?.label || 'Le sujet sera choisi au demarrage.'}</p>
</div>
<div className="today-card exercise-card">
<div className="today-card-head">
<strong>Exercice</strong>
<span>{exerciseLabel}</span>
</div>
<p>{assessment?.skill_label || lessonFocus?.label || 'Aucun exercice lance.'}</p>
<button type="button" onClick={fetchAssessment} disabled={!selectedStudentId || !sessionActive}>
{exerciseStatus === 'not-started' ? 'Lancer exercice' : 'Nouvel exercice'}
</button>
</div>
</div>
)}
@@ -924,11 +1004,13 @@ function TutorApp({ currentUser, onLogout }) {
</form>
)}
<form onSubmit={sendMessage} className="composer">
<form onSubmit={assessment ? submitAssessment : sendMessage} className="composer">
<input
value={input}
onChange={(event) => setInput(event.target.value)}
placeholder="Pose une question ou demande une explication..."
value={assessment ? assessmentAnswer : input}
onChange={(event) =>
assessment ? setAssessmentAnswer(event.target.value) : setInput(event.target.value)
}
placeholder={assessment ? 'Ta réponse au mini-test...' : 'Pose une question ou demande une explication...'}
/>
<select
value={selectedVoiceProfileId}

View File

@@ -64,6 +64,35 @@ button:disabled { opacity: 0.5; cursor: not-allowed; }
padding: 0.75rem;
background: #f8fafc;
}
.student-layout .sidebar > button,
.student-layout .sidebar > h3,
.student-layout .sidebar > .stack.small-gap {
display: none;
}
.today-panel {
display: flex;
flex-direction: column;
gap: 1rem;
}
.today-card {
border: 1px solid #e5e7eb;
border-radius: 16px;
padding: 0.85rem;
background: #f8fafc;
}
.today-card-head {
display: flex;
justify-content: space-between;
gap: 0.75rem;
margin-bottom: 0.5rem;
}
.today-card-head span {
color: #2563eb;
font-weight: 700;
}
.today-card p {
margin: 0 0 0.75rem;
}
.session-user {
margin-left: auto;
display: flex;
@@ -84,6 +113,11 @@ button:disabled { opacity: 0.5; cursor: not-allowed; }
border-radius: 20px;
padding: 1.25rem;
}
.messages,
.voice-note,
.main > .voice-status {
display: none;
}
.instruction-stage p {
margin: 0;
font-size: 1.35rem;
@@ -98,14 +132,7 @@ button:disabled { opacity: 0.5; cursor: not-allowed; }
font-weight: 700;
text-transform: uppercase;
}
.messages {
flex: 0 0 150px;
min-height: 0;
overflow: auto;
background: #f8fafc;
border-radius: 20px;
padding: 1rem;
}
.messages { display: none; }
.message {
max-width: 92%;
margin-bottom: 0.45rem;
@@ -128,11 +155,12 @@ button:disabled { opacity: 0.5; cursor: not-allowed; }
}
.composer {
display: grid;
grid-template-columns: 1fr auto auto;
grid-template-columns: 1fr 220px 130px 130px;
gap: 0.75rem;
flex: 0 0 auto;
}
.assessment {
display: none;
flex: 0 0 auto;
background: #fff7ed;
padding: 1rem;