1import { useMemo } from 'react';2import { useQuery } from '@tanstack/react-query';3import { BookOpen, Download, FileSpreadsheet, FileText, Image as ImageIcon, Pin, X } from 'lucide-react';4import { api, downloadFile } from '@/lib/api';5import type { UploadedFile } from '@/lib/types';6import { useChat } from '@/stores/chat';7import { fmtBytes } from '@/lib/format';89export function ArtifactsPanel({ conversationId, onClose }: { conversationId: string; onClose?: () => void }) {10 const { data: files, refetch } = useQuery({ queryKey: ['files', conversationId], queryFn: () => api<UploadedFile[]>(`/conversations/${conversationId}/files`) });11 const msgs = useChat((s) => s.messages[conversationId] || []);12 const sources = useMemo(() => {13 const seen = new Map<string, { module: string; section: string; url: string; course: string }>();14 for (const m of msgs) for (const t of m.tool_calls || []) if (t.name === 'search_course_content') {15 const list = (t.payload as { sources?: { module: string; section: string; url: string; course: string }[] }).sources || [];16 for (const s of list) seen.set(s.url || s.module + s.section, s);17 }18 return [...seen.values()];19 }, [msgs]);20 const streamingCount = msgs.filter((m) => m.streaming).length;21 void streamingCount;2223 const icon = (t: string) => (t === 'xlsx' ? FileSpreadsheet : t === 'image' ? ImageIcon : FileText);24 const pin = async (f: UploadedFile) => {25 await api(`/files/${f.file_id}/pin`, { method: 'POST', body: JSON.stringify({ pinned: !f.pinned }) });26 refetch();27 };2829 return (30 <div className="flex h-full max-h-[80dvh] md:max-h-none flex-col">31 <div className="flex items-center justify-between px-4 py-3 border-b border-neutral-line">32 <h2 className="font-semibold text-uqo-blue-dark">Fichiers et sources</h2>33 {onClose && <button onClick={onClose} className="h-11 w-11 inline-flex items-center justify-center rounded-xl hover:bg-neutral-surface" aria-label="Fermer"><X size={20} /></button>}34 </div>35 <div className="flex-1 overflow-y-auto scroll-thin p-4 space-y-5 pb-[calc(16px+var(--safe-bottom))]">36 <section>37 <h3 className="text-xs font-semibold uppercase tracking-wide text-neutral-muted">Fichiers ({files?.length || 0})</h3>38 {!files?.length && <p className="mt-2 text-sm text-neutral-muted">Les fichiers déposés et les classeurs produits apparaîtront ici (conservés 7 jours, 30 si épinglés).</p>}39 <ul className="mt-2 space-y-1.5">40 {files?.map((f) => {41 const Icon = icon(f.type);42 return (43 <li key={f.file_id} className="flex items-center gap-2 rounded-xl border border-neutral-line p-2">44 <Icon size={18} className="text-uqo-blue shrink-0" />45 <span className="min-w-0 flex-1"><span className="block text-sm truncate">{f.filename}</span><span className="block text-[11px] text-neutral-muted">{f.kind === 'upload' ? 'déposé' : 'produit'} · {fmtBytes(f.size)}</span></span>46 <button onClick={() => pin(f)} className={`h-9 w-9 inline-flex items-center justify-center rounded-lg hover:bg-neutral-surface ${f.pinned ? 'text-uqo-blue' : 'text-neutral-muted'}`} aria-label="Épingler"><Pin size={15} /></button>47 <button onClick={() => downloadFile(f.file_id, f.filename)} className="h-9 w-9 inline-flex items-center justify-center rounded-lg hover:bg-neutral-surface text-neutral-muted" aria-label="Télécharger"><Download size={15} /></button>48 </li>49 );50 })}51 </ul>52 </section>53 <section>54 <h3 className="text-xs font-semibold uppercase tracking-wide text-neutral-muted">Sources du cours citées ({sources.length})</h3>55 <ul className="mt-2 space-y-1.5">56 {sources.map((s) => (57 <li key={s.url + s.section}>58 <a href={s.url || '#'} target="_blank" rel="noopener noreferrer" className="flex gap-2 rounded-xl border border-neutral-line p-2 hover:bg-neutral-surface">59 <BookOpen size={16} className="text-uqo-blue shrink-0 mt-0.5" />60 <span className="min-w-0"><span className="block text-sm truncate">{s.module}</span><span className="block text-[11px] text-neutral-muted truncate">{s.course} · {s.section}</span></span>61 </a>62 </li>63 ))}64 </ul>65 </section>66 </div>67 </div>68 );69}70