From b1addcad5dff82ae388092fe10b8fd556bb457fb Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Tue, 2 Jun 2026 17:44:38 -0400 Subject: [PATCH] fix: Handle project search and stale ids --- .../Conversations/ProjectConversations.tsx | 42 ++++++++++++++++--- .../data-schemas/src/methods/conversation.ts | 35 ++++++++++++++-- 2 files changed, 67 insertions(+), 10 deletions(-) diff --git a/client/src/components/Conversations/ProjectConversations.tsx b/client/src/components/Conversations/ProjectConversations.tsx index 2f35c5f00a..ec36c85cbf 100644 --- a/client/src/components/Conversations/ProjectConversations.tsx +++ b/client/src/components/Conversations/ProjectConversations.tsx @@ -151,6 +151,7 @@ const ProjectConversations: FC = ({ const search = useRecoilValue(store.search); const isSmallScreen = useMediaQuery('(max-width: 768px)'); const [expandedSectionId, setExpandedSectionId] = useState(null); + const isSearching = Boolean(search.debouncedQuery); const projectSortBy = mode === 'recentProjects' ? 'lastConversationAt' : 'name'; const projectSortDirection = mode === 'recentProjects' ? 'desc' : 'asc'; @@ -179,14 +180,14 @@ const ProjectConversations: FC = ({ isLoading: isConversationsLoading, } = useConversationsInfiniteQuery( { - projectId: expandedSectionId ?? undefined, + projectId: isSearching ? undefined : (expandedSectionId ?? undefined), tags: tags.length === 0 ? undefined : tags, sortBy: chatSortBy, sortDirection: 'desc', search: search.debouncedQuery || undefined, }, { - enabled: isAuthenticated && isChatsExpanded && expandedSectionId != null, + enabled: isAuthenticated && isChatsExpanded && (isSearching || expandedSectionId != null), staleTime: 30000, cacheTime: 300000, }, @@ -236,6 +237,29 @@ const ProjectConversations: FC = ({ return items; } + if (isSearching) { + if (isConversationsLoading) { + items.push({ type: 'loading', key: 'loading-search-conversations' }); + return items; + } + + if (conversations.length === 0) { + items.push({ type: 'empty', title: localize('com_ui_no_results_found') }); + return items; + } + + groupedConversations.forEach(([groupName, convos]) => { + items.push({ type: 'date', groupName }); + convos.forEach((convo) => items.push({ type: 'convo', convo })); + }); + + if (isFetchingNextConversationPage) { + items.push({ type: 'loading', key: 'loading-more-search-conversations' }); + } + + return items; + } + sections.forEach((section) => { const isExpanded = expandedSectionId === section.id; items.push({ type: 'section', section, isExpanded }); @@ -278,6 +302,7 @@ const ProjectConversations: FC = ({ }, [ sections, isChatsExpanded, + isSearching, expandedSectionId, isConversationsLoading, conversations.length, @@ -309,18 +334,18 @@ const ProjectConversations: FC = ({ return 'project-chats-header'; } if (item.type === 'date') { - return `project-date-${expandedSectionId}-${item.groupName}`; + return `project-date-${isSearching ? 'search' : expandedSectionId}-${item.groupName}`; } if (item.type === 'convo') { return `project-convo-${item.convo.conversationId}`; } if (item.type === 'empty') { - return `project-empty-${expandedSectionId}`; + return `project-empty-${isSearching ? 'search' : expandedSectionId}`; } return item.key; }, }), - [expandedSectionId, rowHeight], + [expandedSectionId, isSearching, rowHeight], ); useEffect(() => { @@ -372,7 +397,11 @@ const ProjectConversations: FC = ({ if (!isChatsExpanded) { return; } - if (expandedSectionId && conversationHasNextPage && !isFetchingNextConversationPage) { + if ( + (isSearching || expandedSectionId) && + conversationHasNextPage && + !isFetchingNextConversationPage + ) { fetchNextConversationPage(); return; } @@ -381,6 +410,7 @@ const ProjectConversations: FC = ({ } }, [ expandedSectionId, + isSearching, conversationHasNextPage, isFetchingNextConversationPage, fetchNextConversationPage, diff --git a/packages/data-schemas/src/methods/conversation.ts b/packages/data-schemas/src/methods/conversation.ts index 6c7c6e5f4c..a9a98b9a99 100644 --- a/packages/data-schemas/src/methods/conversation.ts +++ b/packages/data-schemas/src/methods/conversation.ts @@ -1,10 +1,11 @@ import type { FilterQuery, Model, SortOrder } from 'mongoose'; import { RetentionMode } from 'librechat-data-provider'; +import { isValidObjectIdString } from '~/utils/objectId'; import { createTempChatExpirationDate } from '~/utils/tempChatRetention'; import { buildRetentionVisibilityFilter, createFallbackRetentionDate } from '~/utils/retention'; import { tenantSafeBulkWrite } from '~/utils/tenantBulkWrite'; import logger from '~/config/winston'; -import type { AppConfig, IConversation } from '~/types'; +import type { AppConfig, IChatProjectDocument, IConversation } from '~/types'; import { refreshChatProjectStatsForUser, updateChatProjectLastConversationForUser, @@ -213,6 +214,26 @@ export function createConversationMethods( const messages = await getMessages({ conversationId, user: userId }, '_id'); const update: Record = { ...convo, messages, user: userId }; + const unsetFields: Record = { ...(metadata?.unsetFields ?? {}) }; + + if (Object.prototype.hasOwnProperty.call(update, 'chatProjectId') && update.chatProjectId) { + const chatProjectId = typeof update.chatProjectId === 'string' ? update.chatProjectId : ''; + let isValidChatProject = isValidObjectIdString(chatProjectId); + + if (isValidChatProject) { + const ChatProject = mongoose.models.ChatProject as Model; + const project = await ChatProject.exists({ + _id: new mongoose.Types.ObjectId(chatProjectId), + user: userId, + }); + isValidChatProject = project != null; + } + + if (!isValidChatProject) { + delete update.chatProjectId; + unsetFields.chatProjectId = 1; + } + } if (newConversationId) { update.conversationId = newConversationId; @@ -253,8 +274,8 @@ export function createConversationMethods( } const updateOperation: Record = { $set: update }; - if (metadata?.unsetFields && Object.keys(metadata.unsetFields).length > 0) { - updateOperation.$unset = metadata.unsetFields; + if (Object.keys(unsetFields).length > 0) { + updateOperation.$unset = unsetFields; } if (createdAtOnInsert) { updateOperation.$setOnInsert = { createdAt: createdAtOnInsert }; @@ -302,9 +323,15 @@ export function createConversationMethods( } if (conversation.chatProjectId) { + const isRetentionVisibilityUpdate = + typeof update.isTemporary === 'boolean' || + Object.prototype.hasOwnProperty.call(convo, 'expiredAt') || + Object.prototype.hasOwnProperty.call(unsetFields, 'isTemporary') || + Object.prototype.hasOwnProperty.call(unsetFields, 'expiredAt'); const shouldRefreshProjectStats = typeof update.isArchived === 'boolean' || - Object.prototype.hasOwnProperty.call(metadata?.unsetFields ?? {}, 'isArchived'); + Object.prototype.hasOwnProperty.call(unsetFields, 'isArchived') || + isRetentionVisibilityUpdate; if (shouldRefreshProjectStats) { await refreshChatProjectStatsForUser(mongoose, userId, conversation.chatProjectId);