Mercurial
view love/epi/src/components/Sidebar/Sidebar.tsx @ 279:b3b547563ec7
Add Google connector service and agent wiki
Implement the C/Seobeo Google Drive and Gmail connector with encrypted OAuth storage, Zenbu authentication, browser testing, AI tool discovery, chunked HTTP decoding, and Bazel coverage. Consolidate repository guidance into progressive wiki documentation and enforce arena-first allocation for new first-party C code.
Co-authored-by: Copilot <[email protected]>
Copilot-Session: 84c338fd-0939-4bb3-b7f3-1062eb213e5d
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Mon, 17 Aug 2026 22:22:36 -0700 |
| parents | cf9caa4abc3e |
| children |
line wrap: on
line source
import { useAtom } from 'jotai'; import { chatsAtom, currentChatId } from '@/atoms/chatAtoms'; import type { Chat } from '@/atoms/chatAtoms'; import { useEffect, useState } from 'react'; import { Loader2, MessageSquare, Plus, ChevronDown } from 'lucide-react'; import { apiUrl } from '@/utils'; import { useNavigate } from '@tanstack/react-router'; export function Sidebar() { const [chats, setChats] = useAtom(chatsAtom); const [loading, setLoading] = useState(true); const [loadingMore, setLoadingMore] = useState(false); const [error, setError] = useState<string | null>(null); const [cursor, setCursor] = useState<string | null>(null); const [hasMore, setHasMore] = useState(true); const [currentChatIdValue, setCurrentChatId] = useAtom(currentChatId); const navigate = useNavigate(); const LIMIT = 12; const fetchChats = async (currentCursor: string | null = null, append: boolean = false) => { try { if (append) { setLoadingMore(true); } else { setLoading(true); } setError(null); const url = currentCursor ? apiUrl(`/chats?limit=${LIMIT}&cursor=${currentCursor}`) : apiUrl(`/chats?limit=${LIMIT}`); const res = await fetch(url); if (!res.ok) { throw new Error('Failed to fetch chats'); } const data: Chat[] = (await res.json()).chats; if (data.length < LIMIT) { setHasMore(false); } if (data.length > 0) { setCursor(data[data.length - 1].id); } setChats((prev) => ({ ...prev, ...data.reduce((acc, chat) => { acc[chat.id] = chat; return acc; }, {} as Record<string, Chat>), })); } catch (err) { setError(err instanceof Error ? err.message : 'Something went wrong'); console.error('Error fetching chats:', err); } finally { setLoading(false); setLoadingMore(false); } }; useEffect(() => { fetchChats(); }, []); const loadMore = () => { if (!loadingMore && hasMore && cursor) { fetchChats(cursor, true); } }; const chatList = Object.values(chats); if (loading) { return ( <div className="w-[35%] h-full border-r border-gray-800 bg-gray-900 flex items-center justify-center"> <Loader2 className="w-6 h-6 animate-spin text-gray-400" /> </div> ); } if (error) { return ( <div className="w-[35%] h-full border-r border-gray-800 bg-gray-900 flex flex-col items-center justify-center p-4"> <p className="text-red-400 text-sm text-center">Error: {error}</p> <button onClick={() => window.location.reload()} className="mt-3 px-4 py-2 text-xs font-medium text-white bg-gray-800 hover:bg-gray-700 rounded-md transition-colors" > Retry </button> </div> ); } return ( <div className="w-[35%] h-full flex flex-col border-r border-gray-800 bg-gray-900 text-gray-100 h-screen"> {/* HEADER: Static height, does not scroll */} <div className="p-4 border-b border-gray-800 flex justify-between items-center shrink-0"> <h2 className="text-lg font-semibold text-white tracking-tight">Chats</h2> <button onClick={() => window.location.replace('/')} className="p-1.5 hover:bg-gray-800 rounded-md text-gray-400 hover:text-white transition-colors" > <Plus className="w-5 h-5" /> </button> </div> <div className="flex-1 overflow-y-scroll"> {chatList.length === 0 ? ( <div className="flex flex-col items-center justify-center h-full text-gray-500"> <MessageSquare className="w-10 h-10 mb-3 opacity-20" /> <p className="text-sm font-medium">No chats yet</p> </div> ) : ( <ul className="flex flex-col p-2 gap-1"> {chatList.map((chat) => { const isActive = currentChatIdValue === chat.id; return ( <li key={chat.id}> <button onClick={() => { navigate( { to: '/chat/$chatId', params: { chatId: chat.id } }, ); setCurrentChatId(chat.id); }} className={`w-full text-left px-3 py-3 rounded-lg transition-all h-[8%] duration-200 ease-in-out group ${ isActive ? 'bg-gray-800 text-white' : 'text-gray-400 hover:bg-gray-800/50 hover:text-gray-200' }`} > <div className="flex justify-between items-center"> <div className="flex-1 min-w-0 pr-2"> <h3 className={`text-sm font-medium truncate ${isActive ? 'text-white' : 'text-gray-300 group-hover:text-white'}`}> {chat.title || 'Untitled Chat'} </h3> </div> </div> </button> </li> ); })} {hasMore && ( <li className="pt-2 pb-4 px-2"> <button onClick={loadMore} disabled={loadingMore} className="w-full flex items-center justify-center gap-2 py-2 text-xs font-medium text-gray-500 hover:text-gray-300 transition-colors border border-dashed border-gray-700 rounded-md hover:border-gray-600 disabled:opacity-50 disabled:cursor-not-allowed" > {loadingMore ? ( <> <Loader2 className="w-3 h-3 animate-spin" /> Loading... </> ) : ( <> Load More <ChevronDown className="w-3 h-3" /> </> )} </button> </li> )} </ul> )} </div> </div> ); }