import { useEffect, useMemo, useState } from 'react'; import { classNames } from '../utils/misc'; import { Conversation } from '../utils/types'; import StorageUtils from '../utils/storage'; import { useNavigate, useParams } from 'react-router'; import { ArrowDownTrayIcon, EllipsisVerticalIcon, PencilIcon, TrashIcon, XMarkIcon, } from '@heroicons/react/24/outline'; import { BtnWithTooltips } from '../utils/common'; import { useAppContext } from '../utils/app.context'; import toast from 'react-hot-toast'; export default function Sidebar() { const params = useParams(); const navigate = useNavigate(); const { isGenerating } = useAppContext(); const [conversations, setConversations] = useState([]); const [currConv, setCurrConv] = useState(null); useEffect(() => { StorageUtils.getOneConversation(params.convId ?? '').then(setCurrConv); }, [params.convId]); useEffect(() => { const handleConversationChange = async () => { setConversations(await StorageUtils.getAllConversations()); }; StorageUtils.onConversationChanged(handleConversationChange); handleConversationChange(); return () => { StorageUtils.offConversationChanged(handleConversationChange); }; }, []); const groupedConv = useMemo( () => groupConversationsByDate(conversations), [conversations] ); return ( <>

Conversations

{/* close sidebar button */}
{/* new conversation button */}
navigate('/')} > + New conversation
{/* list of conversations */} {groupedConv.map((group) => (
{/* group name (by date) */} {group.title ? ( {group.title} ) : (
)} {group.conversations.map((conv) => ( { navigate(`/chat/${conv.id}`); }} onDelete={() => { if (isGenerating(conv.id)) { toast.error( 'Cannot delete conversation while generating' ); return; } if ( window.confirm( 'Are you sure to delete this conversation?' ) ) { toast.success('Conversation deleted'); StorageUtils.remove(conv.id); navigate('/'); } }} onDownload={() => { if (isGenerating(conv.id)) { toast.error( 'Cannot download conversation while generating' ); return; } const conversationJson = JSON.stringify(conv, null, 2); const blob = new Blob([conversationJson], { type: 'application/json', }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `conversation_${conv.id}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }} onRename={() => { if (isGenerating(conv.id)) { toast.error( 'Cannot rename conversation while generating' ); return; } const newName = window.prompt( 'Enter new name for the conversation', conv.name ); if (newName && newName.trim().length > 0) { StorageUtils.updateConversationName(conv.id, newName); } }} /> ))}
))}
Conversations are saved to browser's IndexedDB
); } function ConversationItem({ conv, isCurrConv, onSelect, onDelete, onDownload, onRename, }: { conv: Conversation; isCurrConv: boolean; onSelect: () => void; onDelete: () => void; onDownload: () => void; onRename: () => void; }) { return (
{conv.name}
{/* dropdown menu */}
); } // WARN: vibe code below export interface GroupedConversations { title?: string; conversations: Conversation[]; } // TODO @ngxson : add test for this function // Group conversations by date // - "Previous 7 Days" // - "Previous 30 Days" // - "Month Year" (e.g., "April 2023") export function groupConversationsByDate( conversations: Conversation[] ): GroupedConversations[] { const now = new Date(); const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); // Start of today const sevenDaysAgo = new Date(today); sevenDaysAgo.setDate(today.getDate() - 7); const thirtyDaysAgo = new Date(today); thirtyDaysAgo.setDate(today.getDate() - 30); const groups: { [key: string]: Conversation[] } = { Today: [], 'Previous 7 Days': [], 'Previous 30 Days': [], }; const monthlyGroups: { [key: string]: Conversation[] } = {}; // Key format: "Month Year" e.g., "April 2023" // Sort conversations by lastModified date in descending order (newest first) // This helps when adding to groups, but the final output order of groups is fixed. const sortedConversations = [...conversations].sort( (a, b) => b.lastModified - a.lastModified ); for (const conv of sortedConversations) { const convDate = new Date(conv.lastModified); if (convDate >= today) { groups['Today'].push(conv); } else if (convDate >= sevenDaysAgo) { groups['Previous 7 Days'].push(conv); } else if (convDate >= thirtyDaysAgo) { groups['Previous 30 Days'].push(conv); } else { const monthName = convDate.toLocaleString('default', { month: 'long' }); const year = convDate.getFullYear(); const monthYearKey = `${monthName} ${year}`; if (!monthlyGroups[monthYearKey]) { monthlyGroups[monthYearKey] = []; } monthlyGroups[monthYearKey].push(conv); } } const result: GroupedConversations[] = []; if (groups['Today'].length > 0) { result.push({ title: undefined, // no title for Today conversations: groups['Today'], }); } if (groups['Previous 7 Days'].length > 0) { result.push({ title: 'Previous 7 Days', conversations: groups['Previous 7 Days'], }); } if (groups['Previous 30 Days'].length > 0) { result.push({ title: 'Previous 30 Days', conversations: groups['Previous 30 Days'], }); } // Sort monthly groups by date (most recent month first) const sortedMonthKeys = Object.keys(monthlyGroups).sort((a, b) => { const dateA = new Date(a); // "Month Year" can be parsed by Date constructor const dateB = new Date(b); return dateB.getTime() - dateA.getTime(); }); for (const monthKey of sortedMonthKeys) { if (monthlyGroups[monthKey].length > 0) { result.push({ title: monthKey, conversations: monthlyGroups[monthKey] }); } } return result; }