central view
This commit is contained in:
@@ -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`
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 }) {
|
||||
</div>
|
||||
|
||||
<section className="instruction-stage">
|
||||
<span className="stage-label">Derniere instruction</span>
|
||||
{latestAssistantMessage ? (
|
||||
<p>{latestAssistantMessage.content}</p>
|
||||
<span className="stage-label">Cours en cours</span>
|
||||
{currentInstruction ? (
|
||||
<p>{currentInstruction}</p>
|
||||
) : (
|
||||
<p className="muted">
|
||||
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.
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="messages">
|
||||
<section className="messages" aria-label="Journal compact de la seance">
|
||||
{messages.length === 0 && <p className="muted">Le dialogue apparaîtra ici.</p>}
|
||||
{messages.map((message) => (
|
||||
<div key={message.id} className={`message ${message.role}`}>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user