@lobehub/chat 0.119.3 → 0.119.4

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.
Files changed (26) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +3 -3
  3. package/README.zh-CN.md +3 -3
  4. package/package.json +2 -1
  5. package/src/app/chat/features/TopicListContent/Topic/SkeletonList.tsx +46 -12
  6. package/src/app/chat/features/TopicListContent/Topic/index.tsx +67 -33
  7. package/src/app/chat/features/TopicListContent/index.tsx +2 -1
  8. package/src/app/settings/llm/LLM/index.tsx +7 -1
  9. package/src/features/Conversation/components/AutoScroll.tsx +25 -0
  10. package/src/features/Conversation/components/BackBottom/index.tsx +31 -0
  11. package/src/features/Conversation/components/BackBottom/style.ts +36 -0
  12. package/src/features/Conversation/components/ChatItem/index.tsx +63 -42
  13. package/src/features/Conversation/components/ChatList/index.tsx +6 -29
  14. package/src/features/Conversation/components/SkeletonList.tsx +5 -2
  15. package/src/features/Conversation/components/VirtualizedList/index.tsx +78 -0
  16. package/src/features/Conversation/index.tsx +8 -18
  17. package/src/locales/create.ts +1 -1
  18. package/src/locales/default/chat.ts +5 -1
  19. package/src/locales/default/index.ts +0 -2
  20. package/src/store/chat/slices/message/selectors.ts +6 -0
  21. package/vercel.json +1 -1
  22. package/src/features/Conversation/components/ChatList/style.ts +0 -10
  23. package/src/features/Conversation/components/ScrollAnchor/index.tsx +0 -29
  24. package/src/features/Conversation/components/ScrollAnchor/useAtBottom.ts +0 -20
  25. package/src/locales/default/empty.ts +0 -6
  26. /package/src/features/Conversation/components/{ChatList → ChatItem}/HistoryDivider.tsx +0 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,51 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ### [Version 0.119.4](https://github.com/lobehub/lobe-chat/compare/v0.119.3...v0.119.4)
