@lobehub/chat 1.26.20 → 1.27.0
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 +50 -0
- package/locales/ar/chat.json +1 -21
- package/locales/ar/error.json +4 -1
- package/locales/ar/topic.json +37 -0
- package/locales/bg-BG/chat.json +1 -21
- package/locales/bg-BG/error.json +4 -1
- package/locales/bg-BG/topic.json +37 -0
- package/locales/de-DE/chat.json +1 -21
- package/locales/de-DE/error.json +4 -1
- package/locales/de-DE/topic.json +37 -0
- package/locales/en-US/chat.json +1 -21
- package/locales/en-US/error.json +4 -1
- package/locales/en-US/topic.json +37 -0
- package/locales/es-ES/chat.json +1 -21
- package/locales/es-ES/error.json +4 -1
- package/locales/es-ES/topic.json +37 -0
- package/locales/fa-IR/chat.json +1 -21
- package/locales/fa-IR/components.json +1 -0
- package/locales/fa-IR/error.json +4 -1
- package/locales/fa-IR/topic.json +37 -0
- package/locales/fr-FR/chat.json +1 -21
- package/locales/fr-FR/error.json +4 -1
- package/locales/fr-FR/topic.json +37 -0
- package/locales/it-IT/chat.json +1 -21
- package/locales/it-IT/error.json +4 -1
- package/locales/it-IT/topic.json +37 -0
- package/locales/ja-JP/chat.json +1 -21
- package/locales/ja-JP/error.json +4 -1
- package/locales/ja-JP/topic.json +37 -0
- package/locales/ko-KR/chat.json +1 -21
- package/locales/ko-KR/error.json +4 -1
- package/locales/ko-KR/topic.json +37 -0
- package/locales/nl-NL/chat.json +1 -21
- package/locales/nl-NL/error.json +4 -1
- package/locales/nl-NL/topic.json +37 -0
- package/locales/pl-PL/chat.json +1 -21
- package/locales/pl-PL/error.json +4 -1
- package/locales/pl-PL/topic.json +37 -0
- package/locales/pt-BR/chat.json +1 -21
- package/locales/pt-BR/error.json +4 -1
- package/locales/pt-BR/topic.json +37 -0
- package/locales/ru-RU/chat.json +1 -21
- package/locales/ru-RU/error.json +4 -1
- package/locales/ru-RU/topic.json +37 -0
- package/locales/tr-TR/chat.json +1 -21
- package/locales/tr-TR/error.json +4 -1
- package/locales/tr-TR/topic.json +37 -0
- package/locales/vi-VN/chat.json +1 -21
- package/locales/vi-VN/error.json +4 -1
- package/locales/vi-VN/topic.json +37 -0
- package/locales/zh-CN/chat.json +1 -21
- package/locales/zh-CN/topic.json +37 -0
- package/locales/zh-TW/chat.json +1 -21
- package/locales/zh-TW/error.json +4 -1
- package/locales/zh-TW/topic.json +37 -0
- package/package.json +1 -1
- package/src/app/(main)/@nav/_layout/Desktop/TopActions.tsx +3 -2
- package/src/app/(main)/@nav/_layout/Desktop/index.tsx +4 -1
- package/src/app/(main)/chat/(workspace)/@topic/features/Header.tsx +27 -9
- package/src/app/(main)/chat/(workspace)/@topic/features/TopicListContent/ByTimeMode/GroupItem.tsx +30 -0
- package/src/app/(main)/chat/(workspace)/@topic/features/TopicListContent/ByTimeMode/index.tsx +72 -0
- package/src/app/(main)/chat/(workspace)/@topic/features/TopicListContent/DefaultContent.tsx +2 -2
- package/src/app/(main)/chat/(workspace)/@topic/features/TopicListContent/FlatMode/index.tsx +60 -0
- package/src/app/(main)/chat/(workspace)/@topic/features/TopicListContent/TopicContent.tsx +7 -7
- package/src/app/(main)/chat/(workspace)/@topic/features/TopicListContent/index.tsx +17 -53
- package/src/app/(main)/chat/(workspace)/@topic/features/TopicSearchBar/index.tsx +2 -2
- package/src/app/(main)/chat/(workspace)/_layout/Mobile/ChatHeader/ChatHeaderTitle.tsx +2 -2
- package/src/app/(main)/chat/(workspace)/_layout/Mobile/TopicModal.tsx +5 -2
- package/src/const/user.ts +2 -0
- package/src/hooks/useFetchTopics.ts +11 -0
- package/src/locales/default/chat.ts +0 -20
- package/src/locales/default/index.ts +3 -1
- package/src/locales/default/topic.ts +37 -0
- package/src/services/__tests__/chat.test.ts +45 -4
- package/src/services/chat.ts +9 -1
- package/src/store/chat/slices/topic/action.test.ts +1 -1
- package/src/store/chat/slices/topic/action.ts +4 -4
- package/src/store/chat/slices/topic/selectors.test.ts +78 -1
- package/src/store/chat/slices/topic/selectors.ts +28 -1
- package/src/store/user/slices/preference/selectors.ts +4 -0
- package/src/types/topic.ts +24 -0
- package/src/types/user/index.ts +2 -0
- package/src/utils/client/topic.test.ts +142 -0
- package/src/utils/client/topic.ts +86 -0
@@ -0,0 +1,72 @@
|
|
1
|
+
'use client';
|
2
|
+
|
3
|
+
import isEqual from 'fast-deep-equal';
|
4
|
+
import React, { memo, useCallback, useMemo, useRef } from 'react';
|
5
|
+
import { useTranslation } from 'react-i18next';
|
6
|
+
import { GroupedVirtuoso, VirtuosoHandle } from 'react-virtuoso';
|
7
|
+
|
8
|
+
import { useChatStore } from '@/store/chat';
|
9
|
+
import { topicSelectors } from '@/store/chat/selectors';
|
10
|
+
import { ChatTopic } from '@/types/topic';
|
11
|
+
|
12
|
+
import TopicItem from '../TopicItem';
|
13
|
+
import TopicGroupItem from './GroupItem';
|
14
|
+
|
15
|
+
const ByTimeMode = memo(() => {
|
16
|
+
const { t } = useTranslation('topic');
|
17
|
+
const virtuosoRef = useRef<VirtuosoHandle>(null);
|
18
|
+
const [activeTopicId] = useChatStore((s) => [s.activeTopicId]);
|
19
|
+
const groupTopics = useChatStore(topicSelectors.groupedTopicsSelector, isEqual);
|
20
|
+
|
21
|
+
const { groups, groupCounts, topics } = useMemo(() => {
|
22
|
+
return {
|
23
|
+
groupCounts: [1, ...groupTopics.map((group) => group.children.length)],
|
24
|
+
groups: [
|
25
|
+
{ id: 'default' },
|
26
|
+
...groupTopics.map((group) => ({ id: group.id, title: group.title })),
|
27
|
+
],
|
28
|
+
topics: [
|
29
|
+
{ favorite: false, id: 'default', title: t('defaultTitle') } as ChatTopic,
|
30
|
+
...groupTopics.flatMap((group) => group.children),
|
31
|
+
],
|
32
|
+
};
|
33
|
+
}, [groupTopics]);
|
34
|
+
|
35
|
+
const itemContent = useCallback(
|
36
|
+
(index: number) => {
|
37
|
+
const { id, favorite, title } = topics[index];
|
38
|
+
|
39
|
+
return index === 0 ? (
|
40
|
+
<TopicItem active={!activeTopicId} fav={favorite} title={title} />
|
41
|
+
) : (
|
42
|
+
<TopicItem active={activeTopicId === id} fav={favorite} id={id} key={id} title={title} />
|
43
|
+
);
|
44
|
+
},
|
45
|
+
[activeTopicId, topics],
|
46
|
+
);
|
47
|
+
|
48
|
+
const groupContent = useCallback(
|
49
|
+
(index: number) => {
|
50
|
+
if (index === 0) return undefined;
|
51
|
+
|
52
|
+
const topicGroup = groups[index];
|
53
|
+
return <TopicGroupItem {...topicGroup} />;
|
54
|
+
},
|
55
|
+
[groups],
|
56
|
+
);
|
57
|
+
|
58
|
+
// const activeIndex = topics.findIndex((topic) => topic.id === activeTopicId);
|
59
|
+
|
60
|
+
return (
|
61
|
+
<GroupedVirtuoso
|
62
|
+
groupContent={groupContent}
|
63
|
+
groupCounts={groupCounts}
|
64
|
+
itemContent={itemContent}
|
65
|
+
ref={virtuosoRef}
|
66
|
+
/>
|
67
|
+
);
|
68
|
+
});
|
69
|
+
|
70
|
+
ByTimeMode.displayName = 'ByTimeMode';
|
71
|
+
|
72
|
+
export default ByTimeMode;
|
@@ -9,7 +9,7 @@ import { Flexbox } from 'react-layout-kit';
|
|
9
9
|
const { Paragraph } = Typography;
|
10
10
|
|
11
11
|
const DefaultContent = memo(() => {
|
12
|
-
const { t } = useTranslation('
|
12
|
+
const { t } = useTranslation('topic');
|
13
13
|
|
14
14
|
const theme = useTheme();
|
15
15
|
|
@@ -19,7 +19,7 @@ const DefaultContent = memo(() => {
|
|
19
19
|
<Icon color={theme.colorTextDescription} icon={MessageSquareDashed} />
|
20
20
|
</Flexbox>
|
21
21
|
<Paragraph ellipsis={{ rows: 1 }} style={{ margin: 0 }}>
|
22
|
-
{t('
|
22
|
+
{t('defaultTitle')}
|
23
23
|
</Paragraph>
|
24
24
|
<Tag>{t('temp')}</Tag>
|
25
25
|
</Flexbox>
|
@@ -0,0 +1,60 @@
|
|
1
|
+
'use client';
|
2
|
+
|
3
|
+
import isEqual from 'fast-deep-equal';
|
4
|
+
import React, { memo, useCallback, useMemo, useRef } from 'react';
|
5
|
+
import { useTranslation } from 'react-i18next';
|
6
|
+
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
|
7
|
+
|
8
|
+
import { useChatStore } from '@/store/chat';
|
9
|
+
import { topicSelectors } from '@/store/chat/selectors';
|
10
|
+
import { ChatTopic } from '@/types/topic';
|
11
|
+
|
12
|
+
import TopicItem from '../TopicItem';
|
13
|
+
|
14
|
+
const FlatMode = memo(() => {
|
15
|
+
const { t } = useTranslation('topic');
|
16
|
+
const virtuosoRef = useRef<VirtuosoHandle>(null);
|
17
|
+
const [activeTopicId] = useChatStore((s) => [s.activeTopicId]);
|
18
|
+
const activeTopicList = useChatStore(topicSelectors.displayTopics, isEqual);
|
19
|
+
|
20
|
+
const topics = useMemo(
|
21
|
+
() => [
|
22
|
+
{ favorite: false, id: 'default', title: t('defaultTitle') } as ChatTopic,
|
23
|
+
...(activeTopicList || []),
|
24
|
+
],
|
25
|
+
[activeTopicList],
|
26
|
+
);
|
27
|
+
|
28
|
+
const itemContent = useCallback(
|
29
|
+
(index: number, { id, favorite, title }: ChatTopic) =>
|
30
|
+
index === 0 ? (
|
31
|
+
<TopicItem active={!activeTopicId} fav={favorite} title={title} />
|
32
|
+
) : (
|
33
|
+
<TopicItem active={activeTopicId === id} fav={favorite} id={id} key={id} title={title} />
|
34
|
+
),
|
35
|
+
[activeTopicId],
|
36
|
+
);
|
37
|
+
|
38
|
+
const activeIndex = topics.findIndex((topic) => topic.id === activeTopicId);
|
39
|
+
|
40
|
+
return (
|
41
|
+
<Virtuoso
|
42
|
+
// components={{ ScrollSeekPlaceholder: Placeholder }}
|
43
|
+
computeItemKey={(_, item) => item.id}
|
44
|
+
data={topics}
|
45
|
+
fixedItemHeight={44}
|
46
|
+
initialTopMostItemIndex={Math.max(activeIndex, 0)}
|
47
|
+
itemContent={itemContent}
|
48
|
+
overscan={44 * 10}
|
49
|
+
ref={virtuosoRef}
|
50
|
+
// scrollSeekConfiguration={{
|
51
|
+
// enter: (velocity) => Math.abs(velocity) > 350,
|
52
|
+
// exit: (velocity) => Math.abs(velocity) < 10,
|
53
|
+
// }}
|
54
|
+
/>
|
55
|
+
);
|
56
|
+
});
|
57
|
+
|
58
|
+
FlatMode.displayName = 'FlatMode';
|
59
|
+
|
60
|
+
export default FlatMode;
|
@@ -42,7 +42,7 @@ interface TopicContentProps {
|
|
42
42
|
}
|
43
43
|
|
44
44
|
const TopicContent = memo<TopicContentProps>(({ id, title, fav, showMore }) => {
|
45
|
-
const { t } = useTranslation('common');
|
45
|
+
const { t } = useTranslation(['topic', 'common']);
|
46
46
|
|
47
47
|
const mobile = useIsMobile();
|
48
48
|
|
@@ -76,7 +76,7 @@ const TopicContent = memo<TopicContentProps>(({ id, title, fav, showMore }) => {
|
|
76
76
|
{
|
77
77
|
icon: <Icon icon={Wand2} />,
|
78
78
|
key: 'autoRename',
|
79
|
-
label: t('
|
79
|
+
label: t('actions.autoRename'),
|
80
80
|
onClick: () => {
|
81
81
|
autoRenameTopicTitle(id);
|
82
82
|
},
|
@@ -84,7 +84,7 @@ const TopicContent = memo<TopicContentProps>(({ id, title, fav, showMore }) => {
|
|
84
84
|
{
|
85
85
|
icon: <Icon icon={PencilLine} />,
|
86
86
|
key: 'rename',
|
87
|
-
label: t('rename'),
|
87
|
+
label: t('rename', { ns: 'common' }),
|
88
88
|
onClick: () => {
|
89
89
|
toggleEditing(true);
|
90
90
|
},
|
@@ -95,7 +95,7 @@ const TopicContent = memo<TopicContentProps>(({ id, title, fav, showMore }) => {
|
|
95
95
|
{
|
96
96
|
icon: <Icon icon={LucideCopy} />,
|
97
97
|
key: 'duplicate',
|
98
|
-
label: t('
|
98
|
+
label: t('actions.duplicate'),
|
99
99
|
onClick: () => {
|
100
100
|
duplicateTopic(id);
|
101
101
|
},
|
@@ -103,7 +103,7 @@ const TopicContent = memo<TopicContentProps>(({ id, title, fav, showMore }) => {
|
|
103
103
|
// {
|
104
104
|
// icon: <Icon icon={LucideDownload} />,
|
105
105
|
// key: 'export',
|
106
|
-
// label: t('topic.actions.export'
|
106
|
+
// label: t('topic.actions.export'),
|
107
107
|
// onClick: () => {
|
108
108
|
// configService.exportSingleTopic(sessionId, id);
|
109
109
|
// },
|
@@ -120,7 +120,7 @@ const TopicContent = memo<TopicContentProps>(({ id, title, fav, showMore }) => {
|
|
120
120
|
danger: true,
|
121
121
|
icon: <Icon icon={Trash} />,
|
122
122
|
key: 'delete',
|
123
|
-
label: t('delete'),
|
123
|
+
label: t('delete', { ns: 'common' }),
|
124
124
|
onClick: () => {
|
125
125
|
if (!id) return;
|
126
126
|
|
@@ -130,7 +130,7 @@ const TopicContent = memo<TopicContentProps>(({ id, title, fav, showMore }) => {
|
|
130
130
|
onOk: async () => {
|
131
131
|
await removeTopic(id);
|
132
132
|
},
|
133
|
-
title: t('
|
133
|
+
title: t('actions.confirmRemoveTopic'),
|
134
134
|
});
|
135
135
|
},
|
136
136
|
},
|
@@ -3,61 +3,38 @@
|
|
3
3
|
import { EmptyCard } from '@lobehub/ui';
|
4
4
|
import { useThemeMode } from 'antd-style';
|
5
5
|
import isEqual from 'fast-deep-equal';
|
6
|
-
import React, { memo
|
6
|
+
import React, { memo } from 'react';
|
7
7
|
import { useTranslation } from 'react-i18next';
|
8
8
|
import { Flexbox } from 'react-layout-kit';
|
9
|
-
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
|
10
9
|
|
11
10
|
import { imageUrl } from '@/const/url';
|
11
|
+
import { useFetchTopics } from '@/hooks/useFetchTopics';
|
12
12
|
import { useChatStore } from '@/store/chat';
|
13
13
|
import { topicSelectors } from '@/store/chat/selectors';
|
14
|
-
import { useSessionStore } from '@/store/session';
|
15
14
|
import { useUserStore } from '@/store/user';
|
16
|
-
import {
|
15
|
+
import { preferenceSelectors } from '@/store/user/selectors';
|
16
|
+
import { TopicDisplayMode } from '@/types/topic';
|
17
17
|
|
18
18
|
import { SkeletonList } from '../SkeletonList';
|
19
|
-
import
|
19
|
+
import ByTimeMode from './ByTimeMode';
|
20
|
+
import FlatMode from './FlatMode';
|
20
21
|
|
21
22
|
const TopicListContent = memo(() => {
|
22
|
-
const { t } = useTranslation('
|
23
|
-
const virtuosoRef = useRef<VirtuosoHandle>(null);
|
23
|
+
const { t } = useTranslation('topic');
|
24
24
|
const { isDarkMode } = useThemeMode();
|
25
|
-
const [topicsInit,
|
25
|
+
const [topicsInit, topicLength] = useChatStore((s) => [
|
26
26
|
s.topicsInit,
|
27
|
-
s.activeTopicId,
|
28
27
|
topicSelectors.currentTopicLength(s),
|
29
|
-
s.useFetchTopics,
|
30
28
|
]);
|
31
|
-
const
|
29
|
+
const activeTopicList = useChatStore(topicSelectors.displayTopics, isEqual);
|
30
|
+
|
31
|
+
const [visible, updateGuideState, topicDisplayMode] = useUserStore((s) => [
|
32
32
|
s.preference.guide?.topic,
|
33
33
|
s.updateGuideState,
|
34
|
+
preferenceSelectors.topicDisplayMode(s),
|
34
35
|
]);
|
35
36
|
|
36
|
-
|
37
|
-
|
38
|
-
const topics = useMemo(
|
39
|
-
() => [
|
40
|
-
{ favorite: false, id: 'default', title: t('topic.defaultTitle') } as ChatTopic,
|
41
|
-
...(activeTopicList || []),
|
42
|
-
],
|
43
|
-
[activeTopicList],
|
44
|
-
);
|
45
|
-
|
46
|
-
const [sessionId] = useSessionStore((s) => [s.activeId]);
|
47
|
-
|
48
|
-
useFetchTopics(sessionId);
|
49
|
-
|
50
|
-
const itemContent = useCallback(
|
51
|
-
(index: number, { id, favorite, title }: ChatTopic) =>
|
52
|
-
index === 0 ? (
|
53
|
-
<TopicItem active={!activeTopicId} fav={favorite} title={title} />
|
54
|
-
) : (
|
55
|
-
<TopicItem active={activeTopicId === id} fav={favorite} id={id} key={id} title={title} />
|
56
|
-
),
|
57
|
-
[activeTopicId],
|
58
|
-
);
|
59
|
-
|
60
|
-
const activeIndex = topics.findIndex((topic) => topic.id === activeTopicId);
|
37
|
+
useFetchTopics();
|
61
38
|
|
62
39
|
// first time loading or has no data
|
63
40
|
if (!topicsInit || !activeTopicList) return <SkeletonList />;
|
@@ -67,9 +44,9 @@ const TopicListContent = memo(() => {
|
|
67
44
|
{topicLength === 0 && visible && (
|
68
45
|
<Flexbox paddingInline={8}>
|
69
46
|
<EmptyCard
|
70
|
-
alt={t('
|
47
|
+
alt={t('guide.desc')}
|
71
48
|
cover={imageUrl(`empty_topic_${isDarkMode ? 'dark' : 'light'}.webp`)}
|
72
|
-
desc={t('
|
49
|
+
desc={t('guide.desc')}
|
73
50
|
height={120}
|
74
51
|
imageProps={{
|
75
52
|
priority: true,
|
@@ -78,26 +55,13 @@ const TopicListContent = memo(() => {
|
|
78
55
|
updateGuideState({ topic: visible });
|
79
56
|
}}
|
80
57
|
style={{ flex: 'none', marginBottom: 12 }}
|
81
|
-
title={t('
|
58
|
+
title={t('guide.title')}
|
82
59
|
visible={visible}
|
83
60
|
width={200}
|
84
61
|
/>
|
85
62
|
</Flexbox>
|
86
63
|
)}
|
87
|
-
<
|
88
|
-
// components={{ ScrollSeekPlaceholder: Placeholder }}
|
89
|
-
computeItemKey={(_, item) => item.id}
|
90
|
-
data={topics}
|
91
|
-
fixedItemHeight={44}
|
92
|
-
initialTopMostItemIndex={Math.max(activeIndex, 0)}
|
93
|
-
itemContent={itemContent}
|
94
|
-
overscan={44 * 10}
|
95
|
-
ref={virtuosoRef}
|
96
|
-
// scrollSeekConfiguration={{
|
97
|
-
// enter: (velocity) => Math.abs(velocity) > 350,
|
98
|
-
// exit: (velocity) => Math.abs(velocity) < 10,
|
99
|
-
// }}
|
100
|
-
/>
|
64
|
+
{topicDisplayMode === TopicDisplayMode.ByTime ? <ByTimeMode /> : <FlatMode />}
|
101
65
|
</>
|
102
66
|
);
|
103
67
|
});
|
@@ -9,7 +9,7 @@ import { useChatStore } from '@/store/chat';
|
|
9
9
|
import { useServerConfigStore } from '@/store/serverConfig';
|
10
10
|
|
11
11
|
const TopicSearchBar = memo<{ onClear?: () => void }>(({ onClear }) => {
|
12
|
-
const { t } = useTranslation('
|
12
|
+
const { t } = useTranslation('topic');
|
13
13
|
|
14
14
|
const [keywords, setKeywords] = useState('');
|
15
15
|
const mobile = useServerConfigStore((s) => s.isMobile);
|
@@ -31,7 +31,7 @@ const TopicSearchBar = memo<{ onClear?: () => void }>(({ onClear }) => {
|
|
31
31
|
setKeywords(value);
|
32
32
|
useChatStore.setState({ isSearchingTopic: !!value });
|
33
33
|
}}
|
34
|
-
placeholder={t('
|
34
|
+
placeholder={t('searchPlaceholder')}
|
35
35
|
spotlight={!mobile}
|
36
36
|
type={mobile ? 'block' : 'ghost'}
|
37
37
|
value={keywords}
|
@@ -12,7 +12,7 @@ import { useSessionStore } from '@/store/session';
|
|
12
12
|
import { sessionMetaSelectors, sessionSelectors } from '@/store/session/selectors';
|
13
13
|
|
14
14
|
const ChatHeaderTitle = memo(() => {
|
15
|
-
const { t } = useTranslation('chat');
|
15
|
+
const { t } = useTranslation(['chat', 'topic']);
|
16
16
|
const toggleConfig = useGlobalStore((s) => s.toggleMobileTopic);
|
17
17
|
const [topicLength, topic] = useChatStore((s) => [
|
18
18
|
topicSelectors.currentTopicLength(s),
|
@@ -30,7 +30,7 @@ const ChatHeaderTitle = memo(() => {
|
|
30
30
|
<MobileNavBarTitle
|
31
31
|
desc={
|
32
32
|
<Flexbox align={'center'} gap={4} horizontal onClick={() => toggleConfig()}>
|
33
|
-
<span>{topic?.title || t('
|
33
|
+
<span>{topic?.title || t('title', { ns: 'topic' })}</span>
|
34
34
|
<ActionIcon
|
35
35
|
active
|
36
36
|
icon={ChevronDown}
|
@@ -4,6 +4,7 @@ import { Modal } from '@lobehub/ui';
|
|
4
4
|
import { PropsWithChildren, memo } from 'react';
|
5
5
|
import { useTranslation } from 'react-i18next';
|
6
6
|
|
7
|
+
import { useFetchTopics } from '@/hooks/useFetchTopics';
|
7
8
|
import { useGlobalStore } from '@/store/global';
|
8
9
|
import { systemStatusSelectors } from '@/store/global/selectors';
|
9
10
|
|
@@ -15,7 +16,9 @@ const Topics = memo(({ children }: PropsWithChildren) => {
|
|
15
16
|
s.toggleMobileTopic,
|
16
17
|
]);
|
17
18
|
const [open, setOpen] = useWorkspaceModal(showAgentSettings, toggleConfig);
|
18
|
-
const { t } = useTranslation('
|
19
|
+
const { t } = useTranslation('topic');
|
20
|
+
|
21
|
+
useFetchTopics();
|
19
22
|
|
20
23
|
return (
|
21
24
|
<Modal
|
@@ -25,7 +28,7 @@ const Topics = memo(({ children }: PropsWithChildren) => {
|
|
25
28
|
styles={{
|
26
29
|
body: { padding: 0 },
|
27
30
|
}}
|
28
|
-
title={t('
|
31
|
+
title={t('title')}
|
29
32
|
>
|
30
33
|
{children}
|
31
34
|
</Modal>
|
package/src/const/user.ts
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
import { TopicDisplayMode } from '@/types/topic';
|
1
2
|
import { UserPreference } from '@/types/user';
|
2
3
|
|
3
4
|
export const DEFAULT_PREFERENCE: UserPreference = {
|
@@ -6,5 +7,6 @@ export const DEFAULT_PREFERENCE: UserPreference = {
|
|
6
7
|
topic: true,
|
7
8
|
},
|
8
9
|
telemetry: null,
|
10
|
+
topicDisplayMode: TopicDisplayMode.ByTime,
|
9
11
|
useCmdEnterToSend: false,
|
10
12
|
};
|
@@ -0,0 +1,11 @@
|
|
1
|
+
import { useChatStore } from '@/store/chat';
|
2
|
+
import { useSessionStore } from '@/store/session';
|
3
|
+
|
4
|
+
/**
|
5
|
+
* Fetch topics for the current session
|
6
|
+
*/
|
7
|
+
export const useFetchTopics = () => {
|
8
|
+
const [sessionId] = useSessionStore((s) => [s.activeId]);
|
9
|
+
const [useFetchTopics] = useChatStore((s) => [s.useFetchTopics]);
|
10
|
+
useFetchTopics(sessionId);
|
11
|
+
};
|
@@ -117,7 +117,6 @@ export default {
|
|
117
117
|
loading: '识别中...',
|
118
118
|
prettifying: '润色中...',
|
119
119
|
},
|
120
|
-
temp: '临时',
|
121
120
|
tokenDetails: {
|
122
121
|
chats: '会话消息',
|
123
122
|
rest: '剩余可用',
|
@@ -133,29 +132,10 @@ export default {
|
|
133
132
|
used: '使用',
|
134
133
|
},
|
135
134
|
topic: {
|
136
|
-
actions: {
|
137
|
-
autoRename: '智能重命名',
|
138
|
-
duplicate: '创建副本',
|
139
|
-
export: '导出话题',
|
140
|
-
},
|
141
135
|
checkOpenNewTopic: '是否开启新话题?',
|
142
136
|
checkSaveCurrentMessages: '是否保存当前会话为话题?',
|
143
|
-
confirmRemoveAll: '即将删除全部话题,删除后将不可恢复,请谨慎操作。',
|
144
|
-
confirmRemoveTopic: '即将删除该话题,删除后将不可恢复,请谨慎操作。',
|
145
|
-
confirmRemoveUnstarred: '即将删除未收藏话题,删除后将不可恢复,请谨慎操作。',
|
146
|
-
defaultTitle: '默认话题',
|
147
|
-
duplicateLoading: '话题复制中...',
|
148
|
-
duplicateSuccess: '话题复制成功',
|
149
|
-
guide: {
|
150
|
-
desc: '点击发送左侧按钮可将当前会话保存为历史话题,并开启新一轮会话',
|
151
|
-
title: '话题列表',
|
152
|
-
},
|
153
137
|
openNewTopic: '开启新话题',
|
154
|
-
removeAll: '删除全部话题',
|
155
|
-
removeUnstarred: '删除未收藏话题',
|
156
138
|
saveCurrentMessages: '将当前会话保存为话题',
|
157
|
-
searchPlaceholder: '搜索话题...',
|
158
|
-
title: '话题',
|
159
139
|
},
|
160
140
|
translate: {
|
161
141
|
action: '翻译',
|
@@ -1,4 +1,3 @@
|
|
1
|
-
import tool from '../default/tool';
|
2
1
|
import auth from './auth';
|
3
2
|
import chat from './chat';
|
4
3
|
import clerk from './clerk';
|
@@ -17,6 +16,8 @@ import portal from './portal';
|
|
17
16
|
import providers from './providers';
|
18
17
|
import ragEval from './ragEval';
|
19
18
|
import setting from './setting';
|
19
|
+
import tool from './tool';
|
20
|
+
import topic from './topic';
|
20
21
|
import welcome from './welcome';
|
21
22
|
|
22
23
|
const resources = {
|
@@ -39,6 +40,7 @@ const resources = {
|
|
39
40
|
ragEval,
|
40
41
|
setting,
|
41
42
|
tool,
|
43
|
+
topic,
|
42
44
|
welcome,
|
43
45
|
} as const;
|
44
46
|
|
@@ -0,0 +1,37 @@
|
|
1
|
+
export default {
|
2
|
+
actions: {
|
3
|
+
autoRename: '智能重命名',
|
4
|
+
confirmRemoveAll: '即将删除全部话题,删除后将不可恢复,请谨慎操作。',
|
5
|
+
confirmRemoveTopic: '即将删除该话题,删除后将不可恢复,请谨慎操作。',
|
6
|
+
confirmRemoveUnstarred: '即将删除未收藏话题,删除后将不可恢复,请谨慎操作。',
|
7
|
+
duplicate: '创建副本',
|
8
|
+
export: '导出话题',
|
9
|
+
removeAll: '删除全部话题',
|
10
|
+
removeUnstarred: '删除未收藏话题',
|
11
|
+
},
|
12
|
+
defaultTitle: '默认话题',
|
13
|
+
duplicateLoading: '话题复制中...',
|
14
|
+
duplicateSuccess: '话题复制成功',
|
15
|
+
favorite: '收藏',
|
16
|
+
groupMode: {
|
17
|
+
ascMessages: '按消息总数顺序',
|
18
|
+
byTime: '按时间分组',
|
19
|
+
descMessages: '按消息总数倒序',
|
20
|
+
flat: '不分组',
|
21
|
+
},
|
22
|
+
groupTitle: {
|
23
|
+
byTime: {
|
24
|
+
month: '本月',
|
25
|
+
today: '今天',
|
26
|
+
week: '本周',
|
27
|
+
yesterday: '昨天',
|
28
|
+
},
|
29
|
+
},
|
30
|
+
guide: {
|
31
|
+
desc: '点击发送左侧按钮可将当前会话保存为历史话题,并开启新一轮会话',
|
32
|
+
title: '话题列表',
|
33
|
+
},
|
34
|
+
searchPlaceholder: '搜索话题...',
|
35
|
+
temp: '临时',
|
36
|
+
title: '话题',
|
37
|
+
};
|
@@ -2,10 +2,8 @@ import { LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
|
|
2
2
|
import { act } from '@testing-library/react';
|
3
3
|
import { merge } from 'lodash-es';
|
4
4
|
import OpenAI from 'openai';
|
5
|
-
import {
|
5
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
6
6
|
|
7
|
-
import { getAppConfig } from '@/config/app';
|
8
|
-
import { getServerDBConfig } from '@/config/db';
|
9
7
|
import { DEFAULT_AGENT_CONFIG } from '@/const/settings';
|
10
8
|
import {
|
11
9
|
LobeAnthropicAI,
|
@@ -28,7 +26,6 @@ import {
|
|
28
26
|
ModelProvider,
|
29
27
|
} from '@/libs/agent-runtime';
|
30
28
|
import { AgentRuntime } from '@/libs/agent-runtime';
|
31
|
-
import { useFileStore } from '@/store/file';
|
32
29
|
import { useToolStore } from '@/store/tool';
|
33
30
|
import { UserStore } from '@/store/user';
|
34
31
|
import { UserSettingsState, initialSettingsState } from '@/store/user/slices/settings/initialState';
|
@@ -38,6 +35,8 @@ import { ChatStreamPayload, type OpenAIChatMessage } from '@/types/openai/chat';
|
|
38
35
|
import { LobeTool } from '@/types/tool';
|
39
36
|
|
40
37
|
import { chatService, initializeWithClientStore } from '../chat';
|
38
|
+
import { useUserStore } from '@/store/user';
|
39
|
+
import {modelConfigSelectors} from "@/store/user/selectors";
|
41
40
|
|
42
41
|
// Mocking external dependencies
|
43
42
|
vi.mock('i18next', () => ({
|
@@ -524,6 +523,48 @@ describe('ChatService', () => {
|
|
524
523
|
});
|
525
524
|
});
|
526
525
|
|
526
|
+
it('should throw InvalidAccessCode error when enableFetchOnClient is true and auth is enabled but user is not signed in', async () => {
|
527
|
+
// Mock userStore
|
528
|
+
const mockUserStore = {
|
529
|
+
enableAuth: () => true,
|
530
|
+
isSignedIn: false,
|
531
|
+
};
|
532
|
+
|
533
|
+
// Mock modelConfigSelectors
|
534
|
+
const mockModelConfigSelectors = {
|
535
|
+
isProviderFetchOnClient: () => () => true,
|
536
|
+
};
|
537
|
+
|
538
|
+
vi.spyOn(useUserStore, 'getState').mockImplementationOnce(() => mockUserStore as any);
|
539
|
+
vi.spyOn(modelConfigSelectors, 'isProviderFetchOnClient').mockImplementationOnce(mockModelConfigSelectors.isProviderFetchOnClient);
|
540
|
+
|
541
|
+
const params: Partial<ChatStreamPayload> = {
|
542
|
+
model: 'test-model',
|
543
|
+
messages: [],
|
544
|
+
};
|
545
|
+
const options = {};
|
546
|
+
const expectedPayload = {
|
547
|
+
model: DEFAULT_AGENT_CONFIG.model,
|
548
|
+
stream: true,
|
549
|
+
...DEFAULT_AGENT_CONFIG.params,
|
550
|
+
...params,
|
551
|
+
};
|
552
|
+
|
553
|
+
const result = await chatService.getChatCompletion(params,options);
|
554
|
+
|
555
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
556
|
+
expect.any(String),
|
557
|
+
{
|
558
|
+
body: JSON.stringify(expectedPayload),
|
559
|
+
headers: expect.objectContaining({
|
560
|
+
'Content-Type': 'application/json',
|
561
|
+
}),
|
562
|
+
method: 'POST',
|
563
|
+
},
|
564
|
+
);
|
565
|
+
expect(result.status).toBe(401);
|
566
|
+
});
|
567
|
+
|
527
568
|
// Add more test cases to cover different scenarios and edge cases
|
528
569
|
});
|
529
570
|
|
package/src/services/chat.ts
CHANGED
@@ -8,7 +8,7 @@ import { INBOX_SESSION_ID } from '@/const/session';
|
|
8
8
|
import { DEFAULT_AGENT_CONFIG } from '@/const/settings';
|
9
9
|
import { TracePayload, TraceTagMap } from '@/const/trace';
|
10
10
|
import { isServerMode } from '@/const/version';
|
11
|
-
import {
|
11
|
+
import {AgentRuntime, AgentRuntimeError, ChatCompletionErrorPayload, ModelProvider} from '@/libs/agent-runtime';
|
12
12
|
import { filesPrompts } from '@/prompts/files';
|
13
13
|
import { useSessionStore } from '@/store/session';
|
14
14
|
import { sessionMetaSelectors } from '@/store/session/selectors';
|
@@ -533,6 +533,14 @@ class ChatService {
|
|
533
533
|
const agentRuntime = await initializeWithClientStore(params.provider, params.payload);
|
534
534
|
const data = params.payload as ChatStreamPayload;
|
535
535
|
|
536
|
+
/**
|
537
|
+
* if enable login and not signed in, return unauthorized error
|
538
|
+
*/
|
539
|
+
const userStore = useUserStore.getState();
|
540
|
+
if (userStore.enableAuth() && !userStore.isSignedIn) {
|
541
|
+
throw AgentRuntimeError.createError(ChatErrorType.InvalidAccessCode);
|
542
|
+
}
|
543
|
+
|
536
544
|
return agentRuntime.chat(data, { signal: params.signal });
|
537
545
|
};
|
538
546
|
|
@@ -487,7 +487,7 @@ describe('topic action', () => {
|
|
487
487
|
expect(createTopicSpy).toHaveBeenCalledWith({
|
488
488
|
sessionId: activeId,
|
489
489
|
messages: messages.map((m) => m.id),
|
490
|
-
title: '
|
490
|
+
title: 'defaultTitle',
|
491
491
|
});
|
492
492
|
expect(refreshTopicSpy).toHaveBeenCalled();
|
493
493
|
});
|
@@ -84,7 +84,7 @@ export const chatTopic: StateCreator<
|
|
84
84
|
set({ creatingTopic: true }, false, n('creatingTopic/start'));
|
85
85
|
const topicId = await internal_createTopic({
|
86
86
|
sessionId: activeId,
|
87
|
-
title: t('
|
87
|
+
title: t('defaultTitle', { ns: 'topic' }),
|
88
88
|
messages: messages.map((m) => m.id),
|
89
89
|
});
|
90
90
|
set({ creatingTopic: false }, false, n('creatingTopic/end'));
|
@@ -102,7 +102,7 @@ export const chatTopic: StateCreator<
|
|
102
102
|
// 1. create topic and bind these messages
|
103
103
|
const topicId = await internal_createTopic({
|
104
104
|
sessionId: activeId,
|
105
|
-
title: t('
|
105
|
+
title: t('defaultTitle', { ns: 'topic' }),
|
106
106
|
messages: messages.map((m) => m.id),
|
107
107
|
});
|
108
108
|
|
@@ -122,7 +122,7 @@ export const chatTopic: StateCreator<
|
|
122
122
|
const newTitle = t('duplicateTitle', { ns: 'chat', title: topic?.title });
|
123
123
|
|
124
124
|
message.loading({
|
125
|
-
content: t('
|
125
|
+
content: t('duplicateLoading', { ns: 'topic' }),
|
126
126
|
key: 'duplicateTopic',
|
127
127
|
duration: 0,
|
128
128
|
});
|
@@ -130,7 +130,7 @@ export const chatTopic: StateCreator<
|
|
130
130
|
const newTopicId = await topicService.cloneTopic(id, newTitle);
|
131
131
|
await refreshTopic();
|
132
132
|
message.destroy('duplicateTopic');
|
133
|
-
message.success(t('
|
133
|
+
message.success(t('duplicateSuccess', { ns: 'topic' }));
|
134
134
|
|
135
135
|
await switchTopic(newTopicId);
|
136
136
|
},
|