"use client"; import Link from "next/link"; import { usePathname, useRouter } from "next/navigation"; import useSWR, { mutate } from "swr"; import { useMemo, useState } from "react"; import { Plus, Search, FolderKanban, Brain, BarChart3, Settings, FlaskConical, Columns2, Telescope, Pin, MoreHorizontal, Archive, Trash2, Pencil, FolderInput, PanelLeftClose, LogOut, ChevronDown, ChevronRight, Images } from "lucide-react"; import { fetcher, api } from "@/lib/client/fetcher"; import type { ConversationDTO, ProjectDTO } from "@/lib/client/types"; import { cn, Kbd, toast } from "./ui"; import { Logo } from "./logo"; import { setPrefs } from "@/lib/client/prefs"; import { resetDraft } from "@/lib/client/use-chat"; const NAV = [ { href: "/compare", label: "Comparer", icon: Columns2 }, { href: "/research", label: "Recherche approfondie", icon: Telescope }, { href: "/models", label: "Laboratoire de modèles", icon: FlaskConical }, { href: "/memory", label: "Mémoire", icon: Brain }, { href: "/images", label: "Images", icon: Images }, { href: "/usage", label: "Coûts & usage", icon: BarChart3 }, { href: "/settings", label: "Réglages", icon: Settings }, ]; function group(convs: ConversationDTO[]) { const now = new Date(); const d0 = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime(); const g: Record = { "Épinglées": [], "Aujourd'hui": [], "Hier": [], "7 derniers jours": [], "30 derniers jours": [], "Plus ancien": [] }; for (const c of convs) { if (c.pinned) { g["Épinglées"].push(c); continue; } const t = new Date(c.last_message_at ?? c.created_at).getTime(); if (t >= d0) g["Aujourd'hui"].push(c); else if (t >= d0 - 86_400_000) g["Hier"].push(c); else if (t >= d0 - 7 * 86_400_000) g["7 derniers jours"].push(c); else if (t >= d0 - 30 * 86_400_000) g["30 derniers jours"].push(c); else g["Plus ancien"].push(c); } return Object.entries(g).filter(([, v]) => v.length); } export function Sidebar({ onOpenPalette, onNavigate }: { onOpenPalette: () => void; onNavigate?: () => void }) { const pathname = usePathname(); const router = useRouter(); const { data: convData } = useSWR<{ conversations: ConversationDTO[] }>("/api/conversations", fetcher, { refreshInterval: 30_000 }); const { data: projData } = useSWR<{ projects: ProjectDTO[] }>("/api/projects", fetcher); const [projectsOpen, setProjectsOpen] = useState(true); const convs = useMemo(() => (convData?.conversations ?? []).filter(c => !c.project_id || true), [convData]); const groups = useMemo(() => group(convs), [convs]); const current = pathname.startsWith("/c/") ? pathname.slice(3) : null; async function newChat() { resetDraft(); router.push("/"); onNavigate?.(); } async function logout() { await api("/api/auth/logout", { method: "POST" }); navigator.serviceWorker?.controller?.postMessage("logout"); location.href = "/login"; } return (
{ resetDraft(); onNavigate?.(); }} className="flex items-center gap-2 rounded-lg px-1 py-1 hover:bg-surface-2">SPBoucher AI
{/* projects */}
{projectsOpen &&
{(projData?.projects ?? []).map(p => {p.name}{p.conversation_count ?? ""})} {!projData?.projects?.length &&
Aucun projet. Créer
}
} {/* history */} {groups.map(([label, items]) => (
{label}
{items.map(c => )}
))} {!convs.length && convData &&
Aucune conversation pour l'instant.
}
Réglages
); } function ConvItem({ c, active, onNavigate, projects }: { c: ConversationDTO; active: boolean; onNavigate?: () => void; projects: ProjectDTO[] }) { const [menu, setMenu] = useState(false); const [renaming, setRenaming] = useState(false); const [title, setTitle] = useState(c.title); const router = useRouter(); async function patch(p: Record) { await api(`/api/conversations/${c.id}`, { method: "PATCH", json: p }); void mutate("/api/conversations"); } async function del() { if (!confirm(`Supprimer « ${c.title} » ?`)) return; await api(`/api/conversations/${c.id}`, { method: "DELETE" }); void mutate("/api/conversations"); if (active) router.push("/"); toast("Conversation supprimée", "ok"); } return (
{renaming ?
{ e.preventDefault(); await patch({ title }); setRenaming(false); }}> setTitle(e.target.value)} onBlur={() => setRenaming(false)} className="input !py-0.5 !px-1.5 text-[13px]" />
: {c.pinned && }{c.mode === "research" && }{c.title}} {menu && <>
setMenu(false)} />
setMenu(false)}> setRenaming(true)}>Renommer patch({ pinned: !c.pinned })}>{c.pinned ? "Désépingler" : "Épingler"}
Déplacer vers
patch({ projectId: null })}>Aucun projet {projects.map(p => patch({ projectId: p.id })}>{p.name})}
patch({ archived: true })}>Archiver Supprimer
}
); } function MenuItem({ icon: Icon, children, onClick, danger }: { icon: React.ComponentType<{ className?: string }>; children: React.ReactNode; onClick: () => void; danger?: boolean }) { return ; }