"use client"; // Visionneuse interactive des diapositives : navigation par séance et par diapo, // clavier ←/→, recherche plein texte, boîtes sémantiques rendues en encadrés, // PDF original, et « poser une question sur cette diapo » directement dans le chat. import Link from "next/link"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useRouter, useSearchParams } from "next/navigation"; import { ChevronLeft, ChevronRight, FileDown, List, MessageSquareText, Presentation, Search, X, } from "lucide-react"; import { Badge, Button, Card, Skeleton, cn } from "@/components/ui"; import { Markdown } from "@/components/chat/markdown"; import { ErrorBanner } from "./shared"; type Deck = { week: number; title: string; slides: number; sections: number }; type Slide = { ref_number: number; title: string; section_title: string; display_content: string; box_types: string }; const CALLOUTS: Record = { "Définition": { label: "Définition", cls: "border-brand-500 bg-brand-50 dark:bg-brand-900/30" }, "Concept": { label: "Concept", cls: "border-brand-500 bg-brand-50 dark:bg-brand-900/30" }, "Important": { label: "Important", cls: "border-red-500 bg-red-50 dark:bg-red-950/30" }, "Attention": { label: "Attention", cls: "border-red-500 bg-red-50 dark:bg-red-950/30" }, "Exemple": { label: "Exemple", cls: "border-emerald-500 bg-emerald-50 dark:bg-emerald-950/30" }, "Note": { label: "Note", cls: "border-gold-500 bg-gold-500/8 dark:bg-gold-500/10" }, "Question éclair": { label: "Question éclair", cls: "border-violet-500 bg-violet-50 dark:bg-violet-950/30" }, "Formule": { label: "Formule", cls: "border-brand-400 bg-surface-1 dark:bg-brand-950/40" }, "Calcul": { label: "Calcul", cls: "border-brand-400 bg-surface-1 dark:bg-brand-950/40" }, "Données": { label: "Données", cls: "border-slate-400 bg-surface-1 dark:bg-brand-950/40" }, "Rappel": { label: "Rappel", cls: "border-gold-500 bg-gold-500/8" }, "Astuce": { label: "Astuce", cls: "border-emerald-500 bg-emerald-50 dark:bg-emerald-950/30" }, "Conseil": { label: "Conseil", cls: "border-emerald-500 bg-emerald-50 dark:bg-emerald-950/30" }, "Information": { label: "Information", cls: "border-brand-400 bg-brand-50 dark:bg-brand-900/30" }, "Résultat": { label: "Résultat", cls: "border-emerald-600 bg-emerald-50 dark:bg-emerald-950/30" }, "Énoncé": { label: "Énoncé", cls: "border-slate-400 bg-surface-1" }, "Travail demandé": { label: "Travail demandé", cls: "border-violet-500 bg-violet-50 dark:bg-violet-950/30" }, "Réconciliation": { label: "Réconciliation", cls: "border-gold-600 bg-gold-500/8" }, }; const CALLOUT_RE = new RegExp(`^(${Object.keys(CALLOUTS).join("|")})(?:\\s*—\\s*([^:\\n]{0,120}?))?\\s*:\\s*`); /** Convertit le texte aplati d'une diapo en blocs affichables. */ function parseBlocks(content: string): { kind: "callout" | "text" | "table"; label?: string; title?: string; body: string }[] { const out: { kind: "callout" | "text" | "table"; label?: string; title?: string; body: string }[] = []; for (const rawBlock of content.split(/\n\n+/)) { const block = rawBlock.trim(); if (!block) continue; const m = block.match(CALLOUT_RE); const lines = block.split("\n"); const pipeLines = lines.filter((l) => l.includes(" | ")); if (m) { out.push({ kind: "callout", label: m[1], title: m[2]?.trim(), body: mdify(block.slice(m[0].length)) }); } else if (pipeLines.length >= 2 && pipeLines.length >= lines.length - 1) { out.push({ kind: "table", body: block }); } else { out.push({ kind: "text", body: mdify(block) }); } } return out; } function mdify(s: string): string { return s .split("\n") .map((l) => (l.trim().startsWith("• ") ? l.replace(/^\s*• /, "- ") : l)) .join("\n"); } function TableBlock({ body }: { body: string }) { const rows = body.split("\n").filter((l) => l.trim()); return (
{rows.map((r, i) => ( {r.split(" | ").map((c, j) => ( ))} ))}
{c}
); } export function SlidesViewer({ course }: { course: string }) { const router = useRouter(); const search = useSearchParams(); const [decks, setDecks] = useState(null); const [week, setWeek] = useState(null); const [slides, setSlides] = useState(null); const [index, setIndex] = useState(0); const [error, setError] = useState(null); const [panelOpen, setPanelOpen] = useState(false); const [query, setQuery] = useState(""); const mainRef = useRef(null); const courseCode = course.toUpperCase(); useEffect(() => { fetch(`/api/slides/${course}`) .then((r) => (r.ok ? r.json() : Promise.reject(new Error("Chargement impossible")))) .then((d) => { setDecks(d.decks); const w = parseInt(search.get("semaine") ?? "", 10); if (d.decks.length) setWeek(Number.isInteger(w) && d.decks.some((x: Deck) => x.week === w) ? w : d.decks[0].week); }) .catch((e) => setError(e.message)); // eslint-disable-next-line react-hooks/exhaustive-deps }, [course]); useEffect(() => { if (week == null) return; setSlides(null); setIndex(0); fetch(`/api/slides/${course}/${week}`) .then((r) => (r.ok ? r.json() : Promise.reject(new Error("Séance introuvable")))) .then((d) => setSlides(d.slides)) .catch((e) => setError(e.message)); }, [course, week]); const go = useCallback( (delta: number) => { if (!slides) return; setIndex((i) => Math.min(slides.length - 1, Math.max(0, i + delta))); mainRef.current?.scrollTo({ top: 0 }); }, [slides] ); useEffect(() => { const onKey = (e: KeyboardEvent) => { if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return; if (e.key === "ArrowRight" || e.key === " ") { e.preventDefault(); go(1); } if (e.key === "ArrowLeft") { e.preventDefault(); go(-1); } if (e.key === "Escape") setPanelOpen(false); }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [go]); const results = useMemo(() => { if (!slides || query.trim().length < 2) return null; const q = query.toLowerCase(); return slides .map((s, i) => ({ s, i })) .filter(({ s }) => s.title.toLowerCase().includes(q) || s.display_content.toLowerCase().includes(q)) .slice(0, 30); }, [slides, query]); if (error) return
; if (!decks) { return (
); } if (!decks.length) { return (
Aucune diapositive indexée pour ce cours — demandez au professeur de lancer l'ingestion.
); } const slide = slides?.[index]; const blocks = slide ? parseBlocks(slide.display_content) : []; const deck = decks.find((d) => d.week === week); return (
{/* Barre de la visionneuse */}
{slide && ( Demander au chat )} PDF
{/* Panneau sommaire / recherche */} {panelOpen && ( )} {/* Diapositive */}
{!slides ? (
) : slide ? ( <>
{slide.section_title && (

{slide.section_title}

)}

{slide.title}

{blocks.map((b, i) => b.kind === "callout" ? (

{b.label}{b.title ? ` — ${b.title}` : ""}

) : b.kind === "table" ? ( ) : ( ) )}
{/* Pied : progression + navigation */}
{slide.ref_number} / {slides.at(-1)?.ref_number ?? slides.length} Séance {week}
) : null}

← → pour naviguer · {deck?.slides ?? 0} diapositives · contenu reconstruit depuis la source du cours — le PDF original reste disponible

); }