SPB Git forge

spb/immbot-ai

Public
1commits 1branches 0releases
1.5 MBsize
maindefault branch
20 days agolast push
TypeScript 98.3% CSS 0.9% Shell 0.7%
4.6 KB · 105 lines tsx
Raw Blame History
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