import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import type { UIEvent } from 'react'; import { useAppStore, ChatSession } from '@/store/app-store'; import { useShallow } from 'zustand/react/shallow'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Plus, MessageSquare, Archive, Trash2, MoreVertical, Search, ChevronLeft, ArchiveRestore, } from 'lucide-react'; import { cn } from '@/lib/utils'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuSeparator, } from '@/components/ui/dropdown-menu'; import { Badge } from '@/components/ui/badge'; const CHAT_SESSION_ROW_HEIGHT_PX = 84; const CHAT_SESSION_OVERSCAN_COUNT = 6; const CHAT_SESSION_LIST_PADDING_PX = 8; export function ChatHistory() { const { chatSessions, currentProject, currentChatSession, chatHistoryOpen, createChatSession, setCurrentChatSession, archiveChatSession, unarchiveChatSession, deleteChatSession, setChatHistoryOpen, } = useAppStore( useShallow((state) => ({ chatSessions: state.chatSessions, currentProject: state.currentProject, currentChatSession: state.currentChatSession, chatHistoryOpen: state.chatHistoryOpen, createChatSession: state.createChatSession, setCurrentChatSession: state.setCurrentChatSession, archiveChatSession: state.archiveChatSession, unarchiveChatSession: state.unarchiveChatSession, deleteChatSession: state.deleteChatSession, setChatHistoryOpen: state.setChatHistoryOpen, })) ); const [searchQuery, setSearchQuery] = useState(''); const [showArchived, setShowArchived] = useState(false); const listRef = useRef(null); const scrollRafRef = useRef(null); const [scrollTop, setScrollTop] = useState(0); const [viewportHeight, setViewportHeight] = useState(0); const normalizedQuery = searchQuery.trim().toLowerCase(); const currentProjectId = currentProject?.id; // Filter sessions for current project const projectSessions = useMemo(() => { if (!currentProjectId) return []; return chatSessions.filter((session) => session.projectId === currentProjectId); }, [chatSessions, currentProjectId]); // Filter by search query and archived status const filteredSessions = useMemo(() => { return projectSessions.filter((session) => { const matchesSearch = session.title.toLowerCase().includes(normalizedQuery); const matchesArchivedStatus = showArchived ? session.archived : !session.archived; return matchesSearch && matchesArchivedStatus; }); }, [projectSessions, normalizedQuery, showArchived]); // Sort by most recently updated const sortedSessions = useMemo(() => { return [...filteredSessions].sort( (a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime() ); }, [filteredSessions]); const totalHeight = sortedSessions.length * CHAT_SESSION_ROW_HEIGHT_PX + CHAT_SESSION_LIST_PADDING_PX * 2; const startIndex = Math.max( 0, Math.floor(scrollTop / CHAT_SESSION_ROW_HEIGHT_PX) - CHAT_SESSION_OVERSCAN_COUNT ); const endIndex = Math.min( sortedSessions.length, Math.ceil((scrollTop + viewportHeight) / CHAT_SESSION_ROW_HEIGHT_PX) + CHAT_SESSION_OVERSCAN_COUNT ); const offsetTop = startIndex * CHAT_SESSION_ROW_HEIGHT_PX; const visibleSessions = sortedSessions.slice(startIndex, endIndex); const handleScroll = useCallback((event: UIEvent) => { const target = event.currentTarget; if (scrollRafRef.current !== null) { cancelAnimationFrame(scrollRafRef.current); } scrollRafRef.current = requestAnimationFrame(() => { setScrollTop(target.scrollTop); scrollRafRef.current = null; }); }, []); useEffect(() => { const container = listRef.current; if (!container || typeof window === 'undefined') return; const updateHeight = () => { setViewportHeight(container.clientHeight); }; updateHeight(); if (typeof ResizeObserver === 'undefined') { window.addEventListener('resize', updateHeight); return () => window.removeEventListener('resize', updateHeight); } const observer = new ResizeObserver(() => updateHeight()); observer.observe(container); return () => observer.disconnect(); }, [chatHistoryOpen]); useEffect(() => { if (!chatHistoryOpen) return; setScrollTop(0); if (listRef.current) { listRef.current.scrollTop = 0; } }, [chatHistoryOpen, normalizedQuery, showArchived, currentProjectId]); useEffect(() => { return () => { if (scrollRafRef.current !== null) { cancelAnimationFrame(scrollRafRef.current); } }; }, []); if (!currentProjectId) { return null; } const handleCreateNewChat = () => { createChatSession(); }; const handleSelectSession = (session: ChatSession) => { setCurrentChatSession(session); }; const handleArchiveSession = (sessionId: string, e: React.MouseEvent) => { e.stopPropagation(); archiveChatSession(sessionId); }; const handleUnarchiveSession = (sessionId: string, e: React.MouseEvent) => { e.stopPropagation(); unarchiveChatSession(sessionId); }; const handleDeleteSession = (sessionId: string, e: React.MouseEvent) => { e.stopPropagation(); if (confirm('Are you sure you want to delete this chat session?')) { deleteChatSession(sessionId); } }; return (
{chatHistoryOpen && ( <> {/* Header */}

Chat History

{/* New Chat Button */}
{/* Search */}
setSearchQuery(e.target.value)} className="pl-9" />
{/* Archive Toggle */}
{/* Chat Sessions List */}
{sortedSessions.length === 0 ? (
{searchQuery ? ( <>No chats match your search ) : showArchived ? ( <>No archived chats ) : ( <>No active chats. Create your first chat to get started! )}
) : (
{visibleSessions.map((session) => (
handleSelectSession(session)} >

{session.title}

{session.messages.length} messages

{new Date(session.updatedAt).toLocaleDateString()}

{session.archived ? ( handleUnarchiveSession(session.id, e)} > Unarchive ) : ( handleArchiveSession(session.id, e)} > Archive )} handleDeleteSession(session.id, e)} className="text-destructive" > Delete
))}
)}
)}
); }