import { useState, useCallback, useMemo } from 'react'; import { QueryKeys } from 'librechat-data-provider'; import { TrashIcon, ArchiveRestore } from 'lucide-react'; import { useQueryClient, InfiniteData } from '@tanstack/react-query'; import { Button, OGDialog, OGDialogTrigger, OGDialogTemplate, OGDialogContent, OGDialogHeader, OGDialogTitle, Label, TooltipAnchor, Spinner, useToastContext, useMediaQuery, DataTable, type TableColumn, } from '@librechat/client'; import type { ConversationListParams, TConversation } from 'librechat-data-provider'; import type { SortingState } from '@tanstack/react-table'; import { useArchiveConvoMutation, useConversationsInfiniteQuery, useDeleteConversationMutation, } from '~/data-provider'; import { MinimalIcon } from '~/components/Endpoints'; import { NotificationSeverity } from '~/common'; import { formatDate, cn } from '~/utils'; import { useLocalize } from '~/hooks'; const DEFAULT_PARAMS = { isArchived: true, sortBy: 'createdAt', sortDirection: 'desc', search: '', } as const satisfies ConversationListParams; type SortKey = 'createdAt' | 'title'; const isSortKey = (v: string): v is SortKey => v === 'createdAt' || v === 'title'; const defaultSort: SortingState = [ { id: 'createdAt', desc: true, }, ]; /** * Helper: remove a conversation from all infinite queries whose key starts with the provided root */ function removeConversationFromInfinite( queryClient: ReturnType, rootKey: string, conversationId: string, ) { const queries = queryClient.getQueryCache().findAll([rootKey], { exact: false }); for (const query of queries) { queryClient.setQueryData< InfiniteData<{ conversations: TConversation[]; nextCursor?: string | null }> >(query.queryKey, (old) => { if (!old) return old; return { ...old, pages: old.pages.map((page) => ({ ...page, conversations: page.conversations.filter((c) => c.conversationId !== conversationId), })), }; }); } } export default function ArchivedChatsTable() { const localize = useLocalize(); const isSmallScreen = useMediaQuery('(max-width: 768px)'); const { showToast } = useToastContext(); const queryClient = useQueryClient(); const [isOpen, setIsOpen] = useState(false); const [isDeleteOpen, setIsDeleteOpen] = useState(false); const [deleteRow, setDeleteRow] = useState(null); const [unarchivingId, setUnarchivingId] = useState(null); const [queryParams, setQueryParams] = useState(DEFAULT_PARAMS); const [sorting, setSorting] = useState(defaultSort); const [searchValue, setSearchValue] = useState(''); const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } = useConversationsInfiniteQuery(queryParams, { enabled: isOpen, keepPreviousData: false, staleTime: 30 * 1000, refetchOnWindowFocus: false, refetchOnMount: false, }); const handleSearchChange = useCallback((value: string) => { setSearchValue(value); setQueryParams((prev) => ({ ...prev, search: value, })); }, []); const handleSortingChange = useCallback( (updater: SortingState | ((old: SortingState) => SortingState)) => { setSorting((prev) => { const next = typeof updater === 'function' ? updater(prev) : updater; const primary = next[0]; setQueryParams((p) => { let sortBy: SortKey = 'createdAt'; let sortDirection: 'asc' | 'desc' = 'desc'; if (primary && isSortKey(primary.id)) { sortBy = primary.id; sortDirection = primary.desc ? 'desc' : 'asc'; } return { ...p, sortBy, sortDirection, }; }); return next; }); }, [], ); const flattenedConversations = useMemo( () => data?.pages?.flatMap((page) => page?.conversations?.filter(Boolean) ?? []) ?? [], [data?.pages], ); const unarchiveMutation = useArchiveConvoMutation({ onSuccess: (_res, variables) => { const { conversationId } = variables; if (conversationId) { removeConversationFromInfinite( queryClient, QueryKeys.archivedConversations, conversationId, ); } queryClient.invalidateQueries([QueryKeys.allConversations]); setUnarchivingId(null); }, onError: () => { showToast({ message: localize('com_ui_unarchive_error'), severity: NotificationSeverity.ERROR, }); setUnarchivingId(null); }, }); const deleteMutation = useDeleteConversationMutation({ onSuccess: (_data, variables) => { const { conversationId } = variables; if (conversationId) { removeConversationFromInfinite( queryClient, QueryKeys.archivedConversations, conversationId, ); } showToast({ message: localize('com_ui_archived_conversation_delete_success'), severity: NotificationSeverity.SUCCESS, }); setIsDeleteOpen(false); }, onError: () => { showToast({ message: localize('com_ui_archive_delete_error'), severity: NotificationSeverity.ERROR, }); }, }); const handleFetchNextPage = useCallback(async () => { if (!hasNextPage || isFetchingNextPage) return; await fetchNextPage(); }, [fetchNextPage, hasNextPage, isFetchingNextPage]); const effectiveIsLoading = isLoading; const effectiveIsFetching = isFetchingNextPage; const confirmDelete = useCallback(() => { if (!deleteRow?.conversationId) { showToast({ message: localize('com_ui_convo_delete_error'), severity: NotificationSeverity.WARNING, }); return; } deleteMutation.mutate({ conversationId: deleteRow.conversationId }); }, [deleteMutation, deleteRow, localize, showToast]); const handleUnarchive = useCallback( (conversationId: string) => { setUnarchivingId(conversationId); unarchiveMutation.mutate( { conversationId, isArchived: false }, { onSettled: () => setUnarchivingId(null) }, ); }, [unarchiveMutation], ); const columns: TableColumn, unknown>[] = useMemo( () => [ { accessorKey: 'title', accessorFn: (row: Record): unknown => { const convo = row as TConversation; return convo.title; }, header: () => ( {localize('com_nav_archive_name')} ), cell: ({ row }) => { const convo = row.original as TConversation; const { conversationId, title } = convo; return ( ); }, meta: { className: 'min-w-[150px] flex-1', isRowHeader: true, }, enableSorting: true, }, { accessorKey: 'createdAt', accessorFn: (row: Record): unknown => { const convo = row as TConversation; return convo.createdAt; }, header: () => ( {localize('com_nav_archive_created_at')} ), cell: ({ row }) => { const convo = row.original as TConversation; return formatDate(convo.createdAt?.toString() ?? '', isSmallScreen); }, meta: { className: 'w-32 sm:w-40', desktopOnly: true, }, enableSorting: true, }, { id: 'actions', accessorFn: () => null, header: () => ( {localize('com_assistants_actions')} ), cell: ({ row }) => { const convo = row.original as TConversation; const { title } = convo; const isRowUnarchiving = unarchivingId === convo.conversationId; return (
{ const conversationId = convo.conversationId; if (!conversationId) return; handleUnarchive(conversationId); }} disabled={isRowUnarchiving} aria-label={localize('com_ui_unarchive_conversation_title', { 0: title })} > {isRowUnarchiving ? : } } /> { setDeleteRow(convo); setIsDeleteOpen(true); }} aria-label={localize('com_ui_delete_conversation_title', { 0: title })} > } />
); }, meta: { className: 'w-24', }, enableSorting: false, }, ], [isSmallScreen, localize, handleUnarchive, unarchivingId], ); return (
{localize('com_nav_archived_chats')}
} selection={{ selectHandler: confirmDelete, selectClasses: `bg-red-700 dark:bg-red-600 hover:bg-red-800 dark:hover:bg-red-800 text-white ${ deleteMutation.isLoading ? 'cursor-not-allowed opacity-80' : '' }`, selectText: deleteMutation.isLoading ? : localize('com_ui_delete'), }} /> ); }