mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-22 06:00:56 +02:00
🔃 refactor: Streamline Navigation, Message Loading UX (#7118)
* chore: fix logging for illegal target endpoints in getEndpointFromSetup * fix: prevent querying agent by ID for ephemeral agents * refactor: reorder variable declarations in MessagesView for clarity * fix: localize 'nothing found' message in MessagesView * refactor: streamline navigation logic and enhance loading spinner component in ChatView * refactor: simplify loading spinner logic in ChatView component * fix: ensure message queries are invalidated after new conversation creation in HeaderNewChat, MobileNav, and NewChat components * 🐛 First run dev mode will have error occur. 🐛 First run dev mode will have error occur. * fix font-size localstorage presist bug * Don't ping meilisearch if the search is disabled via env var * simplify logic in search/enable endpoint * refactor: simplify enable endpoint condition check * feat: add useIdChangeEffect hook and integrate it into ChatRoute --------- Co-authored-by: Ne0 <20765145+zeeklog@users.noreply.github.com> Co-authored-by: TinyTin <garychangcn@hotmail.com> Co-authored-by: Denis Palnitsky <denis.palnitsky@zendesk.com>
This commit is contained in:
parent
fc30482f65
commit
0e8041bcac
17 changed files with 80 additions and 131 deletions
|
@ -1,40 +1,17 @@
|
|||
const { Keyv } = require('keyv');
|
||||
const express = require('express');
|
||||
const { MeiliSearch } = require('meilisearch');
|
||||
const { Conversation } = require('~/models/Conversation');
|
||||
const requireJwtAuth = require('~/server/middleware/requireJwtAuth');
|
||||
const { Message } = require('~/models/Message');
|
||||
const { isEnabled } = require('~/server/utils');
|
||||
const keyvRedis = require('~/cache/keyvRedis');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const expiration = 60 * 1000;
|
||||
const cache = isEnabled(process.env.USE_REDIS)
|
||||
? new Keyv({ store: keyvRedis })
|
||||
: new Keyv({ namespace: 'search', ttl: expiration });
|
||||
|
||||
router.use(requireJwtAuth);
|
||||
|
||||
router.get('/sync', async function (req, res) {
|
||||
await Message.syncWithMeili();
|
||||
await Conversation.syncWithMeili();
|
||||
res.send('synced');
|
||||
});
|
||||
|
||||
router.get('/test', async function (req, res) {
|
||||
const { q } = req.query;
|
||||
const messages = (
|
||||
await Message.meiliSearch(q, { attributesToHighlight: ['text'] }, true)
|
||||
).hits.map((message) => {
|
||||
const { _formatted, ...rest } = message;
|
||||
return { ...rest, searchResult: true, text: _formatted.text };
|
||||
});
|
||||
res.send(messages);
|
||||
});
|
||||
|
||||
router.get('/enable', async function (req, res) {
|
||||
let result = false;
|
||||
if (!isEnabled(process.env.SEARCH)) {
|
||||
return res.send(false);
|
||||
}
|
||||
|
||||
try {
|
||||
const client = new MeiliSearch({
|
||||
host: process.env.MEILI_HOST,
|
||||
|
@ -42,8 +19,7 @@ router.get('/enable', async function (req, res) {
|
|||
});
|
||||
|
||||
const { status } = await client.health();
|
||||
result = status === 'available' && !!process.env.SEARCH;
|
||||
return res.send(result);
|
||||
return res.send(status === 'available');
|
||||
} catch (error) {
|
||||
return res.send(false);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { memo, useCallback } from 'react';
|
||||
import { memo, useMemo, useCallback } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
@ -19,6 +19,16 @@ import Header from './Header';
|
|||
import Footer from './Footer';
|
||||
import store from '~/store';
|
||||
|
||||
function LoadingSpinner() {
|
||||
return (
|
||||
<div className="relative flex-1 overflow-hidden overflow-y-auto">
|
||||
<div className="relative flex h-full items-center justify-center">
|
||||
<Spinner className="text-text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ChatView({ index = 0 }: { index?: number }) {
|
||||
const { conversationId } = useParams();
|
||||
const rootSubmission = useRecoilValue(store.submissionByIndex(index));
|
||||
|
@ -52,15 +62,12 @@ function ChatView({ index = 0 }: { index?: number }) {
|
|||
const isLandingPage =
|
||||
(!messagesTree || messagesTree.length === 0) &&
|
||||
(conversationId === Constants.NEW_CONVO || !conversationId);
|
||||
const isNavigating = (!messagesTree || messagesTree.length === 0) && conversationId != null;
|
||||
|
||||
if (isLoading && conversationId !== Constants.NEW_CONVO) {
|
||||
content = (
|
||||
<div className="relative flex-1 overflow-hidden overflow-y-auto">
|
||||
<div className="relative flex h-full items-center justify-center">
|
||||
<Spinner className="text-text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
content = <LoadingSpinner />;
|
||||
} else if ((isLoading || isNavigating) && !isLandingPage) {
|
||||
content = <LoadingSpinner />;
|
||||
} else if (!isLandingPage) {
|
||||
content = <MessagesView messagesTree={messagesTree} />;
|
||||
} else {
|
||||
|
|
|
@ -20,6 +20,7 @@ export default function HeaderNewChat() {
|
|||
[QueryKeys.messages, conversation?.conversationId ?? Constants.NEW_CONVO],
|
||||
[],
|
||||
);
|
||||
queryClient.invalidateQueries([QueryKeys.messages]);
|
||||
newConversation();
|
||||
};
|
||||
|
||||
|
|
|
@ -14,10 +14,9 @@ export default function MessagesView({
|
|||
messagesTree?: TMessage[] | null;
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
const scrollButtonPreference = useRecoilValue(store.showScrollButton);
|
||||
const maximizeChatSpace = useRecoilValue(store.maximizeChatSpace);
|
||||
const fontSize = useRecoilValue(store.fontSize);
|
||||
const { screenshotTargetRef } = useScreenshot();
|
||||
const scrollButtonPreference = useRecoilValue(store.showScrollButton);
|
||||
const [currentEditId, setCurrentEditId] = useState<number | string | null>(-1);
|
||||
|
||||
const {
|
||||
|
|
|
@ -13,7 +13,7 @@ export default function SearchButtons({ message }: { message: TMessage }) {
|
|||
const localize = useLocalize();
|
||||
const queryClient = useQueryClient();
|
||||
const search = useRecoilValue(store.search);
|
||||
const { navigateWithLastTools } = useNavigateToConvo();
|
||||
const { navigateToConvo } = useNavigateToConvo();
|
||||
const conversationId = message.conversationId ?? '';
|
||||
|
||||
const clickHandler = async (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
|
@ -39,14 +39,13 @@ export default function SearchButtons({ message }: { message: TMessage }) {
|
|||
}
|
||||
|
||||
document.title = title;
|
||||
navigateWithLastTools(
|
||||
navigateToConvo(
|
||||
cachedConvo ??
|
||||
({
|
||||
conversationId,
|
||||
title,
|
||||
} as TConversation),
|
||||
true,
|
||||
true,
|
||||
{ resetLatestMessage: true },
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -31,11 +31,11 @@ export default function Conversation({
|
|||
const params = useParams();
|
||||
const localize = useLocalize();
|
||||
const { showToast } = useToastContext();
|
||||
const { navigateToConvo } = useNavigateToConvo();
|
||||
const { data: endpointsConfig } = useGetEndpointsQuery();
|
||||
const currentConvoId = useMemo(() => params.conversationId, [params.conversationId]);
|
||||
const updateConvoMutation = useUpdateConversationMutation(currentConvoId ?? '');
|
||||
const activeConvos = useRecoilValue(store.allConversationsSelector);
|
||||
const { data: endpointsConfig } = useGetEndpointsQuery();
|
||||
const { navigateWithLastTools } = useNavigateToConvo();
|
||||
const isSmallScreen = useMediaQuery('(max-width: 768px)');
|
||||
const { conversationId, title = '' } = conversation;
|
||||
|
||||
|
@ -118,10 +118,10 @@ export default function Conversation({
|
|||
document.title = title;
|
||||
}
|
||||
|
||||
navigateWithLastTools(
|
||||
conversation,
|
||||
!(conversationId ?? '') || conversationId === Constants.NEW_CONVO,
|
||||
);
|
||||
navigateToConvo(conversation, {
|
||||
currentConvoId,
|
||||
resetLatestMessage: !(conversationId ?? '') || conversationId === Constants.NEW_CONVO,
|
||||
});
|
||||
};
|
||||
|
||||
const convoOptionsProps = {
|
||||
|
|
|
@ -61,6 +61,7 @@ export default function MobileNav({
|
|||
[QueryKeys.messages, conversation?.conversationId ?? Constants.NEW_CONVO],
|
||||
[],
|
||||
);
|
||||
queryClient.invalidateQueries([QueryKeys.messages]);
|
||||
newConversation();
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -40,6 +40,7 @@ export default function NewChat({
|
|||
[QueryKeys.messages, conversation?.conversationId ?? Constants.NEW_CONVO],
|
||||
[],
|
||||
);
|
||||
queryClient.invalidateQueries([QueryKeys.messages]);
|
||||
newConvo();
|
||||
navigate('/c/new', { state: { focusChat: true } });
|
||||
if (isSmallScreen) {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { useState } from 'react';
|
||||
import type { TMessage } from 'librechat-data-provider';
|
||||
import MultiMessage from './MultiMessage';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
export default function MessagesView({
|
||||
messagesTree: _messagesTree,
|
||||
|
@ -9,6 +10,7 @@ export default function MessagesView({
|
|||
messagesTree?: TMessage[] | null;
|
||||
conversationId: string;
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
const [currentEditId, setCurrentEditId] = useState<number | string | null>(-1);
|
||||
return (
|
||||
<div className="flex-1 pb-[50px]">
|
||||
|
@ -23,7 +25,7 @@ export default function MessagesView({
|
|||
<div className="flex flex-col pb-9 text-sm dark:bg-transparent">
|
||||
{(_messagesTree && _messagesTree.length == 0) || _messagesTree === null ? (
|
||||
<div className="flex w-full items-center justify-center gap-1 bg-gray-50 p-3 text-sm text-gray-500 dark:border-gray-800/50 dark:bg-gray-800 dark:text-gray-300">
|
||||
Nothing found
|
||||
{localize('com_ui_nothing_found')}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
|
|
|
@ -4,6 +4,7 @@ import { useWatch, useForm, FormProvider } from 'react-hook-form';
|
|||
import { useGetModelsQuery } from 'librechat-data-provider/react-query';
|
||||
import {
|
||||
Tools,
|
||||
Constants,
|
||||
SystemRoles,
|
||||
EModelEndpoint,
|
||||
isAssistantsEndpoint,
|
||||
|
@ -45,7 +46,7 @@ export default function AgentPanel({
|
|||
|
||||
const modelsQuery = useGetModelsQuery();
|
||||
const agentQuery = useGetAgentByIdQuery(current_agent_id ?? '', {
|
||||
enabled: !!(current_agent_id ?? ''),
|
||||
enabled: !!(current_agent_id ?? '') && current_agent_id !== Constants.EPHEMERAL_AGENT_ID,
|
||||
});
|
||||
|
||||
const models = useMemo(() => modelsQuery.data ?? {}, [modelsQuery.data]);
|
||||
|
|
|
@ -18,7 +18,7 @@ export default function useSelectAgent() {
|
|||
);
|
||||
|
||||
const agentQuery = useGetAgentByIdQuery(selectedAgentId ?? '', {
|
||||
enabled: !!(selectedAgentId ?? ''),
|
||||
enabled: !!(selectedAgentId ?? '') && selectedAgentId !== Constants.EPHEMERAL_AGENT_ID,
|
||||
});
|
||||
|
||||
const updateConversation = useCallback(
|
||||
|
|
|
@ -2,4 +2,5 @@ export { default as useChatHelpers } from './useChatHelpers';
|
|||
export { default as useAddedHelpers } from './useAddedHelpers';
|
||||
export { default as useAddedResponse } from './useAddedResponse';
|
||||
export { default as useChatFunctions } from './useChatFunctions';
|
||||
export { default as useIdChangeEffect } from './useIdChangeEffect';
|
||||
export { default as useFocusChatEffect } from './useFocusChatEffect';
|
||||
|
|
21
client/src/hooks/Chat/useIdChangeEffect.ts
Normal file
21
client/src/hooks/Chat/useIdChangeEffect.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { useEffect, useRef } from 'react';
|
||||
import { useResetRecoilState } from 'recoil';
|
||||
import { logger } from '~/utils';
|
||||
import store from '~/store';
|
||||
|
||||
/**
|
||||
* Hook to reset artifacts when the conversation ID changes
|
||||
* @param conversationId - The current conversation ID
|
||||
*/
|
||||
export default function useIdChangeEffect(conversationId: string) {
|
||||
const lastConvoId = useRef<string | null>(null);
|
||||
const resetArtifacts = useResetRecoilState(store.artifactsState);
|
||||
|
||||
useEffect(() => {
|
||||
if (conversationId !== lastConvoId.current) {
|
||||
logger.log('conversation', 'Conversation ID change');
|
||||
resetArtifacts();
|
||||
}
|
||||
lastConvoId.current = conversationId;
|
||||
}, [conversationId, resetArtifacts]);
|
||||
}
|
|
@ -1,13 +1,7 @@
|
|||
import { useSetRecoilState } from 'recoil';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useSetRecoilState, useResetRecoilState } from 'recoil';
|
||||
import {
|
||||
QueryKeys,
|
||||
Constants,
|
||||
dataService,
|
||||
EModelEndpoint,
|
||||
LocalStorageKeys,
|
||||
} from 'librechat-data-provider';
|
||||
import { QueryKeys, Constants } from 'librechat-data-provider';
|
||||
import type { TConversation, TEndpointsConfig, TModelsConfig } from 'librechat-data-provider';
|
||||
import { buildDefaultConvo, getDefaultEndpoint, getEndpointField, logger } from '~/utils';
|
||||
import store from '~/store';
|
||||
|
@ -16,48 +10,28 @@ const useNavigateToConvo = (index = 0) => {
|
|||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
const clearAllConversations = store.useClearConvoState();
|
||||
const resetArtifacts = useResetRecoilState(store.artifactsState);
|
||||
const setSubmission = useSetRecoilState(store.submissionByIndex(index));
|
||||
const clearAllLatestMessages = store.useClearLatestMessages(`useNavigateToConvo ${index}`);
|
||||
const { hasSetConversation, setConversation } = store.useCreateConversationAtom(index);
|
||||
|
||||
const fetchFreshData = async (conversationId?: string | null) => {
|
||||
if (!conversationId) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const data = await queryClient.fetchQuery([QueryKeys.conversation, conversationId], () =>
|
||||
dataService.getConversationById(conversationId),
|
||||
);
|
||||
logger.log('conversation', 'Fetched fresh conversation data', data);
|
||||
await queryClient.invalidateQueries([QueryKeys.messages, conversationId]);
|
||||
setConversation(data);
|
||||
navigate(`/c/${conversationId ?? Constants.NEW_CONVO}`, { state: { focusChat: true } });
|
||||
} catch (error) {
|
||||
console.error('Error fetching conversation data on navigation', error);
|
||||
}
|
||||
};
|
||||
|
||||
const navigateToConvo = (
|
||||
conversation?: TConversation | null,
|
||||
_resetLatestMessage = true,
|
||||
/** Likely need to remove this since it happens after fetching conversation data */
|
||||
invalidateMessages = false,
|
||||
options?: {
|
||||
resetLatestMessage?: boolean;
|
||||
currentConvoId?: string;
|
||||
},
|
||||
) => {
|
||||
if (!conversation) {
|
||||
logger.warn('conversation', 'Conversation not provided to `navigateToConvo`');
|
||||
return;
|
||||
}
|
||||
const { resetLatestMessage = true, currentConvoId } = options || {};
|
||||
logger.log('conversation', 'Navigating to conversation', conversation);
|
||||
hasSetConversation.current = true;
|
||||
setSubmission(null);
|
||||
if (_resetLatestMessage) {
|
||||
if (resetLatestMessage) {
|
||||
clearAllLatestMessages();
|
||||
}
|
||||
if (invalidateMessages && conversation.conversationId != null && conversation.conversationId) {
|
||||
queryClient.setQueryData([QueryKeys.messages, Constants.NEW_CONVO], []);
|
||||
queryClient.invalidateQueries([QueryKeys.messages, conversation.conversationId]);
|
||||
}
|
||||
|
||||
let convo = { ...conversation };
|
||||
const endpointsConfig = queryClient.getQueryData<TEndpointsConfig>([QueryKeys.endpoints]);
|
||||
|
@ -84,51 +58,13 @@ const useNavigateToConvo = (index = 0) => {
|
|||
});
|
||||
}
|
||||
clearAllConversations(true);
|
||||
resetArtifacts();
|
||||
setConversation(convo);
|
||||
if (convo.conversationId !== Constants.NEW_CONVO && convo.conversationId) {
|
||||
queryClient.invalidateQueries([QueryKeys.conversation, convo.conversationId]);
|
||||
fetchFreshData(convo.conversationId);
|
||||
} else {
|
||||
navigate(`/c/${convo.conversationId ?? Constants.NEW_CONVO}`, { state: { focusChat: true } });
|
||||
}
|
||||
};
|
||||
|
||||
const navigateWithLastTools = (
|
||||
conversation?: TConversation | null,
|
||||
_resetLatestMessage?: boolean,
|
||||
invalidateMessages?: boolean,
|
||||
) => {
|
||||
if (!conversation) {
|
||||
logger.warn('conversation', 'Conversation not provided to `navigateToConvo`');
|
||||
return;
|
||||
}
|
||||
// set conversation to the new conversation
|
||||
if (conversation.endpoint === EModelEndpoint.gptPlugins) {
|
||||
let lastSelectedTools = [];
|
||||
try {
|
||||
lastSelectedTools =
|
||||
JSON.parse(localStorage.getItem(LocalStorageKeys.LAST_TOOLS) ?? '') ?? [];
|
||||
} catch (e) {
|
||||
logger.error('conversation', 'Error parsing last selected tools', e);
|
||||
}
|
||||
const hasTools = (conversation.tools?.length ?? 0) > 0;
|
||||
navigateToConvo(
|
||||
{
|
||||
...conversation,
|
||||
tools: hasTools ? conversation.tools : lastSelectedTools,
|
||||
},
|
||||
_resetLatestMessage,
|
||||
invalidateMessages,
|
||||
);
|
||||
} else {
|
||||
navigateToConvo(conversation, _resetLatestMessage, invalidateMessages);
|
||||
}
|
||||
queryClient.setQueryData([QueryKeys.messages, currentConvoId], []);
|
||||
navigate(`/c/${convo.conversationId ?? Constants.NEW_CONVO}`, { state: { focusChat: true } });
|
||||
};
|
||||
|
||||
return {
|
||||
navigateToConvo,
|
||||
navigateWithLastTools,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -58,10 +58,14 @@ export const ThemeProvider = ({ initialTheme, children }) => {
|
|||
if (fontSize == null) {
|
||||
setFontSize('text-base');
|
||||
applyFontSize('text-base');
|
||||
localStorage.setItem('fontSize', 'text-base');
|
||||
localStorage.setItem('fontSize', JSON.stringify('text-base'));
|
||||
return;
|
||||
}
|
||||
applyFontSize(JSON.parse(fontSize));
|
||||
try {
|
||||
applyFontSize(JSON.parse(fontSize));
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
// Reason: This effect should only run once, and `setFontSize` is a stable function
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
|
|
@ -9,8 +9,8 @@ import {
|
|||
useGetStartupConfig,
|
||||
useGetEndpointsQuery,
|
||||
} from '~/data-provider';
|
||||
import { useNewConvo, useAppStartup, useAssistantListMap, useIdChangeEffect } from '~/hooks';
|
||||
import { getDefaultModelSpec, getModelSpecPreset, logger } from '~/utils';
|
||||
import { useNewConvo, useAppStartup, useAssistantListMap } from '~/hooks';
|
||||
import { ToolCallsMapProvider } from '~/Providers';
|
||||
import ChatView from '~/components/Chat/ChatView';
|
||||
import useAuthRedirect from './useAuthRedirect';
|
||||
|
@ -34,7 +34,7 @@ export default function ChatRoute() {
|
|||
|
||||
const index = 0;
|
||||
const { conversationId = '' } = useParams();
|
||||
|
||||
useIdChangeEffect(conversationId);
|
||||
const { hasSetConversation, conversation } = store.useCreateConversationAtom(index);
|
||||
const { newConversation } = useNewConvo();
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ const getEndpointFromSetup = (
|
|||
if (targetEndpoint && endpointsConfig?.[targetEndpoint]) {
|
||||
return targetEndpoint as EModelEndpoint;
|
||||
} else if (targetEndpoint) {
|
||||
console.warn(`Illegal target endpoint ${targetEndpoint} ${endpointsConfig}`);
|
||||
console.warn(`Illegal target endpoint ${targetEndpoint}`, endpointsConfig);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue