import React, { useEffect, useMemo, useRef, useState } from 'react' import professeurTopImage from './assets/professeur-top.png' const API_BASE = '/api' const AUTO_STOP_SILENCE_MS = 2500 const SPEECH_START_THRESHOLD = 0.02 const SILENCE_THRESHOLD = 0.012 const DEBUG_AUDIO = false const TTS_PROFILE_STORAGE_KEY = 'professeur-top-tts-profile' async function parseApiResponse(res) { const contentType = res.headers.get('content-type') || '' const bodyText = await res.text() const isJson = contentType.includes('application/json') const data = isJson && bodyText ? JSON.parse(bodyText) : null if (!res.ok) { const detail = data?.detail || data?.message || bodyText.trim() || `Erreur API (${res.status})` throw new Error(detail) } if (!isJson) { throw new Error(`Réponse API invalide (${res.status})`) } return data } async function apiFetch(path, options = {}) { const res = await fetch(`${API_BASE}${path}`, { credentials: 'same-origin', ...options, }) return parseApiResponse(res) } async function fetchAudio(path, options) { const res = await fetch(`${API_BASE}${path}`, { credentials: 'same-origin', ...options, }) if (!res.ok) { let detail = `Erreur API (${res.status})` const contentType = res.headers.get('content-type') || '' const bodyText = await res.text() if (contentType.includes('application/json') && bodyText) { try { const data = JSON.parse(bodyText) detail = data?.detail || data?.message || detail } catch { if (bodyText.trim()) detail = bodyText.trim() } } else if (bodyText.trim()) { detail = bodyText.trim() } throw new Error(detail) } return res.blob() } function Avatar({ speaking }) { return (
Professeur TOP
Professeur TOP
) } function ProgressCard({ item }) { const pct = Math.round(item.mastery_score) return (
{item.label} {pct}%
{item.subject} · preuves: {item.evidence_count}
) } function flattenProgramTree(node, depth = 0, rows = []) { if (!node) return rows rows.push({ node, depth }) ;(node.children || []).forEach((child) => flattenProgramTree(child, depth + 1, rows)) return rows } function flattenVisibleProgramTree(node, collapsedKeys, depth = 0, rows = []) { if (!node) return rows rows.push({ node, depth }) if (!collapsedKeys.has(node.key)) { ;(node.children || []).forEach((child) => flattenVisibleProgramTree(child, collapsedKeys, depth + 1, rows)) } return rows } function collectCollapsibleProgramKeys(node, depth = 0, keys = []) { if (!node) return keys if (depth > 0 && node.children?.length) keys.push(node.key) ;(node.children || []).forEach((child) => collectCollapsibleProgramKeys(child, depth + 1, keys)) return keys } function ProgramReviewPanel({ selectedNode, reviewForm, onReviewChange, onSaveReview, readOnly = false, onOpenValidation }) { const [isFullscreen, setIsFullscreen] = useState(false) if (!selectedNode) { return (

Programme

Choisis un élément de l’arborescence pour le relire, le valider ou laisser un commentaire.

) } const status = selectedNode.status || {} const review = selectedNode.review || {} return (
{selectedNode.type}

{selectedNode.label}

{selectedNode.path || selectedNode.key}

{review.status && {review.status}}
{Object.keys(status).length > 0 && (
Micro-fiches: {status.micro_fiches ?? 0} SVG leçon: {status.lesson_svg ?? 0} Cards: {status.cards ?? 0} Contextes: {status.contexts ?? 0} Exercices: {status.text_exercises ?? 0} SVG exercices: {status.exercise_svg ?? 0} Tests adaptatifs: {status.adaptive_tests ?? 0} Parcours: {status.adaptive_profiles ?? 0} SVG kit: {status.adaptive_svg ?? 0}
)} {selectedNode.url && ( )} {selectedNode.content && !selectedNode.url && (
{selectedNode.content}
)} {review.comment && (
Dernier commentaire

{review.comment}

{review.reviewer} · {review.updated_at}
)} {readOnly ? (
Validation centralisée

Cet onglet sert à explorer le programme. Les décisions éditoriales se font dans l’onglet Validation contenus.

) : (