Python 64.6%
TypeScript 33.7%
CSS 0.8%
1import { useMemo } from 'react';2import { useQuery } from '@tanstack/react-query';3import { BookOpen, Download, FileSpreadsheet, FileText, Image as ImageIcon, Pin, Wand2, X } from 'lucide-react';4import { useUI } from '@/stores/ui';5import { api, downloadFile } from '@/lib/api';6import type { UploadedFile } from '@/lib/types';7import { useChat } from '@/stores/chat';8import { fmtBytes } from '@/lib/format';910export function ArtifactsPanel({ conversationId, onClose }: { conversationId: string; onClose?: () => void }) {11 const { data: files, refetch } = useQuery({ queryKey: ['files', conversationId], queryFn: () => api<UploadedFile[]>(`/conversations/${conversationId}/files`) });12 const msgs = useChat((s) => s.messages[conversationId] || []);13 const sources = useMemo(() => {14 const seen = new Map<string, { module: string; section: string; url: string; course: string }>();15 for (const m of msgs) for (const t of m.tool_calls || []) if (t.name === 'search_course_content') {16 const list = (t.payload as { sources?: { module: string; section: string; url: string; course: string }[] }).sources || [];17 for (const s of list) seen.set(s.url || s.module + s.section, s);18 }19 return [...seen.values()];20 }, [msgs]);21 const setDraft = useUI((s) => s.setDraft);22 const setPanel = useUI((s) => s.setPanel);2324 const icon = (t: string) => (t === 'xlsx' ? FileSpreadsheet : t === 'image' ? ImageIcon : FileText);25 const pin = async (f: UploadedFile) => {26 await api(`/files/${f.file_id}/pin`, { method: 'POST', body: JSON.stringify({ pinned: !f.pinned }) });27 refetch();28 };2930 return (31 <div className="flex h-full max-h-[80dvh] md:max-h-none flex-col">32 <div className="flex items-center justify-between px-4 py-3 border-b border-neutral-line">33 <h2 className="font-semibold text-uqo-blue-dark">Fichiers et sources</h2>34 {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>}35 </div>36 <div className="flex-1 overflow-y-auto scroll-thin p-4 space-y-5 pb-[calc(16px+var(--safe-bottom))]">37 <section>38 <h3 className="text-xs font-semibold uppercase tracking-wide text-neutral-muted">Fichiers ({files?.length || 0})</h3>39 {!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>}40 <ul className="mt-2 space-y-1.5">41 {files?.map((f) => {42 const Icon = icon(f.type);43 return (44 <li key={f.file_id} className="flex items-center gap-2 rounded-xl border border-neutral-line p-2">45 <Icon size={18} className="text-uqo-blue shrink-0" />46 <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>47 {(f.type === 'xlsx' || f.type === 'csv') && (48 <button onClick={() => { setDraft(`Modifie le fichier « ${f.filename} » : `); if (window.innerWidth < 768) setPanel(false); onClose?.(); }} className="h-9 w-9 inline-flex items-center justify-center rounded-lg hover:bg-neutral-surface text-uqo-blue" aria-label="Modifier avec le tuteur" title="Modifier avec le tuteur"><Wand2 size={15} /></button>49 )}50 <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>51 <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>52 </li>53 );54 })}55 </ul>56 </section>57 <section>58 <h3 className="text-xs font-semibold uppercase tracking-wide text-neutral-muted">Sources du cours citées ({sources.length})</h3>59 <ul className="mt-2 space-y-1.5">60 {sources.map((s) => (61 <li key={s.url + s.section}>62 <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">63 <BookOpen size={16} className="text-uqo-blue shrink-0 mt-0.5" />64 <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>65 </a>66 </li>67 ))}68 </ul>69 </section>70 </div>71 </div>72 );73}74