"use client"; // Résumés : liste, génération (semaine / concept / préparation d'examen, 3 styles), // viewer Markdown avec citations cliquables, copie et téléchargement .md. import { useCallback, useEffect, useMemo, useState } from "react"; import { useSearchParams } from "next/navigation"; import { Check, Copy, Download, FileText, Sparkles } from "lucide-react"; import { Badge, Button, Card, EmptyState, Label, Modal, Skeleton, Spinner, cn } from "@/components/ui"; import { Markdown } from "@/components/chat/markdown"; import { CitationPanel, type Citation } from "@/components/chat/citation-panel"; import { ErrorBanner, downloadText, fetchJson, fmtDate, postJson } from "./shared"; type Summary = { id: number; scope: "week" | "concept" | "exam-prep"; ref: string; title: string; content: string; citations: string; created_by: string; created_at: string; }; type ConceptOpt = { slug: string; name: string }; const SCOPE_LABELS: Record = { week: "Semaine", concept: "Concept", "exam-prep": "Préparation d'examen", }; const STYLES = [ { key: "ultra-court", label: "Ultra-court", hint: "Une page maximum, l'essentiel en listes serrées." }, { key: "detaille", label: "Détaillé", hint: "Structuré par sous-thèmes, exemples et erreurs fréquentes." }, { key: "avec-formules", label: "Avec formules", hint: "Chaque formule en LaTeX, variables définies, mini-exemple chiffré." }, ] as const; export function SummariesApp({ course }: { course: string }) { const searchParams = useSearchParams(); const conceptParam = searchParams.get("concept"); const [summaries, setSummaries] = useState(null); const [concepts, setConcepts] = useState([]); const [selectedId, setSelectedId] = useState(null); const [citation, setCitation] = useState(null); const [error, setError] = useState(null); const [copied, setCopied] = useState(false); // Génération const [genOpen, setGenOpen] = useState(!!conceptParam); const [scope, setScope] = useState(conceptParam ? "concept" : "week"); const [week, setWeek] = useState(1); const [conceptSlug, setConceptSlug] = useState(conceptParam ?? ""); const [style, setStyle] = useState<(typeof STYLES)[number]["key"]>("detaille"); const [genBusy, setGenBusy] = useState(false); const [genError, setGenError] = useState(null); const load = useCallback(() => { setError(null); fetchJson<{ summaries: Summary[] }>(`/api/learning/${course}/summaries`) .then((d) => setSummaries(d.summaries)) .catch((e) => { setSummaries([]); setError(e instanceof Error ? e.message : "Erreur de chargement."); }); }, [course]); useEffect(() => { load(); }, [load]); useEffect(() => { fetchJson<{ nodes: ConceptOpt[] }>(`/api/learning/${course}/concepts`) .then((d) => setConcepts(d.nodes)) .catch(() => {}); }, [course]); const selected = useMemo(() => summaries?.find((s) => s.id === selectedId) ?? null, [summaries, selectedId]); const selectedCitations = useMemo(() => { if (!selected) return []; try { return JSON.parse(selected.citations || "[]") as Citation[]; } catch { return []; } }, [selected]); async function generate(e: React.FormEvent) { e.preventDefault(); setGenBusy(true); setGenError(null); try { const body: Record = { scope, style }; if (scope === "week") body.week = week; if (scope === "concept") { if (!conceptSlug) { setGenError("Choisissez un concept."); setGenBusy(false); return; } body.conceptSlug = conceptSlug; } const d = await postJson<{ id: number }>(`/api/learning/${course}/summaries`, body); setGenOpen(false); load(); setSelectedId(d.id); } catch (err) { setGenError(err instanceof Error ? err.message : "Erreur de génération."); } finally { setGenBusy(false); } } function copySelected() { if (!selected) return; navigator.clipboard?.writeText(selected.content).then(() => { setCopied(true); setTimeout(() => setCopied(false), 1500); }); } return (

Résumés

Générés depuis le matériel officiel du cours, avec les diapositives citées.

{error && } {summaries === null ? (
) : summaries.length === 0 ? ( } title="Aucun résumé pour l'instant" description="Générez votre premier résumé : par semaine de cours, par concept, ou en préparation d'examen." action={ } /> ) : (
{/* Liste */}
{summaries.map((s) => ( ))}
{/* Viewer */} {selected ? (

{selected.title}

{ const c = selectedCitations.find((x) => x.index === index); if (c) setCitation(c); }} /> {selectedCitations.length > 0 && (
Sources : {selectedCitations.map((c) => ( ))}
)}
) : (

Choisissez un résumé dans la liste, ou générez-en un nouveau.

)}
)} {/* Modale de génération */} setGenOpen(false)} title="Générer un résumé">
{(Object.keys(SCOPE_LABELS) as Summary["scope"][]).map((s) => ( ))}
{scope === "week" && (
)} {scope === "concept" && (
)}
{STYLES.map((s) => ( ))}
{genError && }
setCitation(null)} />
); }