bouton fin de séance
This commit is contained in:
@@ -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`
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user