"use client"; // Bibliothèque personnelle : filtres par type, aperçus, viewer Markdown en modale, // copie, téléchargement et suppression. import { useCallback, useEffect, useMemo, useState } from "react"; import { Bookmark, Copy, Download, Library, Trash2 } from "lucide-react"; import { PageHeader } from "@/components/app-shell"; import { Badge, Card, EmptyState, Modal, Skeleton, Tabs } from "@/components/ui"; import { Markdown } from "@/components/chat/markdown"; import { ErrorBanner, downloadText, fetchJson, fmtDate } from "./shared"; type SavedItem = { id: number; kind: "note" | "answer" | "summary" | "quiz" | "plan"; course_code: string | null; title: string; content: string; meta: string | null; created_at: string; }; const KIND_LABELS: Record = { note: "Note", answer: "Réponse", summary: "Résumé", quiz: "Quiz", plan: "Plan", }; export function LibraryApp() { const [items, setItems] = useState(null); const [filter, setFilter] = useState("tous"); const [viewing, setViewing] = useState(null); const [error, setError] = useState(null); const load = useCallback(() => { setError(null); fetchJson<{ items: SavedItem[] }>("/api/library") .then((d) => setItems(d.items)) .catch((e) => { setItems([]); setError(e instanceof Error ? e.message : "Erreur de chargement."); }); }, []); useEffect(() => { load(); }, [load]); async function remove(id: number) { if (!confirm("Supprimer définitivement cet élément ?")) return; setError(null); try { const res = await fetch("/api/library", { method: "DELETE", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id }), }); if (!res.ok) { const d = await res.json().catch(() => ({})); throw new Error((d as { error?: string }).error ?? `Erreur ${res.status}`); } setItems((xs) => (xs ? xs.filter((x) => x.id !== id) : xs)); setViewing((v) => (v?.id === id ? null : v)); } catch (e) { setError(e instanceof Error ? e.message : "Erreur de suppression."); } } const kinds = useMemo(() => { const present = new Set((items ?? []).map((i) => i.kind)); return ["tous", ...Object.keys(KIND_LABELS).filter((k) => present.has(k as SavedItem["kind"]))]; }, [items]); const filtered = useMemo( () => (items ?? []).filter((i) => filter === "tous" || i.kind === filter), [items, filter] ); return (
{error && } {items === null ? (
) : items.length === 0 ? ( } title="Bibliothèque vide" description="Dans le chat, utilisez l'icône signet sous une réponse pour la conserver ici." /> ) : ( <> {kinds.length > 2 && ( ({ key: k, label: k === "tous" ? `Tous (${items.length})` : KIND_LABELS[k] ?? k }))} /> )} {filtered.length === 0 ? ( } title="Rien dans ce filtre" /> ) : (
{filtered.map((item) => (
{fmtDate(item.created_at)}
))}
)} )} {/* Viewer */} setViewing(null)} title={viewing?.title} wide> {viewing && (
{KIND_LABELS[viewing.kind] ?? viewing.kind} {viewing.course_code && {viewing.course_code}} {fmtDate(viewing.created_at)}
)}
); }