import { useCallback, useEffect, useRef, useState } from 'react' import { useNavigate, useParams } from 'react-router-dom' import { applyCorrections, getHistory, fetchMasterJson, ApiError, type PageMaster, type VersionInfo, } from '../lib/api.ts' import Viewer from '../components/Viewer.tsx' import { RetroMenuBar, RetroWindow, RetroButton, RetroTextarea, RetroSelect, RetroBadge, } from '../components/retro' type Panel = 'transcription' | 'commentary' | 'regions' | 'history' const PANEL_LABELS: Record = { transcription: 'Transcription', commentary: 'Commentaire', regions: 'Regions', history: 'Historique', } export default function Editor() { const { pageId = '' } = useParams() const navigate = useNavigate() const [master, setMaster] = useState(null) const [history, setHistory] = useState([]) const [activePanel, setActivePanel] = useState('transcription') const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(false) const [error, setError] = useState(null) const [saveError, setSaveError] = useState(null) const [saveSuccess, setSaveSuccess] = useState(false) const [ocrText, setOcrText] = useState('') const [commentaryPublic, setCommentaryPublic] = useState('') const [commentaryScholarly, setCommentaryScholarly] = useState('') const [editorialStatus, setEditorialStatus] = useState('') const [regionValidations, setRegionValidations] = useState>({}) const successTimeout = useRef | null>(null) // Nettoyage du timeout de succès lors du démontage du composant useEffect(() => { return () => { if (successTimeout.current) clearTimeout(successTimeout.current) } }, []) const loadData = useCallback(async () => { setLoading(true) setError(null) try { const [m, h] = await Promise.all([fetchMasterJson(pageId), getHistory(pageId)]) setMaster(m) setHistory(h) setOcrText(m.ocr?.diplomatic_text ?? '') setCommentaryPublic(m.commentary?.public ?? '') setCommentaryScholarly(m.commentary?.scholarly ?? '') setEditorialStatus(m.editorial.status) const ext = m.extensions as { region_validations?: Record } | undefined setRegionValidations(ext?.region_validations ?? {}) } catch (e: unknown) { if (e instanceof ApiError && e.status === 404) { setError('Cette page n\'a pas encore été analysée par l\'IA. Lancez le pipeline depuis Administration.') } else { const msg = e instanceof Error ? e.message : '' setError(msg || 'Erreur de chargement') } } finally { setLoading(false) } }, [pageId]) useEffect(() => { void loadData() }, [loadData]) const handleSave = async () => { setSaving(true) setSaveError(null) setSaveSuccess(false) try { const updated = await applyCorrections(pageId, { ocr_diplomatic_text: ocrText !== (master?.ocr?.diplomatic_text ?? '') ? ocrText : undefined, editorial_status: editorialStatus !== master?.editorial.status ? editorialStatus : undefined, commentary_public: commentaryPublic !== (master?.commentary?.public ?? '') ? commentaryPublic : undefined, commentary_scholarly: commentaryScholarly !== (master?.commentary?.scholarly ?? '') ? commentaryScholarly : undefined, region_validations: Object.keys(regionValidations).length > 0 ? regionValidations : undefined, }) setMaster(updated) const h = await getHistory(pageId) setHistory(h) setSaveSuccess(true) if (successTimeout.current) clearTimeout(successTimeout.current) successTimeout.current = setTimeout(() => setSaveSuccess(false), 3000) } catch (e: unknown) { setSaveError((e as Error).message) } finally { setSaving(false) } } const handleRestore = async (version: number) => { setSaving(true) setSaveError(null) try { const updated = await applyCorrections(pageId, { restore_to_version: version }) setMaster(updated) setOcrText(updated.ocr?.diplomatic_text ?? '') setCommentaryPublic(updated.commentary?.public ?? '') setCommentaryScholarly(updated.commentary?.scholarly ?? '') setEditorialStatus(updated.editorial.status) const h = await getHistory(pageId) setHistory(h) } catch (e: unknown) { setSaveError((e as Error).message) } finally { setSaving(false) } } const setRegionValidation = (regionId: string, val: string) => { setRegionValidations((prev) => ({ ...prev, [regionId]: val })) } // ── Loading / Error ───────────────────────────────────────────────── if (loading) { return (
Chargement...
) } if (error) { return (
{error}
navigate(-1)}>Retour
) } const iiifServiceUrl = master?.image?.iiif_service_url ?? null const fallbackImageUrl = master?.image?.derivative_web ?? master?.image?.master ?? '' const regions = master?.layout?.regions ?? [] return (
{/* ── Menu bar ───────────────────────────────────────────────── */} navigate('/') }, { label: `Éditeur — ${master?.folio_label ?? pageId}` }, ]} right={
{master && ( v{master.editorial.version} — {master.editorial.status} )} {saveSuccess && OK} {saveError && Err} void handleSave()} disabled={saving} > {saving ? 'Saving...' : 'Sauvegarder'}
} /> {/* ── Main layout 50/50 ──────────────────────────────────────── */}
{/* ── Viewer window (left) ───────────────────────────────── */}
{}} /> {!iiifServiceUrl && !fallbackImageUrl && (
Aperçu non disponible
)}
{/* ── Editor window (right) ──────────────────────────────── */}
{/* ── Tab bar ──────────────────────────────────────── */}
{(['transcription', 'commentary', 'regions', 'history'] as Panel[]).map((p) => ( setActivePanel(p)} className="flex-1 border-0 border-r border-retro-darkgray last:border-r-0" > {PANEL_LABELS[p]} ))}
{/* ── Panel content ─────────────────────────────────── */}
{/* Transcription */} {activePanel === 'transcription' && (
setOcrText(e.target.value)} rows={12} /> setEditorialStatus(e.target.value)} options={[ { value: 'machine_draft', label: 'machine_draft' }, { value: 'needs_review', label: 'needs_review' }, { value: 'reviewed', label: 'reviewed' }, { value: 'validated', label: 'validated' }, { value: 'published', label: 'published' }, ]} /> {master?.ocr && (
Langue: {master.ocr.language} — Confiance: {(master.ocr.confidence * 100).toFixed(0)}%
)}
)} {/* Commentary */} {activePanel === 'commentary' && (
setCommentaryPublic(e.target.value)} rows={6} /> setCommentaryScholarly(e.target.value)} rows={8} />
)} {/* Regions */} {activePanel === 'regions' && (
{regions.length === 0 ? (

Aucune région détectée.

) : ( regions.map((region) => { const validation = regionValidations[region.id] return (
{region.type.replace(/_/g, ' ')} {region.id}
confiance: {(region.confidence * 100).toFixed(0)}%
setRegionValidation(region.id, 'validated')} > OK setRegionValidation(region.id, 'rejected')} > X
) }) )}
)} {/* History */} {activePanel === 'history' && (
{history.length === 0 ? (

Aucune version archivée.

) : ( history.map((v) => (
v{v.version} {v.status}
{new Date(v.saved_at).toLocaleString('fr-FR')}
void handleRestore(v.version)} disabled={saving} > Restaurer
)) )}
)}
) }