From 4328a25b6bd7311dabf18b8916f7511e7b5a6695 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sun, 22 Sep 2024 17:21:50 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=A7=B9=20fix:=20Resolve=20Unarchive=20Con?= =?UTF-8?q?versation=20Bug,=20Archive=20Pagination=20(#4189)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add cleanup service for 'bugged' conversations (empty/nullish conversationIds) * fix(ArchivedChatsTable): typing and minor styling issues * fix: properly archive conversations * fix: archive convo application crash * chore: remove unused `useEffect` * fix: add basic navigation * chore: typing --- api/models/Conversation.js | 30 ++++ api/server/routes/convos.js | 8 +- api/server/services/AppService.js | 2 + api/server/services/cleanup.js | 13 ++ .../SettingsTabs/General/ArchivedChats.tsx | 1 - .../General/ArchivedChatsTable.tsx | 136 ++++++++++-------- client/src/components/ui/Tooltip.tsx | 3 +- client/src/data-provider/mutations.ts | 71 +++++++-- packages/data-provider/src/schemas.ts | 1 + packages/data-provider/src/types/mutations.ts | 6 + 10 files changed, 202 insertions(+), 69 deletions(-) create mode 100644 api/server/services/cleanup.js diff --git a/api/models/Conversation.js b/api/models/Conversation.js index 19622ba796..0850ed0a71 100644 --- a/api/models/Conversation.js +++ b/api/models/Conversation.js @@ -31,9 +31,39 @@ const getConvo = async (user, conversationId) => { } }; +const deleteNullOrEmptyConversations = async () => { + try { + const filter = { + $or: [ + { conversationId: null }, + { conversationId: '' }, + { conversationId: { $exists: false } }, + ], + }; + + const result = await Conversation.deleteMany(filter); + + // Delete associated messages + const messageDeleteResult = await deleteMessages(filter); + + logger.info( + `[deleteNullOrEmptyConversations] Deleted ${result.deletedCount} conversations and ${messageDeleteResult.deletedCount} messages`, + ); + + return { + conversations: result, + messages: messageDeleteResult, + }; + } catch (error) { + logger.error('[deleteNullOrEmptyConversations] Error deleting conversations', error); + throw new Error('Error deleting conversations with null or empty conversationId'); + } +}; + module.exports = { Conversation, searchConversation, + deleteNullOrEmptyConversations, /** * Saves a conversation to the database. * @param {Object} req - The request object. diff --git a/api/server/routes/convos.js b/api/server/routes/convos.js index 104b0616f8..47a8ef19a8 100644 --- a/api/server/routes/convos.js +++ b/api/server/routes/convos.js @@ -109,8 +109,14 @@ router.post('/clear', async (req, res) => { router.post('/update', async (req, res) => { const update = req.body.arg; + if (!update.conversationId) { + return res.status(400).json({ error: 'conversationId is required' }); + } + try { - const dbResponse = await saveConvo(req, update, { context: 'POST /api/convos/update' }); + const dbResponse = await saveConvo(req, update, { + context: `POST /api/convos/update ${update.conversationId}`, + }); res.status(201).json(dbResponse); } catch (error) { logger.error('Error updating conversation', error); diff --git a/api/server/services/AppService.js b/api/server/services/AppService.js index 19a9fc91a9..f99e962871 100644 --- a/api/server/services/AppService.js +++ b/api/server/services/AppService.js @@ -8,6 +8,7 @@ const { loadDefaultInterface } = require('./start/interface'); const { azureConfigSetup } = require('./start/azureOpenAI'); const { loadAndFormatTools } = require('./ToolService'); const { initializeRoles } = require('~/models/Role'); +const { cleanup } = require('./cleanup'); const paths = require('~/config/paths'); /** @@ -17,6 +18,7 @@ const paths = require('~/config/paths'); * @param {Express.Application} app - The Express application object. */ const AppService = async (app) => { + cleanup(); await initializeRoles(); /** @type {TCustomConfig}*/ const config = (await loadCustomConfig()) ?? {}; diff --git a/api/server/services/cleanup.js b/api/server/services/cleanup.js new file mode 100644 index 0000000000..814c0ecc94 --- /dev/null +++ b/api/server/services/cleanup.js @@ -0,0 +1,13 @@ +const { logger } = require('~/config'); +const { deleteNullOrEmptyConversations } = require('~/models/Conversation'); +const cleanup = async () => { + try { + await deleteNullOrEmptyConversations(); + } catch (error) { + logger.error('[cleanup] Error during app cleanup', error); + } finally { + logger.debug('Startup cleanup complete'); + } +}; + +module.exports = { cleanup }; diff --git a/client/src/components/Nav/SettingsTabs/General/ArchivedChats.tsx b/client/src/components/Nav/SettingsTabs/General/ArchivedChats.tsx index 793954013a..7820e78498 100644 --- a/client/src/components/Nav/SettingsTabs/General/ArchivedChats.tsx +++ b/client/src/components/Nav/SettingsTabs/General/ArchivedChats.tsx @@ -1,7 +1,6 @@ import { useLocalize } from '~/hooks'; import OGDialogTemplate from '~/components/ui/OGDialogTemplate'; import { OGDialog, OGDialogTrigger, Button } from '~/components'; - import ArchivedChatsTable from './ArchivedChatsTable'; export default function ArchivedChats() { diff --git a/client/src/components/Nav/SettingsTabs/General/ArchivedChatsTable.tsx b/client/src/components/Nav/SettingsTabs/General/ArchivedChatsTable.tsx index a86676bb7d..85b193ea24 100644 --- a/client/src/components/Nav/SettingsTabs/General/ArchivedChatsTable.tsx +++ b/client/src/components/Nav/SettingsTabs/General/ArchivedChatsTable.tsx @@ -1,68 +1,80 @@ -import { useState, useCallback, useEffect } from 'react'; -import { useConversationsInfiniteQuery } from '~/data-provider'; +import { useState, useCallback, useMemo } from 'react'; import { Search, - ChevronRight, - ChevronLeft, TrashIcon, + ChevronLeft, + ChevronRight, + // ChevronsLeft, + // ChevronsRight, MessageCircle, ArchiveRestore, - ChevronsRight, - ChevronsLeft, } from 'lucide-react'; import type { TConversation } from 'librechat-data-provider'; -import { useAuthContext, useLocalize, useArchiveHandler } from '~/hooks'; -import { DeleteConversationDialog } from '~/components/Conversations/ConvoOptions'; import { - TooltipAnchor, Table, - TableBody, + Input, + Button, + TableRow, + Skeleton, + OGDialog, + Separator, TableCell, + TableBody, TableHead, TableHeader, - TableRow, - Separator, - Skeleton, - Button, - Input, - OGDialog, + TooltipAnchor, OGDialogTrigger, } from '~/components'; +import { useConversationsInfiniteQuery, useArchiveConvoMutation } from '~/data-provider'; +import { DeleteConversationDialog } from '~/components/Conversations/ConvoOptions'; +import { useAuthContext, useLocalize } from '~/hooks'; import { cn } from '~/utils'; export default function ArchivedChatsTable() { const localize = useLocalize(); const { isAuthenticated } = useAuthContext(); - const [conversationId, setConversationId] = useState(null); + const [isOpened, setIsOpened] = useState(false); const [currentPage, setCurrentPage] = useState(1); const [searchQuery, setSearchQuery] = useState(''); - const [totalPages, setTotalPages] = useState(1); - const [isOpened, setIsOpened] = useState(false); - const { data, isLoading, refetch } = useConversationsInfiniteQuery( - { pageNumber: currentPage.toString(), limit: 10, isArchived: true }, - { enabled: isAuthenticated && isOpened }, + const { data, isLoading, fetchNextPage, hasNextPage, isFetchingNextPage, refetch } = + useConversationsInfiniteQuery( + { pageNumber: currentPage.toString(), isArchived: true }, + { enabled: isAuthenticated && isOpened }, + ); + const mutation = useArchiveConvoMutation(); + const handleUnarchive = useCallback( + (conversationId: string) => { + mutation.mutate({ conversationId, isArchived: false }); + }, + [mutation], ); - useEffect(() => { - if (data) { - setTotalPages(Math.ceil(Number(data.pages))); + const conversations = useMemo( + () => data?.pages[currentPage - 1]?.conversations ?? [], + [data, currentPage], + ); + const totalPages = useMemo(() => Math.ceil(Number(data?.pages[0].pages ?? 1)) ?? 1, [data]); + + const handleChatClick = useCallback((conversationId: string) => { + if (!conversationId) { + return; } - }, [data]); - - const archiveHandler = useArchiveHandler(conversationId ?? '', false, () => { - refetch(); - }); - - const handleChatClick = useCallback((conversationId) => { window.open(`/c/${conversationId}`, '_blank'); }, []); - const handlePageChange = useCallback((newPage) => { - setCurrentPage(newPage); - }, []); + const handlePageChange = useCallback( + (newPage: number) => { + setCurrentPage(newPage); + if (!(hasNextPage ?? false)) { + return; + } + fetchNextPage({ pageParam: newPage }); + }, + [fetchNextPage, hasNextPage], + ); - const handleSearch = useCallback((query) => { + const handleSearch = useCallback((query: string) => { setSearchQuery(query); setCurrentPage(1); }, []); @@ -86,16 +98,14 @@ export default function ArchivedChatsTable() { ); }); - if (isLoading) { - return
{skeletons}
; + if (isLoading || isFetchingNextPage) { + return
{skeletons}
; } - if (!data || data.pages.length === 0 || data.pages[0].conversations.length === 0) { - return
{localize('com_nav_archived_chats_empty')}
; + if (!data || (conversations.length === 0 && totalPages === 0)) { + return
{localize('com_nav_archived_chats_empty')}
; } - const conversations = data.pages.flatMap((page) => page.conversations); - return (
handleSearch(e.target.value)} - className="w-full border-none" + className="w-full border-none placeholder:text-text-secondary" />
@@ -137,9 +147,16 @@ export default function ArchivedChatsTable() { } - > + /> @@ -181,6 +202,7 @@ export default function ArchivedChatsTable() { description={localize('com_ui_delete')} render={ } - > + /> - {DeleteConversationDialog({ - conversationId: conversation.conversationId ?? '', - retainView: refetch, - title: conversation.title ?? '', - })} + @@ -208,7 +230,7 @@ export default function ArchivedChatsTable() { Page {currentPage} of {totalPages}
- + */} - + */}
diff --git a/client/src/components/ui/Tooltip.tsx b/client/src/components/ui/Tooltip.tsx index 817195062b..ec969095ec 100644 --- a/client/src/components/ui/Tooltip.tsx +++ b/client/src/components/ui/Tooltip.tsx @@ -7,6 +7,7 @@ interface TooltipAnchorProps extends Ariakit.TooltipAnchorProps { description: string; side?: 'top' | 'bottom' | 'left' | 'right'; className?: string; + role?: string; } export const TooltipAnchor = forwardRef(function TooltipAnchor( @@ -50,7 +51,7 @@ export const TooltipAnchor = forwardRef(func className={cn('cursor-pointer', className)} /> - {mounted && ( + {mounted === true && ( dataService.archiveConversation(payload), { onSuccess: (_data, vars) => { - if (vars.isArchived) { + const isArchived = vars.isArchived === true; + if (isArchived) { queryClient.setQueryData([QueryKeys.conversation, id], null); } else { queryClient.setQueryData([QueryKeys.conversation, id], _data); @@ -151,17 +152,17 @@ export const useArchiveConversationMutation = ( const pageSize = convoData.pages[0].pageSize as number; return normalizeData( - vars.isArchived ? deleteConversation(convoData, id) : addConversation(convoData, _data), + isArchived ? deleteConversation(convoData, id) : addConversation(convoData, _data), 'conversations', pageSize, ); }); - if (vars.isArchived) { + if (isArchived) { const current = queryClient.getQueryData([ QueryKeys.allConversations, ]); - refetch({ refetchPage: (page, index) => index === (current?.pages.length || 1) - 1 }); + refetch({ refetchPage: (page, index) => index === (current?.pages.length ?? 1) - 1 }); } queryClient.setQueryData( @@ -172,21 +173,19 @@ export const useArchiveConversationMutation = ( } const pageSize = convoData.pages[0].pageSize as number; return normalizeData( - vars.isArchived - ? addConversation(convoData, _data) - : deleteConversation(convoData, id), + isArchived ? addConversation(convoData, _data) : deleteConversation(convoData, id), 'conversations', pageSize, ); }, ); - if (!vars.isArchived) { + if (!isArchived) { const currentArchive = queryClient.getQueryData([ QueryKeys.archivedConversations, ]); archiveRefetch({ - refetchPage: (page, index) => index === (currentArchive?.pages.length || 1) - 1, + refetchPage: (page, index) => index === (currentArchive?.pages.length ?? 1) - 1, }); } }, @@ -194,6 +193,60 @@ export const useArchiveConversationMutation = ( ); }; +export const useArchiveConvoMutation = (options?: t.ArchiveConvoOptions) => { + const queryClient = useQueryClient(); + const { onSuccess, ..._options } = options ?? {}; + + return useMutation( + (payload: t.TArchiveConversationRequest) => dataService.archiveConversation(payload), + { + onSuccess: (_data, vars) => { + const { conversationId } = vars; + const isArchived = vars.isArchived === true; + if (isArchived) { + queryClient.setQueryData([QueryKeys.conversation, conversationId], null); + } else { + queryClient.setQueryData([QueryKeys.conversation, conversationId], _data); + } + + queryClient.setQueryData([QueryKeys.allConversations], (convoData) => { + if (!convoData) { + return convoData; + } + const pageSize = convoData.pages[0].pageSize as number; + return normalizeData( + isArchived + ? deleteConversation(convoData, conversationId) + : addConversation(convoData, _data), + 'conversations', + pageSize, + ); + }); + + queryClient.setQueryData( + [QueryKeys.archivedConversations], + (convoData) => { + if (!convoData) { + return convoData; + } + const pageSize = convoData.pages[0].pageSize as number; + return normalizeData( + isArchived + ? addConversation(convoData, _data) + : deleteConversation(convoData, conversationId), + 'conversations', + pageSize, + ); + }, + ); + + onSuccess?.(_data, vars); + }, + ..._options, + }, + ); +}; + export const useCreateSharedLinkMutation = ( options?: t.CreateSharedLinkOptions, ): UseMutationResult => { diff --git a/packages/data-provider/src/schemas.ts b/packages/data-provider/src/schemas.ts index baccf72cf1..980cb3b2dd 100644 --- a/packages/data-provider/src/schemas.ts +++ b/packages/data-provider/src/schemas.ts @@ -508,6 +508,7 @@ export const tConversationSchema = z.object({ conversationId: z.string().nullable(), endpoint: eModelEndpointSchema.nullable(), endpointType: eModelEndpointSchema.optional(), + isArchived: z.boolean().optional(), title: z.string().nullable().or(z.literal('New Chat')).default('New Chat'), user: z.string().optional(), messages: z.array(z.string()).optional(), diff --git a/packages/data-provider/src/types/mutations.ts b/packages/data-provider/src/types/mutations.ts index 42c67d2e02..b9f8f2c385 100644 --- a/packages/data-provider/src/types/mutations.ts +++ b/packages/data-provider/src/types/mutations.ts @@ -165,6 +165,12 @@ export type UpdateSharedLinkOptions = MutationOptions< types.TSharedLink, Partial >; + +export type ArchiveConvoOptions = MutationOptions< + types.TArchiveConversationResponse, + types.TArchiveConversationRequest +>; + export type DeleteSharedLinkOptions = MutationOptions; export type TUpdatePromptContext =