From 96a566fa0932f7a8b982d28f8f43740760ce6f7b Mon Sep 17 00:00:00 2001 From: laurentbarontini Date: Sat, 25 Apr 2026 21:04:22 +0200 Subject: [PATCH] central view --- MEMORY.md | 4 +++- backend/app/main.py | 12 ++++++++++ frontend/src/App.jsx | 21 +++++++++--------- frontend/src/styles.css | 49 +++++++++++++++++++++++++++++++---------- 4 files changed, 63 insertions(+), 23 deletions(-) diff --git a/MEMORY.md b/MEMORY.md index 5ea31aa..971427e 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -26,7 +26,9 @@ Ce projet est un POC de professeur virtuel pour enfants, avec: - Le frontend masque le choix/creation d'eleve pour un compte `student` et ouvre directement sa propre seance. - Le backend expose `GET /students/{student_id}/messages` pour relire l'historique de conversation stocke en base. - 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 et met la derniere instruction de Professeur TOP dans une zone centrale dediee. +- 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. - 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` diff --git a/backend/app/main.py b/backend/app/main.py index f0daa92..8ac5d0d 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -194,6 +194,18 @@ def start_session( f"Bonjour {student.first_name} ! Je suis Professeur TOP, ton professeur virtuel. " "Aujourd'hui, on va apprendre pas à pas et faire un petit test pour voir ce que tu maîtrises déjà." ) + previous_messages_count = db.query(models.Message).filter_by(student_id=student.id).count() + if previous_messages_count: + message = build_llm_reply( + db, + student.id, + ( + "L'eleve revient pour une nouvelle seance. Fais une courte reprise: " + "explique ou on s'etait arrete la derniere fois a partir de l'historique, " + "puis annonce clairement ce qu'on va faire aujourd'hui. Termine par une " + "premiere consigne simple." + ), + ) db.add(models.Message(student_id=student.id, role="assistant", content=message)) db.commit() return schemas.ChatResponse(reply=message) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 3e65a25..35db720 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -182,6 +182,7 @@ function TutorApp({ currentUser, onLogout }) { password: '', }) const [messages, setMessages] = useState([]) + const [currentInstruction, setCurrentInstruction] = useState('') const [input, setInput] = useState('') const [progress, setProgress] = useState([]) const [assessment, setAssessment] = useState(null) @@ -225,11 +226,6 @@ function TutorApp({ currentUser, onLogout }) { [voiceProfiles, selectedVoiceProfileId] ) - const latestAssistantMessage = useMemo( - () => [...messages].reverse().find((message) => message.role === 'assistant') || null, - [messages] - ) - const averageScore = useMemo(() => { if (!progress.length) return 0 const total = progress.reduce((sum, item) => sum + item.mastery_score, 0) @@ -263,6 +259,7 @@ function TutorApp({ currentUser, onLogout }) { }, [speaking]) useEffect(() => { + setCurrentInstruction('') if (selectedStudentId) { loadProgress(selectedStudentId) loadMessages(selectedStudentId) @@ -370,6 +367,7 @@ function TutorApp({ currentUser, onLogout }) { setErrorMessage('') const data = await apiFetch(`/session/start?student_id=${selectedStudentId}`, { method: 'POST' }) appendMessage('assistant', data.reply) + setCurrentInstruction(data.reply) speak(data.reply) await loadProgress(selectedStudentId) } catch (error) { @@ -398,6 +396,7 @@ function TutorApp({ currentUser, onLogout }) { const data = await apiFetch(`/assessment/next/${selectedStudentId}`) setAssessment(data) setAssessmentAnswer('') + setCurrentInstruction(data.question) } catch (error) { setErrorMessage(error.message || 'Impossible de charger le mini-test.') } @@ -419,6 +418,7 @@ function TutorApp({ currentUser, onLogout }) { }) appendMessage('user', `Reponse au mini-test: ${assessmentAnswer}`) appendMessage('assistant', data.feedback) + setCurrentInstruction(data.feedback) speak(data.feedback) setAssessment(null) setAssessmentAnswer('') @@ -564,6 +564,7 @@ function TutorApp({ currentUser, onLogout }) { body: JSON.stringify({ student_id: Number(selectedStudentId), message: text }), }) appendMessage('assistant', data.reply) + setCurrentInstruction(data.reply) speak(data.reply) } @@ -890,17 +891,17 @@ function TutorApp({ currentUser, onLogout }) {
- Derniere instruction - {latestAssistantMessage ? ( -

{latestAssistantMessage.content}

+ Cours en cours + {currentInstruction ? ( +

{currentInstruction}

) : (

- Demarre la seance pour afficher ici la consigne de Professeur TOP. + Demarre la seance pour que Professeur TOP reprenne ou tu t'etais arrete et lance le cours du jour.

)}
-
+
{messages.length === 0 &&

Le dialogue apparaîtra ici.

} {messages.map((message) => (
diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 893a10e..5acb697 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -5,7 +5,11 @@ } * { box-sizing: border-box; } -body { margin: 0; } +html, body, #root { height: 100%; } +body { + margin: 0; + overflow: hidden; +} button, input, select { font: inherit; border-radius: 12px; @@ -27,9 +31,10 @@ button:disabled { opacity: 0.5; cursor: not-allowed; } .layout { display: grid; grid-template-columns: 360px 1fr; - min-height: 100vh; + height: 100vh; gap: 1rem; padding: 1rem; + overflow: hidden; } .card { background: white; @@ -37,7 +42,16 @@ button:disabled { opacity: 0.5; cursor: not-allowed; } box-shadow: 0 10px 30px rgba(20,33,61,0.08); padding: 1rem; } -.sidebar, .main { display: flex; flex-direction: column; gap: 1rem; } +.sidebar, .main { + display: flex; + flex-direction: column; + gap: 1rem; + min-height: 0; + overflow: hidden; +} +.sidebar { + overflow-y: auto; +} .stack { display: flex; flex-direction: column; gap: 0.75rem; } .small-gap { gap: 0.5rem; } .hero { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; } @@ -59,7 +73,8 @@ button:disabled { opacity: 0.5; cursor: not-allowed; } font-size: 0.95rem; } .instruction-stage { - min-height: 180px; + flex: 1; + min-height: 0; display: flex; flex-direction: column; justify-content: center; @@ -71,9 +86,11 @@ button:disabled { opacity: 0.5; cursor: not-allowed; } } .instruction-stage p { margin: 0; - font-size: 1.25rem; + font-size: 1.35rem; line-height: 1.5; white-space: pre-wrap; + max-height: 100%; + overflow: auto; } .stage-label { color: #2563eb; @@ -82,18 +99,19 @@ button:disabled { opacity: 0.5; cursor: not-allowed; } text-transform: uppercase; } .messages { - flex: 1; - min-height: 220px; + flex: 0 0 150px; + min-height: 0; overflow: auto; background: #f8fafc; border-radius: 20px; padding: 1rem; } .message { - max-width: 80%; - margin-bottom: 0.75rem; - padding: 0.9rem 1rem; - border-radius: 18px; + max-width: 92%; + margin-bottom: 0.45rem; + padding: 0.55rem 0.75rem; + border-radius: 12px; + font-size: 0.92rem; } .message.user { margin-left: auto; @@ -102,13 +120,20 @@ button:disabled { opacity: 0.5; cursor: not-allowed; } .message.assistant { background: #eaf7e7; } -.message p { margin: 0.35rem 0 0; white-space: pre-wrap; } +.message p { + margin: 0.25rem 0 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} .composer { display: grid; grid-template-columns: 1fr auto auto; gap: 0.75rem; + flex: 0 0 auto; } .assessment { + flex: 0 0 auto; background: #fff7ed; padding: 1rem; border-radius: 18px;