diff --git a/backend/app/main.py b/backend/app/main.py index 612af8d..ae4d843 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -304,27 +304,31 @@ def start_session( first_asset = (assigned_lesson.get("assets") or [None])[0] first_section = (first_asset.get("sections") or [None])[0] if first_asset else None section_label = first_section["title"] if first_section else first_asset["title"] if first_asset else assigned_lesson["title"] - message = ( - f"Bonjour {student.first_name} ! Aujourd’hui, on travaille la leçon " - f"{assigned_lesson['title']}. Regarde la carte « {section_label} ». " - "Dis-moi si tu comprends cette partie, ou ce qui te gêne." + lesson_context = ( + f"Leçon courante: {assigned_lesson['title']}\n" + f"Fiche courante: {first_asset['title'] if first_asset else 'non précisée'}\n" + f"Card courante: {section_label}\n" + f"Clé de contexte: {first_section.get('context_key') if first_section else 'non précisée'}\n" + "Avancement affiché: début de leçon\n" + "Règle: commence directement sur cette card. Ne fais pas de reprise de la dernière séance.\n" ) + if first_section and first_section.get("context"): + lesson_context += f"\nTexte pédagogique de la card courante:\n{first_section['context'].strip()}" + lesson_reply = build_lesson_reply( + db, + student.id, + "La séance démarre. Présente la card courante et pose une question de compréhension.", + lesson_context, + ) + message = lesson_reply["reply"] previous_messages_count = db.query(models.Message).filter_by(student_id=student.id).count() - if previous_messages_count: + if previous_messages_count and not assigned_lesson: prompt = ( "L’élève revient pour une nouvelle séance. Fais une courte reprise: " "explique où on s’était arrêté la dernière fois à partir de l’historique, " "puis annonce clairement ce qu'on va faire aujourd'hui. Termine par une " "première consigne simple." ) - if assigned_lesson: - first_asset = (assigned_lesson.get("assets") or [None])[0] - first_section = (first_asset.get("sections") or [None])[0] if first_asset else None - section_label = first_section["title"] if first_section else first_asset["title"] if first_asset else assigned_lesson["title"] - prompt += ( - f" La leçon active choisie par le professeur est: {assigned_lesson['title']}. " - f"La carte affichée est: {section_label}. Parle de cette carte, pas d'une ancienne leçon." - ) message = build_llm_reply(db, student.id, prompt) db.add(models.Message(student_id=student.id, role="assistant", content=message)) db.commit() diff --git a/backend/app/services.py b/backend/app/services.py index dc814d3..7255524 100644 --- a/backend/app/services.py +++ b/backend/app/services.py @@ -106,7 +106,7 @@ def ensure_student_mastery(db: Session, student: models.Student) -> None: db.commit() -def get_student_context(db: Session, student_id: int) -> str: +def get_student_context(db: Session, student_id: int, include_history: bool = True) -> str: student = db.query(models.Student).filter_by(id=student_id).first() mastery = ( db.query(models.StudentSkillMastery, models.Skill) @@ -114,13 +114,15 @@ def get_student_context(db: Session, student_id: int) -> str: .filter(models.StudentSkillMastery.student_id == student_id) .all() ) - recent_messages = ( - db.query(models.Message) - .filter_by(student_id=student_id) - .order_by(models.Message.created_at.desc()) - .limit(6) - .all() - ) + recent_messages = [] + if include_history: + recent_messages = ( + db.query(models.Message) + .filter_by(student_id=student_id) + .order_by(models.Message.created_at.desc()) + .limit(6) + .all() + ) assignment = db.query(models.StudentProgramAssignment).filter_by(student_id=student_id).first() lesson = get_lesson(assignment.lesson_id) if assignment else None @@ -142,9 +144,10 @@ def get_student_context(db: Session, student_id: int) -> str: lines.append( f"- {skill.label}: score={mastery_row.mastery_score:.1f}, confiance={mastery_row.confidence:.2f}, preuves={mastery_row.evidence_count}" ) - lines.append("Historique récent:") - for message in reversed(recent_messages): - lines.append(f"- {message.role}: {message.content}") + if include_history: + lines.append("Historique récent:") + for message in reversed(recent_messages): + lines.append(f"- {message.role}: {message.content}") return "\n".join(lines) @@ -166,7 +169,7 @@ def build_llm_reply(db: Session, student_id: int, user_message: str) -> str: def build_lesson_reply(db: Session, student_id: int, user_message: str, lesson_context: str) -> dict: - context = get_student_context(db, student_id) + context = get_student_context(db, student_id, include_history=False) response = client.responses.create( model="gpt-4.1-mini", input=[ @@ -177,6 +180,8 @@ def build_lesson_reply(db: Session, student_id: int, user_message: str, lesson_c "content": ( "Tu reçois un contexte courant de leçon lié à la card affichée. " "Tu dois parler uniquement de cette card, sauf si l'élève pose une question de clarification immédiate. " + "N'utilise pas l'historique des séances précédentes pour choisir le thème de ta réponse. " + "Ta réponse visible doit tenir dans une zone de 3 lignes: fais 1 ou 2 phrases très courtes. " "Si la réponse de l'élève montre clairement qu'il a compris la card courante, mets " "`advance_lesson_step` à true. Sinon, mets-le à false. " "Réponds strictement en JSON valide avec les clés `reply` et `advance_lesson_step`. " diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index c028ec8..741e06d 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -627,7 +627,7 @@ function TutorApp({ currentUser, onLogout }) { const cleanText = text.trim() if (!cleanText) return '' const sentences = cleanText.match(/[^.!?]+[.!?]+|[^.!?]+$/g) || [cleanText] - return sentences.slice(0, 2).join(' ').trim() + return sentences.slice(0, 3).join(' ').trim() } async function finishSpeechPlayback(debugMessage) { diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 39ec34f..ac50285 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -318,10 +318,10 @@ button:disabled { opacity: 0.5; cursor: not-allowed; } .avatar { width: 144px; height: 144px; - border-radius: 28px; + border-radius: 0; overflow: hidden; - background: white; - box-shadow: 0 10px 20px rgba(245, 158, 11, 0.28); + background: transparent; + box-shadow: none; } .avatar.speaking { animation: pulse 0.7s infinite alternate; } .avatar-image { @@ -440,7 +440,7 @@ button:disabled { opacity: 0.5; cursor: not-allowed; } .lesson-topbar .avatar { width: 92px; height: 92px; - border-radius: 20px; + border-radius: 0; } .lesson-topbar .avatar-caption { display: none; @@ -454,9 +454,9 @@ button:disabled { opacity: 0.5; cursor: not-allowed; } flex-direction: column; gap: 0.6rem; overflow: hidden; - background: #eef6ff; - border: 1px solid #cfe4ff; - border-radius: 18px; + background: transparent; + border: 0; + border-radius: 0; padding: 1rem; } .lesson-display p { @@ -477,11 +477,12 @@ button:disabled { opacity: 0.5; cursor: not-allowed; } font-weight: 700; } .teacher-transcript { - max-height: 3.2rem; - overflow: auto; + min-height: calc(1.35em * 3); + max-height: calc(1.35em * 3); + overflow: hidden; color: #14213d; font-size: clamp(0.95rem, 1.5vw, 1.08rem); - line-height: 1.45; + line-height: 1.35; } .lesson-asset-view { min-height: 0; @@ -521,9 +522,9 @@ button:disabled { opacity: 0.5; cursor: not-allowed; } min-height: 0; flex: 1 1 auto; object-fit: contain; - border: 1px solid #dbeafe; - border-radius: 14px; - background: white; + border: 0; + border-radius: 0; + background: transparent; } .lesson-bubble { min-height: 68px; @@ -742,7 +743,7 @@ button:disabled { opacity: 0.5; cursor: not-allowed; } .lesson-topbar .avatar { width: 72px; height: 72px; - border-radius: 16px; + border-radius: 0; } .lesson-topbar h1 { @@ -761,7 +762,8 @@ button:disabled { opacity: 0.5; cursor: not-allowed; } } .teacher-transcript { - max-height: 2.15rem; + min-height: calc(1.35em * 3); + max-height: calc(1.35em * 3); font-size: 0.9rem; padding: 0 0.65rem; }