admin mode
This commit is contained in:
@@ -146,7 +146,7 @@ function LoginPage({ onLogin }) {
|
||||
<Avatar speaking={false} />
|
||||
<div className="login-copy">
|
||||
<h1>Professeur TOP</h1>
|
||||
<p className="muted">Connexion eleve, professeur ou maintenance.</p>
|
||||
<p className="muted">Connexion élève, professeur ou maintenance.</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={submitLogin} className="stack login-form">
|
||||
@@ -208,6 +208,8 @@ function TutorApp({ currentUser, onLogout }) {
|
||||
const [audioDebug, setAudioDebug] = useState([])
|
||||
const [voiceProfiles, setVoiceProfiles] = useState([])
|
||||
const [selectedVoiceProfileId, setSelectedVoiceProfileId] = useState('')
|
||||
const [programLessons, setProgramLessons] = useState([])
|
||||
const [studentProgram, setStudentProgram] = useState(null)
|
||||
const [progressPanelOpen, setProgressPanelOpen] = useState(false)
|
||||
const [settingsPanelOpen, setSettingsPanelOpen] = useState(false)
|
||||
const mediaRecorderRef = useRef(null)
|
||||
@@ -294,13 +296,20 @@ function TutorApp({ currentUser, onLogout }) {
|
||||
setLastStudentTransmission('')
|
||||
setCurrentFocusSkill(null)
|
||||
setExerciseStatus('not-started')
|
||||
setStudentProgram(null)
|
||||
setProgramLessons([])
|
||||
if (selectedStudentId) {
|
||||
loadProgress(selectedStudentId)
|
||||
loadMessages(selectedStudentId)
|
||||
loadStudentProgram(selectedStudentId)
|
||||
if (!isStudentUser) {
|
||||
const student = students.find((item) => String(item.id) === String(selectedStudentId))
|
||||
if (student) loadProgramLessons(student.grade)
|
||||
}
|
||||
} else {
|
||||
setMessages([])
|
||||
}
|
||||
}, [selectedStudentId])
|
||||
}, [selectedStudentId, students])
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedVoiceProfileId) {
|
||||
@@ -324,7 +333,7 @@ function TutorApp({ currentUser, onLogout }) {
|
||||
} else {
|
||||
setStudents([])
|
||||
setSelectedStudentId('')
|
||||
setErrorMessage('Ce compte eleve n est pas encore rattache a une fiche eleve.')
|
||||
setErrorMessage('Ce compte élève n’est pas encore rattaché à une fiche élève.')
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -371,7 +380,7 @@ function TutorApp({ currentUser, onLogout }) {
|
||||
return
|
||||
}
|
||||
if (!form.username.trim() || !form.password) {
|
||||
setErrorMessage('Identifiant et mot de passe eleve obligatoires.')
|
||||
setErrorMessage('Identifiant et mot de passe élève obligatoires.')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -391,7 +400,7 @@ function TutorApp({ currentUser, onLogout }) {
|
||||
setSelectedStudentId(String(data.student.id))
|
||||
setForm({ first_name: '', age: 8, grade: 'CM1', username: '', password: '' })
|
||||
} catch (error) {
|
||||
setErrorMessage(error.message || 'Impossible de creer le compte eleve.')
|
||||
setErrorMessage(error.message || 'Impossible de créer le compte élève.')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -527,6 +536,40 @@ function TutorApp({ currentUser, onLogout }) {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadProgramLessons(grade) {
|
||||
if (!grade) return
|
||||
try {
|
||||
const data = await apiFetch(`/program/lessons?grade=${encodeURIComponent(grade)}`)
|
||||
setProgramLessons(data.lessons || [])
|
||||
} catch (error) {
|
||||
setErrorMessage(error.message || 'Impossible de charger le programme.')
|
||||
}
|
||||
}
|
||||
|
||||
async function loadStudentProgram(studentId) {
|
||||
try {
|
||||
const data = await apiFetch(`/students/${studentId}/program`)
|
||||
setStudentProgram(data.lesson || null)
|
||||
} catch (error) {
|
||||
setErrorMessage(error.message || 'Impossible de charger la leçon de l’élève.')
|
||||
}
|
||||
}
|
||||
|
||||
async function assignProgramLesson(lessonId) {
|
||||
if (!selectedStudentId || !lessonId) return
|
||||
try {
|
||||
setErrorMessage('')
|
||||
const data = await apiFetch(`/admin/students/${selectedStudentId}/program`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ lesson_id: lessonId }),
|
||||
})
|
||||
setStudentProgram(data.lesson || null)
|
||||
} catch (error) {
|
||||
setErrorMessage(error.message || 'Impossible d’affecter cette leçon.')
|
||||
}
|
||||
}
|
||||
|
||||
async function finishSpeechPlayback(debugMessage) {
|
||||
setSpeaking(false)
|
||||
if (audioPlaybackUrlRef.current) {
|
||||
@@ -877,11 +920,14 @@ function TutorApp({ currentUser, onLogout }) {
|
||||
|
||||
const lastAssistantMessage = [...messages].reverse().find((message) => message.role === 'assistant')
|
||||
const lastUserMessage = [...messages].reverse().find((message) => message.role === 'user')
|
||||
const lessonSummary = lastAssistantMessage?.content || 'Aucune ancienne seance trouvee pour le moment. Professeur TOP commencera par une reprise courte.'
|
||||
const lastStudentWork = lastUserMessage?.content || 'Aucune reponse recente enregistree.'
|
||||
const nextLessonAdvice = lessonFocus
|
||||
? `Conseil: reprendre ${lessonFocus.label}, puis lancer un exercice court pour equilibrer la progression.`
|
||||
: 'Conseil: demarrer une seance pour laisser Professeur TOP choisir la prochaine lecon.'
|
||||
const activeLessonTitle = studentProgram?.title || lessonFocus?.label || 'Cours du jour'
|
||||
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.`
|
||||
: 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.'
|
||||
|
||||
if (isStudentUser) {
|
||||
return (
|
||||
@@ -892,26 +938,26 @@ function TutorApp({ currentUser, onLogout }) {
|
||||
<div>
|
||||
<p className="eyebrow">Bonjour</p>
|
||||
<h1>{selectedStudent?.first_name || currentUser.username}</h1>
|
||||
<p className="muted">{selectedStudent?.grade || 'Classe non renseignee'}</p>
|
||||
<p className="muted">{selectedStudent?.grade || 'Classe non renseignée'}</p>
|
||||
</div>
|
||||
<div className="student-home-actions">
|
||||
<button onClick={toggleSession} disabled={!selectedStudentId}>
|
||||
Demarrer la lecon
|
||||
Démarrer la leçon
|
||||
</button>
|
||||
<button type="button" className="secondary-button" onClick={onLogout}>
|
||||
Deconnexion
|
||||
Déconnexion
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="student-home-grid">
|
||||
<article className="student-info-card card">
|
||||
<span className="stage-label">Derniere seance</span>
|
||||
<span className="stage-label">Dernière séance</span>
|
||||
<p>{lessonSummary}</p>
|
||||
</article>
|
||||
|
||||
<article className="student-info-card card">
|
||||
<span className="stage-label">Derniere reponse</span>
|
||||
<span className="stage-label">Dernière réponse</span>
|
||||
<p>{lastStudentWork}</p>
|
||||
</article>
|
||||
|
||||
@@ -920,14 +966,14 @@ function TutorApp({ currentUser, onLogout }) {
|
||||
<span className="stage-label">Avancement</span>
|
||||
<strong>{lessonPercent}%</strong>
|
||||
</div>
|
||||
<h2>{lessonFocus?.label || 'Programme a definir'}</h2>
|
||||
<h2>{activeLessonTitle}</h2>
|
||||
<div className="progress-bar">
|
||||
<div className="progress-fill" style={{ width: `${lessonPercent}%` }} />
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article className="student-info-card card advice-card">
|
||||
<span className="stage-label">Prochaine lecon conseillee</span>
|
||||
<span className="stage-label">Prochaine leçon conseillée</span>
|
||||
<p>{nextLessonAdvice}</p>
|
||||
</article>
|
||||
</section>
|
||||
@@ -940,15 +986,15 @@ function TutorApp({ currentUser, onLogout }) {
|
||||
<header className="lesson-topbar">
|
||||
<Avatar speaking={speaking} />
|
||||
<div>
|
||||
<p className="eyebrow">Lecon en cours</p>
|
||||
<h1>{lessonFocus?.label || 'Cours du jour'}</h1>
|
||||
<p className="eyebrow">Leçon en cours</p>
|
||||
<h1>{activeLessonTitle}</h1>
|
||||
</div>
|
||||
<div className="lesson-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button"
|
||||
aria-label="Parametres"
|
||||
title="Parametres"
|
||||
aria-label="Paramètres"
|
||||
title="Paramètres"
|
||||
onClick={() => setSettingsPanelOpen(true)}
|
||||
>
|
||||
...
|
||||
@@ -967,13 +1013,20 @@ function TutorApp({ currentUser, onLogout }) {
|
||||
{currentInstruction ? (
|
||||
<p>{currentInstruction}</p>
|
||||
) : (
|
||||
<p className="muted">Professeur TOP prepare la suite de la lecon.</p>
|
||||
<p className="muted">Professeur TOP prépare la suite de la leçon.</p>
|
||||
)}
|
||||
{studentProgram?.assets?.length > 0 && (
|
||||
<div className="lesson-assets" aria-label="Fiches de leçon">
|
||||
{studentProgram.assets.map((asset) => (
|
||||
<img key={asset.path} src={asset.url} alt={asset.title} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="student-transmission lesson-bubble">
|
||||
<span>Ta reponse</span>
|
||||
<p>{lastStudentTransmission || 'Dicte ou ecris ta reponse quand tu es pret.'}</p>
|
||||
<span>Ta réponse</span>
|
||||
<p>{lastStudentTransmission || 'Dicte ou écris ta réponse quand tu es prêt.'}</p>
|
||||
</section>
|
||||
|
||||
<form onSubmit={assessment ? submitAssessment : sendMessage} className="lesson-composer">
|
||||
@@ -982,7 +1035,7 @@ function TutorApp({ currentUser, onLogout }) {
|
||||
onChange={(event) =>
|
||||
assessment ? setAssessmentAnswer(event.target.value) : setInput(event.target.value)
|
||||
}
|
||||
placeholder={assessment ? 'Ta reponse au mini-test...' : 'Ecris ta reponse...'}
|
||||
placeholder={assessment ? 'Ta réponse au mini-test...' : 'Écris ta réponse...'}
|
||||
/>
|
||||
<button type="button" onClick={startVoiceInput} disabled={isTranscribing}>
|
||||
{isAutoListening ? 'Stop micro' : isTranscribing ? 'Transcription...' : 'Dicter'}
|
||||
@@ -1014,9 +1067,9 @@ function TutorApp({ currentUser, onLogout }) {
|
||||
|
||||
{settingsPanelOpen && (
|
||||
<div className="progress-panel-backdrop" role="presentation" onClick={() => setSettingsPanelOpen(false)}>
|
||||
<aside className="progress-panel card" role="dialog" aria-label="Parametres" onClick={(event) => event.stopPropagation()}>
|
||||
<aside className="progress-panel card" role="dialog" aria-label="Paramètres" onClick={(event) => event.stopPropagation()}>
|
||||
<div className="progress-panel-head">
|
||||
<h2>Parametres</h2>
|
||||
<h2>Paramètres</h2>
|
||||
<button type="button" className="secondary-button" onClick={() => setSettingsPanelOpen(false)}>
|
||||
Fermer
|
||||
</button>
|
||||
@@ -1052,11 +1105,11 @@ function TutorApp({ currentUser, onLogout }) {
|
||||
return (
|
||||
<div className={`layout ${isStudentUser ? 'student-layout' : 'admin-layout'}`}>
|
||||
<aside className="sidebar card">
|
||||
<h2>{isStudentUser ? 'Ma seance' : 'Eleves'}</h2>
|
||||
<h2>{isStudentUser ? 'Ma séance' : 'Élèves'}</h2>
|
||||
{!isStudentUser && (
|
||||
<>
|
||||
<select value={selectedStudentId} onChange={(event) => setSelectedStudentId(event.target.value)}>
|
||||
<option value="">Choisir un eleve</option>
|
||||
<option value="">Choisir un élève</option>
|
||||
{students.map((student) => (
|
||||
<option key={student.id} value={student.id}>
|
||||
{student.first_name} · {student.grade}
|
||||
@@ -1066,7 +1119,7 @@ function TutorApp({ currentUser, onLogout }) {
|
||||
|
||||
<form onSubmit={createStudent} className="stack">
|
||||
<input
|
||||
placeholder="Prenom"
|
||||
placeholder="Prénom"
|
||||
value={form.first_name}
|
||||
onChange={(event) => setForm({ ...form, first_name: event.target.value })}
|
||||
/>
|
||||
@@ -1084,171 +1137,99 @@ function TutorApp({ currentUser, onLogout }) {
|
||||
<option>6e</option>
|
||||
</select>
|
||||
<input
|
||||
placeholder="Identifiant eleve"
|
||||
placeholder="Identifiant élève"
|
||||
value={form.username}
|
||||
onChange={(event) => setForm({ ...form, username: event.target.value })}
|
||||
/>
|
||||
<input
|
||||
placeholder="Mot de passe eleve"
|
||||
placeholder="Mot de passe élève"
|
||||
type="password"
|
||||
value={form.password}
|
||||
onChange={(event) => setForm({ ...form, password: event.target.value })}
|
||||
/>
|
||||
<button type="submit">Creer le compte eleve</button>
|
||||
<button type="submit">Créer le compte élève</button>
|
||||
</form>
|
||||
|
||||
{selectedStudent && (
|
||||
<section className="admin-program-panel">
|
||||
<h3>Programme de l’élève</h3>
|
||||
<p className="muted">
|
||||
{studentProgram
|
||||
? `Leçon active: ${studentProgram.title}`
|
||||
: 'Aucune leçon active pour cet élève.'}
|
||||
</p>
|
||||
<select
|
||||
value={studentProgram?.id || ''}
|
||||
onChange={(event) => assignProgramLesson(event.target.value)}
|
||||
>
|
||||
<option value="">Choisir une leçon pour {selectedStudent.grade}</option>
|
||||
{programLessons.map((lesson) => (
|
||||
<option key={lesson.id} value={lesson.id}>
|
||||
{lesson.subject} · {lesson.title} · {lesson.asset_count} fiche(s)
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</section>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{isStudentUser && selectedStudent && (
|
||||
<div className="session-summary">
|
||||
<strong>{selectedStudent.first_name}</strong>
|
||||
<span>{selectedStudent.grade}</span>
|
||||
<span>{sessionActive ? 'Seance en cours' : 'Pret pour la seance'}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isStudentUser && (
|
||||
<div className="today-panel">
|
||||
<button onClick={toggleSession} disabled={!selectedStudentId}>
|
||||
{sessionActive ? 'Fin de la seance' : 'Demarrer la seance'}
|
||||
</button>
|
||||
<h3>Themes du jour</h3>
|
||||
<div className="today-card lesson-card">
|
||||
<div className="today-card-head">
|
||||
<strong>Lecon</strong>
|
||||
<span>{lessonPercent}% acquis</span>
|
||||
</div>
|
||||
<p>{lessonFocus?.label || 'Le sujet sera choisi au demarrage.'}</p>
|
||||
</div>
|
||||
<div className="today-card exercise-card">
|
||||
<div className="today-card-head">
|
||||
<strong>Exercice</strong>
|
||||
<span>{exerciseLabel}</span>
|
||||
</div>
|
||||
<p>{assessment?.skill_label || lessonFocus?.label || 'Aucun exercice lance.'}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button onClick={startSession} disabled={!selectedStudentId}>Démarrer la séance</button>
|
||||
<button onClick={fetchAssessment} disabled={!selectedStudentId}>Lancer un mini-test</button>
|
||||
|
||||
<h3>Progression</h3>
|
||||
<div className="stack small-gap">
|
||||
{progress.map((item) => <ProgressCard key={item.code} item={item} />)}
|
||||
</div>
|
||||
<button type="button" className="secondary-button" onClick={onLogout}>
|
||||
Déconnexion
|
||||
</button>
|
||||
</aside>
|
||||
|
||||
<main className="main card">
|
||||
<div className="hero">
|
||||
<Avatar speaking={speaking} />
|
||||
<main className="admin-main card">
|
||||
<header className="admin-header">
|
||||
<div>
|
||||
<h1>Professeur virtuel</h1>
|
||||
<p>
|
||||
{selectedStudent
|
||||
? `Séance active pour ${selectedStudent.first_name}, ${selectedStudent.grade}`
|
||||
: 'Choisis ou crée un élève pour commencer.'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="session-user">
|
||||
<span>{currentUser.username} · {currentUser.role}</span>
|
||||
<button type="button" className="secondary-button" onClick={onLogout}>
|
||||
Deconnexion
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section className="instruction-stage">
|
||||
<span className="stage-label">Cours en cours</span>
|
||||
{currentInstruction ? (
|
||||
<p>{currentInstruction}</p>
|
||||
) : (
|
||||
<h1>Gestion du programme</h1>
|
||||
<p className="muted">
|
||||
Demarre la seance pour que Professeur TOP reprenne ou tu t'etais arrete et lance le cours du jour.
|
||||
Configure les élèves, leur niveau et la leçon active visible dans le mode élève.
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
<span>{currentUser.username} · {currentUser.role}</span>
|
||||
</header>
|
||||
|
||||
<section className="messages" aria-label="Journal compact de la seance">
|
||||
{messages.length === 0 && <p className="muted">Le dialogue apparaîtra ici.</p>}
|
||||
{messages.map((message) => (
|
||||
<div key={message.id} className={`message ${message.role}`}>
|
||||
<strong>{message.role === 'assistant' ? 'Professeur TOP' : 'Élève'}</strong>
|
||||
<p>{message.content}</p>
|
||||
{selectedStudent ? (
|
||||
<section className="admin-detail">
|
||||
<div className="admin-student-card">
|
||||
<span className="stage-label">Élève sélectionné</span>
|
||||
<h2>{selectedStudent.first_name}</h2>
|
||||
<p>{selectedStudent.grade} · {selectedStudent.age} ans</p>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
|
||||
{assessment && (
|
||||
<form className="assessment" onSubmit={submitAssessment}>
|
||||
<h3>Mini-test · {assessment.skill_label}</h3>
|
||||
<p>{assessment.question}</p>
|
||||
<input
|
||||
value={assessmentAnswer}
|
||||
onChange={(event) => setAssessmentAnswer(event.target.value)}
|
||||
placeholder="Ta réponse"
|
||||
/>
|
||||
<button type="submit">Valider</button>
|
||||
</form>
|
||||
<div className="admin-student-card">
|
||||
<span className="stage-label">Leçon active</span>
|
||||
<h2>{studentProgram?.title || 'Aucune leçon'}</h2>
|
||||
<p>
|
||||
{studentProgram
|
||||
? `${studentProgram.subject} · ${studentProgram.asset_count} fiche(s)`
|
||||
: 'Choisis une leçon dans la colonne de gauche.'}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
) : (
|
||||
<section className="admin-empty">
|
||||
<h2>Aucun élève sélectionné</h2>
|
||||
<p className="muted">Choisis un élève ou crée un compte pour configurer son programme.</p>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section className="student-transmission">
|
||||
<span>Transmis au prof</span>
|
||||
<p>{lastStudentTransmission || 'Le texte reconnu par le micro ou envoye au clavier apparaitra ici.'}</p>
|
||||
</section>
|
||||
{studentProgram?.assets?.length > 0 && (
|
||||
<section className="admin-preview">
|
||||
<h2>Fiches de la leçon</h2>
|
||||
<div className="admin-asset-grid">
|
||||
{studentProgram.assets.map((asset) => (
|
||||
<article key={asset.path} className="admin-asset-card">
|
||||
<img src={asset.url} alt={asset.title} />
|
||||
<strong>{asset.title}</strong>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<form onSubmit={assessment ? submitAssessment : sendMessage} className="composer">
|
||||
<input
|
||||
value={assessment ? assessmentAnswer : input}
|
||||
onChange={(event) =>
|
||||
assessment ? setAssessmentAnswer(event.target.value) : setInput(event.target.value)
|
||||
}
|
||||
placeholder={assessment ? 'Ta réponse au mini-test...' : 'Pose une question ou demande une explication...'}
|
||||
/>
|
||||
<select
|
||||
value={selectedVoiceProfileId}
|
||||
onChange={(event) => setSelectedVoiceProfileId(event.target.value)}
|
||||
title="Choisir la personnalité vocale de Professeur TOP"
|
||||
>
|
||||
<option value="">Voix de Professeur TOP</option>
|
||||
{voiceProfiles.map((profile) => (
|
||||
<option key={profile.id} value={profile.id}>
|
||||
{formatVoiceLabel(profile)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{selectedVoiceProfile && (
|
||||
<span
|
||||
className="voice-tooltip"
|
||||
title={`Voix IA non humaine: ${formatVoiceLabel(selectedVoiceProfile)} - ${selectedVoiceProfile.description}`}
|
||||
aria-label={`Voix IA non humaine: ${formatVoiceLabel(selectedVoiceProfile)} - ${selectedVoiceProfile.description}`}
|
||||
>
|
||||
i
|
||||
</span>
|
||||
)}
|
||||
<button type="button" onClick={startVoiceInput} disabled={isTranscribing}>
|
||||
{isAutoListening ? 'Couper le micro' : isTranscribing ? 'Transcription...' : 'Micro auto'}
|
||||
</button>
|
||||
<button type="submit">Envoyer</button>
|
||||
</form>
|
||||
|
||||
{isAutoListening && (
|
||||
<div className="voice-status">
|
||||
Niveau micro: {Math.round(Math.min(micLevel * 1200, 100))}%
|
||||
</div>
|
||||
)}
|
||||
{selectedVoiceProfile && (
|
||||
<p className="muted voice-note">
|
||||
Voix IA non humaine: {selectedVoiceProfile.label} · {selectedVoiceProfile.description}
|
||||
</p>
|
||||
)}
|
||||
{voiceStatus && <p className="muted voice-status">{voiceStatus}</p>}
|
||||
{errorMessage && <p className="muted voice-status">{errorMessage}</p>}
|
||||
{DEBUG_AUDIO && (
|
||||
<div className="voice-status">
|
||||
{audioDebug.length === 0 ? 'Debug audio: aucune trace' : `Debug audio: ${audioDebug.join(' | ')}`}
|
||||
</div>
|
||||
)}
|
||||
{errorMessage && <p className="muted voice-status visible-status">{errorMessage}</p>}
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -96,6 +96,85 @@ button:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.today-card p {
|
||||
margin: 0 0 0.75rem;
|
||||
}
|
||||
.admin-program-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
border: 1px solid #dbeafe;
|
||||
border-radius: 16px;
|
||||
background: #f8fbff;
|
||||
padding: 0.85rem;
|
||||
}
|
||||
.admin-program-panel h3,
|
||||
.admin-program-panel p {
|
||||
margin: 0;
|
||||
}
|
||||
.admin-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.admin-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
.admin-header h1,
|
||||
.admin-header p,
|
||||
.admin-detail h2,
|
||||
.admin-detail p,
|
||||
.admin-empty h2,
|
||||
.admin-empty p,
|
||||
.admin-preview h2 {
|
||||
margin: 0;
|
||||
}
|
||||
.admin-header span {
|
||||
color: #64748b;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.admin-detail {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
.admin-student-card,
|
||||
.admin-empty {
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 16px;
|
||||
background: #f8fafc;
|
||||
padding: 1rem;
|
||||
}
|
||||
.admin-preview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
.admin-asset-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
.admin-asset-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.6rem;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 16px;
|
||||
background: #f8fafc;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
.admin-asset-card img {
|
||||
width: 100%;
|
||||
aspect-ratio: 4 / 3;
|
||||
object-fit: contain;
|
||||
border-radius: 12px;
|
||||
background: white;
|
||||
}
|
||||
.session-user {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
@@ -382,6 +461,24 @@ button:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
font-size: clamp(1rem, 2.2vw, 1.35rem);
|
||||
line-height: 1.5;
|
||||
}
|
||||
.lesson-assets {
|
||||
min-height: 0;
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
padding-bottom: 0.25rem;
|
||||
}
|
||||
.lesson-assets img {
|
||||
width: min(100%, 720px);
|
||||
max-height: 100%;
|
||||
flex: 0 0 min(100%, 720px);
|
||||
object-fit: contain;
|
||||
border: 1px solid #dbeafe;
|
||||
border-radius: 14px;
|
||||
background: white;
|
||||
}
|
||||
.lesson-bubble {
|
||||
min-height: 68px;
|
||||
}
|
||||
@@ -471,6 +568,15 @@ button:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.admin-header,
|
||||
.admin-detail {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.admin-header {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.instruction-stage {
|
||||
flex: 0 0 auto;
|
||||
justify-content: flex-start;
|
||||
|
||||
Reference in New Issue
Block a user