TypeScript 98.3%
CSS 0.9%
Shell 0.7%
1"use client";2// Panneau source : extrait exact, contexte voisin, métadonnées du document.3import { useEffect, useState } from "react";4import { FileText, X } from "lucide-react";5import { Badge, Skeleton } from "@/components/ui";67export type Citation = {8 tag: string;9 index: number;10 courseCode: string | null;11 docTitle: string;12 filename: string;13 refLabel: string;14 title: string;15 excerpt: string;16 chunkId: number;17};1819type SourceDetail = {20 source: {21 ref_label: string; title: string; display_content: string; section_title: string;22 doc_title: string; filename: string; course_code: string | null; ingested_at: string | null; week: number | null;23 };24 neighbors: { id: number; ref_number: number; title: string; preview: string }[];25};2627export function CitationPanel({ citation, onClose }: { citation: Citation | null; onClose: () => void }) {28 const [detail, setDetail] = useState<SourceDetail | null>(null);29 const [loading, setLoading] = useState(false);3031 useEffect(() => {32 if (!citation) return;33 setDetail(null);34 setLoading(true);35 fetch(`/api/citations/${citation.chunkId}`)36 .then((r) => (r.ok ? r.json() : null))37 .then((d) => setDetail(d))38 .finally(() => setLoading(false));39 }, [citation]);4041 if (!citation) return null;42 return (43 <div className="fixed inset-0 z-50 flex justify-end" role="dialog" aria-modal="true" aria-label="Source de la citation">44 <div className="absolute inset-0 bg-black/35 animate-fade-in" onClick={onClose} />45 <aside className="relative w-full sm:w-[430px] h-full bg-card border-l border-app shadow-2xl overflow-y-auto animate-fade-in">46 <div className="sticky top-0 bg-card border-b border-app px-5 py-3.5 flex items-center justify-between gap-3 z-10">47 <div className="flex items-center gap-2 min-w-0">48 <span className="citation-chip shrink-0">{citation.tag}</span>49 <h2 className="font-semibold text-sm text-fg truncate">{citation.refLabel || citation.docTitle}</h2>50 </div>51 <button onClick={onClose} aria-label="Fermer" className="p-1.5 rounded-md text-muted hover:text-fg shrink-0">52 <X size={17} />53 </button>54 </div>55 <div className="p-5 space-y-4">56 <div className="flex flex-wrap gap-1.5">57 {citation.courseCode && <Badge tone="brand">{citation.courseCode}</Badge>}58 <Badge tone="neutral"><FileText size={11} /> {citation.filename}</Badge>59 {detail?.source.week != null && <Badge tone="gold">Semaine {detail.source.week}</Badge>}60 </div>61 {loading ? (62 <div className="space-y-2.5">63 <Skeleton className="h-4 w-3/4" />64 <Skeleton className="h-4 w-full" />65 <Skeleton className="h-4 w-full" />66 <Skeleton className="h-4 w-2/3" />67 </div>68 ) : detail ? (69 <>70 {detail.source.section_title && (71 <p className="text-[12px] text-muted font-medium uppercase tracking-wide">{detail.source.section_title}</p>72 )}73 <h3 className="font-semibold text-fg -mt-2">{detail.source.title}</h3>74 <div className="bg-surface-1 dark:bg-brand-950/50 border border-app rounded-xl p-4 text-sm text-fg whitespace-pre-wrap leading-relaxed max-h-96 overflow-y-auto">75 {detail.source.display_content}76 </div>77 {detail.neighbors.length > 0 && (78 <div>79 <p className="text-[12px] font-semibold text-muted uppercase tracking-wide mb-2">Diapositives voisines</p>80 <div className="space-y-2">81 {detail.neighbors.map((n) => (82 <div key={n.id} className="border border-app rounded-lg p-3">83 <p className="text-[12px] font-medium text-fg">Diapositive {n.ref_number} — {n.title}</p>84 <p className="text-[12px] text-muted mt-1 line-clamp-3 whitespace-pre-wrap">{n.preview}</p>85 </div>86 ))}87 </div>88 </div>89 )}90 {detail.source.ingested_at && (91 <p className="text-[11px] text-muted">Indexé le {new Date(detail.source.ingested_at + "Z").toLocaleDateString("fr-CA")}</p>92 )}93 </>94 ) : (95 <div className="text-sm text-muted bg-surface-1 dark:bg-brand-950/50 rounded-xl p-4">96 <p className="font-medium text-fg mb-1">Extrait cité :</p>97 <p className="whitespace-pre-wrap">{citation.excerpt}</p>98 </div>99 )}100 </div>101 </aside>102 </div>103 );104}105