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>
  );
}