6
+
7
+ <sup>Released on **2024-01-06**</sup>
8
+
9
+ #### ♻ Code Refactoring
10
+
11
+ - **misc**: Refactor to virtual list with react-virtuoso.
12
+
13
+ #### 🐛 Bug Fixes
14
+
15
+ - **misc**: Fix auto scroll error and BackBottom error.
16
+
17
+ #### 💄 Styles
18
+
19
+ - **misc**: Fix BackBottom zIndex, improve chat list on mobile, improve chat list scrolling to bottom at initial render, improve custom model input, improve topic scroll.
20
+
21
+ <br/>
22
+
23
+ <details>
24
+ <summary><kbd>Improvements and Fixes</kbd></summary>
25
+
26
+ #### Code refactoring
27
+
28
+ - **misc**: Refactor to virtual list with react-virtuoso ([d6d63b2](https://github.com/lobehub/lobe-chat/commit/d6d63b2))
29
+
30
+ #### What's fixed
31
+
32
+ - **misc**: Fix auto scroll error and BackBottom error ([6100970](https://github.com/lobehub/lobe-chat/commit/6100970))
33
+
34
+ #### Styles
35
+
36
+ - **misc**: Fix BackBottom zIndex ([254cc54](https://github.com/lobehub/lobe-chat/commit/254cc54))
37
+ - **misc**: Improve chat list on mobile ([a894fc5](https://github.com/lobehub/lobe-chat/commit/a894fc5))
38
+ - **misc**: Improve chat list scrolling to bottom at initial render ([476304b](https://github.com/lobehub/lobe-chat/commit/476304b))
39
+ - **misc**: Improve custom model input ([1c4722f](https://github.com/lobehub/lobe-chat/commit/1c4722f))
40
+ - **misc**: Improve topic scroll ([8daf3ac](https://github.com/lobehub/lobe-chat/commit/8daf3ac))
41
+
42
+ </details>
43
+
44
+ <div align="right">
45
+
46
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
47
+
48
+ </div>
49
+
5
50
  ### [Version 0.119.3](https://github.com/lobehub/lobe-chat/compare/v0.119.2...v0.119.3)
6
51
 
7
52
  <sup>Released on **2024-01-06**</sup>
package/README.md CHANGED
@@ -210,12 +210,12 @@ Our marketplace is not just a showcase platform but also a collaborative space.
210
210
 
211
211
  | Recent Submits | Description |
212
212
  | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
213
+ | [Performance Evaluation Superhero](https://chat-preview.lobehub.com/market?agent=kpi-hero)<br/><sup>By **[canisminor1990](https://github.com/canisminor1990)** on **2024-01-05**</sup> | Specializes in writing performance evaluation reports and year-end summaries<br/>`performance-evaluation` `report-writing` `data-analysis` `professional-insights` `okr` `kpi` |
213
214
  | [SVG Flowchart Explanation Assistant](https://chat-preview.lobehub.com/market?agent=svg-flowchart-explanation-assistant)<br/><sup>By **[Justin3go](https://github.com/Justin3go)** on **2024-01-05**</sup> | Explain SVG flowcharts by inputting SVG source code and interpreting the flowchart.<br/>`flowchart-explanation` `technical-documentation-writing` `business-knowledge` |
215
+ | [Weekly Report Assistant](https://chat-preview.lobehub.com/market?agent=write-report-assistant-development)<br/><sup>By **[CaoYunzhou](https://github.com/CaoYunzhou)** on **2024-01-05**</sup> | Weekly report generation assistant<br/>`weekly-report` `daily-report` `writing` `summary` |
214
216
  | [3D Animation Engineer](https://chat-preview.lobehub.com/market?agent=react-three-3-d-expert)<br/><sup>By **[arvinxx](https://github.com/arvinxx)** on **2024-01-03**</sup> | Proficient in React, Three.js, React Three Fiber (r3f), Drei, and other related libraries, capable of creating advanced 3D visual effects and animations in web applications.<br/>`3-d-animation` `react` `three-js` `web-design` `animation` |
215
- | [Amazon Title Assistant](https://chat-preview.lobehub.com/market?agent=amazon)<br/><sup>By **[cm2457618290](https://github.com/cm2457618290)** on **2024-01-02**</sup> | Provide product keywords or product links to automatically write titles and product introductions<br/>`assistant` |
216
- | [Exam Assistant](https://chat-preview.lobehub.com/market?agent=generador-examenes)<br/><sup>By **[aitorroma](https://github.com/aitorroma)** on **2024-01-02**</sup> | I am a skills summary assistant and cannot conduct interactive exams. However, I can help you summarize your skills and knowledge in a clear and concise format.<br/>`exam` `learning` `statistics` |
217
217
 
218
- > 📊 Total agents: [<kbd>**89**</kbd> ](https://github.com/lobehub/lobe-chat-agents)
218
+ > 📊 Total agents: [<kbd>**91**</kbd> ](https://github.com/lobehub/lobe-chat-agents)
219
219
 
220
220
  <!-- AGENT LIST -->
221
221
 
package/README.zh-CN.md CHANGED
@@ -186,12 +186,12 @@ LobeChat 的插件生态系统是其核心功能的重要扩展,它极大地
186
186
 
187
187
  | 最近新增 | 助手说明 |
188
188
  | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
189
+ | [绩效评估超人](https://chat-preview.lobehub.com/market?agent=kpi-hero)<br/><sup>By **[canisminor1990](https://github.com/canisminor1990)** on **2024-01-05**</sup> | 擅长写绩效评估报告与年终总结<br/>`绩效评估` `报告撰写` `数据分析` `专业见解` `okr` `kpi` |
189
190
  | [SVG 流程图解释助手](https://chat-preview.lobehub.com/market?agent=svg-flowchart-explanation-assistant)<br/><sup>By **[Justin3go](https://github.com/Justin3go)** on **2024-01-05**</sup> | SVG 流程图解释,输入 SVG 源代码,解释该流程图<br/>`流程图解释` `技术文档编写` `业务知识` |
191
+ | [周报助手](https://chat-preview.lobehub.com/market?agent=write-report-assistant-development)<br/><sup>By **[CaoYunzhou](https://github.com/CaoYunzhou)** on **2024-01-05**</sup> | 周报生成助手<br/>`周报` `日报` `写作` `总结` |
190
192
  | [三维动画工程师](https://chat-preview.lobehub.com/market?agent=react-three-3-d-expert)<br/><sup>By **[arvinxx](https://github.com/arvinxx)** on **2024-01-03**</sup> | 擅长 React、Three.js、React Three Fiber(r3f)、Drei 等库,能够在网页应用中创造高级别的三维视觉效果和动画。<br/>`3-d动画` `react` `three-js` `网页设计` `动画` |
191
- | [Amazon Title Assistant](https://chat-preview.lobehub.com/market?agent=amazon)<br/><sup>By **[cm2457618290](https://github.com/cm2457618290)** on **2024-01-02**</sup> | Provide product keywords or product links to automatically write titles and product introductions<br/>`assistant` |
192
- | [考试助手](https://chat-preview.lobehub.com/market?agent=generador-examenes)<br/><sup>By **[aitorroma](https://github.com/aitorroma)** on **2024-01-02**</sup> | 我是一个技能总结助手,无法进行互动式考试。但我可以帮助你以清晰简洁的格式总结你的技能和知识。<br/>`考试` `学习` `统计` |
193
193
 
194
- > 📊 Total agents: [<kbd>**89**</kbd> ](https://github.com/lobehub/lobe-chat-agents)
194
+ > 📊 Total agents: [<kbd>**91**</kbd> ](https://github.com/lobehub/lobe-chat-agents)
195
195
 
196
196
  <!-- AGENT LIST -->
197
197
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "0.119.3",
3
+ "version": "0.119.4",
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",
@@ -112,6 +112,7 @@
112
112
  "react-intersection-observer": "^9",
113
113
  "react-layout-kit": "^1",
114
114
  "react-lazy-load": "^4",
115
+ "react-virtuoso": "^4.6.2",
115
116
  "react-wrap-balancer": "^1",
116
117
  "remark": "^14",
117
118
  "remark-gfm": "^3",
@@ -1,21 +1,55 @@
1
1
  import { Skeleton } from 'antd';
2
+ import { createStyles } from 'antd-style';
2
3
  import { memo } from 'react';
3
4
  import { Flexbox } from 'react-layout-kit';
4
5
 
5
- const SkeletonList = memo(() => {
6
+ const useStyles = createStyles(({ css, prefixCls }) => ({
7
+ container: css`
8
+ display: flex;
9
+ flex-direction: column;
10
+ justify-content: center;
11
+
12
+ height: 44px;
13
+ padding: 8px;
14
+
15
+ .${prefixCls}-skeleton-content {
16
+ display: flex;
17
+ flex-direction: column;
18
+ }
19
+ `,
20
+
21
+ paragraph: css`
22
+ > li {
23
+ height: 24px !important;
24
+ }
25
+ `,
26
+ }));
27
+
28
+ export const Placeholder = memo(() => {
29
+ const { styles } = useStyles();
30
+
6
31
  return (
7
- <Flexbox gap={8} paddingInline={10} style={{ marginTop: 8 }}>
8
- {Array.from({ length: 8 }).map((_, i) => (
9
- <Skeleton
10
- active
11
- avatar={false}
12
- key={i}
13
- paragraph={{ rows: 1, width: '100%' }}
14
- title={false}
15
- />
16
- ))}
17
- </Flexbox>
32
+ <Skeleton
33
+ active
34
+ avatar={false}
35
+ className={styles.container}
36
+ paragraph={{
37
+ className: styles.paragraph,
38
+ rows: 1,
39
+ style: { marginBottom: 0 },
40
+ width: '100%',
41
+ }}
42
+ title={false}
43
+ />
18
44
  );
19
45
  });
20
46
 
47
+ export const SkeletonList = memo(() => (
48
+ <Flexbox>
49
+ {Array.from({ length: 8 }).map((_, i) => (
50
+ <Placeholder key={i} />
51
+ ))}
52
+ </Flexbox>
53
+ ));
54
+
21
55
  export default SkeletonList;
@@ -1,58 +1,92 @@
1
1
  import { EmptyCard } from '@lobehub/ui';
2
2
  import { useThemeMode } from 'antd-style';
3
3
  import isEqual from 'fast-deep-equal';
4
+ import React, { memo, useCallback, useRef } from 'react';
4
5
  import { useTranslation } from 'react-i18next';
5
6
  import { Flexbox } from 'react-layout-kit';
7
+ import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
6
8
 
7
9
  import { useChatStore } from '@/store/chat';
8
10
  import { topicSelectors } from '@/store/chat/selectors';
9
11
  import { useGlobalStore } from '@/store/global';
12
+ import { ChatTopic } from '@/types/topic';
10
13
 
11
- import SkeletonList from './SkeletonList';
14
+ import { Placeholder, SkeletonList } from './SkeletonList';
12
15
  import TopicItem from './TopicItem';
13
16
 
14
- export const Topic = () => {
17
+ export const Topic = memo(() => {
18
+ const { t } = useTranslation('chat');
19
+ const virtuosoRef = useRef<VirtuosoHandle>(null);
15
20
  const { isDarkMode } = useThemeMode();
16
-
17
- const topics = useChatStore(topicSelectors.displayTopics, isEqual);
18
-
19
- const [topicsInit, activeTopicId] = useChatStore((s) => [s.topicsInit, s.activeTopicId]);
20
-
21
- const { t } = useTranslation('empty');
21
+ const [topicsInit, activeTopicId, topicLength] = useChatStore((s) => [
22
+ s.topicsInit,
23
+ s.activeTopicId,
24
+ topicSelectors.currentTopicLength(s),
25
+ ]);
22
26
  const [visible, updateGuideState] = useGlobalStore((s) => [
23
27
  s.preference.guide?.topic,
24
28
  s.updateGuideState,
25
29
  ]);
26
30
 
31
+ const topics = useChatStore(
32
+ (s) => [
33
+ {
34
+ favorite: false,
35
+ id: 'default',
36
+ title: t('topic.defaultTitle'),
37
+ } as ChatTopic,
38
+ ...topicSelectors.displayTopics(s),
39
+ ],
40
+ isEqual,
41
+ );
42
+
43
+ const itemContent = useCallback(
44
+ (index: number, { id, favorite, title }: ChatTopic) =>
45
+ index === 0 ? (
46
+ <TopicItem active={!activeTopicId} fav={favorite} title={title} />
47
+ ) : (
48
+ <TopicItem active={activeTopicId === id} fav={favorite} id={id} key={id} title={title} />
49
+ ),
50
+ [activeTopicId],
51
+ );
52
+
53
+ const activeIndex = topics.findIndex((topic) => topic.id === activeTopicId);
54
+
27
55
  return !topicsInit ? (
28
56
  <SkeletonList />
29
57
  ) : (
30
- <Flexbox gap={2} style={{ marginBottom: 12 }}>
31
- {topics?.length === 0 && (
32
- <EmptyCard
33
- alt={t('topic.desc')}
34
- cover={`/images/empty_topic_${isDarkMode ? 'dark' : 'light'}.webp`}
35
- desc={t('topic.desc')}
36
- height={120}
37
- onVisibleChange={(visible) => {
38
- updateGuideState({ topic: visible });
39
- }}
40
- style={{ marginBottom: 6 }}
41
- title={t('topic.title')}
42
- visible={visible}
43
- width={200}
44
- />
58
+ <Flexbox gap={2} height={'100%'} style={{ marginBottom: 12 }}>
59
+ {topicLength === 0 && (
60
+ <Flexbox flex={1}>
61
+ <EmptyCard
62
+ alt={t('topic.guide.desc')}
63
+ cover={`/images/empty_topic_${isDarkMode ? 'dark' : 'light'}.webp`}
64
+ desc={t('topic.guide.desc')}
65
+ height={120}
66
+ onVisibleChange={(visible) => {
67
+ updateGuideState({ topic: visible });
68
+ }}
69
+ style={{ marginBottom: 6 }}
70
+ title={t('topic.guide.title')}
71
+ visible={visible}
72
+ width={200}
73
+ />
74
+ </Flexbox>
45
75
  )}
46
-
47
- <TopicItem
48
- active={!activeTopicId}
49
- fav={false}
50
- title={t('topic.defaultTitle', { ns: 'chat' })}
76
+ <Virtuoso
77
+ components={{ ScrollSeekPlaceholder: Placeholder }}
78
+ computeItemKey={(_, item) => item.id}
79
+ data={topics}
80
+ fixedItemHeight={44}
81
+ initialTopMostItemIndex={Math.max(activeIndex, 0)}
82
+ itemContent={itemContent}
83
+ overscan={44 * 10}
84
+ ref={virtuosoRef}
85
+ scrollSeekConfiguration={{
86
+ enter: (velocity) => Math.abs(velocity) > 350,
87
+ exit: (velocity) => Math.abs(velocity) < 10,
88
+ }}
51
89
  />
52
-
53
- {topics.map(({ id, favorite, title }) => (
54
- <TopicItem active={activeTopicId === id} fav={favorite} id={id} key={id} title={title} />
55
- ))}
56
90
  </Flexbox>
57
91
  );
58
- };
92
+ });
@@ -11,8 +11,9 @@ const TopicListContent = memo<{ mobile?: boolean }>(({ mobile }) => {
11
11
  {mobile ? <TopicSearchBar /> : <Header />}
12
12
  <Flexbox
13
13
  gap={16}
14
+ height={'100%'}
14
15
  paddingInline={mobile ? 0 : 8}
15
- style={{ overflowY: 'auto', paddingTop: 6, position: 'relative' }}
16
+ style={{ paddingTop: 6, position: 'relative' }}
16
17
  >
17
18
  <Topic />
18
19
  </Flexbox>
@@ -75,7 +75,13 @@ const LLM = memo(() => {
75
75
  name: [configKey, 'openAI', 'endpoint'],
76
76
  },
77
77
  {
78
- children: <Input allowClear placeholder={t('llm.OpenAI.customModelName.placeholder')} />,
78
+ children: (
79
+ <Input.TextArea
80
+ allowClear
81
+ placeholder={t('llm.OpenAI.customModelName.placeholder')}
82
+ style={{ height: 100 }}
83
+ />
84
+ ),
79
85
  desc: t('llm.OpenAI.customModelName.desc'),
80
86
  label: t('llm.OpenAI.customModelName.title'),
81
87
  name: [configKey, 'openAI', 'customModelName'],
@@ -0,0 +1,25 @@
1
+ import { memo, useEffect } from 'react';
2
+
3
+ import { useChatStore } from '@/store/chat';
4
+ import { chatSelectors } from '@/store/chat/selectors';
5
+
6
+ import BackBottom from './BackBottom';
7
+
8
+ interface AutoScrollProps {
9
+ atBottom: boolean;
10
+ onScrollToBottom: (type: 'auto' | 'click') => void;
11
+ }
12
+ const AutoScroll = memo<AutoScrollProps>(({ atBottom, onScrollToBottom }) => {
13
+ const trackVisibility = useChatStore((s) => !!s.chatLoadingId);
14
+ const str = useChatStore(chatSelectors.chatsMessageString);
15
+
16
+ useEffect(() => {
17
+ if (atBottom && trackVisibility) {
18
+ onScrollToBottom?.('auto');
19
+ }
20
+ }, [atBottom, trackVisibility, str]);
21
+
22
+ return <BackBottom onScrollToBottom={() => onScrollToBottom('click')} visible={!atBottom} />;
23
+ });
24
+
25
+ export default AutoScroll;
@@ -0,0 +1,31 @@
1
+ import { Icon } from '@lobehub/ui';
2
+ import { Button } from 'antd';
3
+ import { ListEnd } from 'lucide-react';
4
+ import { memo } from 'react';
5
+ import { useTranslation } from 'react-i18next';
6
+
7
+ import { useStyles } from './style';
8
+
9
+ export interface BackBottomProps {
10
+ onScrollToBottom: () => void;
11
+ visible: boolean;
12
+ }
13
+
14
+ const BackBottom = memo<BackBottomProps>(({ visible, onScrollToBottom }) => {
15
+ const { styles, cx } = useStyles();
16
+
17
+ const { t } = useTranslation('chat');
18
+
19
+ return (
20
+ <Button
21
+ className={cx(styles.container, visible && styles.visible)}
22
+ icon={<Icon icon={ListEnd} />}
23
+ onClick={onScrollToBottom}
24
+ size={'small'}
25
+ >
26
+ {t('backToBottom', { defaultValue: 'Back to bottom' })}
27
+ </Button>
28
+ );
29
+ });
30
+
31
+ export default BackBottom;
@@ -0,0 +1,36 @@
1
+ import { createStyles } from 'antd-style';
2
+ import { rgba } from 'polished';
3
+
4
+ export const useStyles = createStyles(({ token, css, stylish, cx, responsive }) => ({
5
+ container: cx(
6
+ stylish.blur,
7
+ css`
8
+ pointer-events: none;
9
+
10
+ position: absolute;
11
+ z-index: 1000;
12
+ right: 16px;
13
+ bottom: 16px;
14
+ transform: translateY(16px);
15
+
16
+ padding-inline: 12px !important;
17
+
18
+ opacity: 0;
19
+ background: ${rgba(token.colorBgContainer, 0.5)};
20
+ border-color: ${token.colorFillTertiary} !important;
21
+ border-radius: 16px !important;
22
+
23
+ ${responsive.mobile} {
24
+ right: 0;
25
+ border-right: none;
26
+ border-top-right-radius: 0 !important;
27
+ border-bottom-right-radius: 0 !important;
28
+ }
29
+ `,
30
+ ),
31
+ visible: css`
32
+ pointer-events: all;
33
+ transform: translateY(0);
34
+ opacity: 1;
35
+ `,
36
+ }));
@@ -13,12 +13,14 @@ import { renderErrorMessages } from '../../Error';
13
13
  import { renderMessagesExtra } from '../../Extras';
14
14
  import { renderMessages, useAvatarsClick } from '../../Messages';
15
15
  import ActionsBar from './ActionsBar';
16
+ import HistoryDivider from './HistoryDivider';
16
17
 
17
18
  export interface ChatListItemProps {
19
+ id: string;
18
20
  index: number;
19
21
  }
20
22
 
21
- const Item = memo<ChatListItemProps>(({ index }) => {
23
+ const Item = memo<ChatListItemProps>(({ index, id }) => {
22
24
  const { t } = useTranslation('common');
23
25
 
24
26
  const [editing, setEditing] = useState(false);
@@ -28,13 +30,18 @@ const Item = memo<ChatListItemProps>(({ index }) => {
28
30
  });
29
31
 
30
32
  const meta = useSessionStore(agentSelectors.currentAgentMeta, isEqual);
31
- const item = useChatStore(
32
- (s) => chatSelectors.currentChatsWithGuideMessage(meta)(s)[index],
33
- isEqual,
34
- );
33
+ const item = useChatStore((s) => {
34
+ const chats = chatSelectors.currentChatsWithGuideMessage(meta)(s);
35
+
36
+ if (index >= chats.length) return;
37
+
38
+ return chatSelectors.currentChatsWithGuideMessage(meta)(s)[index];
39
+ }, isEqual);
40
+
41
+ const historyLength = useChatStore((s) => chatSelectors.currentChats(s).length);
35
42
 
36
43
  const [loading, onMessageChange] = useChatStore((s) => [
37
- s.chatLoadingId === item.id,
44
+ s.chatLoadingId === id,
38
45
  s.updateMessageContent,
39
46
  ]);
40
47
 
@@ -49,7 +56,7 @@ const Item = memo<ChatListItemProps>(({ index }) => {
49
56
 
50
57
  return <RenderFunction {...data} editableContent={editableContent} />;
51
58
  },
52
- [item.role],
59
+ [item?.role],
53
60
  );
54
61
 
55
62
  const MessageExtra = useCallback(
@@ -61,7 +68,7 @@ const Item = memo<ChatListItemProps>(({ index }) => {
61
68
  if (!RenderFunction) return;
62
69
  return <RenderFunction {...data} />;
63
70
  },
64
- [item.role],
71
+ [item?.role],
65
72
  );
66
73
 
67
74
  const ErrorMessage = useCallback(
@@ -75,51 +82,65 @@ const Item = memo<ChatListItemProps>(({ index }) => {
75
82
  if (!RenderFunction) return;
76
83
  return <RenderFunction {...data} />;
77
84
  },
78
- [item.error],
85
+ [item?.error],
79
86
  );
80
87
 
81
88
  const error = useMemo(() => {
82
- if (!item.error) return;
89
+ if (!item?.error) return;
83
90
  const message = item.error?.message;
84
91
  let alertConfig = {};
85
92
  if (item.error.type && renderErrorMessages?.[item.error.type]) {
86
93
  alertConfig = renderErrorMessages[item.error.type]?.config as AlertProps;
87
94
  }
88
95
  return { message, ...alertConfig };
89
- }, [item.error]);
96
+ }, [item?.error]);
97
+
98
+ const enableHistoryDivider = useSessionStore((s) => {
99
+ const config = agentSelectors.currentAgentConfig(s);
100
+ return (
101
+ config.enableHistoryCount &&
102
+ historyLength > (config.historyCount ?? 0) &&
103
+ config.historyCount === historyLength - index + 1
104
+ );
105
+ });
90
106
 
91
107
  return (
92
- <ChatItem
93
- actions={<ActionsBar index={index} setEditing={setEditing} />}
94
- avatar={item.meta}
95
- editing={editing}
96
- error={error}
97
- errorMessage={<ErrorMessage data={item} />}
98
- loading={loading}
99
- message={item.content}
100
- messageExtra={<MessageExtra data={item} />}
101
- onAvatarClick={onAvatarsClick?.(item.role)}
102
- onChange={(value) => onMessageChange(item.id, value)}
103
- onDoubleClick={(e) => {
104
- if (item.id === 'default' || item.error) return;
105
- if (item.role && ['assistant', 'user'].includes(item.role) && e.altKey) {
106
- setEditing(true);
107
- }
108
- }}
109
- onEditingChange={setEditing}
110
- placement={type === 'chat' ? (item.role === 'user' ? 'right' : 'left') : 'left'}
111
- primary={item.role === 'user'}
112
- renderMessage={(editableContent) => (
113
- <RenderMessage data={item} editableContent={editableContent} />
114
- )}
115
- text={{
116
- cancel: t('cancel'),
117
- confirm: t('ok'),
118
- edit: t('edit'),
119
- }}
120
- time={item.updatedAt || item.createdAt}
121
- type={type === 'chat' ? 'block' : 'pure'}
122
- />
108
+ item && (
109
+ <>
110
+ <HistoryDivider enable={enableHistoryDivider} />
111
+ <ChatItem
112
+ actions={<ActionsBar index={index} setEditing={setEditing} />}
113
+ avatar={item.meta}
114
+ editing={editing}
115
+ error={error}
116
+ errorMessage={<ErrorMessage data={item} />}
117
+ loading={loading}
118
+ message={item.content}
119
+ messageExtra={<MessageExtra data={item} />}
120
+ onAvatarClick={onAvatarsClick?.(item.role)}
121
+ onChange={(value) => onMessageChange(item.id, value)}
122
+ onDoubleClick={(e) => {
123
+ if (item.id === 'default' || item.error) return;
124
+ if (item.role && ['assistant', 'user'].includes(item.role) && e.altKey) {
125
+ setEditing(true);
126
+ }
127
+ }}
128
+ onEditingChange={setEditing}
129
+ placement={type === 'chat' ? (item.role === 'user' ? 'right' : 'left') : 'left'}
130
+ primary={item.role === 'user'}
131
+ renderMessage={(editableContent) => (
132
+ <RenderMessage data={item} editableContent={editableContent} />
133
+ )}
134
+ text={{
135
+ cancel: t('cancel'),
136
+ confirm: t('ok'),
137
+ edit: t('edit'),
138
+ }}
139
+ time={item.updatedAt || item.createdAt}
140
+ type={type === 'chat' ? 'block' : 'pure'}
141
+ />
142
+ </>
143
+ )
123
144
  );
124
145
  });
125
146
 
@@ -1,42 +1,19 @@
1
- import isEqual from 'fast-deep-equal';
2
- import { Fragment, memo } from 'react';
1
+ import { memo } from 'react';
3
2
  import { Flexbox } from 'react-layout-kit';
4
3
 
5
4
  import { useChatStore } from '@/store/chat';
6
5
  import { chatSelectors } from '@/store/chat/selectors';
7
- import { useSessionStore } from '@/store/session';
8
- import { agentSelectors } from '@/store/session/selectors';
9
6
 
10
7
  import Item from '../ChatItem';
11
- import HistoryDivider from './HistoryDivider';
12
- import { useStyles } from './style';
13
8
 
14
9
  const ChatList = memo(() => {
15
- const { styles } = useStyles();
16
-
17
- const data = useChatStore(chatSelectors.currentChatIDsWithGuideMessage, isEqual);
18
-
19
- const [enableHistoryCount, historyCount = 0] = useSessionStore((s) => {
20
- const config = agentSelectors.currentAgentConfig(s);
21
- return [config.enableHistoryCount, config.historyCount];
22
- });
10
+ const ids = useChatStore(chatSelectors.currentChatIDsWithGuideMessage);
23
11
 
24
12
  return (
25
- <Flexbox className={styles.container}>
26
- {data.map((item, index) => {
27
- const historyLength = data.length;
28
- const enableHistoryDivider =
29
- enableHistoryCount &&
30
- historyLength > historyCount &&
31
- historyCount === historyLength - index + 1;
32
-
33
- return (
34
- <Fragment key={item}>
35
- <HistoryDivider enable={enableHistoryDivider} />
36
- <Item index={index} />
37
- </Fragment>
38
- );
39
- })}
13
+ <Flexbox height={'100%'} style={{ position: 'relative' }}>
14
+ {ids.map((id, index) => (
15
+ <Item id={id} index={index} key={id} />
16
+ ))}
40
17
  </Flexbox>
41
18
  );
42
19
  });
@@ -16,11 +16,14 @@ const useStyles = createStyles(({ css, prefixCls }) => ({
16
16
  }
17
17
  `,
18
18
  }));
19
- const SkeletonList = memo(() => {
19
+ interface SkeletonListProps {
20
+ mobile?: boolean;
21
+ }
22
+ const SkeletonList = memo<SkeletonListProps>(({ mobile }) => {
20
23
  const { styles } = useStyles();
21
24
 
22
25
  return (
23
- <Flexbox gap={24} padding={12} style={{ marginTop: 24 }}>
26
+ <Flexbox gap={24} padding={12} style={{ marginTop: 24 + (mobile ? 0 : 64) }}>
24
27
  <Skeleton
25
28
  active
26
29
  avatar={{ size: 40 }}
@@ -0,0 +1,78 @@
1
+ import isEqual from 'fast-deep-equal';
2
+ import React, { memo, useEffect, useRef, useState } from 'react';
3
+ import { Flexbox } from 'react-layout-kit';
4
+ import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
5
+
6
+ import { useChatStore } from '@/store/chat';
7
+ import { chatSelectors } from '@/store/chat/selectors';
8
+ import { isMobileScreen } from '@/utils/screen';
9
+
10
+ import AutoScroll from '../AutoScroll';
11
+ import Item from '../ChatItem';
12
+
13
+ const itemContent = (index: number, id: string) => {
14
+ const isMobile = isMobileScreen();
15
+
16
+ return index === 0 ? (
17
+ <div style={{ height: isMobile ? 24 : 64 }} />
18
+ ) : (
19
+ <Item id={id} index={index - 1} />
20
+ );
21
+ };
22
+
23
+ interface VirtualizedListProps {
24
+ mobile?: boolean;
25
+ }
26
+ const VirtualizedList = memo<VirtualizedListProps>(({ mobile }) => {
27
+ const virtuosoRef = useRef<VirtuosoHandle>(null);
28
+ const [atBottom, setAtBottom] = useState(true);
29
+
30
+ const data = useChatStore(
31
+ (s) => ['empty', ...chatSelectors.currentChatIDsWithGuideMessage(s)],
32
+ isEqual,
33
+ );
34
+ const [id, chatLoading] = useChatStore((s) => [
35
+ chatSelectors.currentChatKey(s),
36
+ chatSelectors.currentChatLoadingState(s),
37
+ ]);
38
+
39
+ useEffect(() => {
40
+ if (virtuosoRef.current) {
41
+ virtuosoRef.current.scrollToIndex({ align: 'end', behavior: 'auto', index: 'LAST' });
42
+ }
43
+ }, [id]);
44
+
45
+ return chatLoading && data.length === 2 ? null : (
46
+ <Flexbox height={'100%'}>
47
+ <Virtuoso
48
+ atBottomStateChange={setAtBottom}
49
+ atBottomThreshold={60 * (mobile ? 2 : 1)}
50
+ computeItemKey={(_, item) => item}
51
+ data={data}
52
+ followOutput={'auto'}
53
+ initialTopMostItemIndex={data?.length - 1}
54
+ itemContent={itemContent}
55
+ overscan={70 * 10}
56
+ ref={virtuosoRef}
57
+ />
58
+ <AutoScroll
59
+ atBottom={atBottom}
60
+ onScrollToBottom={(type) => {
61
+ const virtuoso = virtuosoRef.current;
62
+ switch (type) {
63
+ case 'auto': {
64
+ virtuoso?.scrollToIndex({ align: 'end', behavior: 'auto', index: 'LAST' });
65
+ break;
66
+ }
67
+ case 'click': {
68
+ virtuoso?.scrollToIndex({ align: 'end', behavior: 'smooth', index: 'LAST' });
69
+ break;
70
+ }
71
+ }
72
+ }}
73
+ />
74
+ </Flexbox>
75
+ );
76
+ });
77
+
78
+ export default VirtualizedList;
@@ -1,22 +1,19 @@
1
- import { BackBottom } from '@lobehub/ui';
2
1
  import { createStyles } from 'antd-style';
3
- import { ReactNode, memo, useRef } from 'react';
4
- import { useTranslation } from 'react-i18next';
2
+ import { ReactNode, memo } from 'react';
5
3
  import { Flexbox } from 'react-layout-kit';
6
4
 
7
- import SafeSpacing from '@/components/SafeSpacing';
8
5
  import { useGlobalStore } from '@/store/global';
9
6
  import { settingsSelectors } from '@/store/global/selectors';
10
7
 
11
- import ChatList from './components/ChatList';
12
- import ChatScrollAnchor from './components/ScrollAnchor';
13
8
  import SkeletonList from './components/SkeletonList';
9
+ import ChatList from './components/VirtualizedList';
14
10
  import { useInitConversation } from './hooks/useInitConversation';
15
11
 
16
12
  const useStyles = createStyles(({ css, responsive, stylish, cx }, fontSize: number = 14) =>
17
13
  cx(
18
14
  css`
19
- overflow: hidden scroll;
15
+ position: relative;
16
+ overflow-y: auto;
20
17
  height: 100%;
21
18
 
22
19
  ${responsive.mobile} {
@@ -46,9 +43,7 @@ interface ConversationProps {
46
43
  mobile?: boolean;
47
44
  }
48
45
 
49
- const Conversation = memo<ConversationProps>(({ mobile, chatInput }) => {
50
- const ref = useRef(null);
51
- const { t } = useTranslation('chat');
46
+ const Conversation = memo<ConversationProps>(({ chatInput, mobile }) => {
52
47
  const fontSize = useGlobalStore((s) => settingsSelectors.currentSettings(s).fontSize);
53
48
  const { styles } = useStyles(fontSize);
54
49
 
@@ -56,14 +51,9 @@ const Conversation = memo<ConversationProps>(({ mobile, chatInput }) => {
56
51
  const init = useInitConversation();
57
52
 
58
53
  return (
59
- <Flexbox flex={1} style={{ position: 'relative' }}>
60
- <div style={{ flex: 1, overflow: 'hidden', position: 'relative' }}>
61
- <div className={styles} ref={ref}>
62
- {!mobile && <SafeSpacing />}
63
- {init ? <ChatList /> : <SkeletonList />}
64
- <ChatScrollAnchor />
65
- </div>
66
- <BackBottom target={ref} text={t('backToBottom')} />
54
+ <Flexbox data-id={'conversation'} flex={1}>
55
+ <div className={styles}>
56
+ {init ? <ChatList mobile={mobile} /> : <SkeletonList mobile={mobile} />}
67
57
  </div>
68
58
  {chatInput}
69
59
  </Flexbox>
@@ -28,7 +28,7 @@ export const createI18nNext = (lang?: string) => {
28
28
  init: () =>
29
29
  instance.init({
30
30
  debug: debugMode,
31
- defaultNS: ['error', 'common', 'empty', 'chat'],
31
+ defaultNS: ['error', 'common', 'chat'],
32
32
  detection: {
33
33
  caches: ['cookie'],
34
34
  cookieMinutes: 60 * 24 * COOKIE_CACHE_DAYS,
@@ -65,12 +65,16 @@ export default {
65
65
  confirmRemoveTopic: '即将删除该话题,删除后将不可恢复,请谨慎操作。',
66
66
  confirmRemoveUnstarred: '即将删除未收藏话题,删除后将不可恢复,请谨慎操作。',
67
67
  defaultTitle: '默认话题',
68
+ guide: {
69
+ desc: '点击发送左侧按钮可将当前会话保存为历史话题,并开启新一轮会话',
70
+ title: '话题列表',
71
+ },
68
72
  openNewTopic: '开启新话题',
69
73
  removeAll: '删除全部话题',
70
74
  removeUnstarred: '删除未收藏话题',
71
75
  saveCurrentMessages: '将当前会话保存为话题',
72
76
  searchPlaceholder: '搜索话题...',
73
- title: '话题列表',
77
+ title: '话题',
74
78
  },
75
79
  translate: {
76
80
  action: '翻译',
@@ -1,7 +1,6 @@
1
1
  import tool from '../default/tool';
2
2
  import chat from './chat';
3
3
  import common from './common';
4
- import empty from './empty';
5
4
  import error from './error';
6
5
  import market from './market';
7
6
  import migration from './migration';
@@ -12,7 +11,6 @@ import welcome from './welcome';
12
11
  const resources = {
13
12
  chat,
14
13
  common,
15
- empty,
16
14
  error,
17
15
  market,
18
16
  migration,
@@ -39,6 +39,8 @@ const getMeta = (message: ChatMessage) => {
39
39
  }
40
40
  };
41
41
 
42
+ const currentChatKey = (s: ChatStore) => `${s.activeId}_${s.activeTopicId}`;
43
+
42
44
  // 当前激活的消息列表
43
45
  const currentChats = (s: ChatStore): ChatMessage[] => {
44
46
  if (!s.activeId) return [];
@@ -117,9 +119,13 @@ const getMessageById = (id: string) => (s: ChatStore) => chatHelpers.getMessageB
117
119
 
118
120
  const latestMessage = (s: ChatStore) => currentChats(s).at(-1);
119
121
 
122
+ const currentChatLoadingState = (s: ChatStore) => !s.messagesInit;
123
+
120
124
  export const chatSelectors = {
121
125
  chatsMessageString,
122
126
  currentChatIDsWithGuideMessage,
127
+ currentChatKey,
128
+ currentChatLoadingState,
123
129
  currentChats,
124
130
  currentChatsWithGuideMessage,
125
131
  currentChatsWithHistoryConfig,
package/vercel.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "installCommand": "/bun1/bun install"
2
+ "installCommand": "npx bun@1.0.15 install"
3
3
  }
@@ -1,10 +0,0 @@
1
- import { createStyles } from 'antd-style';
2
-
3
- export const useStyles = createStyles(({ css }) => {
4
- return {
5
- container: css`
6
- position: relative;
7
- margin-top: 24px;
8
- `,
9
- };
10
- });
@@ -1,29 +0,0 @@
1
- import { memo, useEffect } from 'react';
2
- import { useInView } from 'react-intersection-observer';
3
-
4
- import { useChatStore } from '@/store/chat';
5
- import { chatSelectors } from '@/store/chat/selectors';
6
-
7
- import { useAtBottom } from './useAtBottom';
8
-
9
- const ChatScrollAnchor = memo(() => {
10
- const trackVisibility = useChatStore((s) => !!s.chatLoadingId);
11
- const str = useChatStore(chatSelectors.chatsMessageString);
12
-
13
- const isAtBottom = useAtBottom();
14
- const { ref, entry, inView } = useInView({
15
- delay: 100,
16
- rootMargin: '0px 0px -150px 0px',
17
- trackVisibility,
18
- });
19
-
20
- useEffect(() => {
21
- if (isAtBottom && trackVisibility && !inView) {
22
- entry?.target.scrollIntoView({ block: 'start' });
23
- }
24
- }, [inView, entry, isAtBottom, trackVisibility, str]);
25
-
26
- return <div ref={ref} style={{ height: 1, width: '100%' }} />;
27
- });
28
-
29
- export default ChatScrollAnchor;
@@ -1,20 +0,0 @@
1
- import { useEffect, useState } from 'react';
2
-
3
- export const useAtBottom = (offset = 0) => {
4
- const [isAtBottom, setIsAtBottom] = useState(false);
5
-
6
- useEffect(() => {
7
- const handleScroll = () => {
8
- setIsAtBottom(window.innerHeight + window.scrollY >= document.body.offsetHeight - offset);
9
- };
10
-
11
- window.addEventListener('scroll', handleScroll, { passive: true });
12
- handleScroll();
13
-
14
- return () => {
15
- window.removeEventListener('scroll', handleScroll);
16
- };
17
- }, [offset]);
18
-
19
- return isAtBottom;
20
- };
@@ -1,6 +0,0 @@
1
- export default {
2
- topic: {
3
- desc: '点击发送左侧按钮可将当前会话保存为历史话题,并开启新一轮会话',
4
- title: '话题列表',
5
- },
6
- };