Mercurial
diff love/epi/src/components/Sidebar/Sidebar.tsx @ 38:cf9caa4abc3e
[Love] FE and BE. Can chat and render images. Also created MCP for powerpoint generations.
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Mon, 01 Dec 2025 20:35:56 -0800 |
| parents | |
| children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/love/epi/src/components/Sidebar/Sidebar.tsx Mon Dec 01 20:35:56 2025 -0800 @@ -0,0 +1,176 @@ +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> + ); +}