@lobehub/chat 1.31.8 → 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 CHANGED
@@ -2,6 +2,48 @@
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
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
19
+
20
+ </div>
21
+
22
+ ### [Version 1.31.9](https://github.com/lobehub/lobe-chat/compare/v1.31.8...v1.31.9)
23
+
24
+ <sup>Released on **2024-11-16**</sup>
25
+
26
+ #### 💄 Styles
27
+
28
+ - **misc**: Add gemini-exp-1114 model.
29
+
30
+ <br/>
31
+
32
+ <details>
33
+ <summary><kbd>Improvements and Fixes</kbd></summary>
34
+
35
+ #### Styles
36
+
37
+ - **misc**: Add gemini-exp-1114 model, closes [#4702](https://github.com/lobehub/lobe-chat/issues/4702) ([b3306f2](https://github.com/lobehub/lobe-chat/commit/b3306f2))
38
+
39
+ </details>
40
+
41
+ <div align="right">
42
+
43
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
44
+
45
+ </div>
46
+
5
47
  ### [Version 1.31.8](https://github.com/lobehub/lobe-chat/compare/v1.31.7...v1.31.8)
6
48
 
7
49
  <sup>Released on **2024-11-15**</sup>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "1.31.8",
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 setExpandMock: (expand: boolean) => void;
10
+ let onSendMock: () => void;
11
11
 
12
12
  beforeEach(() => {
13
- setExpandMock = vi.fn();
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 setExpand={setExpandMock} />);
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 setExpand={setExpandMock} />);
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 setExpand={setExpandMock} />);
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(setExpandMock).not.toHaveBeenCalled();
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(setExpandMock).toHaveBeenCalled();
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 setExpand={setExpandMock} />);
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 setExpand={setExpandMock} />);
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 setExpand={setExpandMock} />);
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 setExpand={setExpandMock} />);
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 setExpand={setExpandMock} />);
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 setExpand={setExpandMock} />);
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 { WELCOME_GUIDE_CHAT_ID } from '@/const/session';
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((s) => [
18
- s.activeTopicId,
19
- s.useFetchMessages,
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((s) => {
26
- const showInboxWelcome = chatSelectors.showInboxWelcome(s);
27
- if (showInboxWelcome) return [WELCOME_GUIDE_CHAT_ID];
28
+ const data = useChatStore(chatSelectors.currentChatIDsWithGuideMessage, isEqual);
28
29
 
29
- return chatSelectors.currentChatIDsWithGuideMessage(s);
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;
@@ -3,6 +3,23 @@ import { ModelProviderCard } from '@/types/llm';
3
3
  // ref: https://ai.google.dev/gemini-api/docs/models/gemini
4
4
  const Google: ModelProviderCard = {
5
5
  chatModels: [
6
+ {
7
+ description:
8
+ 'Gemini Exp 1114 是Google最新的实验性多模态AI模型,具备快速处理能力,支持文本、图像和视频输入,适用于多种任务的高效扩展。',
9
+ displayName: 'Gemini Experimental 1114',
10
+ enabled: true,
11
+ functionCall: true,
12
+ id: 'gemini-exp-1114',
13
+ maxOutput: 8192,
14
+ pricing: {
15
+ cachedInput: 0,
16
+ input: 0,
17
+ output: 0,
18
+ },
19
+ releasedAt: '2024-11-14',
20
+ tokens: 32_767 + 8192,
21
+ vision: true,
22
+ },
6
23
  {
7
24
  description:
8
25
  'Gemini 1.5 Flash 是Google最新的多模态AI模型,具备快速处理能力,支持文本、图像和视频输入,适用于多种任务的高效扩展。',
@@ -49,53 +66,6 @@ const Google: ModelProviderCard = {
49
66
  tokens: 1_000_000 + 8192,
50
67
  vision: true,
51
68
  },
52
- {
53
- description: 'Gemini 1.5 Flash 0827 提供了优化后的多模态处理能力,适用多种复杂任务场景。',
54
- displayName: 'Gemini 1.5 Flash 0827',
55
- functionCall: true,
56
- id: 'gemini-1.5-flash-exp-0827',
57
- maxOutput: 8192,
58
- pricing: {
59
- cachedInput: 0.018_75,
60
- input: 0.075,
61
- output: 0.3,
62
- },
63
- releasedAt: '2024-08-27',
64
- tokens: 1_000_000 + 8192,
65
- vision: true,
66
- },
67
- {
68
- description: 'Gemini 1.5 Flash 8B 是一款高效的多模态模型,支持广泛应用的扩展。',
69
- displayName: 'Gemini 1.5 Flash 8B',
70
- enabled: true,
71
- functionCall: true,
72
- id: 'gemini-1.5-flash-8b',
73
- maxOutput: 8192,
74
- pricing: {
75
- cachedInput: 0.02,
76
- input: 0.075,
77
- output: 0.3,
78
- },
79
- releasedAt: '2024-10-03',
80
- tokens: 1_000_000 + 8192,
81
- vision: true,
82
- },
83
- {
84
- description:
85
- 'Gemini 1.5 Flash 8B 0924 是最新的实验性模型,在文本和多模态用例中都有显著的性能提升。',
86
- displayName: 'Gemini 1.5 Flash 8B 0924',
87
- functionCall: true,
88
- id: 'gemini-1.5-flash-8b-exp-0924',
89
- maxOutput: 8192,
90
- pricing: {
91
- cachedInput: 0.018_75,
92
- input: 0.075,
93
- output: 0.3,
94
- },
95
- releasedAt: '2024-09-24',
96
- tokens: 1_000_000 + 8192,
97
- vision: true,
98
- },
99
69
  {
100
70
  description:
101
71
  'Gemini 1.5 Pro 支持高达200万个tokens,是中型多模态模型的理想选择,适用于复杂任务的多方面支持。',
@@ -146,35 +116,38 @@ const Google: ModelProviderCard = {
146
116
  vision: true,
147
117
  },
148
118
  {
149
- description: 'Gemini 1.5 Pro 0827 结合最新优化技术,带来更高效的多模态数据处理能力。',
150
- displayName: 'Gemini 1.5 Pro 0827',
119
+ description: 'Gemini 1.5 Flash 8B 是一款高效的多模态模型,支持广泛应用的扩展。',
120
+ displayName: 'Gemini 1.5 Flash 8B',
121
+ enabled: true,
151
122
  functionCall: true,
152
- id: 'gemini-1.5-pro-exp-0827',
123
+ id: 'gemini-1.5-flash-8b',
153
124
  maxOutput: 8192,
154
125
  pricing: {
155
- cachedInput: 0.875,
156
- input: 3.5,
157
- output: 10.5,
126
+ cachedInput: 0.02,
127
+ input: 0.075,
128
+ output: 0.3,
158
129
  },
159
- releasedAt: '2024-08-27',
160
- tokens: 2_000_000 + 8192,
130
+ releasedAt: '2024-10-03',
131
+ tokens: 1_000_000 + 8192,
161
132
  vision: true,
162
133
  },
163
134
  {
164
- description: 'Gemini 1.5 Pro 0801 提供出色的多模态处理能力,为应用开发带来更大灵活性。',
165
- displayName: 'Gemini 1.5 Pro 0801',
135
+ description:
136
+ 'Gemini 1.5 Flash 8B 0924 是最新的实验性模型,在文本和多模态用例中都有显著的性能提升。',
137
+ displayName: 'Gemini 1.5 Flash 8B 0924',
166
138
  functionCall: true,
167
- id: 'gemini-1.5-pro-exp-0801',
139
+ id: 'gemini-1.5-flash-8b-exp-0924',
168
140
  maxOutput: 8192,
169
141
  pricing: {
170
- cachedInput: 0.875,
171
- input: 3.5,
172
- output: 10.5,
142
+ cachedInput: 0.018_75,
143
+ input: 0.075,
144
+ output: 0.3,
173
145
  },
174
- releasedAt: '2024-08-01',
175
- tokens: 2_000_000 + 8192,
146
+ releasedAt: '2024-09-24',
147
+ tokens: 1_000_000 + 8192,
176
148
  vision: true,
177
149
  },
150
+ // Gemini 1.0 Pro will be removed on 2025.02.15
178
151
  {
179
152
  description: 'Gemini 1.0 Pro 是Google的高性能AI模型,专为广泛任务扩展而设计。',
180
153
  displayName: 'Gemini 1.0 Pro',
@@ -1,3 +1,7 @@
1
1
  export const LOADING_FLAT = '...';
2
2
 
3
3
  export const MESSAGE_CANCEL_FLAT = 'canceled';
4
+
5
+ export const MESSAGE_THREAD_DIVIDER_ID = 'thread-divider';
6
+
7
+ export const MESSAGE_WELCOME_GUIDE_ID = 'welcome';
@@ -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 { Icon } from '@lobehub/ui';
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 { Center, Flexbox } from 'react-layout-kit';
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 { theme, styles } = useStyles();
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
- <Flexbox
111
- gap={4}
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 './useAutoFocus';
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
- setExpand?: (expand: boolean) => void;
34
+ loading?: boolean;
35
+ onChange: (string: string) => void;
36
+ onSend: () => void;
37
+ value: string;
38
38
  }
39
39
 
40
- const InputArea = memo<InputAreaProps>(({ setExpand }) => {
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
- updateInputMessage?.(e.target.value);
73
+ onChange?.(e.target.value);
82
74
  }}
83
75
  onChange={(e) => {
84
- updateInputMessage?.(e.target.value);
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
- sendMessage();
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
- updateInputMessage?.((e.target as any).value + '\n');
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 = 'InputArea';
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
- leftActions?: ActionKeys[];
32
- rightActions?: ActionKeys[];
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 = defaultLeftActions, rightActions = defaultRightActions }) => {
27
+ ({ leftActions, rightActions, footer, renderTextArea, inputHeight, onInputHeightChange }) => {
36
28
  const [expand, setExpand] = useState<boolean>(false);
37
29
 
38
- const [inputHeight, updatePreference] = useGlobalStore((s) => [
39
- systemStatusSelectors.inputHeight(s),
40
- s.updateSystemStatus,
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
- updatePreference({
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
- <TextArea setExpand={setExpand} />
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, OnAvatarsClick, RenderBelowMessage, RenderMessage } from '../types';
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 = (): OnAvatarsClick => {
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 (role) => {
36
+ return useCallback(() => {
35
37
  switch (role) {
36
38
  case 'assistant': {
37
- return () => {
38
- if (!isInbox) {
39
- toggleSystemRole(true);
40
- } else {
41
- openChatSettings();
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[index];
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 RenderMessage = useCallback(
89
- ({ editableContent, data }: { data: ChatMessage; editableContent: ReactNode }) => {
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 {...data} editableContent={editableContent} />;
99
+ return <RenderFunction {...item} editableContent={editableContent} />;
96
100
  },
97
- [item?.role],
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={<BelowMessage data={item} />}
234
+ belowMessage={belowMessage}
181
235
  className={cx(styles.message, isMessageLoading && styles.loading)}
182
236
  editing={editing}
183
237
  error={error}
184
- errorMessage={<ErrorMessageExtra data={item} />}
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={<MessageExtra data={item} />}
194
- onAvatarClick={onAvatarsClick?.(item.role)}
195
- onChange={(value) => updateMessageContent(item.id, value)}
196
- onDoubleClick={(e) => {
197
- if (item.id === 'default' || item.error) return;
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={(editableContent) => (
208
- <RenderMessage data={item} editableContent={editableContent} />
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>(({ mobile, dataSource }) => {
26
- const virtuosoRef = useRef<VirtuosoHandle>(null);
27
- const [atBottom, setAtBottom] = useState(true);
28
- const [isScrolling, setIsScrolling] = useState(false);
29
-
30
- const [id] = useChatStore((s) => [chatSelectors.currentChatKey(s)]);
31
- const [isFirstLoading, isCurrentChatLoaded] = useChatStore((s) => [
32
- chatSelectors.currentChatLoadingState(s),
33
- chatSelectors.isCurrentChatLoaded(s),
34
- ]);
35
-
36
- useEffect(() => {
37
- if (virtuosoRef.current) {
38
- virtuosoRef.current.scrollToIndex({ align: 'end', behavior: 'auto', index: 'LAST' });
39
- }
40
- }, [id]);
41
-
42
- const prevDataLengthRef = useRef(dataSource.length);
43
-
44
- const getFollowOutput = useCallback(() => {
45
- const newFollowOutput = dataSource.length > prevDataLengthRef.current ? 'auto' : false;
46
- prevDataLengthRef.current = dataSource.length;
47
- return newFollowOutput;
48
- }, [dataSource.length]);
49
-
50
- const theme = useTheme();
51
- // overscan should be 3 times the height of the window
52
- const overscan = typeof window !== 'undefined' ? window.innerHeight * 3 : 0;
53
-
54
- const itemContent = useCallback(
55
- (index: number, id: string) => {
56
- if (id === WELCOME_GUIDE_CHAT_ID) return <InboxWelcome />;
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
- return (
83
- <Flexbox height={'100%'}>
84
- <Virtuoso
85
- atBottomStateChange={setAtBottom}
86
- atBottomThreshold={50 * (mobile ? 2 : 1)}
87
- computeItemKey={(_, item) => item}
88
- data={dataSource}
89
- followOutput={getFollowOutput}
90
- increaseViewportBy={overscan}
91
- initialTopMostItemIndex={dataSource?.length - 1}
92
- isScrolling={setIsScrolling}
93
- itemContent={itemContent}
94
- overscan={overscan}
95
- ref={virtuosoRef}
96
- />
97
- <AutoScroll
98
- atBottom={atBottom}
99
- isScrolling={isScrolling}
100
- onScrollToBottom={(type) => {
101
- const virtuoso = virtuosoRef.current;
102
- switch (type) {
103
- case 'auto': {
104
- virtuoso?.scrollToIndex({ align: 'end', behavior: 'auto', index: 'LAST' });
105
- break;
106
- }
107
- case 'click': {
108
- virtuoso?.scrollToIndex({ align: 'end', behavior: 'smooth', index: 'LAST' });
109
- break;
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
- </Flexbox>
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