Mercurial
view love/epi/src/components/Sidebar/Sidebar.tsx @ 41:d2bb317e01db
[Experiment] Calling seobeo in a python server and see.
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Mon, 01 Dec 2025 20:58:04 -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> ); }