spb/chat-spboucher Public
Private universal chat interface over the OpenRouter ecosystem — 400+ models, branching, streaming, usage tracking. Next.js 16 + SQLite, PWA, deployed on m4m64a at chat.spboucher.ai
TypeScript 78.8%
CSS 15.1%
JavaScript 4.9%
Shell 1.2%
1// Author: Simon-Pierre Boucher2// Contact: contact@spboucher.ai3// Project: chat.spboucher.ai45"use client";67import { useRouter } from "next/navigation";8import type { ApiConversation } from "./types";910interface SidebarProps {11 conversations: ApiConversation[];12 activeId: string | null;13 open: boolean;14 onSelect: (id: string) => void;15 onNew: () => void;16 onDelete: (id: string) => void;17 onClose: () => void;18}1920export function Sidebar({ conversations, activeId, open, onSelect, onNew, onDelete, onClose }: SidebarProps) {21 const router = useRouter();2223 async function logout() {24 await fetch("/api/auth/logout", { method: "POST" });25 router.replace("/login");26 router.refresh();27 }2829 return (30 <>31 <div className={`drawer-scrim${open ? " open" : ""}`} onClick={onClose} aria-hidden />32 <aside className={`sidebar${open ? " open" : ""}`}>33 <div className="sidebar-header">34 <span className="wordmark">35 chat.spboucher<span className="tld">.ai</span>36 </span>37 </div>38 <button className="new-chat-btn" onClick={onNew}>39 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.6">40 <path d="M8 3v10M3 8h10" strokeLinecap="round" />41 </svg>42 New conversation43 </button>44 <nav className="conv-list">45 {conversations.map((c) => (46 <div47 key={c.id}48 className={`conv-item${c.id === activeId ? " active" : ""}`}49 role="button"50 tabIndex={0}51 onClick={() => onSelect(c.id)}52 onKeyDown={(e) => e.key === "Enter" && onSelect(c.id)}53 >54 <span className="conv-title">{c.title}</span>55 <button56 className="conv-del"57 aria-label={`Delete "${c.title}"`}58 onClick={(e) => {59 e.stopPropagation();60 if (confirm(`Delete "${c.title}"? This cannot be undone.`)) onDelete(c.id);61 }}62 >63 <svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.4">64 <path d="M2 3.5h10M5.5 3.5V2h3v1.5M3.5 3.5l.7 8a1 1 0 0 0 1 .9h3.6a1 1 0 0 0 1-.9l.7-8" strokeLinecap="round" />65 </svg>66 </button>67 </div>68 ))}69 {conversations.length === 0 && (70 <p style={{ padding: "10px", fontSize: 13, color: "var(--text-dim)" }}>71 No conversations yet.72 </p>73 )}74 </nav>75 <div className="sidebar-footer">76 <a className="foot-btn" href="/settings" style={{ textDecoration: "none" }}>77 <svg width="15" height="15" viewBox="0 0 15 15" fill="none" stroke="currentColor" strokeWidth="1.4">78 <circle cx="7.5" cy="7.5" r="2.2" />79 <path d="M7.5 1.5v2M7.5 11.5v2M1.5 7.5h2M11.5 7.5h2M3.3 3.3l1.4 1.4M10.3 10.3l1.4 1.4M11.7 3.3l-1.4 1.4M4.7 10.3l-1.4 1.4" strokeLinecap="round" />80 </svg>81 Settings82 </a>83 <button className="foot-btn" onClick={logout}>84 <svg width="15" height="15" viewBox="0 0 15 15" fill="none" stroke="currentColor" strokeWidth="1.4">85 <path d="M9.5 2H4a1 1 0 0 0-1 1v9a1 1 0 0 0 1 1h5.5M12.5 7.5H6.5M10.5 5l2.5 2.5L10.5 10" strokeLinecap="round" />86 </svg>87 Sign out88 </button>89 </div>90 </aside>91 </>92 );93}94