last message
This commit is contained in:
@@ -24,6 +24,9 @@ Ce projet est un POC de professeur virtuel pour enfants, avec:
|
||||
- `student` ne peut acceder qu'a son propre `student_id`
|
||||
- Le backend expose `POST /admin/student-accounts` pour creer une fiche eleve et son compte login en une seule operation.
|
||||
- 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 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`
|
||||
|
||||
@@ -155,6 +155,29 @@ def create_student_account(
|
||||
return schemas.StudentAccountResponse(student=student, user=user)
|
||||
|
||||
|
||||
@app.get("/students/{student_id}/messages", response_model=list[schemas.MessageRead])
|
||||
def list_student_messages(
|
||||
student_id: int,
|
||||
limit: int = 80,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: models.User = Depends(get_current_user),
|
||||
):
|
||||
ensure_student_access(current_user, student_id)
|
||||
student = db.query(models.Student).filter_by(id=student_id).first()
|
||||
if not student:
|
||||
raise HTTPException(status_code=404, detail="Eleve introuvable")
|
||||
|
||||
safe_limit = min(max(limit, 1), 200)
|
||||
rows = (
|
||||
db.query(models.Message)
|
||||
.filter_by(student_id=student_id)
|
||||
.order_by(models.Message.created_at.desc(), models.Message.id.desc())
|
||||
.limit(safe_limit)
|
||||
.all()
|
||||
)
|
||||
return list(reversed(rows))
|
||||
|
||||
|
||||
@app.post("/session/start", response_model=schemas.ChatResponse)
|
||||
def start_session(
|
||||
student_id: int,
|
||||
@@ -300,6 +323,13 @@ def answer_assessment(
|
||||
correct, feedback, mastery_score = evaluate_answer(
|
||||
db, payload.student_id, payload.skill_code, payload.answer
|
||||
)
|
||||
db.add(
|
||||
models.Message(
|
||||
student_id=student.id,
|
||||
role="user",
|
||||
content=f"Reponse au mini-test ({payload.skill_code}): {payload.answer}",
|
||||
)
|
||||
)
|
||||
db.add(models.Message(student_id=student.id, role="assistant", content=feedback))
|
||||
db.commit()
|
||||
return schemas.AssessmentAnswerResponse(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from pydantic import BaseModel, Field
|
||||
from datetime import datetime
|
||||
from typing import List
|
||||
|
||||
|
||||
@@ -60,6 +61,17 @@ class ChatResponse(BaseModel):
|
||||
should_speak: bool = True
|
||||
|
||||
|
||||
class MessageRead(BaseModel):
|
||||
id: int
|
||||
student_id: int
|
||||
role: str
|
||||
content: str
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class TTSProfile(BaseModel):
|
||||
id: str
|
||||
label: str
|
||||
|
||||
@@ -225,6 +225,11 @@ 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)
|
||||
@@ -260,6 +265,9 @@ function TutorApp({ currentUser, onLogout }) {
|
||||
useEffect(() => {
|
||||
if (selectedStudentId) {
|
||||
loadProgress(selectedStudentId)
|
||||
loadMessages(selectedStudentId)
|
||||
} else {
|
||||
setMessages([])
|
||||
}
|
||||
}, [selectedStudentId])
|
||||
|
||||
@@ -409,7 +417,8 @@ function TutorApp({ currentUser, onLogout }) {
|
||||
answer: assessmentAnswer,
|
||||
}),
|
||||
})
|
||||
appendMessage('assistant', `Quiz: ${data.feedback}`)
|
||||
appendMessage('user', `Reponse au mini-test: ${assessmentAnswer}`)
|
||||
appendMessage('assistant', data.feedback)
|
||||
speak(data.feedback)
|
||||
setAssessment(null)
|
||||
setAssessmentAnswer('')
|
||||
@@ -432,6 +441,23 @@ function TutorApp({ currentUser, onLogout }) {
|
||||
setSpeaking(false)
|
||||
}
|
||||
|
||||
async function loadMessages(studentId) {
|
||||
try {
|
||||
setErrorMessage('')
|
||||
const data = await apiFetch(`/students/${studentId}/messages?limit=80`)
|
||||
setMessages(
|
||||
(data || []).map((message) => ({
|
||||
id: message.id,
|
||||
role: message.role,
|
||||
content: message.content,
|
||||
created_at: message.created_at,
|
||||
}))
|
||||
)
|
||||
} catch (error) {
|
||||
setErrorMessage(error.message || 'Impossible de charger la conversation.')
|
||||
}
|
||||
}
|
||||
|
||||
async function finishSpeechPlayback(debugMessage) {
|
||||
setSpeaking(false)
|
||||
if (audioPlaybackUrlRef.current) {
|
||||
@@ -863,6 +889,17 @@ function TutorApp({ currentUser, onLogout }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section className="instruction-stage">
|
||||
<span className="stage-label">Derniere instruction</span>
|
||||
{latestAssistantMessage ? (
|
||||
<p>{latestAssistantMessage.content}</p>
|
||||
) : (
|
||||
<p className="muted">
|
||||
Demarre la seance pour afficher ici la consigne de Professeur TOP.
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="messages">
|
||||
{messages.length === 0 && <p className="muted">Le dialogue apparaîtra ici.</p>}
|
||||
{messages.map((message) => (
|
||||
|
||||
@@ -58,9 +58,32 @@ button:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
color: #64748b;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
.instruction-stage {
|
||||
min-height: 180px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
background: #eef6ff;
|
||||
border: 1px solid #cfe4ff;
|
||||
border-radius: 20px;
|
||||
padding: 1.25rem;
|
||||
}
|
||||
.instruction-stage p {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.stage-label {
|
||||
color: #2563eb;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.messages {
|
||||
flex: 1;
|
||||
min-height: 360px;
|
||||
min-height: 220px;
|
||||
overflow: auto;
|
||||
background: #f8fafc;
|
||||
border-radius: 20px;
|
||||
|
||||
Reference in New Issue
Block a user