@lobehub/chat 1.31.9 → 1.31.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/package.json +1 -1
- package/src/{features → app/(main)/chat/(workspace)/@conversation/features}/ChatInput/Desktop/TextArea.test.tsx +13 -13
- package/src/app/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/TextArea.tsx +29 -0
- package/src/app/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/index.tsx +46 -0
- package/src/app/(main)/chat/(workspace)/@conversation/features/ChatInput/index.tsx +2 -1
- package/src/app/(main)/chat/(workspace)/@conversation/features/ChatList/Content.tsx +13 -11
- package/src/const/message.ts +4 -0
- package/src/features/ChatInput/Desktop/Footer/ShortcutHint.tsx +61 -0
- package/src/features/ChatInput/Desktop/Footer/index.tsx +11 -46
- package/src/features/ChatInput/Desktop/{TextArea.tsx → InputArea/index.tsx} +11 -20
- package/src/features/ChatInput/Desktop/index.tsx +23 -32
- package/src/features/Conversation/Messages/index.ts +11 -11
- package/src/features/Conversation/components/ChatItem/index.tsx +83 -44
- package/src/features/Conversation/components/VirtualizedList/index.tsx +90 -92
- package/src/features/Conversation/index.ts +2 -0
- package/src/types/topic/index.ts +1 -0
- /package/src/types/{topic.ts → topic/topic.ts} +0 -0
package/CHANGELOG.md
CHANGED
@@ -2,6 +2,23 @@
|
|
2
2
|
|
3
3
|
# Changelog
|
4
4
|
|
5
|
+
### [Version 1.31.10](https://github.com/lobehub/lobe-chat/compare/v1.31.9...v1.31.10)
|
6
|
+
|
7
|
+
<sup>Released on **2024-11-16**</sup>
|
8
|
+
|
9
|
+
<br/>
|
10
|
+
|
11
|
+
<details>
|
12
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
13
|
+
|
14
|
+
</details>
|
15
|
+
|
16
|
+
<div align="right">
|
17
|
+
|
18
|
+
[](#readme-top)
|
19
|
+
|
20
|
+
</div>
|
21
|
+
|
5
22
|
### [Version 1.31.9](https://github.com/lobehub/lobe-chat/compare/v1.31.8...v1.31.9)
|
6
23
|
|
7
24
|
<sup>Released on **2024-11-16**</sup>
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@lobehub/chat",
|
3
|
-
"version": "1.31.
|
3
|
+
"version": "1.31.10",
|
4
4
|
"description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
|
5
5
|
"keywords": [
|
6
6
|
"framework",
|
@@ -7,10 +7,10 @@ import { useUserStore } from '@/store/user';
|
|
7
7
|
|
8
8
|
import InputArea from './TextArea';
|
9
9
|
|
10
|
-
let
|
10
|
+
let onSendMock: () => void;
|
11
11
|
|
12
12
|
beforeEach(() => {
|
13
|
-
|
13
|
+
onSendMock = vi.fn();
|
14
14
|
});
|
15
15
|
|
16
16
|
describe('<InputArea />', () => {
|
@@ -29,13 +29,13 @@ describe('<InputArea />', () => {
|
|
29
29
|
});
|
30
30
|
|
31
31
|
it('renders with correct placeholder text', () => {
|
32
|
-
render(<InputArea
|
32
|
+
render(<InputArea onSend={onSendMock} />);
|
33
33
|
const textArea = screen.getByPlaceholderText('sendPlaceholder');
|
34
34
|
expect(textArea).toBeInTheDocument();
|
35
35
|
});
|
36
36
|
|
37
37
|
it('has the correct initial value', () => {
|
38
|
-
render(<InputArea
|
38
|
+
render(<InputArea onSend={onSendMock} />);
|
39
39
|
const textArea = screen.getByRole('textbox');
|
40
40
|
expect(textArea).toHaveValue('');
|
41
41
|
});
|
@@ -82,7 +82,7 @@ describe('<InputArea />', () => {
|
|
82
82
|
useChatStore.setState({ updateInputMessage: updateInputMessageMock });
|
83
83
|
});
|
84
84
|
|
85
|
-
render(<InputArea
|
85
|
+
render(<InputArea onSend={onSendMock} />);
|
86
86
|
const textArea = screen.getByRole('textbox');
|
87
87
|
|
88
88
|
// Start composition (IME input starts)
|
@@ -92,7 +92,7 @@ describe('<InputArea />', () => {
|
|
92
92
|
fireEvent.keyDown(textArea, { code: 'Enter', key: 'Enter' });
|
93
93
|
|
94
94
|
// Since we are in the middle of IME composition, the message should not be sent
|
95
|
-
expect(
|
95
|
+
expect(onSendMock).not.toHaveBeenCalled();
|
96
96
|
expect(updateInputMessageMock).not.toHaveBeenCalled();
|
97
97
|
|
98
98
|
// End composition (IME input ends)
|
@@ -102,7 +102,7 @@ describe('<InputArea />', () => {
|
|
102
102
|
fireEvent.keyDown(textArea, { code: 'Enter', key: 'Enter' });
|
103
103
|
|
104
104
|
// Since IME composition has ended, now the message should be sent
|
105
|
-
expect(
|
105
|
+
expect(onSendMock).toHaveBeenCalled();
|
106
106
|
expect(updateInputMessageMock).toHaveBeenCalled();
|
107
107
|
});
|
108
108
|
|
@@ -112,7 +112,7 @@ describe('<InputArea />', () => {
|
|
112
112
|
useChatStore.setState({ updateInputMessage: updateInputMessageMock });
|
113
113
|
});
|
114
114
|
|
115
|
-
render(<InputArea
|
115
|
+
render(<InputArea onSend={onSendMock} />);
|
116
116
|
const textArea = screen.getByRole('textbox');
|
117
117
|
const newText = 'New input text';
|
118
118
|
|
@@ -199,7 +199,7 @@ describe('<InputArea />', () => {
|
|
199
199
|
useChatStore.setState({ chatLoadingIds: ['123'], sendMessage: sendMessageMock });
|
200
200
|
});
|
201
201
|
|
202
|
-
render(<InputArea
|
202
|
+
render(<InputArea onSend={onSendMock} />);
|
203
203
|
const textArea = screen.getByRole('textbox');
|
204
204
|
|
205
205
|
fireEvent.keyDown(textArea, { code: 'Enter', key: 'Enter', shiftKey: true });
|
@@ -236,7 +236,7 @@ describe('<InputArea />', () => {
|
|
236
236
|
useUserStore.getState().updatePreference({ useCmdEnterToSend: true });
|
237
237
|
});
|
238
238
|
|
239
|
-
render(<InputArea
|
239
|
+
render(<InputArea onSend={onSendMock} />);
|
240
240
|
const textArea = screen.getByRole('textbox');
|
241
241
|
|
242
242
|
fireEvent.keyDown(textArea, { code: 'Enter', ctrlKey: true, key: 'Enter' });
|
@@ -256,7 +256,7 @@ describe('<InputArea />', () => {
|
|
256
256
|
useUserStore.getState().updatePreference({ useCmdEnterToSend: false });
|
257
257
|
});
|
258
258
|
|
259
|
-
render(<InputArea
|
259
|
+
render(<InputArea onSend={onSendMock} />);
|
260
260
|
const textArea = screen.getByRole('textbox');
|
261
261
|
|
262
262
|
fireEvent.keyDown(textArea, { code: 'Enter', ctrlKey: true, key: 'Enter' });
|
@@ -279,7 +279,7 @@ describe('<InputArea />', () => {
|
|
279
279
|
useUserStore.getState().updatePreference({ useCmdEnterToSend: true });
|
280
280
|
});
|
281
281
|
|
282
|
-
render(<InputArea
|
282
|
+
render(<InputArea onSend={onSendMock} />);
|
283
283
|
const textArea = screen.getByRole('textbox');
|
284
284
|
|
285
285
|
fireEvent.keyDown(textArea, { code: 'Enter', key: 'Enter', metaKey: true });
|
@@ -304,7 +304,7 @@ describe('<InputArea />', () => {
|
|
304
304
|
useUserStore.getState().updatePreference({ useCmdEnterToSend: false });
|
305
305
|
});
|
306
306
|
|
307
|
-
render(<InputArea
|
307
|
+
render(<InputArea onSend={onSendMock} />);
|
308
308
|
const textArea = screen.getByRole('textbox');
|
309
309
|
|
310
310
|
fireEvent.keyDown(textArea, { code: 'Enter', key: 'Enter', metaKey: true });
|
@@ -0,0 +1,29 @@
|
|
1
|
+
import { memo } from 'react';
|
2
|
+
|
3
|
+
import InputArea from '@/features/ChatInput/Desktop/InputArea';
|
4
|
+
import { useSendMessage } from '@/features/ChatInput/useSend';
|
5
|
+
import { useChatStore } from '@/store/chat';
|
6
|
+
import { chatSelectors } from '@/store/chat/slices/message/selectors';
|
7
|
+
|
8
|
+
const TextArea = memo<{ onSend?: () => void }>(({ onSend }) => {
|
9
|
+
const [loading, value, updateInputMessage] = useChatStore((s) => [
|
10
|
+
chatSelectors.isAIGenerating(s),
|
11
|
+
s.inputMessage,
|
12
|
+
s.updateInputMessage,
|
13
|
+
]);
|
14
|
+
const { send: sendMessage } = useSendMessage();
|
15
|
+
|
16
|
+
return (
|
17
|
+
<InputArea
|
18
|
+
loading={loading}
|
19
|
+
onChange={updateInputMessage}
|
20
|
+
onSend={() => {
|
21
|
+
sendMessage();
|
22
|
+
onSend?.();
|
23
|
+
}}
|
24
|
+
value={value}
|
25
|
+
/>
|
26
|
+
);
|
27
|
+
});
|
28
|
+
|
29
|
+
export default TextArea;
|
@@ -0,0 +1,46 @@
|
|
1
|
+
'use client';
|
2
|
+
|
3
|
+
import { memo } from 'react';
|
4
|
+
|
5
|
+
import { ActionKeys } from '@/features/ChatInput/ActionBar/config';
|
6
|
+
import DesktopChatInput from '@/features/ChatInput/Desktop';
|
7
|
+
import { useGlobalStore } from '@/store/global';
|
8
|
+
import { systemStatusSelectors } from '@/store/global/selectors';
|
9
|
+
|
10
|
+
import TextArea from './TextArea';
|
11
|
+
|
12
|
+
const leftActions = [
|
13
|
+
'model',
|
14
|
+
'fileUpload',
|
15
|
+
'knowledgeBase',
|
16
|
+
'temperature',
|
17
|
+
'history',
|
18
|
+
'stt',
|
19
|
+
'tools',
|
20
|
+
'token',
|
21
|
+
] as ActionKeys[];
|
22
|
+
|
23
|
+
const rightActions = ['clear'] as ActionKeys[];
|
24
|
+
|
25
|
+
const renderTextArea = (onSend: () => void) => <TextArea onSend={onSend} />;
|
26
|
+
|
27
|
+
const Desktop = memo(() => {
|
28
|
+
const [inputHeight, updatePreference] = useGlobalStore((s) => [
|
29
|
+
systemStatusSelectors.inputHeight(s),
|
30
|
+
s.updateSystemStatus,
|
31
|
+
]);
|
32
|
+
|
33
|
+
return (
|
34
|
+
<DesktopChatInput
|
35
|
+
inputHeight={inputHeight}
|
36
|
+
leftActions={leftActions}
|
37
|
+
onInputHeightChange={(height) => {
|
38
|
+
updatePreference({ inputHeight: height });
|
39
|
+
}}
|
40
|
+
renderTextArea={renderTextArea}
|
41
|
+
rightActions={rightActions}
|
42
|
+
/>
|
43
|
+
);
|
44
|
+
});
|
45
|
+
|
46
|
+
export default Desktop;
|
@@ -1,7 +1,8 @@
|
|
1
|
-
import DesktopChatInput from '@/features/ChatInput/Desktop';
|
2
1
|
import MobileChatInput from '@/features/ChatInput/Mobile';
|
3
2
|
import { isMobileDevice } from '@/utils/server/responsive';
|
4
3
|
|
4
|
+
import DesktopChatInput from './Desktop';
|
5
|
+
|
5
6
|
const ChatInput = () => {
|
6
7
|
const mobile = isMobileDevice();
|
7
8
|
const Input = mobile ? MobileChatInput : DesktopChatInput;
|
@@ -3,8 +3,7 @@
|
|
3
3
|
import isEqual from 'fast-deep-equal';
|
4
4
|
import React, { memo } from 'react';
|
5
5
|
|
6
|
-
import {
|
7
|
-
import { VirtualizedList } from '@/features/Conversation';
|
6
|
+
import { InboxWelcome, VirtualizedList } from '@/features/Conversation';
|
8
7
|
import { useChatStore } from '@/store/chat';
|
9
8
|
import { chatSelectors } from '@/store/chat/selectors';
|
10
9
|
import { useSessionStore } from '@/store/session';
|
@@ -14,22 +13,25 @@ interface ListProps {
|
|
14
13
|
}
|
15
14
|
|
16
15
|
const Content = memo<ListProps>(({ mobile }) => {
|
17
|
-
const [activeTopicId, useFetchMessages] = useChatStore(
|
18
|
-
s
|
19
|
-
|
20
|
-
|
16
|
+
const [activeTopicId, useFetchMessages, showInboxWelcome, isCurrentChatLoaded] = useChatStore(
|
17
|
+
(s) => [
|
18
|
+
s.activeTopicId,
|
19
|
+
s.useFetchMessages,
|
20
|
+
chatSelectors.showInboxWelcome(s),
|
21
|
+
chatSelectors.isCurrentChatLoaded(s),
|
22
|
+
],
|
23
|
+
);
|
21
24
|
|
22
25
|
const [sessionId] = useSessionStore((s) => [s.activeId]);
|
23
26
|
useFetchMessages(sessionId, activeTopicId);
|
24
27
|
|
25
|
-
const data = useChatStore(
|
26
|
-
const showInboxWelcome = chatSelectors.showInboxWelcome(s);
|
27
|
-
if (showInboxWelcome) return [WELCOME_GUIDE_CHAT_ID];
|
28
|
+
const data = useChatStore(chatSelectors.currentChatIDsWithGuideMessage, isEqual);
|
28
29
|
|
29
|
-
|
30
|
-
}, isEqual);
|
30
|
+
if (showInboxWelcome && isCurrentChatLoaded) return <InboxWelcome />;
|
31
31
|
|
32
32
|
return <VirtualizedList dataSource={data} mobile={mobile} />;
|
33
33
|
});
|
34
34
|
|
35
|
+
Content.displayName = 'ChatListRender';
|
36
|
+
|
35
37
|
export default Content;
|
package/src/const/message.ts
CHANGED
@@ -0,0 +1,61 @@
|
|
1
|
+
import { Icon } from '@lobehub/ui';
|
2
|
+
import { Skeleton } from 'antd';
|
3
|
+
import { useTheme } from 'antd-style';
|
4
|
+
import { ChevronUp, CornerDownLeft, LucideCommand } from 'lucide-react';
|
5
|
+
import { memo, useEffect, useState } from 'react';
|
6
|
+
import { useTranslation } from 'react-i18next';
|
7
|
+
import { Center, Flexbox } from 'react-layout-kit';
|
8
|
+
|
9
|
+
import { useUserStore } from '@/store/user';
|
10
|
+
import { preferenceSelectors } from '@/store/user/selectors';
|
11
|
+
import { isMacOS } from '@/utils/platform';
|
12
|
+
|
13
|
+
const ShortcutHint = memo(() => {
|
14
|
+
const { t } = useTranslation('chat');
|
15
|
+
const theme = useTheme();
|
16
|
+
const useCmdEnterToSend = useUserStore(preferenceSelectors.useCmdEnterToSend);
|
17
|
+
const [isMac, setIsMac] = useState<boolean>();
|
18
|
+
|
19
|
+
useEffect(() => {
|
20
|
+
setIsMac(isMacOS());
|
21
|
+
}, []);
|
22
|
+
|
23
|
+
const cmdEnter = (
|
24
|
+
<Flexbox gap={2} horizontal>
|
25
|
+
{typeof isMac === 'boolean' ? (
|
26
|
+
<Icon icon={isMac ? LucideCommand : ChevronUp} />
|
27
|
+
) : (
|
28
|
+
<Skeleton.Node active style={{ height: '100%', width: 12 }}>
|
29
|
+
{' '}
|
30
|
+
</Skeleton.Node>
|
31
|
+
)}
|
32
|
+
<Icon icon={CornerDownLeft} />
|
33
|
+
</Flexbox>
|
34
|
+
);
|
35
|
+
|
36
|
+
const enter = (
|
37
|
+
<Center>
|
38
|
+
<Icon icon={CornerDownLeft} />
|
39
|
+
</Center>
|
40
|
+
);
|
41
|
+
|
42
|
+
const sendShortcut = useCmdEnterToSend ? cmdEnter : enter;
|
43
|
+
|
44
|
+
const wrapperShortcut = useCmdEnterToSend ? enter : cmdEnter;
|
45
|
+
|
46
|
+
return (
|
47
|
+
<Flexbox
|
48
|
+
gap={4}
|
49
|
+
horizontal
|
50
|
+
style={{ color: theme.colorTextDescription, fontSize: 12, marginRight: 12 }}
|
51
|
+
>
|
52
|
+
{sendShortcut}
|
53
|
+
<span>{t('input.send')}</span>
|
54
|
+
<span>/</span>
|
55
|
+
{wrapperShortcut}
|
56
|
+
<span>{t('input.warp')}</span>
|
57
|
+
</Flexbox>
|
58
|
+
);
|
59
|
+
});
|
60
|
+
|
61
|
+
export default ShortcutHint;
|
@@ -1,23 +1,20 @@
|
|
1
|
-
import {
|
2
|
-
import { Button, Skeleton, Space } from 'antd';
|
1
|
+
import { Button, Space } from 'antd';
|
3
2
|
import { createStyles } from 'antd-style';
|
4
|
-
import { ChevronUp, CornerDownLeft, LucideCommand } from 'lucide-react';
|
5
3
|
import { rgba } from 'polished';
|
6
4
|
import { memo, useEffect, useState } from 'react';
|
7
5
|
import { useTranslation } from 'react-i18next';
|
8
|
-
import {
|
6
|
+
import { Flexbox } from 'react-layout-kit';
|
9
7
|
|
10
8
|
import StopLoadingIcon from '@/components/StopLoading';
|
11
|
-
import SaveTopic from '@/features/ChatInput/Topic';
|
12
9
|
import { useSendMessage } from '@/features/ChatInput/useSend';
|
13
10
|
import { useChatStore } from '@/store/chat';
|
14
11
|
import { chatSelectors } from '@/store/chat/selectors';
|
15
|
-
import { useUserStore } from '@/store/user';
|
16
|
-
import { preferenceSelectors } from '@/store/user/selectors';
|
17
12
|
import { isMacOS } from '@/utils/platform';
|
18
13
|
|
14
|
+
import SaveTopic from '../../Topic';
|
19
15
|
import LocalFiles from '../FilePreview';
|
20
16
|
import SendMore from './SendMore';
|
17
|
+
import ShortcutHint from './ShortcutHint';
|
21
18
|
|
22
19
|
const useStyles = createStyles(({ css, prefixCls, token }) => {
|
23
20
|
return {
|
@@ -48,51 +45,29 @@ const useStyles = createStyles(({ css, prefixCls, token }) => {
|
|
48
45
|
|
49
46
|
interface FooterProps {
|
50
47
|
expand: boolean;
|
48
|
+
saveTopic?: boolean;
|
51
49
|
setExpand?: (expand: boolean) => void;
|
50
|
+
shortcutHint?: boolean;
|
52
51
|
}
|
53
52
|
|
54
|
-
const Footer = memo<FooterProps>(({ setExpand, expand }) => {
|
53
|
+
const Footer = memo<FooterProps>(({ setExpand, expand, shortcutHint = true, saveTopic = true }) => {
|
55
54
|
const { t } = useTranslation('chat');
|
56
55
|
|
57
|
-
const {
|
56
|
+
const { styles } = useStyles();
|
58
57
|
|
59
58
|
const [isAIGenerating, stopGenerateMessage] = useChatStore((s) => [
|
60
59
|
chatSelectors.isAIGenerating(s),
|
61
60
|
s.stopGenerateMessage,
|
62
61
|
]);
|
63
62
|
|
64
|
-
const [useCmdEnterToSend] = useUserStore((s) => [preferenceSelectors.useCmdEnterToSend(s)]);
|
65
|
-
|
66
63
|
const { send: sendMessage, canSend } = useSendMessage();
|
67
64
|
|
68
65
|
const [isMac, setIsMac] = useState<boolean>();
|
66
|
+
|
69
67
|
useEffect(() => {
|
70
68
|
setIsMac(isMacOS());
|
71
69
|
}, [setIsMac]);
|
72
70
|
|
73
|
-
const cmdEnter = (
|
74
|
-
<Flexbox gap={2} horizontal>
|
75
|
-
{typeof isMac === 'boolean' ? (
|
76
|
-
<Icon icon={isMac ? LucideCommand : ChevronUp} />
|
77
|
-
) : (
|
78
|
-
<Skeleton.Node active style={{ height: '100%', width: 12 }}>
|
79
|
-
{' '}
|
80
|
-
</Skeleton.Node>
|
81
|
-
)}
|
82
|
-
<Icon icon={CornerDownLeft} />
|
83
|
-
</Flexbox>
|
84
|
-
);
|
85
|
-
|
86
|
-
const enter = (
|
87
|
-
<Center>
|
88
|
-
<Icon icon={CornerDownLeft} />
|
89
|
-
</Center>
|
90
|
-
);
|
91
|
-
|
92
|
-
const sendShortcut = useCmdEnterToSend ? cmdEnter : enter;
|
93
|
-
|
94
|
-
const wrapperShortcut = useCmdEnterToSend ? enter : cmdEnter;
|
95
|
-
|
96
71
|
return (
|
97
72
|
<Flexbox
|
98
73
|
align={'end'}
|
@@ -107,18 +82,8 @@ const Footer = memo<FooterProps>(({ setExpand, expand }) => {
|
|
107
82
|
{expand && <LocalFiles />}
|
108
83
|
</Flexbox>
|
109
84
|
<Flexbox align={'center'} flex={'none'} gap={8} horizontal>
|
110
|
-
<
|
111
|
-
|
112
|
-
horizontal
|
113
|
-
style={{ color: theme.colorTextDescription, fontSize: 12, marginRight: 12 }}
|
114
|
-
>
|
115
|
-
{sendShortcut}
|
116
|
-
<span>{t('input.send')}</span>
|
117
|
-
<span>/</span>
|
118
|
-
{wrapperShortcut}
|
119
|
-
<span>{t('input.warp')}</span>
|
120
|
-
</Flexbox>
|
121
|
-
<SaveTopic />
|
85
|
+
{shortcutHint && <ShortcutHint />}
|
86
|
+
{saveTopic && <SaveTopic />}
|
122
87
|
<Flexbox style={{ minWidth: 92 }}>
|
123
88
|
{isAIGenerating ? (
|
124
89
|
<Button
|
@@ -4,14 +4,11 @@ import { TextAreaRef } from 'antd/es/input/TextArea';
|
|
4
4
|
import { memo, useEffect, useRef } from 'react';
|
5
5
|
import { useTranslation } from 'react-i18next';
|
6
6
|
|
7
|
-
import { useSendMessage } from '@/features/ChatInput/useSend';
|
8
|
-
import { useChatStore } from '@/store/chat';
|
9
|
-
import { chatSelectors } from '@/store/chat/selectors';
|
10
7
|
import { useUserStore } from '@/store/user';
|
11
8
|
import { preferenceSelectors } from '@/store/user/selectors';
|
12
9
|
import { isCommandPressed } from '@/utils/keyboard';
|
13
10
|
|
14
|
-
import { useAutoFocus } from '
|
11
|
+
import { useAutoFocus } from '../useAutoFocus';
|
15
12
|
|
16
13
|
const useStyles = createStyles(({ css }) => {
|
17
14
|
return {
|
@@ -34,25 +31,20 @@ const useStyles = createStyles(({ css }) => {
|
|
34
31
|
});
|
35
32
|
|
36
33
|
interface InputAreaProps {
|
37
|
-
|
34
|
+
loading?: boolean;
|
35
|
+
onChange: (string: string) => void;
|
36
|
+
onSend: () => void;
|
37
|
+
value: string;
|
38
38
|
}
|
39
39
|
|
40
|
-
const InputArea = memo<InputAreaProps>(({
|
40
|
+
const InputArea = memo<InputAreaProps>(({ onSend, value, loading, onChange }) => {
|
41
41
|
const { t } = useTranslation('chat');
|
42
42
|
const { styles } = useStyles();
|
43
43
|
const ref = useRef<TextAreaRef>(null);
|
44
44
|
const isChineseInput = useRef(false);
|
45
45
|
|
46
|
-
const [loading, value, updateInputMessage] = useChatStore((s) => [
|
47
|
-
chatSelectors.isAIGenerating(s),
|
48
|
-
s.inputMessage,
|
49
|
-
s.updateInputMessage,
|
50
|
-
]);
|
51
|
-
|
52
46
|
const useCmdEnterToSend = useUserStore(preferenceSelectors.useCmdEnterToSend);
|
53
47
|
|
54
|
-
const { send: sendMessage } = useSendMessage();
|
55
|
-
|
56
48
|
useAutoFocus(ref);
|
57
49
|
|
58
50
|
const hasValue = !!value;
|
@@ -78,10 +70,10 @@ const InputArea = memo<InputAreaProps>(({ setExpand }) => {
|
|
78
70
|
autoFocus
|
79
71
|
className={styles.textarea}
|
80
72
|
onBlur={(e) => {
|
81
|
-
|
73
|
+
onChange?.(e.target.value);
|
82
74
|
}}
|
83
75
|
onChange={(e) => {
|
84
|
-
|
76
|
+
onChange?.(e.target.value);
|
85
77
|
}}
|
86
78
|
onCompositionEnd={() => {
|
87
79
|
isChineseInput.current = false;
|
@@ -98,8 +90,7 @@ const InputArea = memo<InputAreaProps>(({ setExpand }) => {
|
|
98
90
|
// refs: https://github.com/lobehub/lobe-chat/pull/989
|
99
91
|
e.preventDefault();
|
100
92
|
|
101
|
-
|
102
|
-
setExpand?.(false);
|
93
|
+
onSend();
|
103
94
|
};
|
104
95
|
const commandKey = isCommandPressed(e);
|
105
96
|
|
@@ -109,7 +100,7 @@ const InputArea = memo<InputAreaProps>(({ setExpand }) => {
|
|
109
100
|
} else {
|
110
101
|
// cmd + enter to wrap
|
111
102
|
if (commandKey) {
|
112
|
-
|
103
|
+
onChange?.((e.target as any).value + '\n');
|
113
104
|
return;
|
114
105
|
}
|
115
106
|
|
@@ -125,6 +116,6 @@ const InputArea = memo<InputAreaProps>(({ setExpand }) => {
|
|
125
116
|
);
|
126
117
|
});
|
127
118
|
|
128
|
-
InputArea.displayName = '
|
119
|
+
InputArea.displayName = 'DesktopInputArea';
|
129
120
|
|
130
121
|
export default InputArea;
|
@@ -1,59 +1,50 @@
|
|
1
1
|
'use client';
|
2
2
|
|
3
3
|
import { DraggablePanel } from '@lobehub/ui';
|
4
|
-
import { memo, useState } from 'react';
|
4
|
+
import { ReactNode, memo, useCallback, useState } from 'react';
|
5
5
|
import { Flexbox } from 'react-layout-kit';
|
6
6
|
|
7
7
|
import { CHAT_TEXTAREA_HEIGHT, CHAT_TEXTAREA_MAX_HEIGHT } from '@/const/layoutTokens';
|
8
|
-
import { ActionKeys } from '@/features/ChatInput/ActionBar/config';
|
9
|
-
import { useGlobalStore } from '@/store/global';
|
10
|
-
import { systemStatusSelectors } from '@/store/global/selectors';
|
11
8
|
|
9
|
+
import { ActionKeys } from '../ActionBar/config';
|
12
10
|
import LocalFiles from './FilePreview';
|
13
11
|
import Footer from './Footer';
|
14
12
|
import Head from './Header';
|
15
|
-
import TextArea from './TextArea';
|
16
|
-
|
17
|
-
const defaultLeftActions = [
|
18
|
-
'model',
|
19
|
-
'fileUpload',
|
20
|
-
'knowledgeBase',
|
21
|
-
'temperature',
|
22
|
-
'history',
|
23
|
-
'stt',
|
24
|
-
'tools',
|
25
|
-
'token',
|
26
|
-
] as ActionKeys[];
|
27
|
-
|
28
|
-
const defaultRightActions = ['clear'] as ActionKeys[];
|
29
13
|
|
30
14
|
interface DesktopChatInputProps {
|
31
|
-
|
32
|
-
|
15
|
+
footer?: {
|
16
|
+
saveTopic?: boolean;
|
17
|
+
shortcutHint?: boolean;
|
18
|
+
};
|
19
|
+
inputHeight: number;
|
20
|
+
leftActions: ActionKeys[];
|
21
|
+
onInputHeightChange?: (height: number) => void;
|
22
|
+
renderTextArea: (onSend: () => void) => ReactNode;
|
23
|
+
rightActions: ActionKeys[];
|
33
24
|
}
|
25
|
+
|
34
26
|
const DesktopChatInput = memo<DesktopChatInputProps>(
|
35
|
-
({ leftActions
|
27
|
+
({ leftActions, rightActions, footer, renderTextArea, inputHeight, onInputHeightChange }) => {
|
36
28
|
const [expand, setExpand] = useState<boolean>(false);
|
37
29
|
|
38
|
-
const
|
39
|
-
|
40
|
-
|
41
|
-
]);
|
30
|
+
const onSend = useCallback(() => {
|
31
|
+
setExpand(false);
|
32
|
+
}, []);
|
42
33
|
|
43
34
|
return (
|
44
35
|
<>
|
45
|
-
{!expand && <LocalFiles />}
|
36
|
+
{!expand && leftActions.includes('fileUpload') && <LocalFiles />}
|
46
37
|
<DraggablePanel
|
47
38
|
fullscreen={expand}
|
48
39
|
maxHeight={CHAT_TEXTAREA_MAX_HEIGHT}
|
49
40
|
minHeight={CHAT_TEXTAREA_HEIGHT}
|
50
41
|
onSizeChange={(_, size) => {
|
51
42
|
if (!size) return;
|
43
|
+
const height =
|
44
|
+
typeof size.height === 'string' ? Number.parseInt(size.height) : size.height;
|
45
|
+
if (!height) return;
|
52
46
|
|
53
|
-
|
54
|
-
inputHeight:
|
55
|
-
typeof size.height === 'string' ? Number.parseInt(size.height) : size.height,
|
56
|
-
});
|
47
|
+
onInputHeightChange?.(height);
|
57
48
|
}}
|
58
49
|
placement="bottom"
|
59
50
|
size={{ height: inputHeight, width: '100%' }}
|
@@ -71,8 +62,8 @@ const DesktopChatInput = memo<DesktopChatInputProps>(
|
|
71
62
|
rightActions={rightActions}
|
72
63
|
setExpand={setExpand}
|
73
64
|
/>
|
74
|
-
|
75
|
-
<Footer expand={expand} setExpand={setExpand} />
|
65
|
+
{renderTextArea(onSend)}
|
66
|
+
<Footer expand={expand} setExpand={setExpand} {...footer} />
|
76
67
|
</Flexbox>
|
77
68
|
</DraggablePanel>
|
78
69
|
</>
|
@@ -1,9 +1,11 @@
|
|
1
|
+
import { useCallback } from 'react';
|
2
|
+
|
1
3
|
import { useOpenChatSettings } from '@/hooks/useInterceptingRoutes';
|
2
4
|
import { useGlobalStore } from '@/store/global';
|
3
5
|
import { useSessionStore } from '@/store/session';
|
4
6
|
import { sessionSelectors } from '@/store/session/selectors';
|
5
7
|
|
6
|
-
import { MarkdownCustomRender,
|
8
|
+
import { MarkdownCustomRender, RenderBelowMessage, RenderMessage } from '../types';
|
7
9
|
import { AssistantMessage } from './Assistant';
|
8
10
|
import { DefaultBelowMessage, DefaultMessage } from './Default';
|
9
11
|
import { ToolMessage } from './Tool';
|
@@ -26,22 +28,20 @@ export const markdownCustomRenders: Record<string, MarkdownCustomRender> = {
|
|
26
28
|
user: UserMarkdownRender,
|
27
29
|
};
|
28
30
|
|
29
|
-
export const useAvatarsClick = ()
|
31
|
+
export const useAvatarsClick = (role?: string) => {
|
30
32
|
const [isInbox] = useSessionStore((s) => [sessionSelectors.isInboxSession(s)]);
|
31
33
|
const [toggleSystemRole] = useGlobalStore((s) => [s.toggleSystemRole]);
|
32
34
|
const openChatSettings = useOpenChatSettings();
|
33
35
|
|
34
|
-
return (
|
36
|
+
return useCallback(() => {
|
35
37
|
switch (role) {
|
36
38
|
case 'assistant': {
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
}
|
43
|
-
};
|
39
|
+
if (!isInbox) {
|
40
|
+
toggleSystemRole(true);
|
41
|
+
} else {
|
42
|
+
openChatSettings();
|
43
|
+
}
|
44
44
|
}
|
45
45
|
}
|
46
|
-
};
|
46
|
+
}, [isInbox, role]);
|
47
47
|
};
|
@@ -1,7 +1,9 @@
|
|
1
|
+
'use client';
|
2
|
+
|
1
3
|
import { ChatItem } from '@lobehub/ui';
|
2
4
|
import { createStyles } from 'antd-style';
|
3
5
|
import isEqual from 'fast-deep-equal';
|
4
|
-
import { ReactNode, memo, useCallback, useMemo } from 'react';
|
6
|
+
import { MouseEventHandler, ReactNode, memo, useCallback, useMemo } from 'react';
|
5
7
|
import { useTranslation } from 'react-i18next';
|
6
8
|
|
7
9
|
import { useAgentStore } from '@/store/agent';
|
@@ -42,11 +44,13 @@ const useStyles = createStyles(({ css, prefixCls }) => ({
|
|
42
44
|
}));
|
43
45
|
|
44
46
|
export interface ChatListItemProps {
|
47
|
+
hideActionBar?: boolean;
|
45
48
|
id: string;
|
46
49
|
index: number;
|
50
|
+
showThreadDivider?: boolean;
|
47
51
|
}
|
48
52
|
|
49
|
-
const Item = memo<ChatListItemProps>(({ index, id }) => {
|
53
|
+
const Item = memo<ChatListItemProps>(({ index, id, hideActionBar }) => {
|
50
54
|
const fontSize = useUserStore(userGeneralSettingsSelectors.fontSize);
|
51
55
|
const { t } = useTranslation('common');
|
52
56
|
const { styles, cx } = useStyles();
|
@@ -61,7 +65,7 @@ const Item = memo<ChatListItemProps>(({ index, id }) => {
|
|
61
65
|
|
62
66
|
if (index >= chats.length) return;
|
63
67
|
|
64
|
-
return chats
|
68
|
+
return chats.find((s) => s.id === id);
|
65
69
|
}, isEqual);
|
66
70
|
|
67
71
|
const [
|
@@ -83,18 +87,18 @@ const Item = memo<ChatListItemProps>(({ index, id }) => {
|
|
83
87
|
// when the message is in RAG flow or the AI generating, it should be in loading state
|
84
88
|
const isProcessing = isInRAGFlow || generating;
|
85
89
|
|
86
|
-
const onAvatarsClick = useAvatarsClick();
|
90
|
+
const onAvatarsClick = useAvatarsClick(item?.role);
|
87
91
|
|
88
|
-
const
|
89
|
-
(
|
92
|
+
const renderMessage = useCallback(
|
93
|
+
(editableContent: ReactNode) => {
|
90
94
|
if (!item?.role) return;
|
91
95
|
const RenderFunction = renderMessages[item.role] ?? renderMessages['default'];
|
92
96
|
|
93
97
|
if (!RenderFunction) return;
|
94
98
|
|
95
|
-
return <RenderFunction {...
|
99
|
+
return <RenderFunction {...item} editableContent={editableContent} />;
|
96
100
|
},
|
97
|
-
[item
|
101
|
+
[item],
|
98
102
|
);
|
99
103
|
|
100
104
|
const BelowMessage = useCallback(
|
@@ -136,7 +140,7 @@ const Item = memo<ChatListItemProps>(({ index, id }) => {
|
|
136
140
|
|
137
141
|
const error = useErrorContent(item?.error);
|
138
142
|
|
139
|
-
const historyLength = useChatStore((s) => chatSelectors.currentChats(s).length);
|
143
|
+
const [historyLength] = useChatStore((s) => [chatSelectors.currentChats(s).length]);
|
140
144
|
|
141
145
|
const enableHistoryDivider = useAgentStore((s) => {
|
142
146
|
const config = agentSelectors.currentAgentChatConfig(s);
|
@@ -151,6 +155,11 @@ const Item = memo<ChatListItemProps>(({ index, id }) => {
|
|
151
155
|
const message =
|
152
156
|
!editing && item?.role === 'assistant' ? processWithArtifact(item?.content) : item?.content;
|
153
157
|
|
158
|
+
// ======================= Performance Optimization ======================= //
|
159
|
+
// these useMemo/useCallback are all for the performance optimization
|
160
|
+
// maybe we can remove it in React 19
|
161
|
+
// ======================================================================== //
|
162
|
+
|
154
163
|
const components = useMemo(
|
155
164
|
() =>
|
156
165
|
Object.fromEntries(
|
@@ -163,55 +172,83 @@ const Item = memo<ChatListItemProps>(({ index, id }) => {
|
|
163
172
|
[id],
|
164
173
|
);
|
165
174
|
|
175
|
+
const markdownProps = useMemo(
|
176
|
+
() => ({
|
177
|
+
components,
|
178
|
+
customRender: markdownCustomRender,
|
179
|
+
rehypePlugins,
|
180
|
+
}),
|
181
|
+
[components, markdownCustomRender],
|
182
|
+
);
|
183
|
+
|
184
|
+
const onChange = useCallback((value: string) => updateMessageContent(id, value), [id]);
|
185
|
+
|
186
|
+
const onDoubleClick = useCallback<MouseEventHandler<HTMLDivElement>>(
|
187
|
+
(e) => {
|
188
|
+
if (!item) return;
|
189
|
+
if (item.id === 'default' || item.error) return;
|
190
|
+
if (item.role && ['assistant', 'user'].includes(item.role) && e.altKey) {
|
191
|
+
toggleMessageEditing(id, true);
|
192
|
+
}
|
193
|
+
},
|
194
|
+
[item],
|
195
|
+
);
|
196
|
+
|
197
|
+
const text = useMemo(
|
198
|
+
() => ({
|
199
|
+
cancel: t('cancel'),
|
200
|
+
confirm: t('ok'),
|
201
|
+
edit: t('edit'),
|
202
|
+
}),
|
203
|
+
[t],
|
204
|
+
);
|
205
|
+
|
206
|
+
const onEditingChange = useCallback((edit: boolean) => {
|
207
|
+
toggleMessageEditing(id, edit);
|
208
|
+
}, []);
|
209
|
+
|
210
|
+
const actions = useMemo(
|
211
|
+
() =>
|
212
|
+
!hideActionBar && (
|
213
|
+
<ActionsBar
|
214
|
+
index={index}
|
215
|
+
setEditing={(edit) => {
|
216
|
+
toggleMessageEditing(id, edit);
|
217
|
+
}}
|
218
|
+
/>
|
219
|
+
),
|
220
|
+
[hideActionBar, index, id],
|
221
|
+
);
|
222
|
+
|
223
|
+
const belowMessage = useMemo(() => item && <BelowMessage data={item} />, [item]);
|
224
|
+
const errorMessage = useMemo(() => item && <ErrorMessageExtra data={item} />, [item]);
|
225
|
+
const messageExtra = useMemo(() => item && <MessageExtra data={item} />, [item]);
|
226
|
+
|
166
227
|
return (
|
167
228
|
item && (
|
168
229
|
<>
|
169
230
|
{enableHistoryDivider && <History />}
|
170
231
|
<ChatItem
|
171
|
-
actions={
|
172
|
-
<ActionsBar
|
173
|
-
index={index}
|
174
|
-
setEditing={(edit) => {
|
175
|
-
toggleMessageEditing(id, edit);
|
176
|
-
}}
|
177
|
-
/>
|
178
|
-
}
|
232
|
+
actions={actions}
|
179
233
|
avatar={item.meta}
|
180
|
-
belowMessage={
|
234
|
+
belowMessage={belowMessage}
|
181
235
|
className={cx(styles.message, isMessageLoading && styles.loading)}
|
182
236
|
editing={editing}
|
183
237
|
error={error}
|
184
|
-
errorMessage={
|
238
|
+
errorMessage={errorMessage}
|
185
239
|
fontSize={fontSize}
|
186
240
|
loading={isProcessing}
|
187
|
-
markdownProps={
|
188
|
-
components,
|
189
|
-
customRender: markdownCustomRender,
|
190
|
-
rehypePlugins,
|
191
|
-
}}
|
241
|
+
markdownProps={markdownProps}
|
192
242
|
message={message}
|
193
|
-
messageExtra={
|
194
|
-
onAvatarClick={onAvatarsClick
|
195
|
-
onChange={
|
196
|
-
onDoubleClick={
|
197
|
-
|
198
|
-
if (item.role && ['assistant', 'user'].includes(item.role) && e.altKey) {
|
199
|
-
toggleMessageEditing(id, true);
|
200
|
-
}
|
201
|
-
}}
|
202
|
-
onEditingChange={(edit) => {
|
203
|
-
toggleMessageEditing(id, edit);
|
204
|
-
}}
|
243
|
+
messageExtra={messageExtra}
|
244
|
+
onAvatarClick={onAvatarsClick}
|
245
|
+
onChange={onChange}
|
246
|
+
onDoubleClick={onDoubleClick}
|
247
|
+
onEditingChange={onEditingChange}
|
205
248
|
placement={type === 'chat' ? (item.role === 'user' ? 'right' : 'left') : 'left'}
|
206
249
|
primary={item.role === 'user'}
|
207
|
-
renderMessage={
|
208
|
-
|
209
|
-
)}
|
210
|
-
text={{
|
211
|
-
cancel: t('cancel'),
|
212
|
-
confirm: t('ok'),
|
213
|
-
edit: t('edit'),
|
214
|
-
}}
|
250
|
+
renderMessage={renderMessage}
|
251
|
+
text={text}
|
215
252
|
time={item.updatedAt || item.createdAt}
|
216
253
|
type={type === 'chat' ? 'block' : 'pure'}
|
217
254
|
/>
|
@@ -220,4 +257,6 @@ const Item = memo<ChatListItemProps>(({ index, id }) => {
|
|
220
257
|
);
|
221
258
|
});
|
222
259
|
|
260
|
+
Item.displayName = 'ChatItem';
|
261
|
+
|
223
262
|
export default Item;
|
@@ -3,116 +3,114 @@
|
|
3
3
|
import { Icon } from '@lobehub/ui';
|
4
4
|
import { useTheme } from 'antd-style';
|
5
5
|
import { Loader2Icon } from 'lucide-react';
|
6
|
-
import React, { memo, useCallback, useEffect, useRef, useState } from 'react';
|
6
|
+
import React, { ReactNode, memo, useCallback, useEffect, useRef, useState } from 'react';
|
7
7
|
import { Center, Flexbox } from 'react-layout-kit';
|
8
8
|
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
|
9
9
|
|
10
|
-
import { WELCOME_GUIDE_CHAT_ID } from '@/const/session';
|
11
10
|
import { isServerMode } from '@/const/version';
|
12
11
|
import { useChatStore } from '@/store/chat';
|
13
12
|
import { chatSelectors } from '@/store/chat/selectors';
|
14
13
|
|
15
14
|
import AutoScroll from '../AutoScroll';
|
16
15
|
import Item from '../ChatItem';
|
17
|
-
import InboxWelcome from '../InboxWelcome';
|
18
16
|
import SkeletonList from '../SkeletonList';
|
19
17
|
|
20
18
|
interface VirtualizedListProps {
|
21
19
|
dataSource: string[];
|
20
|
+
hideActionBar?: boolean;
|
21
|
+
itemContent?: (index: number, data: any, context: any) => ReactNode;
|
22
22
|
mobile?: boolean;
|
23
23
|
}
|
24
24
|
|
25
|
-
const VirtualizedList = memo<VirtualizedListProps>(
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
virtuosoRef.current
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
const
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
return <Item id={id} index={index} />;
|
59
|
-
},
|
60
|
-
[mobile],
|
61
|
-
);
|
62
|
-
|
63
|
-
// first time loading or not loaded
|
64
|
-
if (isFirstLoading) return <SkeletonList mobile={mobile} />;
|
65
|
-
|
66
|
-
if (!isCurrentChatLoaded)
|
67
|
-
// use skeleton list when not loaded in server mode due to the loading duration is much longer than client mode
|
68
|
-
return isServerMode ? (
|
69
|
-
<SkeletonList mobile={mobile} />
|
70
|
-
) : (
|
71
|
-
// in client mode and switch page, using the center loading for smooth transition
|
72
|
-
<Center height={'100%'} width={'100%'}>
|
73
|
-
<Icon
|
74
|
-
icon={Loader2Icon}
|
75
|
-
size={{ fontSize: 32 }}
|
76
|
-
spin
|
77
|
-
style={{ color: theme.colorTextTertiary }}
|
78
|
-
/>
|
79
|
-
</Center>
|
25
|
+
const VirtualizedList = memo<VirtualizedListProps>(
|
26
|
+
({ mobile, dataSource, hideActionBar, itemContent }) => {
|
27
|
+
const virtuosoRef = useRef<VirtuosoHandle>(null);
|
28
|
+
const [atBottom, setAtBottom] = useState(true);
|
29
|
+
const [isScrolling, setIsScrolling] = useState(false);
|
30
|
+
|
31
|
+
const [id, isFirstLoading, isCurrentChatLoaded] = useChatStore((s) => [
|
32
|
+
chatSelectors.currentChatKey(s),
|
33
|
+
chatSelectors.currentChatLoadingState(s),
|
34
|
+
chatSelectors.isCurrentChatLoaded(s),
|
35
|
+
]);
|
36
|
+
|
37
|
+
useEffect(() => {
|
38
|
+
if (virtuosoRef.current) {
|
39
|
+
virtuosoRef.current.scrollToIndex({ align: 'end', behavior: 'auto', index: 'LAST' });
|
40
|
+
}
|
41
|
+
}, [id]);
|
42
|
+
|
43
|
+
const prevDataLengthRef = useRef(dataSource.length);
|
44
|
+
|
45
|
+
const getFollowOutput = useCallback(() => {
|
46
|
+
const newFollowOutput = dataSource.length > prevDataLengthRef.current ? 'auto' : false;
|
47
|
+
prevDataLengthRef.current = dataSource.length;
|
48
|
+
return newFollowOutput;
|
49
|
+
}, [dataSource.length]);
|
50
|
+
|
51
|
+
const theme = useTheme();
|
52
|
+
// overscan should be 3 times the height of the window
|
53
|
+
const overscan = typeof window !== 'undefined' ? window.innerHeight * 3 : 0;
|
54
|
+
|
55
|
+
const defaultItemContent = useCallback(
|
56
|
+
(index: number, id: string) => <Item hideActionBar={hideActionBar} id={id} index={index} />,
|
57
|
+
[mobile, hideActionBar],
|
80
58
|
);
|
81
59
|
|
82
|
-
|
83
|
-
<
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
60
|
+
// first time loading or not loaded
|
61
|
+
if (isFirstLoading) return <SkeletonList mobile={mobile} />;
|
62
|
+
|
63
|
+
if (!isCurrentChatLoaded)
|
64
|
+
// use skeleton list when not loaded in server mode due to the loading duration is much longer than client mode
|
65
|
+
return isServerMode ? (
|
66
|
+
<SkeletonList mobile={mobile} />
|
67
|
+
) : (
|
68
|
+
// in client mode and switch page, using the center loading for smooth transition
|
69
|
+
<Center height={'100%'} width={'100%'}>
|
70
|
+
<Icon
|
71
|
+
icon={Loader2Icon}
|
72
|
+
size={{ fontSize: 32 }}
|
73
|
+
spin
|
74
|
+
style={{ color: theme.colorTextTertiary }}
|
75
|
+
/>
|
76
|
+
</Center>
|
77
|
+
);
|
78
|
+
|
79
|
+
return (
|
80
|
+
<Flexbox height={'100%'}>
|
81
|
+
<Virtuoso
|
82
|
+
atBottomStateChange={setAtBottom}
|
83
|
+
atBottomThreshold={50 * (mobile ? 2 : 1)}
|
84
|
+
computeItemKey={(_, item) => item}
|
85
|
+
data={dataSource}
|
86
|
+
followOutput={getFollowOutput}
|
87
|
+
increaseViewportBy={overscan}
|
88
|
+
initialTopMostItemIndex={dataSource?.length - 1}
|
89
|
+
isScrolling={setIsScrolling}
|
90
|
+
itemContent={itemContent ?? defaultItemContent}
|
91
|
+
overscan={overscan}
|
92
|
+
ref={virtuosoRef}
|
93
|
+
/>
|
94
|
+
<AutoScroll
|
95
|
+
atBottom={atBottom}
|
96
|
+
isScrolling={isScrolling}
|
97
|
+
onScrollToBottom={(type) => {
|
98
|
+
const virtuoso = virtuosoRef.current;
|
99
|
+
switch (type) {
|
100
|
+
case 'auto': {
|
101
|
+
virtuoso?.scrollToIndex({ align: 'end', behavior: 'auto', index: 'LAST' });
|
102
|
+
break;
|
103
|
+
}
|
104
|
+
case 'click': {
|
105
|
+
virtuoso?.scrollToIndex({ align: 'end', behavior: 'smooth', index: 'LAST' });
|
106
|
+
break;
|
107
|
+
}
|
110
108
|
}
|
111
|
-
}
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
109
|
+
}}
|
110
|
+
/>
|
111
|
+
</Flexbox>
|
112
|
+
);
|
113
|
+
},
|
114
|
+
);
|
117
115
|
|
118
116
|
export default VirtualizedList;
|
@@ -1,2 +1,4 @@
|
|
1
|
+
export { default as ChatItem } from './components/ChatItem';
|
2
|
+
export { default as InboxWelcome } from './components/InboxWelcome';
|
1
3
|
export { default as SkeletonList } from './components/SkeletonList';
|
2
4
|
export { default as VirtualizedList } from './components/VirtualizedList';
|
@@ -0,0 +1 @@
|
|
1
|
+
export * from './topic';
|
File without changes
|