Skip to content

Commit fc30482

Browse files
authored
🪶 refactor: Chat Input Focus for Conversation Navigations & ChatForm Optimizations (danny-avila#7100)
* refactor: improve ChatView layout by keeping ChatForm mounted * feat: implement focusChat functionality for new conversations and navigations * refactor: reset artifacts when navigating to prevent any from rendering in a conversation when none exist; edge case, artifacts get created by search route (TODO: use a different artifact renderer for Search markdown)
1 parent 6826c0e commit fc30482

File tree

8 files changed

+68
-41
lines changed

8 files changed

+68
-41
lines changed

client/src/components/Chat/ChatView.tsx

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { memo, useCallback } from 'react';
22
import { useRecoilValue } from 'recoil';
33
import { useForm } from 'react-hook-form';
44
import { useParams } from 'react-router-dom';
5+
import { Constants } from 'librechat-data-provider';
56
import { useGetMessagesByConvoId } from 'librechat-data-provider/react-query';
67
import type { TMessage } from 'librechat-data-provider';
78
import type { ChatFormValues } from '~/common';
@@ -11,8 +12,8 @@ import ConversationStarters from './Input/ConversationStarters';
1112
import MessagesView from './Messages/MessagesView';
1213
import { Spinner } from '~/components/svg';
1314
import Presentation from './Presentation';
15+
import { buildTree, cn } from '~/utils';
1416
import ChatForm from './Input/ChatForm';
15-
import { buildTree } from '~/utils';
1617
import Landing from './Landing';
1718
import Header from './Header';
1819
import Footer from './Footer';
@@ -48,9 +49,11 @@ function ChatView({ index = 0 }: { index?: number }) {
4849
});
4950

5051
let content: JSX.Element | null | undefined;
51-
const isLandingPage = !messagesTree || messagesTree.length === 0;
52+
const isLandingPage =
53+
(!messagesTree || messagesTree.length === 0) &&
54+
(conversationId === Constants.NEW_CONVO || !conversationId);
5255

