🛠️ fix: Conversation Navigation State (#7210)

* refactor: Enhance initial conversation query condition for better state management and prevent unused network requests

* ifx: Add Prettier plugin to ESLint configuration

* chore: linting and typing in convos.spec.ts

* fix: add back fresh data fetching and improve error handling for  conversation navigation

* fix: set conversation only with  conversation state change intent, to prevent double queries for messages
This commit is contained in:
Danny Avila 2025-05-04 10:44:40 -04:00 committed by GitHub
parent ddb2141eac
commit 6e663b2480
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 52 additions and 23 deletions

View file

@ -30,6 +30,7 @@ import type {
SharedLinksResponse, SharedLinksResponse,
} from 'librechat-data-provider'; } from 'librechat-data-provider';
import type { ConversationCursorData } from '~/utils/convos'; import type { ConversationCursorData } from '~/utils/convos';
import { findConversationInInfinite } from '~/utils';
export const useGetPresetsQuery = ( export const useGetPresetsQuery = (
config?: UseQueryOptions<TPreset[]>, config?: UseQueryOptions<TPreset[]>,
@ -68,14 +69,13 @@ export const useGetConvoIdQuery = (
[QueryKeys.conversation, id], [QueryKeys.conversation, id],
() => { () => {
// Try to find in all fetched infinite pages // Try to find in all fetched infinite pages
const convosQuery = queryClient.getQueryData<InfiniteData<ConversationCursorData>>([ const convosQuery = queryClient.getQueryData<InfiniteData<ConversationCursorData>>(
QueryKeys.allConversations, [QueryKeys.allConversations],
]); { exact: false },
const found = convosQuery?.pages );
.flatMap((page) => page.conversations) const found = findConversationInInfinite(convosQuery, id);
.find((c) => c.conversationId === id);
if (found) { if (found && found.messages != null) {
return found; return found;
} }
// Otherwise, fetch from API // Otherwise, fetch from API

View file

@ -1,7 +1,7 @@
import { useSetRecoilState } from 'recoil'; import { useSetRecoilState } from 'recoil';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { QueryKeys, Constants } from 'librechat-data-provider'; import { QueryKeys, Constants, dataService } from 'librechat-data-provider';
import type { TConversation, TEndpointsConfig, TModelsConfig } from 'librechat-data-provider'; import type { TConversation, TEndpointsConfig, TModelsConfig } from 'librechat-data-provider';
import { buildDefaultConvo, getDefaultEndpoint, getEndpointField, logger } from '~/utils'; import { buildDefaultConvo, getDefaultEndpoint, getEndpointField, logger } from '~/utils';
import store from '~/store'; import store from '~/store';
@ -14,6 +14,27 @@ const useNavigateToConvo = (index = 0) => {
const clearAllLatestMessages = store.useClearLatestMessages(`useNavigateToConvo ${index}`); const clearAllLatestMessages = store.useClearLatestMessages(`useNavigateToConvo ${index}`);
const { hasSetConversation, setConversation } = store.useCreateConversationAtom(index); const { hasSetConversation, setConversation } = store.useCreateConversationAtom(index);
const fetchFreshData = async (conversation?: Partial<TConversation>) => {
const conversationId = conversation?.conversationId;
if (!conversationId) {
return;
}
try {
const data = await queryClient.fetchQuery([QueryKeys.conversation, conversationId], () =>
dataService.getConversationById(conversationId),
);
logger.log('conversation', 'Fetched fresh conversation data', data);
setConversation(data);
navigate(`/c/${conversationId ?? Constants.NEW_CONVO}`, { state: { focusChat: true } });
} catch (error) {
console.error('Error fetching conversation data on navigation', error);
if (conversation) {
setConversation(conversation as TConversation);
navigate(`/c/${conversationId}`, { state: { focusChat: true } });
}
}
};
const navigateToConvo = ( const navigateToConvo = (
conversation?: TConversation | null, conversation?: TConversation | null,
options?: { options?: {
@ -58,9 +79,14 @@ const useNavigateToConvo = (index = 0) => {
}); });
} }
clearAllConversations(true); clearAllConversations(true);
setConversation(convo);
queryClient.setQueryData([QueryKeys.messages, currentConvoId], []); queryClient.setQueryData([QueryKeys.messages, currentConvoId], []);
navigate(`/c/${convo.conversationId ?? Constants.NEW_CONVO}`, { state: { focusChat: true } }); if (convo.conversationId !== Constants.NEW_CONVO && convo.conversationId) {
queryClient.invalidateQueries([QueryKeys.conversation, convo.conversationId]);
fetchFreshData(convo);
} else {
setConversation(convo);
navigate(`/c/${convo.conversationId ?? Constants.NEW_CONVO}`, { state: { focusChat: true } });
}
}; };
return { return {

View file

@ -43,7 +43,8 @@ export default function ChatRoute() {
refetchOnMount: 'always', refetchOnMount: 'always',
}); });
const initialConvoQuery = useGetConvoIdQuery(conversationId, { const initialConvoQuery = useGetConvoIdQuery(conversationId, {
enabled: isAuthenticated && conversationId !== Constants.NEW_CONVO, enabled:
isAuthenticated && conversationId !== Constants.NEW_CONVO && !hasSetConversation.current,
}); });
const endpointsQuery = useGetEndpointsQuery({ enabled: isAuthenticated }); const endpointsQuery = useGetEndpointsQuery({ enabled: isAuthenticated });
const assistantListMap = useAssistantListMap(); const assistantListMap = useAssistantListMap();

View file

@ -431,14 +431,14 @@ describe('Conversation Utilities', () => {
pageParams: [], pageParams: [],
}; };
const newConvo = makeConversation('new'); const newConvo = makeConversation('new');
const updated = addConversationToInfinitePages(data, newConvo); const updated = addConversationToInfinitePages(data, newConvo as TConversation);
expect(updated.pages[0].conversations[0].conversationId).toBe('new'); expect(updated.pages[0].conversations[0].conversationId).toBe('new');
expect(updated.pages[0].conversations[1].conversationId).toBe('1'); expect(updated.pages[0].conversations[1].conversationId).toBe('1');
}); });
it('creates new InfiniteData if data is undefined', () => { it('creates new InfiniteData if data is undefined', () => {
const newConvo = makeConversation('new'); const newConvo = makeConversation('new');
const updated = addConversationToInfinitePages(undefined, newConvo); const updated = addConversationToInfinitePages(undefined, newConvo as TConversation);
expect(updated.pages[0].conversations[0].conversationId).toBe('new'); expect(updated.pages[0].conversations[0].conversationId).toBe('new');
expect(updated.pageParams).toEqual([undefined]); expect(updated.pageParams).toEqual([undefined]);
}); });
@ -531,12 +531,12 @@ describe('Conversation Utilities', () => {
it('stores model for endpoint', () => { it('stores model for endpoint', () => {
const conversation = { const conversation = {
conversationId: '1', conversationId: '1',
endpoint: 'openai', endpoint: 'openAI',
model: 'gpt-3', model: 'gpt-3',
}; };
storeEndpointSettings(conversation as any); storeEndpointSettings(conversation as any);
const stored = JSON.parse(localStorage.getItem('lastModel') || '{}'); const stored = JSON.parse(localStorage.getItem('lastModel') || '{}');
expect([undefined, 'gpt-3']).toContain(stored.openai); expect([undefined, 'gpt-3']).toContain(stored.openAI);
}); });
it('stores secondaryModel for gptPlugins endpoint', () => { it('stores secondaryModel for gptPlugins endpoint', () => {
@ -574,14 +574,14 @@ describe('Conversation Utilities', () => {
conversationId: 'a', conversationId: 'a',
updatedAt: '2024-01-01T12:00:00Z', updatedAt: '2024-01-01T12:00:00Z',
createdAt: '2024-01-01T10:00:00Z', createdAt: '2024-01-01T10:00:00Z',
endpoint: 'openai', endpoint: 'openAI',
model: 'gpt-3', model: 'gpt-3',
title: 'Conversation A', title: 'Conversation A',
} as TConversation; } as TConversation;
convoB = { convoB = {
conversationId: 'b', conversationId: 'b',
updatedAt: '2024-01-02T12:00:00Z', updatedAt: '2024-01-02T12:00:00Z',
endpoint: 'openai', endpoint: 'openAI',
model: 'gpt-3', model: 'gpt-3',
} as TConversation; } as TConversation;
queryClient.setQueryData(['allConversations'], { queryClient.setQueryData(['allConversations'], {

View file

@ -280,11 +280,11 @@ export function updateConvoFieldsInfinite(
pages: data.pages.map((page, pi) => pages: data.pages.map((page, pi) =>
pi === pageIdx pi === pageIdx
? { ? {
...page, ...page,
conversations: page.conversations.map((c, ci) => conversations: page.conversations.map((c, ci) =>
ci === convoIdx ? { ...c, ...updatedConversation } : c, ci === convoIdx ? { ...c, ...updatedConversation } : c,
), ),
} }
: page, : page,
), ),
}; };

View file

@ -4,6 +4,7 @@ import typescriptEslintEslintPlugin from '@typescript-eslint/eslint-plugin';
import { fixupConfigRules, fixupPluginRules } from '@eslint/compat'; import { fixupConfigRules, fixupPluginRules } from '@eslint/compat';
// import perfectionist from 'eslint-plugin-perfectionist'; // import perfectionist from 'eslint-plugin-perfectionist';
import reactHooks from 'eslint-plugin-react-hooks'; import reactHooks from 'eslint-plugin-react-hooks';
import prettier from 'eslint-plugin-prettier';
import tsParser from '@typescript-eslint/parser'; import tsParser from '@typescript-eslint/parser';
import importPlugin from 'eslint-plugin-import'; import importPlugin from 'eslint-plugin-import';
import { FlatCompat } from '@eslint/eslintrc'; import { FlatCompat } from '@eslint/eslintrc';
@ -62,6 +63,7 @@ export default [
'import/parsers': tsParser, 'import/parsers': tsParser,
i18next, i18next,
// perfectionist, // perfectionist,
prettier: fixupPluginRules(prettier),
}, },
languageOptions: { languageOptions: {
@ -357,4 +359,4 @@ export default [
}, },
}, },
}, },
]; ];