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`
|
- `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 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 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.
|
- 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:
|
- La conf nginx reelle route:
|
||||||
- `/api/` vers `tutor-backend:8000`
|
- `/api/` vers `tutor-backend:8000`
|
||||||
|
|||||||
@@ -155,6 +155,29 @@ def create_student_account(
|
|||||||
return schemas.StudentAccountResponse(student=student, user=user)
|
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)
|
@app.post("/session/start", response_model=schemas.ChatResponse)
|
||||||
def start_session(
|
def start_session(
|
||||||
student_id: int,
|
student_id: int,
|
||||||
@@ -300,6 +323,13 @@ def answer_assessment(
|
|||||||
correct, feedback, mastery_score = evaluate_answer(
|
correct, feedback, mastery_score = evaluate_answer(
|
||||||
db, payload.student_id, payload.skill_code, payload.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.add(models.Message(student_id=student.id, role="assistant", content=feedback))
|
||||||
db.commit()
|
db.commit()
|
||||||
return schemas.AssessmentAnswerResponse(
|
return schemas.AssessmentAnswerResponse(
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
from datetime import datetime
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
|
|
||||||
@@ -60,6 +61,17 @@ class ChatResponse(BaseModel):
|
|||||||
should_speak: bool = True
|
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):
|
class TTSProfile(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
label: str
|
label: str
|
||||||
|
|||||||
@@ -225,6 +225,11 @@ function TutorApp({ currentUser, onLogout }) {
|
|||||||
[voiceProfiles, selectedVoiceProfileId]
|
[voiceProfiles, selectedVoiceProfileId]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const latestAssistantMessage = useMemo(
|
||||||
|
() => [...messages].reverse().find((message) => message.role === 'assistant') || null,
|
||||||
|
[messages]
|
||||||
|
)
|
||||||
|
|
||||||
const averageScore = useMemo(() => {
|
const averageScore = useMemo(() => {
|
||||||
if (!progress.length) return 0
|
if (!progress.length) return 0
|
||||||
const total = progress.reduce((sum, item) => sum + item.mastery_score, 0)
|
const total = progress.reduce((sum, item) => sum + item.mastery_score, 0)
|
||||||
@@ -260,6 +265,9 @@ function TutorApp({ currentUser, onLogout }) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedStudentId) {
|
if (selectedStudentId) {
|
||||||
loadProgress(selectedStudentId)
|
loadProgress(selectedStudentId)
|
||||||
|
loadMessages(selectedStudentId)
|
||||||
|
} else {
|
||||||
|
setMessages([])
|
||||||
}
|
}
|
||||||
}, [selectedStudentId])
|
}, [selectedStudentId])
|
||||||
|
|
||||||
@@ -409,7 +417,8 @@ function TutorApp({ currentUser, onLogout }) {
|
|||||||
answer: assessmentAnswer,
|
answer: assessmentAnswer,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
appendMessage('assistant', `Quiz: ${data.feedback}`)
|
appendMessage('user', `Reponse au mini-test: ${assessmentAnswer}`)
|
||||||
|
appendMessage('assistant', data.feedback)
|
||||||
speak(data.feedback)
|
speak(data.feedback)
|
||||||
setAssessment(null)
|
setAssessment(null)
|
||||||
setAssessmentAnswer('')
|
setAssessmentAnswer('')
|
||||||
@@ -432,6 +441,23 @@ function TutorApp({ currentUser, onLogout }) {
|
|||||||
setSpeaking(false)
|
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) {
|
async function finishSpeechPlayback(debugMessage) {
|
||||||
setSpeaking(false)
|
setSpeaking(false)
|
||||||
if (audioPlaybackUrlRef.current) {
|
if (audioPlaybackUrlRef.current) {
|
||||||
@@ -863,6 +889,17 @@ function TutorApp({ currentUser, onLogout }) {
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<section className="messages">
|
||||||
{messages.length === 0 && <p className="muted">Le dialogue apparaîtra ici.</p>}
|
{messages.length === 0 && <p className="muted">Le dialogue apparaîtra ici.</p>}
|
||||||
{messages.map((message) => (
|
{messages.map((message) => (
|
||||||
|
|||||||
@@ -58,9 +58,32 @@ button:disabled { opacity: 0.5; cursor: not-allowed; }
|
|||||||
color: #64748b;
|
color: #64748b;
|
||||||
font-size: 0.95rem;
|
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 {
|
.messages {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 360px;
|
min-height: 220px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
background: #f8fafc;
|
background: #f8fafc;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
|
|||||||
Reference in New Issue
Block a user