"use client"; import Link from "next/link"; import { usePathname } from "next/navigation"; import { useChatStore } from "@/lib/chatStore"; import { bucketByDate, relativeTime, useTick, useIsMobile } from "@/lib/hooks"; import clsx from "clsx"; import { useEffect, useMemo, useRef, useState } from "react"; export function Sidebar() { const pathname = usePathname(); const sidebarOpen = useChatStore((s) => s.sidebarOpen); const list = useChatStore((s) => s.list()); const activeId = useChatStore((s) => s.activeId); const newConversation = useChatStore((s) => s.newConversation); const openConversation = useChatStore((s) => s.openConversation); const deleteConversation = useChatStore((s) => s.deleteConversation); const renameConversation = useChatStore((s) => s.renameConversation); const setSidebarOpen = useChatStore((s) => s.setSidebarOpen); const isMobile = useIsMobile(); useTick(30_000); // refresh "x ago" strings const [filter, setFilter] = useState(""); const [editingId, setEditingId] = useState(null); // Filter + group const groups = useMemo(() => { const f = filter.trim().toLowerCase(); const filtered = f ? list.filter((c) => c.title.toLowerCase().includes(f)) : list; return bucketByDate(filtered); }, [list, filter]); // Default sidebar to closed on mobile (only on first mount) const didInitMobile = useRef(false); useEffect(() => { if (didInitMobile.current) return; if (isMobile) setSidebarOpen(false); didInitMobile.current = true; }, [isMobile, setSidebarOpen]); return ( <> {/* Mobile scrim */} {sidebarOpen && ( {/* Sections */}
{/* Search + history */}
setFilter(e.target.value)} placeholder="Search transcripts…" className="w-full bg-glass text-ink text-caption rounded-md pl-8 pr-3 py-2 outline-none placeholder:text-ink-30 transition-shadow" style={{ boxShadow: "0 0 0 1px rgba(255,255,255,0.06)" }} />
{groups.length === 0 ? (
{filter ? "No matches." : "No conversations yet."}
) : ( groups.map((g) => (
{g.label}
    {g.items.map((c) => { const active = activeId === c.id && pathname === "/"; const editing = editingId === c.id; return (
  • {editing ? ( { if (next.trim()) renameConversation(c.id, next.trim()); setEditingId(null); }} onCancel={() => setEditingId(null)} /> ) : ( )} {!editing && (
    { e.stopPropagation(); setEditingId(c.id); }} > { e.stopPropagation(); if (window.confirm(`Delete "${c.title}"?`)) { deleteConversation(c.id); } }} >
    )}
  • ); })}
)) )}
{/* Footer */}
HF Space · backend
); } function NavLink({ href, active, label, icon, }: { href: string; active: boolean; label: string; icon: React.ReactNode; }) { return ( {icon} {label} ); } function RenameInput({ initial, onCommit, onCancel, }: { initial: string; onCommit: (next: string) => void; onCancel: () => void; }) { const [v, setV] = useState(initial); const ref = useRef(null); useEffect(() => { ref.current?.focus(); ref.current?.select(); }, []); return ( setV(e.target.value)} onBlur={() => onCommit(v)} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); onCommit(v); } else if (e.key === "Escape") { e.preventDefault(); onCancel(); } }} className="w-full bg-glass-stronger text-ink text-caption rounded-md px-3 py-2 outline-none" style={{ boxShadow: "0 0 0 1.5px rgba(255,181,69,0.65)" }} /> ); } function IconBtn({ children, onClick, label, danger, }: { children: React.ReactNode; onClick: (e: React.MouseEvent) => void; label: string; danger?: boolean; }) { return ( ); } function IconSpark() { return ( ); } function IconDoc() { return ( ); } function IconPulse() { return ( ); }