Mercurial
view love/epi/src/components/Sidebar/Sidebar.tsx @ 71:75de5903355c
Giagantic changes that update Dowa library to be more align with stb style array and hashmap. Updated Seobeo to be caching on server side instead of file level caching. Deleted bunch of things I don't really use.
| author | June Park <parkjune1995@gmail.com> |
|---|---|
| date | Sun, 28 Dec 2025 20:34:22 -0800 |
| 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> ); }