Skip to content

Commit

Permalink
⚡️ perf: refactor to improve chat performance (#4708)
Browse files Browse the repository at this point in the history
* ⚡️ perf: refactor to improve chat performance

* ⚡️ perf: refactor to improve chat performance
  • Loading branch information
arvinxx authored Nov 16, 2024
1 parent 9f118a4 commit 5512a82
Show file tree
Hide file tree
Showing 16 changed files with 400 additions and 270 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import { useUserStore } from '@/store/user';

import InputArea from './TextArea';

let setExpandMock: (expand: boolean) => void;
let onSendMock: () => void;

beforeEach(() => {
setExpandMock = vi.fn();
onSendMock = vi.fn();
});

describe('<InputArea />', () => {
Expand All @@ -29,13 +29,13 @@ describe('<InputArea />', () => {
});

it('renders with correct placeholder text', () => {
render(<InputArea setExpand={setExpandMock} />);
render(<InputArea onSend={onSendMock} />);
const textArea = screen.getByPlaceholderText('sendPlaceholder');
expect(textArea).toBeInTheDocument();
});

it('has the correct initial value', () => {
render(<InputArea setExpand={setExpandMock} />);
render(<InputArea onSend={onSendMock} />);
const textArea = screen.getByRole('textbox');
expect(textArea).toHaveValue('');
});
Expand Down Expand Up @@ -82,7 +82,7 @@ describe('<InputArea />', () => {
useChatStore.setState({ updateInputMessage: updateInputMessageMock });
});

render(<InputArea setExpand={setExpandMock} />);
render(<InputArea onSend={onSendMock} />);
const textArea = screen.getByRole('textbox');

// Start composition (IME input starts)
Expand All @@ -92,7 +92,7 @@ describe('<InputArea />', () => {
fireEvent.keyDown(textArea, { code: 'Enter', key: 'Enter' });

// Since we are in the middle of IME composition, the message should not be sent
expect(setExpandMock).not.toHaveBeenCalled();
expect(onSendMock).not.toHaveBeenCalled();
expect(updateInputMessageMock).not.toHaveBeenCalled();

// End composition (IME input ends)
Expand All @@ -102,7 +102,7 @@ describe('<InputArea />', () => {
fireEvent.keyDown(textArea, { code: 'Enter', key: 'Enter' });

// Since IME composition has ended, now the message should be sent
expect(setExpandMock).toHaveBeenCalled();
expect(onSendMock).toHaveBeenCalled();
expect(updateInputMessageMock).toHaveBeenCalled();
});

Expand All @@ -112,7 +112,7 @@ describe('<InputArea />', () => {
useChatStore.setState({ updateInputMessage: updateInputMessageMock });
});

render(<InputArea setExpand={setExpandMock} />);
render(<InputArea onSend={onSendMock} />);
const textArea = screen.getByRole('textbox');
const newText = 'New input text';

Expand Down Expand Up @@ -199,7 +199,7 @@ describe('<InputArea />', () => {
useChatStore.setState({ chatLoadingIds: ['123'], sendMessage: sendMessageMock });
});

render(<InputArea setExpand={setExpandMock} />);
render(<InputArea onSend={onSendMock} />);
const textArea = screen.getByRole('textbox');

fireEvent.keyDown(textArea, { code: 'Enter', key: 'Enter', shiftKey: true });
Expand Down Expand Up @@ -236,7 +236,7 @@ describe('<InputArea />', () => {
useUserStore.getState().updatePreference({ useCmdEnterToSend: true });
});

render(<InputArea setExpand={setExpandMock} />);
render(<InputArea onSend={onSendMock} />);
const textArea = screen.getByRole('textbox');

fireEvent.keyDown(textArea, { code: 'Enter', ctrlKey: true, key: 'Enter' });
Expand All @@ -256,7 +256,7 @@ describe('<InputArea />', () => {
useUserStore.getState().updatePreference({ useCmdEnterToSend: false });
});

render(<InputArea setExpand={setExpandMock} />);
render(<InputArea onSend={onSendMock} />);
const textArea = screen.getByRole('textbox');

fireEvent.keyDown(textArea, { code: 'Enter', ctrlKey: true, key: 'Enter' });
Expand All @@ -279,7 +279,7 @@ describe('<InputArea />', () => {
useUserStore.getState().updatePreference({ useCmdEnterToSend: true });
});

render(<InputArea setExpand={setExpandMock} />);
render(<InputArea onSend={onSendMock} />);
const textArea = screen.getByRole('textbox');

fireEvent.keyDown(textArea, { code: 'Enter', key: 'Enter', metaKey: true });
Expand All @@ -304,7 +304,7 @@ describe('<InputArea />', () => {
useUserStore.getState().updatePreference({ useCmdEnterToSend: false });
});

render(<InputArea setExpand={setExpandMock} />);
render(<InputArea onSend={onSendMock} />);
const textArea = screen.getByRole('textbox');

fireEvent.keyDown(textArea, { code: 'Enter', key: 'Enter', metaKey: true });
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { memo } from 'react';

import InputArea from '@/features/ChatInput/Desktop/InputArea';
import { useSendMessage } from '@/features/ChatInput/useSend';
import { useChatStore } from '@/store/chat';
import { chatSelectors } from '@/store/chat/slices/message/selectors';

const TextArea = memo<{ onSend?: () => void }>(({ onSend }) => {
const [loading, value, updateInputMessage] = useChatStore((s) => [
chatSelectors.isAIGenerating(s),
s.inputMessage,
s.updateInputMessage,
]);
const { send: sendMessage } = useSendMessage();

return (
<InputArea
loading={loading}
onChange={updateInputMessage}
onSend={() => {
sendMessage();
onSend?.();
}}
value={value}
/>
);
});

export default TextArea;
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
'use client';

import { memo } from 'react';

import { ActionKeys } from '@/features/ChatInput/ActionBar/config';
import DesktopChatInput from '@/features/ChatInput/Desktop';
import { useGlobalStore } from '@/store/global';
import { systemStatusSelectors } from '@/store/global/selectors';

import TextArea from './TextArea';

const leftActions = [
'model',
'fileUpload',
'knowledgeBase',
'temperature',
'history',
'stt',
'tools',
'token',
] as ActionKeys[];

const rightActions = ['clear'] as ActionKeys[];

const renderTextArea = (onSend: () => void) => <TextArea onSend={onSend} />;

const Desktop = memo(() => {
const [inputHeight, updatePreference] = useGlobalStore((s) => [
systemStatusSelectors.inputHeight(s),
s.updateSystemStatus,
]);

return (
<DesktopChatInput
inputHeight={inputHeight}
leftActions={leftActions}
onInputHeightChange={(height) => {
updatePreference({ inputHeight: height });
}}
renderTextArea={renderTextArea}
rightActions={rightActions}
/>
);
});

export default Desktop;
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import DesktopChatInput from '@/features/ChatInput/Desktop';
import MobileChatInput from '@/features/ChatInput/Mobile';
import { isMobileDevice } from '@/utils/server/responsive';

import DesktopChatInput from './Desktop';

const ChatInput = () => {
const mobile = isMobileDevice();
const Input = mobile ? MobileChatInput : DesktopChatInput;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
import isEqual from 'fast-deep-equal';
import React, { memo } from 'react';

import { WELCOME_GUIDE_CHAT_ID } from '@/const/session';
import { VirtualizedList } from '@/features/Conversation';
import { InboxWelcome, VirtualizedList } from '@/features/Conversation';
import { useChatStore } from '@/store/chat';
import { chatSelectors } from '@/store/chat/selectors';
import { useSessionStore } from '@/store/session';
Expand All @@ -14,22 +13,25 @@ interface ListProps {
}

const Content = memo<ListProps>(({ mobile }) => {
const [activeTopicId, useFetchMessages] = useChatStore((s) => [
s.activeTopicId,
s.useFetchMessages,
]);
const [activeTopicId, useFetchMessages, showInboxWelcome, isCurrentChatLoaded] = useChatStore(
(s) => [
s.activeTopicId,
s.useFetchMessages,
chatSelectors.showInboxWelcome(s),
chatSelectors.isCurrentChatLoaded(s),
],
);

const [sessionId] = useSessionStore((s) => [s.activeId]);
useFetchMessages(sessionId, activeTopicId);

const data = useChatStore((s) => {
const showInboxWelcome = chatSelectors.showInboxWelcome(s);
if (showInboxWelcome) return [WELCOME_GUIDE_CHAT_ID];
const data = useChatStore(chatSelectors.currentChatIDsWithGuideMessage, isEqual);

return chatSelectors.currentChatIDsWithGuideMessage(s);
}, isEqual);
if (showInboxWelcome && isCurrentChatLoaded) return <InboxWelcome />;

return <VirtualizedList dataSource={data} mobile={mobile} />;
});

Content.displayName = 'ChatListRender';

export default Content;
4 changes: 4 additions & 0 deletions src/const/message.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
export const LOADING_FLAT = '...';

export const MESSAGE_CANCEL_FLAT = 'canceled';

export const MESSAGE_THREAD_DIVIDER_ID = 'thread-divider';

export const MESSAGE_WELCOME_GUIDE_ID = 'welcome';
61 changes: 61 additions & 0 deletions src/features/ChatInput/Desktop/Footer/ShortcutHint.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Icon } from '@lobehub/ui';
import { Skeleton } from 'antd';
import { useTheme } from 'antd-style';
import { ChevronUp, CornerDownLeft, LucideCommand } from 'lucide-react';
import { memo, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Center, Flexbox } from 'react-layout-kit';

import { useUserStore } from '@/store/user';
import { preferenceSelectors } from '@/store/user/selectors';
import { isMacOS } from '@/utils/platform';

const ShortcutHint = memo(() => {
const { t } = useTranslation('chat');
const theme = useTheme();
const useCmdEnterToSend = useUserStore(preferenceSelectors.useCmdEnterToSend);
const [isMac, setIsMac] = useState<boolean>();

useEffect(() => {
setIsMac(isMacOS());
}, []);

const cmdEnter = (
<Flexbox gap={2} horizontal>
{typeof isMac === 'boolean' ? (
<Icon icon={isMac ? LucideCommand : ChevronUp} />
) : (
<Skeleton.Node active style={{ height: '100%', width: 12 }}>
{' '}
</Skeleton.Node>
)}
<Icon icon={CornerDownLeft} />
</Flexbox>
);

const enter = (
<Center>
<Icon icon={CornerDownLeft} />
</Center>
);

const sendShortcut = useCmdEnterToSend ? cmdEnter : enter;

const wrapperShortcut = useCmdEnterToSend ? enter : cmdEnter;

return (
<Flexbox
gap={4}
horizontal
style={{ color: theme.colorTextDescription, fontSize: 12, marginRight: 12 }}
>
{sendShortcut}
<span>{t('input.send')}</span>
<span>/</span>
{wrapperShortcut}
<span>{t('input.warp')}</span>
</Flexbox>
);
});

export default ShortcutHint;
Loading

0 comments on commit 5512a82

Please sign in to comment.