Card central

This commit is contained in:
2026-05-01 21:18:48 +02:00
parent 2b65876525
commit 94a2ef340e
4 changed files with 57 additions and 110 deletions

View File

@@ -300,9 +300,13 @@ def start_session(
assignment = db.query(models.StudentProgramAssignment).filter_by(student_id=student.id).first()
assigned_lesson = get_lesson(assignment.lesson_id) if assignment else None
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"]
message = (
f"Bonjour {student.first_name} ! Aujourdhui, on travaille la leçon "
f"{assigned_lesson['title']}. Regarde la première fiche, puis dis-moi ce que tu remarques."
f"{assigned_lesson['title']}. Regarde la carte « {section_label} ». "
"Dis-moi si tu comprends cette partie, ou ce qui te gêne."
)
previous_messages_count = db.query(models.Message).filter_by(student_id=student.id).count()
if previous_messages_count:
@@ -313,7 +317,13 @@ def start_session(
"première consigne simple."
)
if assigned_lesson:
prompt += f" La leçon active choisie par le professeur est: {assigned_lesson['title']}."
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()
@@ -334,7 +344,19 @@ def chat(
db.add(models.Message(student_id=student.id, role="user", content=payload.message))
db.commit()
reply = build_llm_reply(db, payload.student_id, payload.message)
lesson_context = ""
if payload.lesson_title or payload.section_title or payload.asset_title:
lesson_context = (
"\n\nContexte de lécran actuellement montré à lélève: "
f"leçon={payload.lesson_title or 'non précisée'}, "
f"fiche={payload.asset_title or 'non précisée'}, "
f"carte affichée={payload.section_title or 'non précisée'}, "
f"avancement={payload.step_percent if payload.step_percent is not None else 'non précisé'}%. "
"Réponds uniquement à propos de cette carte. Vérifie que lélève comprend ce qui est affiché. "
"Si lélève dit quil a compris, invite-le à passer à létape suivante."
)
reply = build_llm_reply(db, payload.student_id, f"{payload.message}{lesson_context}")
db.add(models.Message(student_id=student.id, role="assistant", content=reply))
db.commit()

View File

@@ -54,6 +54,10 @@ class StudentAccountResponse(BaseModel):
class ChatRequest(BaseModel):
student_id: int
message: str
lesson_title: str | None = None
asset_title: str | None = None
section_title: str | None = None
step_percent: int | None = None
class ChatResponse(BaseModel):

View File

@@ -213,7 +213,6 @@ function TutorApp({ currentUser, onLogout }) {
const [studentProgram, setStudentProgram] = useState(null)
const [activeAssetIndex, setActiveAssetIndex] = useState(0)
const [activeSectionIndex, setActiveSectionIndex] = useState(0)
const [assetFullscreenOpen, setAssetFullscreenOpen] = useState(false)
const [progressPanelOpen, setProgressPanelOpen] = useState(false)
const [settingsPanelOpen, setSettingsPanelOpen] = useState(false)
const mediaRecorderRef = useRef(null)
@@ -512,7 +511,6 @@ function TutorApp({ currentUser, onLogout }) {
stopSpeechPlayback()
deactivateAutoListening(false)
setSessionActive(false)
setAssetFullscreenOpen(false)
setProgressPanelOpen(false)
setSettingsPanelOpen(false)
setCurrentInstruction('')
@@ -614,6 +612,22 @@ function TutorApp({ currentUser, onLogout }) {
}
}
function getLessonContextPayload() {
return {
lesson_title: activeLessonTitle,
asset_title: activeAsset?.title || null,
section_title: activeSection?.title || activeAsset?.title || null,
step_percent: lessonStepPercent,
}
}
function compactInstruction(text) {
const cleanText = text.trim()
if (!cleanText) return ''
const sentences = cleanText.match(/[^.!?]+[.!?]+|[^.!?]+$/g) || [cleanText]
return sentences.slice(0, 2).join(' ').trim()
}
async function finishSpeechPlayback(debugMessage) {
setSpeaking(false)
if (audioPlaybackUrlRef.current) {
@@ -718,7 +732,11 @@ function TutorApp({ currentUser, onLogout }) {
const data = await apiFetch('/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ student_id: Number(selectedStudentId), message: text }),
body: JSON.stringify({
student_id: Number(selectedStudentId),
message: text,
...getLessonContextPayload(),
}),
})
appendMessage('assistant', data.reply)
setCurrentInstruction(data.reply)
@@ -1079,7 +1097,7 @@ function TutorApp({ currentUser, onLogout }) {
)}
</div>
{currentInstruction ? (
<p className="teacher-transcript">{currentInstruction}</p>
<p className="teacher-transcript">{compactInstruction(currentInstruction)}</p>
) : (
<p className="muted teacher-transcript">Professeur TOP prépare la suite de la leçon.</p>
)}
@@ -1106,18 +1124,12 @@ function TutorApp({ currentUser, onLogout }) {
</button>
</div>
</div>
<button
type="button"
className="lesson-asset-open"
onClick={() => setAssetFullscreenOpen(true)}
aria-label="Agrandir la fiche"
>
<div className="lesson-asset-open">
<img
src={activeSection?.url || activeAsset.url}
alt={activeSection?.title || activeAsset.title}
/>
<span>Agrandir la fiche</span>
</button>
</div>
</div>
)}
</section>
@@ -1195,42 +1207,6 @@ function TutorApp({ currentUser, onLogout }) {
</div>
)}
{assetFullscreenOpen && activeAsset && (
<div className="asset-fullscreen" role="dialog" aria-label="Fiche agrandie">
<div className="asset-fullscreen-bar">
<strong>
Étape {activeLessonStep}/{totalLessonSteps} · {activeSection?.title || activeAsset.title}
</strong>
<button type="button" className="secondary-button" onClick={() => setAssetFullscreenOpen(false)}>
Fermer
</button>
</div>
<div className="asset-fullscreen-stage">
<img
src={activeSection?.url || activeAsset.url}
alt={activeSection?.title || activeAsset.title}
/>
</div>
<div className="asset-fullscreen-controls">
<button
type="button"
className="secondary-button"
onClick={goToPreviousLessonStep}
disabled={activeLessonStep <= 1}
>
Précédente
</button>
<button
type="button"
className="secondary-button"
onClick={goToNextLessonStep}
disabled={activeLessonStep >= totalLessonSteps}
>
Suivante
</button>
</div>
</div>
)}
</main>
)}
</div>

