import { useStore } from '@nanostores/react'; import classNames from 'classnames'; import { motion, type Variants } from 'framer-motion'; import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { toast } from 'sonner'; import { ControlPanel } from '~/components/@settings/core/ControlPanel'; import { Dialog, DialogButton, DialogDescription, DialogRoot, DialogTitle } from '~/components/ui/Dialog'; import { SettingsButton } from '~/components/ui/SettingsButton'; import { useAuth } from '~/lib/hooks'; import { type ServerChatItem, useChatEntries } from '~/lib/hooks/useChatEntries'; import { useChatOperate } from '~/lib/hooks/useChatOperate'; import { aiState } from '~/lib/stores/ai-state'; import { sidebarStore } from '~/lib/stores/sidebar'; import { cubicEasingFn } from '~/utils/easings'; import WithTooltip from '../ui/Tooltip'; import { binDates } from './date-binning'; import { HistoryItem } from './HistoryItem'; 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: ServerChatItem } | { type: 'bulkDelete'; items: ServerChatItem[] } | null; export const Menu = memo(() => { const { duplicateCurrentChat, deleteChat, deleteSelectedItems } = useChatOperate(); const { entries, isLoading, loadChatEntries } = useChatEntries(); const { chatId } = useStore(aiState); const menuRef = useRef(null); const [dialogContent, setDialogContent] = useState(null); const [isSettingsOpen, setIsSettingsOpen] = useState(false); const [selectedItems, setSelectedItems] = useState([]); const [isInitialized, setIsInitialized] = useState(false); const sidebar = useStore(sidebarStore); const [searchTerm, setSearchTerm] = useState(''); const { isAuthenticated } = useAuth(); const isShowMenu = useMemo(() => { return isAuthenticated && sidebar; }, [isAuthenticated, sidebar]); // 处理搜索 const handleSearch = useCallback( (e: React.ChangeEvent) => { const value = e.target.value; setSearchTerm(value); // 重新加载 loadChatEntries(value); }, [loadChatEntries], ); // 初始加载聊天列表,仅在组件挂载时执行一次 useEffect(() => { if (isShowMenu && !isInitialized) { loadChatEntries(); setIsInitialized(true); } }, [isShowMenu, loadChatEntries, isInitialized]); const deleteItem = useCallback( async (event: React.UIEvent, item: ServerChatItem) => { event.preventDefault(); event.stopPropagation(); console.log('Attempting to delete chat:', { id: item.id, description: item.description }); try { await deleteChat(item.id); toast.success('聊天已删除成功'); if (chatId === item.id) { console.log('Navigating away from deleted chat'); window.location.pathname = '/'; } } catch (error) { console.error('Failed to delete chat:', error); toast.error('删除聊天失败'); } finally { loadChatEntries(); } }, [loadChatEntries, deleteChat, chatId], ); const closeDialog = () => { setDialogContent(null); }; 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; }); }, []); const handleBulkDeleteClick = useCallback(() => { if (selectedItems.length === 0) { toast.info('至少选择一个聊天来删除'); return; } const selectedChats = entries.filter((item) => selectedItems.includes(item.id)); if (selectedChats.length === 0) { toast.error('未找到选中的聊天'); return; } setDialogContent({ type: 'bulkDelete', items: selectedChats }); }, [selectedItems, entries]); const selectAll = useCallback(() => { const allFilteredIds = entries.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; } // 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; }); }, [entries]); const handleDuplicate = async (id: string) => { await duplicateCurrentChat(id); loadChatEntries(); }; const handleSettingsClick = () => { setIsSettingsOpen(true); }; const handleSettingsClose = () => { setIsSettingsOpen(false); }; const setDialogContentWithLogging = useCallback((content: DialogContent) => { console.log('Setting dialog content:', content); setDialogContent(content); }, []); const handleDeleteSelectedItems = useCallback( async (itemsToDeleteNow: string[]) => { try { await deleteSelectedItems(itemsToDeleteNow); // 清空选择项 setSelectedItems([]); // 检查是否需要导航 const currentChatId = chatId; if (currentChatId && itemsToDeleteNow.includes(currentChatId)) { console.log('Navigating away from deleted chat'); window.location.pathname = '/'; } toast.success(`${itemsToDeleteNow.length} 个聊天已删除成功`); } catch (error) { console.error('Failed to delete chats:', error); toast.error('删除聊天失败'); } finally { loadChatEntries(); } }, [deleteSelectedItems, loadChatEntries, chatId], ); return ( <>
开始新的聊天
{isLoading && entries.length === 0 ? (
加载中...
) : ( entries.length === 0 && (
没有匹配的聊天记录
) )} {binDates(entries).map(({ category, items }) => (
{category}
{items.map((item) => ( { event.preventDefault(); event.stopPropagation(); setDialogContentWithLogging({ type: 'delete', item }); }} onDuplicate={() => handleDuplicate(item.id)} selectionMode={selectedItems.length > 0} isSelected={selectedItems.includes(item.id)} onToggleSelection={toggleItemSelection} /> ))}
))} {dialogContent?.type === 'delete' && ( <>
删除聊天记录 你确定要删除{' '} {dialogContent.item.description} {' '} 聊天记录吗?
取消 { deleteItem(event, dialogContent.item); closeDialog(); }} > 删除
)} {dialogContent?.type === 'bulkDelete' && ( <>
删除选中的聊天 你确定要删除 {dialogContent.items.length} 聊天:
    {dialogContent.items.map((item) => (
  • {item.description}
  • ))}
你确定要删除这些聊天记录吗?
取消 { /* * Pass the current selectedItems to the delete function. * This captures the state at the moment the user confirms. */ const itemsToDeleteNow = [...selectedItems]; handleDeleteSelectedItems(itemsToDeleteNow); closeDialog(); }} > 删除
)}
{selectedItems.length > 0 && (
)} {import.meta.env.MODE === 'development' && (
)}
{import.meta.env.MODE === 'development' && } ); });