import { motion, type Variants } from 'framer-motion'; import { useCallback, useEffect, useRef, useState } from 'react'; import { toast } from 'react-toastify'; import { Dialog, DialogButton, DialogDescription, DialogRoot, DialogTitle } from '~/components/ui/Dialog'; import { ThemeSwitch } from '~/components/ui/ThemeSwitch'; import { ControlPanel } from '~/components/@settings/core/ControlPanel'; import { SettingsButton } from '~/components/ui/SettingsButton'; import { Button } from '~/components/ui/Button'; import { db, deleteById, getAll, chatId, type ChatHistoryItem, useChatHistory } from '~/lib/persistence'; import { cubicEasingFn } from '~/utils/easings'; import { HistoryItem } from './HistoryItem'; import { binDates } from './date-binning'; import { useSearchFilter } from '~/lib/hooks/useSearchFilter'; import { classNames } from '~/utils/classNames'; import { useStore } from '@nanostores/react'; import { profileStore } from '~/lib/stores/profile'; const menuVariants = { closed: { opacity: 0, visibility: 'hidden', left: '-340px', transition: { duration: 0.2, ease: cubicEasingFn, }, }, open: { opacity: 1, visibility: 'initial', left: 0, transition: { duration: 0.2, ease: cubicEasingFn, }, }, } satisfies Variants; type DialogContent = | { type: 'delete'; item: ChatHistoryItem } | { type: 'bulkDelete'; items: ChatHistoryItem[] } | null; function CurrentDateTime() { const [dateTime, setDateTime] = useState(new Date()); useEffect(() => { const timer = setInterval(() => { setDateTime(new Date()); }, 60000); return () => clearInterval(timer); }, []); return (
{dateTime.toLocaleDateString()} {dateTime.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
); } export const Menu = () => { const { duplicateCurrentChat, exportChat } = useChatHistory(); const menuRef = useRef(null); const [list, setList] = useState([]); const [open, setOpen] = useState(false); const [dialogContent, setDialogContent] = useState(null); const [isSettingsOpen, setIsSettingsOpen] = useState(false); const profile = useStore(profileStore); const [selectionMode, setSelectionMode] = useState(false); const [selectedItems, setSelectedItems] = useState([]); const { filteredItems: filteredList, handleSearchChange } = useSearchFilter({ items: list, searchFields: ['description'], }); const loadEntries = useCallback(() => { if (db) { getAll(db) .then((list) => list.filter((item) => item.urlId && item.description)) .then(setList) .catch((error) => toast.error(error.message)); } }, []); const deleteChat = useCallback( async (id: string): Promise => { if (!db) { throw new Error('Database not available'); } // Delete chat snapshot from localStorage try { const snapshotKey = `snapshot:${id}`; localStorage.removeItem(snapshotKey); console.log('Removed snapshot for chat:', id); } catch (snapshotError) { console.error(`Error deleting snapshot for chat ${id}:`, snapshotError); } // Delete the chat from the database await deleteById(db, id); console.log('Successfully deleted chat:', id); }, [db], ); const deleteItem = useCallback( (event: React.UIEvent, item: ChatHistoryItem) => { event.preventDefault(); event.stopPropagation(); // Log the delete operation to help debugging console.log('Attempting to delete chat:', { id: item.id, description: item.description }); deleteChat(item.id) .then(() => { toast.success('Chat deleted successfully', { position: 'bottom-right', autoClose: 3000, }); // Always refresh the list loadEntries(); if (chatId.get() === item.id) { // hard page navigation to clear the stores console.log('Navigating away from deleted chat'); window.location.pathname = '/'; } }) .catch((error) => { console.error('Failed to delete chat:', error); toast.error('Failed to delete conversation', { position: 'bottom-right', autoClose: 3000, }); // Still try to reload entries in case data has changed loadEntries(); }); }, [loadEntries, deleteChat], ); const deleteSelectedItems = useCallback( async (itemsToDeleteIds: string[]) => { if (!db || itemsToDeleteIds.length === 0) { console.log('Bulk delete skipped: No DB or no items to delete.'); return; } console.log(`Starting bulk delete for ${itemsToDeleteIds.length} chats`, itemsToDeleteIds); let deletedCount = 0; const errors: string[] = []; const currentChatId = chatId.get(); let shouldNavigate = false; // Process deletions sequentially using the shared deleteChat logic for (const id of itemsToDeleteIds) { try { await deleteChat(id); deletedCount++; if (id === currentChatId) { shouldNavigate = true; } } catch (error) { console.error(`Error deleting chat ${id}:`, error); errors.push(id); } } // Show appropriate toast message if (errors.length === 0) { toast.success(`${deletedCount} chat${deletedCount === 1 ? '' : 's'} deleted successfully`); } else { toast.warning(`Deleted ${deletedCount} of ${itemsToDeleteIds.length} chats. ${errors.length} failed.`, { autoClose: 5000, }); } // Reload the list after all deletions await loadEntries(); // Clear selection state setSelectedItems([]); setSelectionMode(false); // Navigate if needed if (shouldNavigate) { console.log('Navigating away from deleted chat'); window.location.pathname = '/'; } }, [deleteChat, loadEntries, db], ); const closeDialog = () => { setDialogContent(null); }; const toggleSelectionMode = () => { setSelectionMode(!selectionMode); if (selectionMode) { // If turning selection mode OFF, clear selection setSelectedItems([]); } }; const toggleItemSelection = useCallback((id: string) => { setSelectedItems((prev) => { const newSelectedItems = prev.includes(id) ? prev.filter((itemId) => itemId !== id) : [...prev, id]; console.log('Selected items updated:', newSelectedItems); return newSelectedItems; // Return the new array }); }, []); // No dependencies needed const handleBulkDeleteClick = useCallback(() => { if (selectedItems.length === 0) { toast.info('Select at least one chat to delete'); return; } const selectedChats = list.filter((item) => selectedItems.includes(item.id)); if (selectedChats.length === 0) { toast.error('Could not find selected chats'); return; } setDialogContent({ type: 'bulkDelete', items: selectedChats }); }, [selectedItems, list]); // Keep list dependency const selectAll = useCallback(() => { const allFilteredIds = filteredList.map((item) => item.id); setSelectedItems((prev) => { const allFilteredAreSelected = allFilteredIds.length > 0 && allFilteredIds.every((id) => prev.includes(id)); if (allFilteredAreSelected) { // Deselect only the filtered items const newSelectedItems = prev.filter((id) => !allFilteredIds.includes(id)); console.log('Deselecting all filtered items. New selection:', newSelectedItems); return newSelectedItems; } else { // Select all filtered items, adding them to any existing selections const newSelectedItems = [...new Set([...prev, ...allFilteredIds])]; console.log('Selecting all filtered items. New selection:', newSelectedItems); return newSelectedItems; } }); }, [filteredList]); // Depends only on filteredList useEffect(() => { if (open) { loadEntries(); } }, [open, loadEntries]); // Exit selection mode when sidebar is closed useEffect(() => { if (!open && selectionMode) { /* * Don't clear selection state anymore when sidebar closes * This allows the selection to persist when reopening the sidebar */ console.log('Sidebar closed, preserving selection state'); } }, [open, selectionMode]); useEffect(() => { const enterThreshold = 40; const exitThreshold = 40; function onMouseMove(event: MouseEvent) { if (isSettingsOpen) { return; } if (event.pageX < enterThreshold) { setOpen(true); } if (menuRef.current && event.clientX > menuRef.current.getBoundingClientRect().right + exitThreshold) { setOpen(false); } } window.addEventListener('mousemove', onMouseMove); return () => { window.removeEventListener('mousemove', onMouseMove); }; }, [isSettingsOpen]); const handleDuplicate = async (id: string) => { await duplicateCurrentChat(id); loadEntries(); // Reload the list after duplication }; const handleSettingsClick = () => { setIsSettingsOpen(true); setOpen(false); }; const handleSettingsClose = () => { setIsSettingsOpen(false); }; const setDialogContentWithLogging = useCallback((content: DialogContent) => { console.log('Setting dialog content:', content); setDialogContent(content); }, []); return ( <>
{profile?.username || 'Guest User'}
{profile?.avatar ? ( {profile?.username ) : (
)}
Your Chats
{selectionMode && (
)}
{filteredList.length === 0 && (
{list.length === 0 ? 'No previous conversations' : 'No matches found'}
)} {binDates(filteredList).map(({ category, items }) => (
{category}
{items.map((item) => ( { event.preventDefault(); event.stopPropagation(); console.log('Delete triggered for item:', item); setDialogContentWithLogging({ type: 'delete', item }); }} onDuplicate={() => handleDuplicate(item.id)} selectionMode={selectionMode} isSelected={selectedItems.includes(item.id)} onToggleSelection={toggleItemSelection} /> ))}
))} {dialogContent?.type === 'delete' && ( <>
Delete Chat?

You are about to delete{' '} {dialogContent.item.description}

Are you sure you want to delete this chat?

Cancel { console.log('Dialog delete button clicked for item:', dialogContent.item); deleteItem(event, dialogContent.item); closeDialog(); }} > Delete
)} {dialogContent?.type === 'bulkDelete' && ( <>
Delete Selected Chats?

You are about to delete {dialogContent.items.length}{' '} {dialogContent.items.length === 1 ? 'chat' : 'chats'}:

    {dialogContent.items.map((item) => (
  • {item.description}
  • ))}

Are you sure you want to delete these chats?

Cancel { /* * Pass the current selectedItems to the delete function. * This captures the state at the moment the user confirms. */ const itemsToDeleteNow = [...selectedItems]; console.log('Bulk delete confirmed for', itemsToDeleteNow.length, 'items', itemsToDeleteNow); deleteSelectedItems(itemsToDeleteNow); closeDialog(); }} > Delete
)}
); };