View File

@@ -477,7 +477,7 @@ button:disabled { opacity: 0.5; cursor: not-allowed; }
font-weight: 700;
}
.teacher-transcript {
max-height: 4.8rem;
max-height: 3.2rem;
overflow: auto;
color: #14213d;
font-size: clamp(0.95rem, 1.5vw, 1.08rem);
@@ -523,43 +523,6 @@ button:disabled { opacity: 0.5; cursor: not-allowed; }
border-radius: 14px;
background: white;
}
.lesson-asset-open span {
display: none;
color: #2563eb;
font-size: 0.9rem;
font-weight: 700;
}
.asset-fullscreen {
position: fixed;
inset: 0;
z-index: 20;
display: grid;
grid-template-rows: auto minmax(0, 1fr) auto;
gap: 0.75rem;
padding: 0.75rem;
background: #f5f7fb;
}
.asset-fullscreen-bar,
.asset-fullscreen-controls {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
}
.asset-fullscreen-stage {
min-height: 0;
overflow: auto;
border: 1px solid #dbeafe;
border-radius: 16px;
background: white;
}
.asset-fullscreen-stage img {
display: block;
width: max(100%, 980px);
height: auto;
min-height: 100%;
object-fit: contain;
}
.lesson-bubble {
min-height: 68px;
}
@@ -794,8 +757,8 @@ button:disabled { opacity: 0.5; cursor: not-allowed; }
}
.teacher-transcript {
max-height: 2.7rem;
font-size: 0.95rem;
max-height: 2.35rem;
font-size: 0.9rem;
}
.lesson-display-head,
@@ -819,26 +782,8 @@ button:disabled { opacity: 0.5; cursor: not-allowed; }
font-size: 0.95rem;
}
.lesson-asset-open span {
display: block;
}
.lesson-asset-open img {
max-height: 31dvh;
}
.asset-fullscreen {
padding: 0.5rem;
}
.asset-fullscreen-stage img {
width: 100%;
min-height: 100%;
}
.asset-fullscreen-controls {
display: grid;
grid-template-columns: 1fr 1fr;
max-height: none;
}
.lesson-composer {