This commit is contained in:
2026-05-06 19:55:37 +02:00
parent 7a7994e8b6
commit b41993c455
43 changed files with 1563 additions and 14 deletions

View File

@@ -150,6 +150,9 @@ function ProgramReviewPanel({ selectedNode, reviewForm, onReviewChange, onSaveRe
<span>Contextes: {status.contexts ?? 0}</span>
<span>Exercices: {status.text_exercises ?? 0}</span>
<span>SVG exercices: {status.exercise_svg ?? 0}</span>
<span>Tests adaptatifs: {status.adaptive_tests ?? 0}</span>
<span>Parcours: {status.adaptive_profiles ?? 0}</span>
<span>SVG kit: {status.adaptive_svg ?? 0}</span>
</div>
)}
@@ -160,6 +163,12 @@ function ProgramReviewPanel({ selectedNode, reviewForm, onReviewChange, onSaveRe
</button>
)}
{selectedNode.content && !selectedNode.url && (
<div className="program-node-text-preview">
<pre>{selectedNode.content}</pre>
</div>
)}
{review.comment && (
<div className="admin-review-note">
<strong>Dernier commentaire</strong>
@@ -387,6 +396,12 @@ function TutorApp({ currentUser, onLogout }) {
const [activeSectionIndex, setActiveSectionIndex] = useState(0)
const [programStart, setProgramStart] = useState({ asset_index: 0, section_index: 0 })
const [cardProgress, setCardProgress] = useState([])
const [adaptiveProfile, setAdaptiveProfile] = useState(null)
const [adaptiveProfileForm, setAdaptiveProfileForm] = useState({
diagnostic_id: '',
profile_id: '',
notes: '',
})
const [sessionCardStates, setSessionCardStates] = useState({})
const [sessionLogs, setSessionLogs] = useState([])
const [selectedSessionLog, setSelectedSessionLog] = useState(null)
@@ -540,6 +555,8 @@ function TutorApp({ currentUser, onLogout }) {
setProgramLessons([])
setProgramStart({ asset_index: 0, section_index: 0 })
setCardProgress([])
setAdaptiveProfile(null)
setAdaptiveProfileForm({ diagnostic_id: '', profile_id: '', notes: '' })
setSessionCardStates({})
setSessionLogs([])
setSelectedSessionLog(null)
@@ -576,6 +593,9 @@ function TutorApp({ currentUser, onLogout }) {
useEffect(() => {
if (selectedStudentId && studentProgram?.id) {
loadCardProgress(selectedStudentId, studentProgram.id)
if (!isStudentUser) {
loadAdaptiveProfile(selectedStudentId, studentProgram.id)
}
}
}, [selectedStudentId, studentProgram?.id])
@@ -913,6 +933,46 @@ function TutorApp({ currentUser, onLogout }) {
}
}
async function loadAdaptiveProfile(studentId, lessonId) {
if (!studentId || !lessonId || isStudentUser) return
try {
const data = await apiFetch(`/admin/students/${studentId}/adaptive-profile?lesson_id=${encodeURIComponent(lessonId)}`)
setAdaptiveProfile(data || null)
setAdaptiveProfileForm({
diagnostic_id: data?.diagnostic_id || '',
profile_id: data?.profile_id || '',
notes: data?.notes || '',
})
} catch {
setAdaptiveProfile(null)
setAdaptiveProfileForm({ diagnostic_id: '', profile_id: '', notes: '' })
}
}
async function saveAdaptiveProfile(event) {
event.preventDefault()
if (!selectedStudentId || !studentProgram?.id || !adaptiveProfileForm.profile_id) return
const profile = (studentProgram.adaptive_kit?.profiles || []).find((item) => item.id === adaptiveProfileForm.profile_id)
if (!profile) return
try {
const data = await apiFetch(`/admin/students/${selectedStudentId}/adaptive-profile`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
lesson_id: studentProgram.id,
diagnostic_id: adaptiveProfileForm.diagnostic_id || null,
profile_id: profile.id,
profile_title: profile.title,
notes: adaptiveProfileForm.notes,
source: 'teacher',
}),
})
setAdaptiveProfile(data)
} catch (error) {
setErrorMessage(error.message || 'Impossible denregistrer le profil adaptatif.')
}
}
async function loadSessionLogs(studentId) {
if (!studentId || isStudentUser) return
try {
@@ -1151,6 +1211,7 @@ function TutorApp({ currentUser, onLogout }) {
function getLessonContextPayload() {
const { asset, section } = getLessonPosition()
return {
lesson_id: studentProgram?.id || null,
lesson_title: activeLessonTitle,
asset_title: asset?.title || null,
section_title: section?.title || asset?.title || null,
@@ -1553,6 +1614,7 @@ function TutorApp({ currentUser, onLogout }) {
const lastUserMessage = [...messages].reverse().find((message) => message.role === 'user')
const activeLessonTitle = studentProgram?.title || lessonFocus?.label || 'Cours du jour'
const lessonAssets = studentProgram?.assets || []
const adaptiveKit = studentProgram?.adaptive_kit || null
const activeAsset = lessonAssets[activeAssetIndex] || lessonAssets[0] || null
const activeSections = activeAsset?.sections || []
const activeSection = activeSections[activeSectionIndex] || null
@@ -1578,7 +1640,9 @@ function TutorApp({ currentUser, onLogout }) {
const lessonSummary = lastAssistantMessage?.content || 'Aucune ancienne séance trouvée pour le moment. Professeur TOP commencera par une reprise courte.'
const lastStudentWork = lastUserMessage?.content || 'Aucune réponse récente enregistrée.'
const nextLessonAdvice = studentProgram
? `Conseil: démarrer la leçon ${studentProgram.title} avec les fiches choisies par le professeur.`
? adaptiveKit
? `Conseil: si lélève hésite, lancer le diagnostic adaptatif ${adaptiveKit.title.toLowerCase()} avant de poursuivre.`
: `Conseil: démarrer la leçon ${studentProgram.title} avec les fiches choisies par le professeur.`
: lessonFocus
? `Conseil: reprendre ${lessonFocus.label}, puis lancer un exercice court pour équilibrer la progression.`
: 'Conseil: démarrer une séance pour laisser Professeur TOP choisir la prochaine leçon.'
@@ -1792,7 +1856,9 @@ function TutorApp({ currentUser, onLogout }) {
const averageComprehension = cardProgress.length
? Math.round(cardProgress.reduce((sum, item) => sum + item.comprehension_score, 0) / cardProgress.length)
: 0
const reviewRows = programRows.filter(({ node }) => node.type === 'lesson' || node.type === 'fiche' || node.type === 'card')
const reviewRows = programRows.filter(({ node }) =>
['lesson', 'fiche', 'card', 'kit', 'diagnostic', 'parcours', 'support'].includes(node.type)
)
const reviewQueue = reviewRows.filter(({ node }) => ['needs_changes', 'blocked', 'draft', undefined].includes(node.review?.status))
const selectedNodeStatus = selectedProgramNode?.status || {}
const totalLessonCards = studentProgram?.assets?.reduce((sum, asset) => sum + (asset.sections?.length || 0), 0) || 0
@@ -1967,6 +2033,17 @@ function TutorApp({ currentUser, onLogout }) {
<strong>{validatedCards}</strong>
<p>{remainingCards} card(s) encore à travailler</p>
</article>
<article>
<span>Kit adaptatif</span>
<strong>{adaptiveProfile?.profile_title || (adaptiveKit ? 'Disponible' : 'Absent')}</strong>
<p>
{adaptiveProfile
? 'Profil de reprise enregistré'
: adaptiveKit
? `${adaptiveKit.diagnostics?.length || 0} test(s), ${adaptiveKit.profiles?.length || 0} parcours`
: 'Aucun diagnostic relié à cette leçon.'}
</p>
</article>
</div>
<div className="student-settings-grid">
@@ -2013,6 +2090,77 @@ function TutorApp({ currentUser, onLogout }) {
</label>
</div>
{adaptiveKit && (
<section className="adaptive-kit-panel">
<div className="admin-panel-head">
<div>
<span className="stage-label">Adaptatif</span>
<h2>Diagnostic et parcours disponibles</h2>
</div>
<button type="button" className="secondary-button" onClick={() => setActiveAdminTab('validation')}>
Valider le kit
</button>
</div>
<div className="adaptive-kit-grid">
<article>
<strong>Tests diagnostics</strong>
{(adaptiveKit.diagnostics || []).map((item) => (
<span key={item.id}>{item.title}</span>
))}
</article>
<article>
<strong>Parcours profils</strong>
{(adaptiveKit.profiles || []).map((item) => (
<span key={item.id}>{item.title}</span>
))}
</article>
<article>
<strong>Supports visuels</strong>
{(adaptiveKit.supports || []).map((item) => (
<span key={item.id}>{item.title}</span>
))}
</article>
</div>
<form className="adaptive-profile-form" onSubmit={saveAdaptiveProfile}>
<label>
<span>Test diagnostic de référence</span>
<select
value={adaptiveProfileForm.diagnostic_id}
onChange={(event) => setAdaptiveProfileForm({ ...adaptiveProfileForm, diagnostic_id: event.target.value })}
>
<option value="">Non précisé</option>
{(adaptiveKit.diagnostics || []).map((item) => (
<option key={item.id} value={item.id}>{item.title}</option>
))}
</select>
</label>
<label>
<span>Parcours recommandé</span>
<select
value={adaptiveProfileForm.profile_id}
onChange={(event) => setAdaptiveProfileForm({ ...adaptiveProfileForm, profile_id: event.target.value })}
>
<option value="">Choisir un parcours</option>
{(adaptiveKit.profiles || []).map((item) => (
<option key={item.id} value={item.id}>{item.title}</option>
))}
</select>
</label>
<label className="wide-field">
<span>Note de suivi</span>
<textarea
value={adaptiveProfileForm.notes}
onChange={(event) => setAdaptiveProfileForm({ ...adaptiveProfileForm, notes: event.target.value })}
placeholder="Exemple: inverse encore numérateur/dénominateur, commencer par le parcours verbal."
/>
</label>
<button type="submit" disabled={!adaptiveProfileForm.profile_id}>
Enregistrer le profil adaptatif
</button>
</form>
</section>
)}
<div className="admin-dashboard-grid">
<section className="admin-preview">
<h2>Compréhension des cards</h2>
@@ -2150,11 +2298,13 @@ function TutorApp({ currentUser, onLogout }) {
{selectedProgramNode?.review?.status && <span className={`review-pill ${selectedProgramNode.review.status}`}>{selectedProgramNode.review.status}</span>}
</div>
<div className="validation-main-preview">
{selectedProgramNode?.url ? (
<img src={selectedProgramNode.url} alt={selectedProgramNode.label} />
) : (
<div className="empty-preview">Aucun visuel direct pour cet élément.</div>
)}
{selectedProgramNode?.url ? (
<img src={selectedProgramNode.url} alt={selectedProgramNode.label} />
) : selectedProgramNode?.content ? (
<pre className="validation-text-preview">{selectedProgramNode.content}</pre>
) : (
<div className="empty-preview">Aucun visuel direct pour cet élément.</div>
)}
</div>
</section>
@@ -2167,6 +2317,9 @@ function TutorApp({ currentUser, onLogout }) {
<span>Contextes: {selectedNodeStatus.contexts ?? '-'}</span>
<span>Exercices: {selectedNodeStatus.text_exercises ?? '-'}</span>
<span>SVG ex.: {selectedNodeStatus.exercise_svg ?? '-'}</span>
<span>Tests: {selectedNodeStatus.adaptive_tests ?? '-'}</span>
<span>Parcours: {selectedNodeStatus.adaptive_profiles ?? '-'}</span>
<span>SVG kit: {selectedNodeStatus.adaptive_svg ?? '-'}</span>
</div>
<form className="admin-review-form" onSubmit={saveProgramReview}>
<label>

View File

@@ -216,7 +216,7 @@ button:disabled { opacity: 0.5; cursor: not-allowed; }
}
.student-report-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.student-report-grid article {
@@ -350,6 +350,67 @@ button:disabled { opacity: 0.5; cursor: not-allowed; }
align-items: flex-start;
}
.adaptive-kit-panel {
display: flex;
flex-direction: column;
gap: 1rem;
border: 1px solid #dbeafe;
border-radius: 16px;
background: #f8fbff;
padding: 1rem;
}
.adaptive-kit-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.75rem;
}
.adaptive-kit-grid article {
display: flex;
flex-direction: column;
gap: 0.4rem;
border: 1px solid #e2e8f0;
border-radius: 14px;
background: white;
padding: 0.85rem;
}
.adaptive-kit-grid span {
color: #475569;
font-size: 0.9rem;
line-height: 1.35;
}
.adaptive-profile-form {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.75rem;
border-top: 1px solid #dbeafe;
padding-top: 1rem;
}
.adaptive-profile-form label {
display: flex;
flex-direction: column;
gap: 0.35rem;
font-weight: 800;
}
.adaptive-profile-form .wide-field,
.adaptive-profile-form button {
grid-column: 1 / -1;
}
.adaptive-profile-form textarea {
min-height: 96px;
resize: vertical;
border: 1px solid #cfd7e6;
border-radius: 12px;
padding: 0.8rem 1rem;
font: inherit;
}
.review-fullscreen {
position: fixed;
inset: 0;
@@ -687,6 +748,23 @@ button:disabled { opacity: 0.5; cursor: not-allowed; }
font-size: 0.82rem;
font-weight: 800;
}
.program-node-text-preview,
.validation-text-preview {
min-height: 260px;
max-height: 520px;
overflow: auto;
border: 1px solid #e5e7eb;
border-radius: 12px;
background: white;
padding: 1rem;
}
.program-node-text-preview pre,
.validation-text-preview {
margin: 0;
color: #14213d;
font: 0.95rem/1.55 Inter, system-ui, sans-serif;
white-space: pre-wrap;
}
.admin-review-note p {
margin: 0.35rem 0;
line-height: 1.45;
@@ -1218,6 +1296,8 @@ button:disabled { opacity: 0.5; cursor: not-allowed; }
.admin-students-layout,
.student-report-grid,
.student-settings-grid,
.adaptive-kit-grid,
.adaptive-profile-form,
.validation-layout,
.admin-program-browser {
grid-template-columns: 1fr;