@lobehub/chat 0.159.6 → 0.159.8
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 +42 -0
- package/package.json +1 -1
- package/src/app/(main)/chat/(workspace)/@topic/features/TopicListContent/index.tsx +24 -6
- package/src/app/(main)/chat/(workspace)/_layout/Desktop/ChatHeader/Main.tsx +18 -2
- package/src/app/(main)/chat/(workspace)/_layout/Mobile/ChatHeader/ChatHeaderTitle.tsx +1 -1
- package/src/app/(main)/chat/(workspace)/_layout/Mobile/ChatHeader/index.tsx +2 -0
- package/src/app/(main)/chat/(workspace)/_layout/useInitAgentConfig.ts +30 -0
- package/src/config/client.ts +4 -0
- package/src/const/user.ts +10 -0
- package/src/const/version.ts +3 -0
- package/src/features/ChatInput/ActionBar/Tools/Dropdown.tsx +145 -0
- package/src/features/ChatInput/ActionBar/Tools/index.tsx +8 -120
- package/src/features/Conversation/components/VirtualizedList/index.tsx +31 -7
- package/src/features/Conversation/index.tsx +3 -2
- package/src/features/PluginStore/PluginItem/Action.tsx +12 -17
- package/src/services/message/type.ts +1 -0
- package/src/services/plugin/client.test.ts +5 -5
- package/src/services/plugin/client.ts +8 -10
- package/src/services/plugin/type.ts +21 -0
- package/src/services/topic/type.ts +1 -0
- package/src/store/agent/slices/chat/action.ts +2 -0
- package/src/store/chat/slices/topic/action.ts +2 -0
- package/src/store/tool/slices/plugin/action.test.ts +10 -2
- package/src/store/tool/slices/plugin/action.ts +8 -2
- package/src/store/tool/slices/plugin/initialState.ts +1 -0
- package/src/store/tool/slices/store/action.test.ts +10 -0
- package/src/store/tool/slices/store/action.ts +6 -1
- package/src/store/user/slices/preference/action.test.ts +3 -3
- package/src/store/user/slices/preference/action.ts +3 -3
- package/src/store/user/slices/preference/initialState.ts +2 -29
- package/src/types/tool/index.ts +2 -2
- package/src/types/user.ts +20 -0
- package/src/utils/fetch.ts +3 -1
- package/src/features/Conversation/hooks/useInitConversation.ts +0 -47
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,48 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
### [Version 0.159.8](https://github.com/lobehub/lobe-chat/compare/v0.159.7...v0.159.8)
|
|
6
|
+
|
|
7
|
+
<sup>Released on **2024-05-14**</sup>
|
|
8
|
+
|
|
9
|
+
#### 🐛 Bug Fixes
|
|
10
|
+
|
|
11
|
+
- **misc**: Fix retry issue when hide page.
|
|
12
|
+
|
|
13
|
+
<br/>
|
|
14
|
+
|
|
15
|
+
<details>
|
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
17
|
+
|
|
18
|
+
#### What's fixed
|
|
19
|
+
|
|
20
|
+
- **misc**: Fix retry issue when hide page, closes [#2503](https://github.com/lobehub/lobe-chat/issues/2503) ([24489bc](https://github.com/lobehub/lobe-chat/commit/24489bc))
|
|
21
|
+
|
|
22
|
+
</details>
|
|
23
|
+
|
|
24
|
+
<div align="right">
|
|
25
|
+
|
|
26
|
+
[](#readme-top)
|
|
27
|
+
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
### [Version 0.159.7](https://github.com/lobehub/lobe-chat/compare/v0.159.6...v0.159.7)
|
|
31
|
+
|
|
32
|
+
<sup>Released on **2024-05-14**</sup>
|
|
33
|
+
|
|
34
|
+
<br/>
|
|
35
|
+
|
|
36
|
+
<details>
|
|
37
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
38
|
+
|
|
39
|
+
</details>
|
|
40
|
+
|
|
41
|
+
<div align="right">
|
|
42
|
+
|
|
43
|
+
[](#readme-top)
|
|
44
|
+
|
|
45
|
+
</div>
|
|
46
|
+
|
|
5
47
|
### [Version 0.159.6](https://github.com/lobehub/lobe-chat/compare/v0.159.5...v0.159.6)
|
|
6
48
|
|
|
7
49
|
<sup>Released on **2024-05-14**</sup>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobehub/chat",
|
|
3
|
-
"version": "0.159.
|
|
3
|
+
"version": "0.159.8",
|
|
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",
|
|
@@ -3,14 +3,16 @@
|
|
|
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, useCallback, useRef } from 'react';
|
|
6
|
+
import React, { Suspense, memo, useCallback, useRef } from 'react';
|
|
7
7
|
import { useTranslation } from 'react-i18next';
|
|
8
8
|
import { Flexbox } from 'react-layout-kit';
|
|
9
9
|
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
|
|
10
10
|
|
|
11
11
|
import { imageUrl } from '@/const/url';
|
|
12
|
+
import { isServerMode } from '@/const/version';
|
|
12
13
|
import { useChatStore } from '@/store/chat';
|
|
13
14
|
import { topicSelectors } from '@/store/chat/selectors';
|
|
15
|
+
import { useSessionStore } from '@/store/session';
|
|
14
16
|
import { useUserStore } from '@/store/user';
|
|
15
17
|
import { ChatTopic } from '@/types/topic';
|
|
16
18
|
|
|
@@ -21,10 +23,11 @@ const TopicListContent = memo(() => {
|
|
|
21
23
|
const { t } = useTranslation('chat');
|
|
22
24
|
const virtuosoRef = useRef<VirtuosoHandle>(null);
|
|
23
25
|
const { isDarkMode } = useThemeMode();
|
|
24
|
-
const [topicsInit, activeTopicId, topicLength] = useChatStore((s) => [
|
|
26
|
+
const [topicsInit, activeTopicId, topicLength, useFetchTopics] = useChatStore((s) => [
|
|
25
27
|
s.topicsInit,
|
|
26
28
|
s.activeTopicId,
|
|
27
29
|
topicSelectors.currentTopicLength(s),
|
|
30
|
+
s.useFetchTopics,
|
|
28
31
|
]);
|
|
29
32
|
const [visible, updateGuideState] = useUserStore((s) => [
|
|
30
33
|
s.preference.guide?.topic,
|
|
@@ -43,6 +46,10 @@ const TopicListContent = memo(() => {
|
|
|
43
46
|
isEqual,
|
|
44
47
|
);
|
|
45
48
|
|
|
49
|
+
const [sessionId] = useSessionStore((s) => [s.activeId]);
|
|
50
|
+
|
|
51
|
+
const { isLoading } = useFetchTopics(sessionId);
|
|
52
|
+
|
|
46
53
|
const itemContent = useCallback(
|
|
47
54
|
(index: number, { id, favorite, title }: ChatTopic) =>
|
|
48
55
|
index === 0 ? (
|
|
@@ -55,9 +62,14 @@ const TopicListContent = memo(() => {
|
|
|
55
62
|
|
|
56
63
|
const activeIndex = topics.findIndex((topic) => topic.id === activeTopicId);
|
|
57
64
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
65
|
+
// first time loading
|
|
66
|
+
if (!topicsInit) return <SkeletonList />;
|
|
67
|
+
|
|
68
|
+
// in server mode and re-loading
|
|
69
|
+
if (isServerMode && isLoading) return <SkeletonList />;
|
|
70
|
+
|
|
71
|
+
// in client mode no need the loading state for better UX
|
|
72
|
+
return (
|
|
61
73
|
<>
|
|
62
74
|
{topicLength === 0 && visible && (
|
|
63
75
|
<Flexbox paddingInline={8}>
|
|
@@ -97,4 +109,10 @@ const TopicListContent = memo(() => {
|
|
|
97
109
|
);
|
|
98
110
|
});
|
|
99
111
|
|
|
100
|
-
|
|
112
|
+
TopicListContent.displayName = 'TopicListContent';
|
|
113
|
+
|
|
114
|
+
export default memo(() => (
|
|
115
|
+
<Suspense fallback={<SkeletonList />}>
|
|
116
|
+
<TopicListContent />;
|
|
117
|
+
</Suspense>
|
|
118
|
+
));
|
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
import { ActionIcon, Avatar, ChatHeaderTitle } from '@lobehub/ui';
|
|
4
4
|
import { Skeleton } from 'antd';
|
|
5
5
|
import { PanelLeftClose, PanelLeftOpen } from 'lucide-react';
|
|
6
|
-
import { memo } from 'react';
|
|
6
|
+
import { Suspense, memo } from 'react';
|
|
7
7
|
import { useTranslation } from 'react-i18next';
|
|
8
8
|
import { Flexbox } from 'react-layout-kit';
|
|
9
9
|
|
|
10
|
+
import { useInitAgentConfig } from '@/app/(main)/chat/(workspace)/_layout/useInitAgentConfig';
|
|
10
11
|
import { useOpenChatSettings } from '@/hooks/useInterceptingRoutes';
|
|
11
12
|
import { useGlobalStore } from '@/store/global';
|
|
12
13
|
import { useSessionStore } from '@/store/session';
|
|
@@ -17,6 +18,8 @@ import Tags from './Tags';
|
|
|
17
18
|
const Main = memo(() => {
|
|
18
19
|
const { t } = useTranslation('chat');
|
|
19
20
|
|
|
21
|
+
useInitAgentConfig();
|
|
22
|
+
|
|
20
23
|
const [init, isInbox, title, description, avatar, backgroundColor] = useSessionStore((s) => [
|
|
21
24
|
sessionSelectors.isSomeSessionActive(s),
|
|
22
25
|
sessionSelectors.isInboxSession(s),
|
|
@@ -70,4 +73,17 @@ const Main = memo(() => {
|
|
|
70
73
|
);
|
|
71
74
|
});
|
|
72
75
|
|
|
73
|
-
export default
|
|
76
|
+
export default () => (
|
|
77
|
+
<Suspense
|
|
78
|
+
fallback={
|
|
79
|
+
<Skeleton
|
|
80
|
+
active
|
|
81
|
+
avatar={{ shape: 'circle', size: 'default' }}
|
|
82
|
+
paragraph={false}
|
|
83
|
+
title={{ style: { margin: 0, marginTop: 8 }, width: 200 }}
|
|
84
|
+
/>
|
|
85
|
+
}
|
|
86
|
+
>
|
|
87
|
+
<Main />
|
|
88
|
+
</Suspense>
|
|
89
|
+
);
|
|
@@ -30,6 +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('topic.title')}</span>
|
|
33
34
|
<ActionIcon
|
|
34
35
|
active
|
|
35
36
|
icon={ChevronDown}
|
|
@@ -39,7 +40,6 @@ const ChatHeaderTitle = memo(() => {
|
|
|
39
40
|
color: theme.colorTextDescription,
|
|
40
41
|
}}
|
|
41
42
|
/>
|
|
42
|
-
<span>{topic?.title || t('topic.title')}</span>
|
|
43
43
|
</Flexbox>
|
|
44
44
|
}
|
|
45
45
|
title={
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { MobileNavBar } from '@lobehub/ui';
|
|
4
4
|
import { memo, useState } from 'react';
|
|
5
5
|
|
|
6
|
+
import { useInitAgentConfig } from '@/app/(main)/chat/(workspace)/_layout/useInitAgentConfig';
|
|
6
7
|
import { useQueryRoute } from '@/hooks/useQueryRoute';
|
|
7
8
|
import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig';
|
|
8
9
|
|
|
@@ -15,6 +16,7 @@ const MobileHeader = memo(() => {
|
|
|
15
16
|
const [open, setOpen] = useState(false);
|
|
16
17
|
|
|
17
18
|
const { isAgentEditable } = useServerConfigStore(featureFlagsSelectors);
|
|
19
|
+
useInitAgentConfig();
|
|
18
20
|
|
|
19
21
|
return (
|
|
20
22
|
<MobileNavBar
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
import { useAgentStore } from '@/store/agent';
|
|
4
|
+
import { useChatStore } from '@/store/chat';
|
|
5
|
+
import { useSessionStore } from '@/store/session';
|
|
6
|
+
|
|
7
|
+
export const useInitAgentConfig = () => {
|
|
8
|
+
const [useFetchAgentConfig] = useAgentStore((s) => [s.useFetchAgentConfig]);
|
|
9
|
+
|
|
10
|
+
const [switchTopic] = useChatStore((s) => [s.switchTopic]);
|
|
11
|
+
const [sessionId] = useSessionStore((s) => [s.activeId]);
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
// // when activeId changed, switch topic to undefined
|
|
15
|
+
const unsubscribe = useSessionStore.subscribe(
|
|
16
|
+
(s) => s.activeId,
|
|
17
|
+
(activeId) => {
|
|
18
|
+
switchTopic();
|
|
19
|
+
|
|
20
|
+
useAgentStore.setState({ activeId }, false, 'updateActiveId');
|
|
21
|
+
},
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
return () => {
|
|
25
|
+
unsubscribe();
|
|
26
|
+
};
|
|
27
|
+
}, []);
|
|
28
|
+
|
|
29
|
+
return useFetchAgentConfig(sessionId);
|
|
30
|
+
};
|
package/src/config/client.ts
CHANGED
|
@@ -30,11 +30,15 @@ declare global {
|
|
|
30
30
|
NEXT_PUBLIC_I18N_DEBUG_SERVER: string;
|
|
31
31
|
|
|
32
32
|
NEXT_PUBLIC_DEVELOPER_DEBUG: string;
|
|
33
|
+
|
|
34
|
+
NEXT_PUBLIC_SERVICE_MODE?: 'server' | 'browser';
|
|
33
35
|
}
|
|
34
36
|
}
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
export const getClientConfig = () => ({
|
|
40
|
+
ENABLED_SERVER_SERVICE: process.env.NEXT_PUBLIC_SERVICE_MODE === 'server',
|
|
41
|
+
|
|
38
42
|
BASE_PATH: process.env.NEXT_PUBLIC_BASE_PATH || '',
|
|
39
43
|
|
|
40
44
|
// Plausible Analytics
|
package/src/const/version.ts
CHANGED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { Avatar, Icon } from '@lobehub/ui';
|
|
2
|
+
import { Dropdown } from 'antd';
|
|
3
|
+
import { createStyles } from 'antd-style';
|
|
4
|
+
import type { ItemType } from 'antd/es/menu/hooks/useItems';
|
|
5
|
+
import isEqual from 'fast-deep-equal';
|
|
6
|
+
import { ArrowRight, Store, ToyBrick } from 'lucide-react';
|
|
7
|
+
import { PropsWithChildren, memo } from 'react';
|
|
8
|
+
import { useTranslation } from 'react-i18next';
|
|
9
|
+
import { Flexbox } from 'react-layout-kit';
|
|
10
|
+
|
|
11
|
+
import { useWorkspaceModal } from '@/app/(main)/chat/(workspace)/features/useWorkspaceModal';
|
|
12
|
+
import PluginStore from '@/features/PluginStore';
|
|
13
|
+
import { useAgentStore } from '@/store/agent';
|
|
14
|
+
import { agentSelectors } from '@/store/agent/selectors';
|
|
15
|
+
import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig';
|
|
16
|
+
import { pluginHelpers, useToolStore } from '@/store/tool';
|
|
17
|
+
import { builtinToolSelectors, pluginSelectors } from '@/store/tool/selectors';
|
|
18
|
+
|
|
19
|
+
import ToolItem from './ToolItem';
|
|
20
|
+
|
|
21
|
+
const useStyles = createStyles(({ css, prefixCls }) => ({
|
|
22
|
+
menu: css`
|
|
23
|
+
&.${prefixCls}-dropdown-menu {
|
|
24
|
+
padding-block: 8px;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.${prefixCls}-dropdown-menu-item-group-list .${prefixCls}-dropdown-menu-item {
|
|
28
|
+
padding: 0;
|
|
29
|
+
border-radius: 4px;
|
|
30
|
+
}
|
|
31
|
+
`,
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
const DropdownMenu = memo<PropsWithChildren>(({ children }) => {
|
|
35
|
+
const { t } = useTranslation('setting');
|
|
36
|
+
const list = useToolStore(pluginSelectors.installedPluginMetaList, isEqual);
|
|
37
|
+
const { showDalle } = useServerConfigStore(featureFlagsSelectors);
|
|
38
|
+
const builtinList = useToolStore(builtinToolSelectors.metaList(showDalle), isEqual);
|
|
39
|
+
|
|
40
|
+
const enablePluginCount = useAgentStore(
|
|
41
|
+
(s) =>
|
|
42
|
+
agentSelectors
|
|
43
|
+
.currentAgentPlugins(s)
|
|
44
|
+
.filter((i) => !builtinList.some((b) => b.identifier === i)).length,
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const [open, setOpen] = useWorkspaceModal();
|
|
48
|
+
const { styles } = useStyles();
|
|
49
|
+
|
|
50
|
+
const items: ItemType[] = [
|
|
51
|
+
(builtinList.length !== 0 && {
|
|
52
|
+
children: builtinList.map((item) => ({
|
|
53
|
+
icon: <Avatar avatar={item.meta.avatar} size={24} />,
|
|
54
|
+
key: item.identifier,
|
|
55
|
+
label: (
|
|
56
|
+
<ToolItem identifier={item.identifier} label={item.meta?.title || item.identifier} />
|
|
57
|
+
),
|
|
58
|
+
})),
|
|
59
|
+
|
|
60
|
+
key: 'builtins',
|
|
61
|
+
label: t('tools.builtins.groupName'),
|
|
62
|
+
type: 'group',
|
|
63
|
+
}) as ItemType,
|
|
64
|
+
{
|
|
65
|
+
children: [
|
|
66
|
+
...list.map((item) => ({
|
|
67
|
+
icon: item.meta?.avatar ? (
|
|
68
|
+
<Avatar avatar={pluginHelpers.getPluginAvatar(item.meta)} size={24} />
|
|
69
|
+
) : (
|
|
70
|
+
<Icon icon={ToyBrick} size={{ fontSize: 16 }} style={{ padding: 4 }} />
|
|
71
|
+
),
|
|
72
|
+
key: item.identifier,
|
|
73
|
+
label: (
|
|
74
|
+
<ToolItem
|
|
75
|
+
identifier={item.identifier}
|
|
76
|
+
label={pluginHelpers.getPluginTitle(item?.meta) || item.identifier}
|
|
77
|
+
/>
|
|
78
|
+
),
|
|
79
|
+
})),
|
|
80
|
+
{
|
|
81
|
+
icon: <Icon icon={Store} size={{ fontSize: 16 }} style={{ padding: 4 }} />,
|
|
82
|
+
|
|
83
|
+
key: 'plugin-store',
|
|
84
|
+
label: (
|
|
85
|
+
<Flexbox gap={40} horizontal justify={'space-between'} padding={'8px 12px'}>
|
|
86
|
+
{t('tools.plugins.store')} <Icon icon={ArrowRight} />
|
|
87
|
+
</Flexbox>
|
|
88
|
+
),
|
|
89
|
+
onClick: (e) => {
|
|
90
|
+
e.domEvent.stopPropagation();
|
|
91
|
+
setOpen(true);
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
key: 'plugins',
|
|
96
|
+
label: (
|
|
97
|
+
<Flexbox align={'center'} gap={40} horizontal justify={'space-between'}>
|
|
98
|
+
{t('tools.plugins.groupName')}
|
|
99
|
+
{enablePluginCount === 0 ? null : (
|
|
100
|
+
<div style={{ fontSize: 12, marginInlineEnd: 4 }}>
|
|
101
|
+
{t('tools.plugins.enabled', { num: enablePluginCount })}
|
|
102
|
+
</div>
|
|
103
|
+
)}
|
|
104
|
+
</Flexbox>
|
|
105
|
+
),
|
|
106
|
+
type: 'group',
|
|
107
|
+
} as ItemType,
|
|
108
|
+
].filter(Boolean);
|
|
109
|
+
|
|
110
|
+
const plugins = useAgentStore((s) => agentSelectors.currentAgentPlugins(s));
|
|
111
|
+
|
|
112
|
+
const [useFetchPluginStore, useFetchInstalledPlugins, checkPluginsIsInstalled] = useToolStore(
|
|
113
|
+
(s) => [s.useFetchPluginStore, s.useFetchInstalledPlugins, s.useCheckPluginsIsInstalled],
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
useFetchPluginStore();
|
|
117
|
+
useFetchInstalledPlugins();
|
|
118
|
+
checkPluginsIsInstalled(plugins);
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<>
|
|
122
|
+
<Dropdown
|
|
123
|
+
arrow={false}
|
|
124
|
+
menu={{
|
|
125
|
+
className: styles.menu,
|
|
126
|
+
items,
|
|
127
|
+
onClick: (e) => {
|
|
128
|
+
e.domEvent.preventDefault();
|
|
129
|
+
},
|
|
130
|
+
style: {
|
|
131
|
+
maxHeight: 500,
|
|
132
|
+
overflowY: 'scroll',
|
|
133
|
+
},
|
|
134
|
+
}}
|
|
135
|
+
placement={'top'}
|
|
136
|
+
trigger={['click']}
|
|
137
|
+
>
|
|
138
|
+
{children}
|
|
139
|
+
</Dropdown>
|
|
140
|
+
<PluginStore open={open} setOpen={setOpen} />
|
|
141
|
+
</>
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
export default DropdownMenu;
|
|
@@ -1,144 +1,32 @@
|
|
|
1
|
-
import { ActionIcon
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import type { ItemType } from 'antd/es/menu/hooks/useItems';
|
|
5
|
-
import isEqual from 'fast-deep-equal';
|
|
6
|
-
import { ArrowRight, Blocks, Store, ToyBrick } from 'lucide-react';
|
|
7
|
-
import { memo } from 'react';
|
|
1
|
+
import { ActionIcon } from '@lobehub/ui';
|
|
2
|
+
import { Blocks, LucideLoader2 } from 'lucide-react';
|
|
3
|
+
import { Suspense, memo } from 'react';
|
|
8
4
|
import { useTranslation } from 'react-i18next';
|
|
9
|
-
import { Flexbox } from 'react-layout-kit';
|
|
10
5
|
|
|
11
|
-
import { useWorkspaceModal } from '@/app/(main)/chat/(workspace)/features/useWorkspaceModal';
|
|
12
|
-
import PluginStore from '@/features/PluginStore';
|
|
13
6
|
import { useAgentStore } from '@/store/agent';
|
|
14
7
|
import { agentSelectors } from '@/store/agent/selectors';
|
|
15
|
-
import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig';
|
|
16
|
-
import { pluginHelpers, useToolStore } from '@/store/tool';
|
|
17
|
-
import { builtinToolSelectors, pluginSelectors } from '@/store/tool/selectors';
|
|
18
8
|
import { useUserStore } from '@/store/user';
|
|
19
9
|
import { modelProviderSelectors } from '@/store/user/selectors';
|
|
20
10
|
|
|
21
|
-
import
|
|
22
|
-
|
|
23
|
-
const useStyles = createStyles(({ css, prefixCls }) => ({
|
|
24
|
-
menu: css`
|
|
25
|
-
&.${prefixCls}-dropdown-menu {
|
|
26
|
-
padding-block: 8px;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
.${prefixCls}-dropdown-menu-item-group-list .${prefixCls}-dropdown-menu-item {
|
|
30
|
-
padding: 0;
|
|
31
|
-
border-radius: 4px;
|
|
32
|
-
}
|
|
33
|
-
`,
|
|
34
|
-
}));
|
|
11
|
+
import DropdownMenu from './Dropdown';
|
|
35
12
|
|
|
36
13
|
const Tools = memo(() => {
|
|
37
14
|
const { t } = useTranslation('setting');
|
|
38
|
-
const list = useToolStore(pluginSelectors.installedPluginMetaList, isEqual);
|
|
39
|
-
const { showDalle } = useServerConfigStore(featureFlagsSelectors);
|
|
40
|
-
const builtinList = useToolStore(builtinToolSelectors.metaList(showDalle), isEqual);
|
|
41
|
-
|
|
42
|
-
const enablePluginCount = useAgentStore(
|
|
43
|
-
(s) =>
|
|
44
|
-
agentSelectors
|
|
45
|
-
.currentAgentPlugins(s)
|
|
46
|
-
.filter((i) => !builtinList.some((b) => b.identifier === i)).length,
|
|
47
|
-
);
|
|
48
|
-
|
|
49
|
-
const [open, setOpen] = useWorkspaceModal();
|
|
50
|
-
const { styles } = useStyles();
|
|
51
15
|
|
|
52
16
|
const model = useAgentStore(agentSelectors.currentAgentModel);
|
|
53
17
|
const enableFC = useUserStore(modelProviderSelectors.isModelEnabledFunctionCall(model));
|
|
54
18
|
|
|
55
|
-
const items: ItemType[] = [
|
|
56
|
-
(builtinList.length !== 0 && {
|
|
57
|
-
children: builtinList.map((item) => ({
|
|
58
|
-
icon: <Avatar avatar={item.meta.avatar} size={24} />,
|
|
59
|
-
key: item.identifier,
|
|
60
|
-
label: (
|
|
61
|
-
<ToolItem identifier={item.identifier} label={item.meta?.title || item.identifier} />
|
|
62
|
-
),
|
|
63
|
-
})),
|
|
64
|
-
|
|
65
|
-
key: 'builtins',
|
|
66
|
-
label: t('tools.builtins.groupName'),
|
|
67
|
-
type: 'group',
|
|
68
|
-
}) as ItemType,
|
|
69
|
-
{
|
|
70
|
-
children: [
|
|
71
|
-
...list.map((item) => ({
|
|
72
|
-
icon: item.meta?.avatar ? (
|
|
73
|
-
<Avatar avatar={pluginHelpers.getPluginAvatar(item.meta)} size={24} />
|
|
74
|
-
) : (
|
|
75
|
-
<Icon icon={ToyBrick} size={{ fontSize: 16 }} style={{ padding: 4 }} />
|
|
76
|
-
),
|
|
77
|
-
key: item.identifier,
|
|
78
|
-
label: (
|
|
79
|
-
<ToolItem
|
|
80
|
-
identifier={item.identifier}
|
|
81
|
-
label={pluginHelpers.getPluginTitle(item?.meta) || item.identifier}
|
|
82
|
-
/>
|
|
83
|
-
),
|
|
84
|
-
})),
|
|
85
|
-
{
|
|
86
|
-
icon: <Icon icon={Store} size={{ fontSize: 16 }} style={{ padding: 4 }} />,
|
|
87
|
-
|
|
88
|
-
key: 'plugin-store',
|
|
89
|
-
label: (
|
|
90
|
-
<Flexbox gap={40} horizontal justify={'space-between'} padding={'8px 12px'}>
|
|
91
|
-
{t('tools.plugins.store')} <Icon icon={ArrowRight} />
|
|
92
|
-
</Flexbox>
|
|
93
|
-
),
|
|
94
|
-
onClick: (e) => {
|
|
95
|
-
e.domEvent.stopPropagation();
|
|
96
|
-
setOpen(true);
|
|
97
|
-
},
|
|
98
|
-
},
|
|
99
|
-
],
|
|
100
|
-
key: 'plugins',
|
|
101
|
-
label: (
|
|
102
|
-
<Flexbox align={'center'} gap={40} horizontal justify={'space-between'}>
|
|
103
|
-
{t('tools.plugins.groupName')}
|
|
104
|
-
{enablePluginCount === 0 ? null : (
|
|
105
|
-
<div style={{ fontSize: 12, marginInlineEnd: 4 }}>
|
|
106
|
-
{t('tools.plugins.enabled', { num: enablePluginCount })}
|
|
107
|
-
</div>
|
|
108
|
-
)}
|
|
109
|
-
</Flexbox>
|
|
110
|
-
),
|
|
111
|
-
type: 'group',
|
|
112
|
-
} as ItemType,
|
|
113
|
-
].filter(Boolean);
|
|
114
|
-
|
|
115
19
|
return (
|
|
116
|
-
|
|
117
|
-
<
|
|
118
|
-
arrow={false}
|
|
119
|
-
menu={{
|
|
120
|
-
className: styles.menu,
|
|
121
|
-
items,
|
|
122
|
-
onClick: (e) => {
|
|
123
|
-
e.domEvent.preventDefault();
|
|
124
|
-
},
|
|
125
|
-
style: {
|
|
126
|
-
maxHeight: 500,
|
|
127
|
-
overflowY: 'scroll',
|
|
128
|
-
},
|
|
129
|
-
}}
|
|
130
|
-
placement={'top'}
|
|
131
|
-
trigger={['click']}
|
|
132
|
-
>
|
|
20
|
+
<Suspense fallback={<ActionIcon icon={LucideLoader2} />}>
|
|
21
|
+
<DropdownMenu>
|
|
133
22
|
<ActionIcon
|
|
134
23
|
disable={!enableFC}
|
|
135
24
|
icon={Blocks}
|
|
136
25
|
placement={'bottom'}
|
|
137
26
|
title={t(enableFC ? 'tools.title' : 'tools.disabled')}
|
|
138
27
|
/>
|
|
139
|
-
</
|
|
140
|
-
|
|
141
|
-
</>
|
|
28
|
+
</DropdownMenu>
|
|
29
|
+
</Suspense>
|
|
142
30
|
);
|
|
143
31
|
});
|
|
144
32
|
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import { Icon } from '@lobehub/ui';
|
|
4
|
+
import { useTheme } from 'antd-style';
|
|
3
5
|
import isEqual from 'fast-deep-equal';
|
|
6
|
+
import { Loader2Icon } from 'lucide-react';
|
|
4
7
|
import React, { memo, useCallback, useEffect, useRef, useState } from 'react';
|
|
5
|
-
import { Flexbox } from 'react-layout-kit';
|
|
8
|
+
import { Center, Flexbox } from 'react-layout-kit';
|
|
6
9
|
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
|
|
7
10
|
|
|
8
11
|
import { WELCOME_GUIDE_CHAT_ID } from '@/const/session';
|
|
12
|
+
import { isServerMode } from '@/const/version';
|
|
9
13
|
import { useChatStore } from '@/store/chat';
|
|
10
14
|
import { chatSelectors } from '@/store/chat/selectors';
|
|
15
|
+
import { useSessionStore } from '@/store/session';
|
|
11
16
|
|
|
12
|
-
import { useInitConversation } from '../../hooks/useInitConversation';
|
|
13
17
|
import AutoScroll from '../AutoScroll';
|
|
14
18
|
import Item from '../ChatItem';
|
|
15
19
|
import InboxWelcome from '../InboxWelcome';
|
|
@@ -19,16 +23,21 @@ interface VirtualizedListProps {
|
|
|
19
23
|
mobile?: boolean;
|
|
20
24
|
}
|
|
21
25
|
const VirtualizedList = memo<VirtualizedListProps>(({ mobile }) => {
|
|
22
|
-
useInitConversation();
|
|
23
26
|
const virtuosoRef = useRef<VirtuosoHandle>(null);
|
|
24
27
|
const [atBottom, setAtBottom] = useState(true);
|
|
25
28
|
const [isScrolling, setIsScrolling] = useState(false);
|
|
26
29
|
|
|
27
|
-
const [id
|
|
28
|
-
|
|
30
|
+
const [id] = useChatStore((s) => [chatSelectors.currentChatKey(s)]);
|
|
31
|
+
|
|
32
|
+
const [activeTopicId, useFetchMessages, isFirstLoading] = useChatStore((s) => [
|
|
33
|
+
s.activeTopicId,
|
|
34
|
+
s.useFetchMessages,
|
|
29
35
|
chatSelectors.currentChatLoadingState(s),
|
|
30
36
|
]);
|
|
31
37
|
|
|
38
|
+
const [sessionId] = useSessionStore((s) => [s.activeId]);
|
|
39
|
+
const { isLoading } = useFetchMessages(sessionId, activeTopicId);
|
|
40
|
+
|
|
32
41
|
const data = useChatStore((s) => {
|
|
33
42
|
const showInboxWelcome = chatSelectors.showInboxWelcome(s);
|
|
34
43
|
const ids = showInboxWelcome
|
|
@@ -51,6 +60,7 @@ const VirtualizedList = memo<VirtualizedListProps>(({ mobile }) => {
|
|
|
51
60
|
return newFollowOutput;
|
|
52
61
|
}, [data.length]);
|
|
53
62
|
|
|
63
|
+
const theme = useTheme();
|
|
54
64
|
// overscan should be 1.5 times the height of the window
|
|
55
65
|
const overscan = typeof window !== 'undefined' ? window.innerHeight * 1.5 : 0;
|
|
56
66
|
|
|
@@ -67,8 +77,22 @@ const VirtualizedList = memo<VirtualizedListProps>(({ mobile }) => {
|
|
|
67
77
|
[mobile],
|
|
68
78
|
);
|
|
69
79
|
|
|
70
|
-
|
|
71
|
-
|
|
80
|
+
// first time loading
|
|
81
|
+
if (isFirstLoading) return <SkeletonList mobile={mobile} />;
|
|
82
|
+
|
|
83
|
+
// in server mode and switch page
|
|
84
|
+
if (isServerMode && isLoading) return <SkeletonList mobile={mobile} />;
|
|
85
|
+
|
|
86
|
+
// in client mode using the center loading for more
|
|
87
|
+
return isLoading ? (
|
|
88
|
+
<Center height={'100%'} width={'100%'}>
|
|
89
|
+
<Icon
|
|
90
|
+
icon={Loader2Icon}
|
|
91
|
+
size={{ fontSize: 32 }}
|
|
92
|
+
spin
|
|
93
|
+
style={{ color: theme.colorTextTertiary }}
|
|
94
|
+
/>
|
|
95
|
+
</Center>
|
|
72
96
|
) : (
|
|
73
97
|
<Flexbox height={'100%'}>
|
|
74
98
|
<Virtuoso
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { Suspense } from 'react';
|
|
1
|
+
import { Suspense, lazy } from 'react';
|
|
2
2
|
import { Flexbox } from 'react-layout-kit';
|
|
3
3
|
|
|
4
4
|
import SkeletonList from './components/SkeletonList';
|
|
5
|
-
|
|
5
|
+
|
|
6
|
+
const ChatList = lazy(() => import('./components/VirtualizedList'));
|
|
6
7
|
|
|
7
8
|
interface ConversationProps {
|
|
8
9
|
mobile?: boolean;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ActionIcon, Icon } from '@lobehub/ui';
|
|
2
|
-
import { Button, Dropdown
|
|
2
|
+
import { App, Button, Dropdown } from 'antd';
|
|
3
3
|
import { InfoIcon, MoreVerticalIcon, Settings, Trash2 } from 'lucide-react';
|
|
4
4
|
import { memo, useState } from 'react';
|
|
5
5
|
import { useTranslation } from 'react-i18next';
|
|
@@ -31,7 +31,7 @@ const Actions = memo<ActionsProps>(({ identifier, type }) => {
|
|
|
31
31
|
const { t } = useTranslation('plugin');
|
|
32
32
|
const [open, setOpen] = useState(false);
|
|
33
33
|
const plugin = useToolStore(pluginSelectors.getPluginManifestById(identifier));
|
|
34
|
-
|
|
34
|
+
const { modal } = App.useApp();
|
|
35
35
|
const [tab, setTab] = useState('info');
|
|
36
36
|
const hasSettings = pluginHelpers.isSettingSchemaNonEmpty(plugin?.settings);
|
|
37
37
|
|
|
@@ -67,21 +67,16 @@ const Actions = memo<ActionsProps>(({ identifier, type }) => {
|
|
|
67
67
|
danger: true,
|
|
68
68
|
icon: <Icon icon={Trash2} />,
|
|
69
69
|
key: 'uninstall',
|
|
70
|
-
label: (
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
okButtonProps
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
title={t('store.actions.confirmUninstall')}
|
|
81
|
-
>
|
|
82
|
-
{t('store.actions.uninstall')}
|
|
83
|
-
</Popconfirm>
|
|
84
|
-
),
|
|
70
|
+
label: t('store.actions.uninstall'),
|
|
71
|
+
onClick: () => {
|
|
72
|
+
modal.confirm({
|
|
73
|
+
centered: true,
|
|
74
|
+
okButtonProps: { danger: true },
|
|
75
|
+
onOk: async () => unInstallPlugin(identifier),
|
|
76
|
+
title: t('store.actions.confirmUninstall'),
|
|
77
|
+
type: 'error',
|
|
78
|
+
});
|
|
79
|
+
},
|
|
85
80
|
},
|
|
86
81
|
],
|
|
87
82
|
}}
|
|
@@ -20,6 +20,7 @@ export interface IMessageService {
|
|
|
20
20
|
getAllMessages(): Promise<ChatMessage[]>;
|
|
21
21
|
getAllMessagesInSession(sessionId: string): Promise<ChatMessage[]>;
|
|
22
22
|
countMessages(): Promise<number>;
|
|
23
|
+
countTodayMessages(): Promise<number>;
|
|
23
24
|
|
|
24
25
|
updateMessageError(id: string, error: ChatMessageError): Promise<any>;
|
|
25
26
|
updateMessage(id: string, message: Partial<DB_Message>): Promise<any>;
|
|
@@ -6,7 +6,8 @@ import { DB_Plugin } from '@/database/client/schemas/plugin';
|
|
|
6
6
|
import { LobeTool } from '@/types/tool';
|
|
7
7
|
import { LobeToolCustomPlugin } from '@/types/tool/plugin';
|
|
8
8
|
|
|
9
|
-
import { ClientService
|
|
9
|
+
import { ClientService } from './client';
|
|
10
|
+
import { InstallPluginParams } from './type';
|
|
10
11
|
|
|
11
12
|
const pluginService = new ClientService();
|
|
12
13
|
|
|
@@ -110,7 +111,7 @@ describe('PluginService', () => {
|
|
|
110
111
|
|
|
111
112
|
// Assert
|
|
112
113
|
expect(PluginModel.update).toHaveBeenCalledWith(id, value);
|
|
113
|
-
expect(result).toEqual(
|
|
114
|
+
expect(result).toEqual(undefined);
|
|
114
115
|
});
|
|
115
116
|
});
|
|
116
117
|
|
|
@@ -126,7 +127,7 @@ describe('PluginService', () => {
|
|
|
126
127
|
|
|
127
128
|
// Assert
|
|
128
129
|
expect(PluginModel.update).toHaveBeenCalledWith(id, { manifest });
|
|
129
|
-
expect(result).toEqual(
|
|
130
|
+
expect(result).toEqual(undefined);
|
|
130
131
|
});
|
|
131
132
|
});
|
|
132
133
|
|
|
@@ -149,14 +150,13 @@ describe('PluginService', () => {
|
|
|
149
150
|
// Arrange
|
|
150
151
|
const id = 'plugin-id';
|
|
151
152
|
const settings = { color: 'blue' };
|
|
152
|
-
vi.mocked(PluginModel.update).mockResolvedValue(1);
|
|
153
153
|
|
|
154
154
|
// Act
|
|
155
155
|
const result = await pluginService.updatePluginSettings(id, settings);
|
|
156
156
|
|
|
157
157
|
// Assert
|
|
158
158
|
expect(PluginModel.update).toHaveBeenCalledWith(id, { settings });
|
|
159
|
-
expect(result).toEqual(
|
|
159
|
+
expect(result).toEqual(undefined);
|
|
160
160
|
});
|
|
161
161
|
});
|
|
162
162
|
});
|
|
@@ -4,13 +4,9 @@ import { PluginModel } from '@/database/client/models/plugin';
|
|
|
4
4
|
import { LobeTool } from '@/types/tool';
|
|
5
5
|
import { LobeToolCustomPlugin } from '@/types/tool/plugin';
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
identifier: string;
|
|
9
|
-
manifest: LobeChatPluginManifest;
|
|
10
|
-
type: 'plugin' | 'customPlugin';
|
|
11
|
-
}
|
|
7
|
+
import { IPluginService, InstallPluginParams } from './type';
|
|
12
8
|
|
|
13
|
-
export class ClientService {
|
|
9
|
+
export class ClientService implements IPluginService {
|
|
14
10
|
installPlugin = async (plugin: InstallPluginParams) => {
|
|
15
11
|
return PluginModel.create(plugin);
|
|
16
12
|
};
|
|
@@ -28,17 +24,19 @@ export class ClientService {
|
|
|
28
24
|
}
|
|
29
25
|
|
|
30
26
|
async updatePlugin(id: string, value: LobeToolCustomPlugin) {
|
|
31
|
-
|
|
27
|
+
await PluginModel.update(id, value);
|
|
28
|
+
return;
|
|
32
29
|
}
|
|
33
30
|
async updatePluginManifest(id: string, manifest: LobeChatPluginManifest) {
|
|
34
|
-
|
|
31
|
+
await PluginModel.update(id, { manifest });
|
|
35
32
|
}
|
|
36
33
|
|
|
37
34
|
async removeAllPlugins() {
|
|
38
35
|
return PluginModel.clear();
|
|
39
36
|
}
|
|
40
37
|
|
|
41
|
-
|
|
42
|
-
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
39
|
+
async updatePluginSettings(id: string, settings: any, _?: AbortSignal) {
|
|
40
|
+
await PluginModel.update(id, { settings });
|
|
43
41
|
}
|
|
44
42
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
|
|
2
|
+
|
|
3
|
+
import { LobeTool } from '@/types/tool';
|
|
4
|
+
import { LobeToolCustomPlugin } from '@/types/tool/plugin';
|
|
5
|
+
|
|
6
|
+
export interface InstallPluginParams {
|
|
7
|
+
identifier: string;
|
|
8
|
+
manifest: LobeChatPluginManifest;
|
|
9
|
+
type: 'plugin' | 'customPlugin';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface IPluginService {
|
|
13
|
+
createCustomPlugin: (customPlugin: LobeToolCustomPlugin) => Promise<void>;
|
|
14
|
+
getInstalledPlugins: () => Promise<LobeTool[]>;
|
|
15
|
+
installPlugin: (plugin: InstallPluginParams) => Promise<void>;
|
|
16
|
+
removeAllPlugins: () => Promise<void>;
|
|
17
|
+
uninstallPlugin: (identifier: string) => Promise<void>;
|
|
18
|
+
updatePlugin: (id: string, value: LobeToolCustomPlugin) => Promise<void>;
|
|
19
|
+
updatePluginManifest: (id: string, manifest: LobeChatPluginManifest) => Promise<void>;
|
|
20
|
+
updatePluginSettings: (id: string, settings: any, signal?: AbortSignal) => Promise<void>;
|
|
21
|
+
}
|
|
@@ -22,6 +22,7 @@ export interface ITopicService {
|
|
|
22
22
|
|
|
23
23
|
getTopics(params: QueryTopicParams): Promise<ChatTopic[]>;
|
|
24
24
|
getAllTopics(): Promise<ChatTopic[]>;
|
|
25
|
+
countTopics(): Promise<number>;
|
|
25
26
|
searchTopics(keyword: string, sessionId?: string): Promise<ChatTopic[]>;
|
|
26
27
|
|
|
27
28
|
updateTopic(id: string, data: Partial<ChatTopic>): Promise<any>;
|
|
@@ -81,11 +81,13 @@ export const createChatSlice: StateCreator<
|
|
|
81
81
|
[FETCH_AGENT_CONFIG_KEY, sessionId],
|
|
82
82
|
([, id]: string[]) => sessionService.getSessionConfig(id),
|
|
83
83
|
{
|
|
84
|
+
fallbackData: DEFAULT_AGENT_CONFIG,
|
|
84
85
|
onSuccess: (data) => {
|
|
85
86
|
if (get().isAgentConfigInit && isEqual(get().agentConfig, data)) return;
|
|
86
87
|
|
|
87
88
|
set({ agentConfig: data, isAgentConfigInit: true }, false, 'fetchAgentConfig');
|
|
88
89
|
},
|
|
90
|
+
suspense: true,
|
|
89
91
|
},
|
|
90
92
|
),
|
|
91
93
|
useFetchDefaultAgentConfig: () =>
|
|
@@ -171,6 +171,8 @@ export const chatTopic: StateCreator<
|
|
|
171
171
|
[SWR_USE_FETCH_TOPIC, sessionId],
|
|
172
172
|
async ([, sessionId]: [string, string]) => topicService.getTopics({ sessionId }),
|
|
173
173
|
{
|
|
174
|
+
suspense: true,
|
|
175
|
+
fallbackData: [],
|
|
174
176
|
onSuccess: (topics) => {
|
|
175
177
|
set({ topics, topicsInit: true }, false, n('useFetchTopics(success)', { sessionId }));
|
|
176
178
|
},
|
|
@@ -90,7 +90,11 @@ describe('useToolStore:plugin', () => {
|
|
|
90
90
|
await result.current.updatePluginSettings(pluginId, newSettings);
|
|
91
91
|
});
|
|
92
92
|
|
|
93
|
-
expect(pluginService.updatePluginSettings).toBeCalledWith(
|
|
93
|
+
expect(pluginService.updatePluginSettings).toBeCalledWith(
|
|
94
|
+
pluginId,
|
|
95
|
+
newSettings,
|
|
96
|
+
expect.any(AbortSignal),
|
|
97
|
+
);
|
|
94
98
|
});
|
|
95
99
|
|
|
96
100
|
it('should merge settings for a plugin with existing settings', async () => {
|
|
@@ -108,7 +112,11 @@ describe('useToolStore:plugin', () => {
|
|
|
108
112
|
await result.current.updatePluginSettings(pluginId, newSettings);
|
|
109
113
|
});
|
|
110
114
|
|
|
111
|
-
expect(pluginService.updatePluginSettings).toBeCalledWith(
|
|
115
|
+
expect(pluginService.updatePluginSettings).toBeCalledWith(
|
|
116
|
+
pluginId,
|
|
117
|
+
mergedSettings,
|
|
118
|
+
expect.any(AbortSignal),
|
|
119
|
+
);
|
|
112
120
|
});
|
|
113
121
|
});
|
|
114
122
|
|
|
@@ -45,10 +45,16 @@ export const createPluginSlice: StateCreator<
|
|
|
45
45
|
await get().refreshPlugins();
|
|
46
46
|
},
|
|
47
47
|
updatePluginSettings: async (id, settings) => {
|
|
48
|
-
const
|
|
48
|
+
const signal = get().updatePluginSettingsSignal;
|
|
49
|
+
if (signal) signal.abort('canceled');
|
|
50
|
+
|
|
51
|
+
const newSignal = new AbortController();
|
|
49
52
|
|
|
53
|
+
const previousSettings = pluginSelectors.getPluginSettingsById(id)(get());
|
|
50
54
|
const nextSettings = merge(previousSettings, settings);
|
|
51
|
-
|
|
55
|
+
|
|
56
|
+
set({ updatePluginSettingsSignal: newSignal });
|
|
57
|
+
await pluginService.updatePluginSettings(id, nextSettings, newSignal.signal);
|
|
52
58
|
|
|
53
59
|
await get().refreshPlugins();
|
|
54
60
|
},
|
|
@@ -157,7 +157,12 @@ describe('useToolStore:pluginStore', () => {
|
|
|
157
157
|
|
|
158
158
|
// Then
|
|
159
159
|
expect(useSWR).toHaveBeenCalledWith('loadPluginStore', expect.any(Function), {
|
|
160
|
+
fallbackData: {
|
|
161
|
+
plugins: [],
|
|
162
|
+
schemaVersion: 1,
|
|
163
|
+
},
|
|
160
164
|
revalidateOnFocus: false,
|
|
165
|
+
suspense: true,
|
|
161
166
|
});
|
|
162
167
|
expect(result.current.data).toEqual(pluginListMock);
|
|
163
168
|
expect(result.current.error).toBeNull();
|
|
@@ -178,7 +183,12 @@ describe('useToolStore:pluginStore', () => {
|
|
|
178
183
|
|
|
179
184
|
// Then
|
|
180
185
|
expect(useSWR).toHaveBeenCalledWith('loadPluginStore', expect.any(Function), {
|
|
186
|
+
fallbackData: {
|
|
187
|
+
plugins: [],
|
|
188
|
+
schemaVersion: 1,
|
|
189
|
+
},
|
|
181
190
|
revalidateOnFocus: false,
|
|
191
|
+
suspense: true,
|
|
182
192
|
});
|
|
183
193
|
expect(result.current.data).toBeNull();
|
|
184
194
|
expect(result.current.error).toEqual(error);
|
|
@@ -45,11 +45,12 @@ export const createPluginStoreSlice: StateCreator<
|
|
|
45
45
|
try {
|
|
46
46
|
updateInstallLoadingState(name, true);
|
|
47
47
|
const data = await toolService.getPluginManifest(plugin.manifest);
|
|
48
|
-
updateInstallLoadingState(name, undefined);
|
|
49
48
|
|
|
50
49
|
// 4. 存储 manifest 信息
|
|
51
50
|
await pluginService.installPlugin({ identifier: plugin.identifier, manifest: data, type });
|
|
52
51
|
await refreshPlugins();
|
|
52
|
+
|
|
53
|
+
updateInstallLoadingState(name, undefined);
|
|
53
54
|
} catch (error) {
|
|
54
55
|
console.error(error);
|
|
55
56
|
updateInstallLoadingState(name, undefined);
|
|
@@ -91,6 +92,7 @@ export const createPluginStoreSlice: StateCreator<
|
|
|
91
92
|
},
|
|
92
93
|
useFetchInstalledPlugins: () =>
|
|
93
94
|
useSWR<LobeTool[]>(INSTALLED_PLUGINS, pluginService.getInstalledPlugins, {
|
|
95
|
+
fallbackData: [],
|
|
94
96
|
onSuccess: (data) => {
|
|
95
97
|
set(
|
|
96
98
|
{ installedPlugins: data, loadingInstallPlugins: false },
|
|
@@ -99,9 +101,12 @@ export const createPluginStoreSlice: StateCreator<
|
|
|
99
101
|
);
|
|
100
102
|
},
|
|
101
103
|
revalidateOnFocus: false,
|
|
104
|
+
suspense: true,
|
|
102
105
|
}),
|
|
103
106
|
useFetchPluginStore: () =>
|
|
104
107
|
useSWR<LobeChatPluginsMarketIndex>('loadPluginStore', get().loadPluginStore, {
|
|
108
|
+
fallbackData: { plugins: [], schemaVersion: 1 },
|
|
105
109
|
revalidateOnFocus: false,
|
|
110
|
+
suspense: true,
|
|
106
111
|
}),
|
|
107
112
|
});
|
|
@@ -2,9 +2,9 @@ import { act, renderHook, waitFor } from '@testing-library/react';
|
|
|
2
2
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
3
|
import { withSWR } from '~test-utils';
|
|
4
4
|
|
|
5
|
+
import { DEFAULT_PREFERENCE } from '@/const/user';
|
|
5
6
|
import { useUserStore } from '@/store/user';
|
|
6
|
-
|
|
7
|
-
import { DEFAULT_PREFERENCE, type Guide, UserPreference } from './initialState';
|
|
7
|
+
import { UserGuide, UserPreference } from '@/types/user';
|
|
8
8
|
|
|
9
9
|
beforeEach(() => {
|
|
10
10
|
vi.clearAllMocks();
|
|
@@ -18,7 +18,7 @@ describe('createPreferenceSlice', () => {
|
|
|
18
18
|
describe('updateGuideState', () => {
|
|
19
19
|
it('should update guide state', () => {
|
|
20
20
|
const { result } = renderHook(() => useUserStore());
|
|
21
|
-
const guide:
|
|
21
|
+
const guide: UserGuide = { topic: true };
|
|
22
22
|
|
|
23
23
|
act(() => {
|
|
24
24
|
result.current.updateGuideState(guide);
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { SWRResponse } from 'swr';
|
|
2
2
|
import type { StateCreator } from 'zustand/vanilla';
|
|
3
3
|
|
|
4
|
+
import { DEFAULT_PREFERENCE } from '@/const/user';
|
|
4
5
|
import { useClientDataSWR } from '@/libs/swr';
|
|
5
6
|
import type { UserStore } from '@/store/user';
|
|
7
|
+
import { UserGuide, UserPreference } from '@/types/user';
|
|
6
8
|
import { merge } from '@/utils/merge';
|
|
7
9
|
import { setNamespace } from '@/utils/storeDebug';
|
|
8
10
|
|
|
9
|
-
import { DEFAULT_PREFERENCE, Guide, UserPreference } from './initialState';
|
|
10
|
-
|
|
11
11
|
const n = setNamespace('preference');
|
|
12
12
|
|
|
13
13
|
export interface PreferenceAction {
|
|
14
|
-
updateGuideState: (guide: Partial<
|
|
14
|
+
updateGuideState: (guide: Partial<UserGuide>) => void;
|
|
15
15
|
updatePreference: (preference: Partial<UserPreference>, action?: any) => void;
|
|
16
16
|
useInitPreference: () => SWRResponse;
|
|
17
17
|
}
|
|
@@ -1,25 +1,7 @@
|
|
|
1
|
+
import { DEFAULT_PREFERENCE } from '@/const/user';
|
|
2
|
+
import { UserPreference } from '@/types/user';
|
|
1
3
|
import { AsyncLocalStorage } from '@/utils/localStorage';
|
|
2
4
|
|
|
3
|
-
export interface Guide {
|
|
4
|
-
/**
|
|
5
|
-
* Move the settings button to the avatar dropdown
|
|
6
|
-
*/
|
|
7
|
-
moveSettingsToAvatar?: boolean;
|
|
8
|
-
|
|
9
|
-
// Topic 引导
|
|
10
|
-
topic?: boolean;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface UserPreference {
|
|
14
|
-
guide?: Guide;
|
|
15
|
-
hideSyncAlert?: boolean;
|
|
16
|
-
telemetry: boolean | null;
|
|
17
|
-
/**
|
|
18
|
-
* whether to use cmd + enter to send message
|
|
19
|
-
*/
|
|
20
|
-
useCmdEnterToSend?: boolean;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
5
|
export interface UserPreferenceState {
|
|
24
6
|
isPreferenceInit: boolean;
|
|
25
7
|
/**
|
|
@@ -29,15 +11,6 @@ export interface UserPreferenceState {
|
|
|
29
11
|
preferenceStorage: AsyncLocalStorage<UserPreference>;
|
|
30
12
|
}
|
|
31
13
|
|
|
32
|
-
export const DEFAULT_PREFERENCE: UserPreference = {
|
|
33
|
-
guide: {
|
|
34
|
-
moveSettingsToAvatar: true,
|
|
35
|
-
topic: true,
|
|
36
|
-
},
|
|
37
|
-
telemetry: null,
|
|
38
|
-
useCmdEnterToSend: false,
|
|
39
|
-
};
|
|
40
|
-
|
|
41
14
|
export const initialPreferenceState: UserPreferenceState = {
|
|
42
15
|
isPreferenceInit: false,
|
|
43
16
|
preference: DEFAULT_PREFERENCE,
|
package/src/types/tool/index.ts
CHANGED
|
@@ -4,9 +4,9 @@ import { CustomPluginParams } from './plugin';
|
|
|
4
4
|
import { LobeToolType } from './tool';
|
|
5
5
|
|
|
6
6
|
export interface LobeTool {
|
|
7
|
-
customParams?: CustomPluginParams;
|
|
7
|
+
customParams?: CustomPluginParams | null;
|
|
8
8
|
identifier: string;
|
|
9
|
-
manifest?: LobeChatPluginManifest;
|
|
9
|
+
manifest?: LobeChatPluginManifest | null;
|
|
10
10
|
settings?: any;
|
|
11
11
|
type: LobeToolType;
|
|
12
12
|
}
|
package/src/types/user.ts
CHANGED
|
@@ -7,3 +7,23 @@ export interface LobeUser {
|
|
|
7
7
|
latestName?: string | null;
|
|
8
8
|
username?: string | null;
|
|
9
9
|
}
|
|
10
|
+
|
|
11
|
+
export interface UserGuide {
|
|
12
|
+
/**
|
|
13
|
+
* Move the settings button to the avatar dropdown
|
|
14
|
+
*/
|
|
15
|
+
moveSettingsToAvatar?: boolean;
|
|
16
|
+
|
|
17
|
+
// Topic 引导
|
|
18
|
+
topic?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface UserPreference {
|
|
22
|
+
guide?: UserGuide;
|
|
23
|
+
hideSyncAlert?: boolean;
|
|
24
|
+
telemetry: boolean | null;
|
|
25
|
+
/**
|
|
26
|
+
* whether to use cmd + enter to send message
|
|
27
|
+
*/
|
|
28
|
+
useCmdEnterToSend?: boolean;
|
|
29
|
+
}
|
package/src/utils/fetch.ts
CHANGED
|
@@ -363,7 +363,9 @@ export const fetchSSE = async (url: string, options: RequestInit & FetchSSEOptio
|
|
|
363
363
|
return;
|
|
364
364
|
}
|
|
365
365
|
},
|
|
366
|
-
|
|
366
|
+
// we should keep open when page hidden, or it will case lots of token cost
|
|
367
|
+
// refs: https://github.com/lobehub/lobe-chat/issues/2501
|
|
368
|
+
openWhenHidden: true,
|
|
367
369
|
signal: options.signal,
|
|
368
370
|
});
|
|
369
371
|
} catch {}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { useEffect } from 'react';
|
|
2
|
-
|
|
3
|
-
import { useAgentStore } from '@/store/agent';
|
|
4
|
-
import { agentSelectors } from '@/store/agent/selectors';
|
|
5
|
-
import { useChatStore } from '@/store/chat';
|
|
6
|
-
import { useSessionStore } from '@/store/session';
|
|
7
|
-
import { useToolStore } from '@/store/tool';
|
|
8
|
-
|
|
9
|
-
export const useInitConversation = () => {
|
|
10
|
-
const [sessionId] = useSessionStore((s) => [s.activeId]);
|
|
11
|
-
const [useFetchAgentConfig] = useAgentStore((s) => [s.useFetchAgentConfig]);
|
|
12
|
-
const plugins = useAgentStore((s) => agentSelectors.currentAgentPlugins(s));
|
|
13
|
-
const [activeTopicId, switchTopic, useFetchMessages, useFetchTopics] = useChatStore((s) => [
|
|
14
|
-
s.activeTopicId,
|
|
15
|
-
s.switchTopic,
|
|
16
|
-
s.useFetchMessages,
|
|
17
|
-
s.useFetchTopics,
|
|
18
|
-
]);
|
|
19
|
-
|
|
20
|
-
useFetchMessages(sessionId, activeTopicId);
|
|
21
|
-
useFetchTopics(sessionId);
|
|
22
|
-
useFetchAgentConfig(sessionId);
|
|
23
|
-
|
|
24
|
-
const [useFetchPluginStore, useFetchInstalledPlugins, checkPluginsIsInstalled] = useToolStore(
|
|
25
|
-
(s) => [s.useFetchPluginStore, s.useFetchInstalledPlugins, s.useCheckPluginsIsInstalled],
|
|
26
|
-
);
|
|
27
|
-
|
|
28
|
-
useFetchPluginStore();
|
|
29
|
-
useFetchInstalledPlugins();
|
|
30
|
-
checkPluginsIsInstalled(plugins);
|
|
31
|
-
|
|
32
|
-
useEffect(() => {
|
|
33
|
-
// // when activeId changed, switch topic to undefined
|
|
34
|
-
const unsubscribe = useSessionStore.subscribe(
|
|
35
|
-
(s) => s.activeId,
|
|
36
|
-
(activeId) => {
|
|
37
|
-
switchTopic();
|
|
38
|
-
|
|
39
|
-
useAgentStore.setState({ activeId }, false, 'updateActiveId');
|
|
40
|
-
},
|
|
41
|
-
);
|
|
42
|
-
|
|
43
|
-
return () => {
|
|
44
|
-
unsubscribe();
|
|
45
|
-
};
|
|
46
|
-
}, []);
|
|
47
|
-
};
|