import { useState, useEffect, useMemo } from "react"; import { cn } from "@/lib/utils"; import { trpc } from "@/lib/trpc"; import { Plus, MessageSquare, Settings, Trash2, Edit3, Check, X, ChevronLeft, ChevronRight, DollarSign, Zap, LogOut, Search, ChevronDown, } from "lucide-react"; import { useAuth } from "@/_core/hooks/useAuth"; import { getLoginUrl } from "@/const"; interface SidebarProps { currentSessionId: number | null; onSelectSession: (id: number) => void; onNewSession: () => void; onOpenSettings: () => void; collapsed: boolean; onToggleCollapse: () => void; } type DateGroup = "Today" | "Yesterday" | "This Week" | "This Month" | "Older"; function getDateGroup(dateStr: string): DateGroup { const date = new Date(dateStr); const now = new Date(); const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); const yesterday = new Date(today); yesterday.setDate(yesterday.getDate() - 1); const weekAgo = new Date(today); weekAgo.setDate(weekAgo.getDate() - 7); const monthAgo = new Date(today); monthAgo.setMonth(monthAgo.getMonth() - 1); if (date >= today) return "Today"; if (date >= yesterday) return "Yesterday"; if (date >= weekAgo) return "This Week"; if (date >= monthAgo) return "This Month"; return "Older"; } const GROUP_ORDER: DateGroup[] = ["Today", "Yesterday", "This Week", "This Month", "Older"]; export function Sidebar({ currentSessionId, onSelectSession, onNewSession, onOpenSettings, collapsed, onToggleCollapse, }: SidebarProps) { const { user } = useAuth(); const utils = trpc.useUtils(); const { data: sessions, isLoading } = trpc.sessions.list.useQuery(); const { data: costSummary } = trpc.costs.summary.useQuery(undefined, { enabled: !!user, }); const deleteMutation = trpc.sessions.delete.useMutation({ onSuccess: () => utils.sessions.list.invalidate(), }); const updateMutation = trpc.sessions.update.useMutation({ onSuccess: () => utils.sessions.list.invalidate(), }); const logoutMutation = trpc.auth.logout.useMutation({ onSuccess: () => window.location.reload(), }); const [editingId, setEditingId] = useState(null); const [editTitle, setEditTitle] = useState(""); const [searchQuery, setSearchQuery] = useState(""); const [collapsedGroups, setCollapsedGroups] = useState>(new Set()); // Filter sessions by search const filteredSessions = useMemo(() => { if (!sessions) return []; if (!searchQuery.trim()) return sessions; const q = searchQuery.toLowerCase(); return sessions.filter( (s) => s.title.toLowerCase().includes(q) || (s.model && s.model.toLowerCase().includes(q)) ); }, [sessions, searchQuery]); // Group sessions by date const groupedSessions = useMemo(() => { const groups = new Map(); for (const session of filteredSessions) { const group = getDateGroup(session.updatedAt || session.createdAt); if (!groups.has(group)) groups.set(group, []); groups.get(group)!.push(session); } return groups; }, [filteredSessions]); const toggleGroup = (group: DateGroup) => { setCollapsedGroups((prev) => { const next = new Set(prev); if (next.has(group)) next.delete(group); else next.add(group); return next; }); }; // Listen for title updates from SSE useEffect(() => { const handler = () => { utils.sessions.list.invalidate(); }; window.addEventListener("session-title-update", handler); return () => window.removeEventListener("session-title-update", handler); }, [utils]); const startEdit = (id: number, title: string) => { setEditingId(id); setEditTitle(title); }; const saveEdit = () => { if (editingId && editTitle.trim()) { updateMutation.mutate({ id: editingId, title: editTitle.trim() }); } setEditingId(null); }; if (collapsed) { return (
); } const renderSession = (session: (typeof filteredSessions)[0]) => (
onSelectSession(session.id)} > {editingId === session.id ? (
setEditTitle(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") saveEdit(); if (e.key === "Escape") setEditingId(null); }} className="flex-1 bg-background text-xs rounded px-1.5 py-0.5 outline-none border border-border" autoFocus onClick={(e) => e.stopPropagation()} />
) : ( <> {session.title}
)}
); return (
{/* Header */}
Claw
{/* Search */}
setSearchQuery(e.target.value)} placeholder="Search sessions..." className="w-full bg-sidebar-accent/50 border border-sidebar-border rounded-md pl-7 pr-2 py-1.5 text-xs outline-none focus:border-primary placeholder:text-muted-foreground/50" />
{/* Sessions list — grouped by date */}
{isLoading && (
Loading sessions...
)} {filteredSessions.length === 0 && !isLoading && (
{searchQuery ? "No matching sessions" : "No sessions yet"}
)} {GROUP_ORDER.map((group) => { const groupSessions = groupedSessions.get(group); if (!groupSessions || groupSessions.length === 0) return null; const isCollapsed = collapsedGroups.has(group); return (
{!isCollapsed && (
{groupSessions.map(renderSession)}
)}
); })}
{/* Cost widget */} {costSummary && (
Total Cost
${Number(costSummary.totalCost || 0).toFixed(4)}
{Number(costSummary.totalPromptTokens || 0).toLocaleString()} +{" "} {Number(costSummary.totalCompletionTokens || 0).toLocaleString()}{" "} tokens
)} {/* Bottom section */}
{user ? (
{user.name?.[0]?.toUpperCase() || "U"}
{user.name || "User"}
) : ( Sign in )}
); }