53-
if (isLoading && conversationId !== 'new') {
56+
if (isLoading && conversationId !== Constants.NEW_CONVO) {
5457
content = (
5558
<div className="relative flex-1 overflow-hidden overflow-y-auto">
5659
<div className="relative flex h-full items-center justify-center">
@@ -71,27 +74,28 @@ function ChatView({ index = 0 }: { index?: number }) {
7174
<Presentation>
7275
<div className="flex h-full w-full flex-col">
7376
{!isLoading && <Header />}
74-
75-
{isLandingPage ? (
76-
<>
77-
<div className="flex flex-1 flex-col items-center justify-end sm:justify-center">
78-
{content}
79-
<div className="w-full max-w-3xl transition-all duration-200 xl:max-w-4xl">
80-
<ChatForm index={index} />
81-
<ConversationStarters />
82-
</div>
83-
</div>
84-
<Footer />
85-
</>
86-
) : (
87-
<div className="flex h-full flex-col overflow-y-auto">
77+
<>
78+
<div
79+
className={cn(
80+
'flex flex-col',
81+
isLandingPage
82+
? 'flex-1 items-center justify-end sm:justify-center'
83+
: 'h-full overflow-y-auto',
84+
)}
85+
>
8886
{content}
89-
<div className="w-full">
87+
<div
88+
className={cn(
89+
'w-full',
90+
isLandingPage && 'max-w-3xl transition-all duration-200 xl:max-w-4xl',
91+
)}
92+
>
9093
<ChatForm index={index} />
91-
<Footer />
94+
{isLandingPage ? <ConversationStarters /> : <Footer />}
9295
</div>
9396
</div>
94-
)}
97+
{isLandingPage && <Footer />}
98+
</>
9599
</div>
96100
</Presentation>
97101
</AddedChatContext.Provider>

client/src/components/Chat/Input/ChatForm.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
useHandleKeyUp,
1616
useQueryParams,
1717
useSubmitMessage,
18+
useFocusChatEffect,
1819
} from '~/hooks';
1920
import { mainTextareaId, BadgeItem } from '~/common';
2021
import AttachFileChat from './Files/AttachFileChat';
@@ -36,14 +37,14 @@ import store from '~/store';
3637
const ChatForm = memo(({ index = 0 }: { index?: number }) => {
3738
const submitButtonRef = useRef<HTMLButtonElement>(null);
3839
const textAreaRef = useRef<HTMLTextAreaElement>(null);
40+
useFocusChatEffect(textAreaRef);
3941

4042
const [isCollapsed, setIsCollapsed] = useState(false);
4143
const [, setIsScrollable] = useState(false);
4244
const [visualRowCount, setVisualRowCount] = useState(1);
4345
const [isTextAreaFocused, setIsTextAreaFocused] = useState(false);
4446
const [backupBadges, setBackupBadges] = useState<Pick<BadgeItem, 'id'>[]>([]);
4547

46-
const search = useRecoilValue(store.search);
4748
const SpeechToText = useRecoilValue(store.speechToText);
4849
const TextToSpeech = useRecoilValue(store.textToSpeech);
4950
const chatDirection = useRecoilValue(store.chatDirection);

client/src/components/Nav/NewChat.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export default function NewChat({
4141
[],
4242
);
4343
newConvo();
44-
navigate('/c/new');
44+
navigate('/c/new', { state: { focusChat: true } });
4545
if (isSmallScreen) {
4646
toggleNav();
4747
}

client/src/hooks/Chat/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export { default as useChatHelpers } from './useChatHelpers';
22
export { default as useAddedHelpers } from './useAddedHelpers';
33
export { default as useAddedResponse } from './useAddedResponse';
44
export { default as useChatFunctions } from './useChatFunctions';
5+
export { default as useFocusChatEffect } from './useFocusChatEffect';

client/src/hooks/Chat/useChatFunctions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ export default function useChatFunctions({
148148
parentMessageId = Constants.NO_PARENT;
149149
currentMessages = [];
150150
conversationId = null;
151-
navigate('/c/new');
151+
navigate('/c/new', { state: { focusChat: true } });
152152
}
153153

154154
const targetParentMessageId = isRegenerate ? messageId : latestMessage?.parentMessageId;
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { useEffect } from 'react';
2+
import { useLocation, useNavigate } from 'react-router-dom';
3+
import { logger } from '~/utils';
4+
5+
export default function useFocusChatEffect(textAreaRef: React.RefObject<HTMLTextAreaElement>) {
6+
const location = useLocation();
7+
const navigate = useNavigate();
8+
useEffect(() => {
9+
if (textAreaRef?.current && location.state?.focusChat) {
10+
logger.log(
11+
'conversation',
12+
`Focusing textarea on location state change: ${location.pathname}`,
13+
);
14+
textAreaRef.current?.focus();
15+
navigate(`${location.pathname}${location.search ?? ''}`, { replace: true, state: {} });
16+
}
17+
}, [navigate, textAreaRef, location.pathname, location.state?.focusChat, location.search]);
18+
}

client/src/hooks/Conversations/useNavigateToConvo.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { useSetRecoilState } from 'recoil';
21
import { useNavigate } from 'react-router-dom';
32
import { useQueryClient } from '@tanstack/react-query';
3+
import { useSetRecoilState, useResetRecoilState } from 'recoil';
44
import {
55
QueryKeys,
66
Constants,
@@ -16,8 +16,9 @@ const useNavigateToConvo = (index = 0) => {
1616
const navigate = useNavigate();
1717
const queryClient = useQueryClient();
1818
const clearAllConversations = store.useClearConvoState();
19-
const clearAllLatestMessages = store.useClearLatestMessages(`useNavigateToConvo ${index}`);
19+
const resetArtifacts = useResetRecoilState(store.artifactsState);
2020
const setSubmission = useSetRecoilState(store.submissionByIndex(index));
21+
const clearAllLatestMessages = store.useClearLatestMessages(`useNavigateToConvo ${index}`);
2122
const { hasSetConversation, setConversation } = store.useCreateConversationAtom(index);
2223

2324
const fetchFreshData = async (conversationId?: string | null) => {
@@ -31,6 +32,7 @@ const useNavigateToConvo = (index = 0) => {
3132
logger.log('conversation', 'Fetched fresh conversation data', data);
3233
await queryClient.invalidateQueries([QueryKeys.messages, conversationId]);
3334
setConversation(data);
35+
navigate(`/c/${conversationId ?? Constants.NEW_CONVO}`, { state: { focusChat: true } });
3436
} catch (error) {
3537
console.error('Error fetching conversation data on navigation', error);
3638
}
@@ -82,11 +84,13 @@ const useNavigateToConvo = (index = 0) => {
8284
});
8385
}
8486
clearAllConversations(true);
87+
resetArtifacts();
8588
setConversation(convo);
86-
navigate(`/c/${convo.conversationId ?? Constants.NEW_CONVO}`);
8789
if (convo.conversationId !== Constants.NEW_CONVO && convo.conversationId) {
8890
queryClient.invalidateQueries([QueryKeys.conversation, convo.conversationId]);
8991
fetchFreshData(convo.conversationId);
92+
} else {
93+
navigate(`/c/${convo.conversationId ?? Constants.NEW_CONVO}`, { state: { focusChat: true } });
9094
}
9195
};
9296

client/src/hooks/useNewConvo.ts

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { useCallback, useRef } from 'react';
1+
import { useCallback } from 'react';
2+
import { useNavigate, useSearchParams } from 'react-router-dom';
23
import { useGetModelsQuery } from 'librechat-data-provider/react-query';
3-
import { useNavigate } from 'react-router-dom';
44
import {
55
Constants,
66
FileSources,
@@ -30,12 +30,12 @@ import { useDeleteFilesMutation, useGetEndpointsQuery, useGetStartupConfig } fro
3030
import useAssistantListMap from './Assistants/useAssistantListMap';
3131
import { useResetChatBadges } from './useChatBadges';
3232
import { usePauseGlobalAudio } from './Audio';
33-
import { mainTextareaId } from '~/common';
3433
import { logger } from '~/utils';
3534
import store from '~/store';
3635

3736
const useNewConvo = (index = 0) => {
3837
const navigate = useNavigate();
38+
const [searchParams] = useSearchParams();
3939
const { data: startupConfig } = useGetStartupConfig();
4040
const clearAllConversations = store.useClearConvoState();
4141
const defaultPreset = useRecoilValue(store.defaultPreset);
@@ -47,7 +47,6 @@ const useNewConvo = (index = 0) => {
4747
const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery();
4848

4949
const modelsQuery = useGetModelsQuery();
50-
const timeoutIdRef = useRef<NodeJS.Timeout>();
5150
const assistantsListMap = useAssistantListMap();
5251
const { pauseGlobalAudio } = usePauseGlobalAudio(index);
5352
const saveDrafts = useRecoilValue<boolean>(store.saveDrafts);
@@ -159,24 +158,24 @@ const useNewConvo = (index = 0) => {
159158
clearAllLatestMessages();
160159
}
161160

161+
const searchParamsString = searchParams?.toString();
162+
const getParams = () => (searchParamsString ? `?${searchParamsString}` : '');
163+
162164
if (conversation.conversationId === Constants.NEW_CONVO && !modelsData) {
163165
const appTitle = localStorage.getItem(LocalStorageKeys.APP_TITLE) ?? '';
164166
if (appTitle) {
165167
document.title = appTitle;
166168
}
167-
navigate(`/c/${Constants.NEW_CONVO}`);
168-
}
169-
170-
clearTimeout(timeoutIdRef.current);
171-
if (disableFocus === true) {
169+
const path = `/c/${Constants.NEW_CONVO}${getParams()}`;
170+
navigate(path, { state: { focusChat: true } });
172171
return;
173172
}
174-
timeoutIdRef.current = setTimeout(() => {
175-
const textarea = document.getElementById(mainTextareaId);
176-
if (textarea) {
177-
textarea.focus();
178-
}
179-
}, 150);
173+
174+
const path = `/c/${conversation.conversationId}${getParams()}`;
175+
navigate(path, {
176+
replace: true,
177+
state: disableFocus ? {} : { focusChat: true },
178+
});
180179
},
181180
[endpointsConfig, defaultPreset, assistantsListMap, modelsQuery.data],
182181
);

0 commit comments

Comments
 (0)