@lobehub/chat 0.27.4 → 0.29.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 +61 -0
- package/package.json +1 -1
- package/src/features/SideBar/BottomAction.tsx +112 -0
- package/src/features/SideBar/index.tsx +14 -91
- package/src/helpers/export.ts +62 -0
- package/src/hooks/useExportConfig.ts +6 -0
- package/src/hooks/useImportConfig.ts +36 -0
- package/src/locales/default/setting.ts +4 -3
- package/src/migrations/index.ts +2 -2
- package/src/pages/chat/SessionList/List/SessionItem.tsx +44 -52
- package/src/pages/chat/SessionList/List/style.ts +3 -0
- package/src/pages/chat/[id]/edit/Header.tsx +14 -1
- package/src/pages/setting/SettingForm.tsx +35 -30
- package/src/pages/setting/layout.tsx +8 -0
- package/src/store/session/slices/session/action.ts +34 -11
- package/src/store/session/slices/session/selectors/export.ts +23 -0
- package/src/store/session/slices/session/selectors/index.ts +6 -2
- package/src/store/session/slices/session/selectors/list.ts +3 -2
- package/src/store/settings/action.ts +16 -4
- package/src/store/settings/initialState.ts +6 -6
- package/src/store/settings/selectors.ts +10 -0
- package/src/store/settings/store.ts +2 -2
- package/src/types/exportConfig.ts +54 -39
- package/src/types/settings.ts +32 -0
- package/src/utils/config.ts +103 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,67 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
## [Version 0.29.0](https://github.com/lobehub/lobe-chat/compare/v0.28.0...v0.29.0)
|
|
6
|
+
|
|
7
|
+
<sup>Released on **2023-07-30**</sup>
|
|
8
|
+
|
|
9
|
+
#### ✨ Features
|
|
10
|
+
|
|
11
|
+
- **misc**: 实现单个会话和角色的导出功能, 实现清空所有会话消息.
|
|
12
|
+
|
|
13
|
+
<br/>
|
|
14
|
+
|
|
15
|
+
<details>
|
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
17
|
+
|
|
18
|
+
#### What's improved
|
|
19
|
+
|
|
20
|
+
- **misc**: 实现单个会话和角色的导出功能 ([d15a481](https://github.com/lobehub/lobe-chat/commit/d15a481))
|
|
21
|
+
- **misc**: 实现清空所有会话消息 ([64c5125](https://github.com/lobehub/lobe-chat/commit/64c5125))
|
|
22
|
+
|
|
23
|
+
</details>
|
|
24
|
+
|
|
25
|
+
<div align="right">
|
|
26
|
+
|
|
27
|
+
[](#readme-top)
|
|
28
|
+
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
## [Version 0.28.0](https://github.com/lobehub/lobe-chat/compare/v0.27.4...v0.28.0)
|
|
32
|
+
|
|
33
|
+
<sup>Released on **2023-07-30**</sup>
|
|
34
|
+
|
|
35
|
+
#### ♻ Code Refactoring
|
|
36
|
+
|
|
37
|
+
- **misc**: 重构 settings 相关类型.
|
|
38
|
+
|
|
39
|
+
#### ✨ Features
|
|
40
|
+
|
|
41
|
+
- **misc**: 优化 SideBar 实现,激活态指示更加明确, 实现 session 导入功能, 实现配置导出功能.
|
|
42
|
+
|
|
43
|
+
<br/>
|
|
44
|
+
|
|
45
|
+
<details>
|
|
46
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
47
|
+
|
|
48
|
+
#### Code refactoring
|
|
49
|
+
|
|
50
|
+
- **misc**: 重构 settings 相关类型 ([6b7c0a0](https://github.com/lobehub/lobe-chat/commit/6b7c0a0))
|
|
51
|
+
|
|
52
|
+
#### What's improved
|
|
53
|
+
|
|
54
|
+
- **misc**: 优化 SideBar 实现,激活态指示更加明确 ([8a467df](https://github.com/lobehub/lobe-chat/commit/8a467df))
|
|
55
|
+
- **misc**: 实现 session 导入功能 ([5650167](https://github.com/lobehub/lobe-chat/commit/5650167))
|
|
56
|
+
- **misc**: 实现配置导出功能 ([c1f73fe](https://github.com/lobehub/lobe-chat/commit/c1f73fe))
|
|
57
|
+
|
|
58
|
+
</details>
|
|
59
|
+
|
|
60
|
+
<div align="right">
|
|
61
|
+
|
|
62
|
+
[](#readme-top)
|
|
63
|
+
|
|
64
|
+
</div>
|
|
65
|
+
|
|
5
66
|
### [Version 0.27.4](https://github.com/lobehub/lobe-chat/compare/v0.27.3...v0.27.4)
|
|
6
67
|
|
|
7
68
|
<sup>Released on **2023-07-29**</sup>
|
package/package.json
CHANGED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { ActionIcon, Icon } from '@lobehub/ui';
|
|
2
|
+
import { Dropdown, MenuProps, Upload } from 'antd';
|
|
3
|
+
import {
|
|
4
|
+
Feather,
|
|
5
|
+
FileClock,
|
|
6
|
+
FolderInput,
|
|
7
|
+
FolderOutput,
|
|
8
|
+
Github,
|
|
9
|
+
Heart,
|
|
10
|
+
Settings,
|
|
11
|
+
} from 'lucide-react';
|
|
12
|
+
import Router from 'next/router';
|
|
13
|
+
import { ReactNode, memo, useMemo } from 'react';
|
|
14
|
+
import { useTranslation } from 'react-i18next';
|
|
15
|
+
|
|
16
|
+
import { useExportConfig } from '@/hooks/useExportConfig';
|
|
17
|
+
import { useImportConfig } from '@/hooks/useImportConfig';
|
|
18
|
+
|
|
19
|
+
import pkg from '../../../package.json';
|
|
20
|
+
|
|
21
|
+
const BottomAction = memo<{ children: ReactNode }>(({ children }) => {
|
|
22
|
+
const { t } = useTranslation('common');
|
|
23
|
+
|
|
24
|
+
const { exportSessions, exportSettings, exportAll, exportAgents } = useExportConfig();
|
|
25
|
+
const { importConfig } = useImportConfig();
|
|
26
|
+
|
|
27
|
+
const items: MenuProps['items'] = useMemo(
|
|
28
|
+
() => [
|
|
29
|
+
{
|
|
30
|
+
icon: <Icon icon={FolderInput} />,
|
|
31
|
+
key: 'import',
|
|
32
|
+
label: (
|
|
33
|
+
<Upload maxCount={1} onChange={importConfig} showUploadList={false}>
|
|
34
|
+
{t('import')}
|
|
35
|
+
</Upload>
|
|
36
|
+
),
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
children: [
|
|
40
|
+
{
|
|
41
|
+
key: 'allAgent',
|
|
42
|
+
label: <div>{t('exportType.allAgent')}</div>,
|
|
43
|
+
onClick: exportAgents,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
key: 'allAgentWithMessage',
|
|
47
|
+
label: <div>{t('exportType.allAgentWithMessage')}</div>,
|
|
48
|
+
onClick: exportSessions,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
key: 'globalSetting',
|
|
52
|
+
label: <div>{t('exportType.globalSetting')}</div>,
|
|
53
|
+
onClick: exportSettings,
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
type: 'divider',
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
key: 'all',
|
|
60
|
+
label: <div>{t('exportType.all')}</div>,
|
|
61
|
+
onClick: exportAll,
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
icon: <Icon icon={FolderOutput} />,
|
|
65
|
+
key: 'export',
|
|
66
|
+
label: t('export'),
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
type: 'divider',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
icon: <Icon icon={Feather} />,
|
|
73
|
+
key: 'feedback',
|
|
74
|
+
label: t('feedback'),
|
|
75
|
+
onClick: () => window.open(pkg.bugs.url, '__blank'),
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
icon: <Icon icon={FileClock} />,
|
|
79
|
+
key: 'changelog',
|
|
80
|
+
label: t('changelog'),
|
|
81
|
+
onClick: () => window.open(`${pkg.homepage}/blob/master/CHANGELOG.md`, '__blank'),
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
icon: <Icon icon={Heart} />,
|
|
85
|
+
key: 'about',
|
|
86
|
+
label: t('about'),
|
|
87
|
+
onClick: () => window.open(pkg.homepage, '__blank'),
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
type: 'divider',
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
icon: <Icon icon={Settings} />,
|
|
94
|
+
key: 'setting',
|
|
95
|
+
label: t('setting'),
|
|
96
|
+
onClick: () => Router.push('/setting'),
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
[],
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<>
|
|
104
|
+
<ActionIcon icon={Github} onClick={() => window.open(pkg.homepage, '__blank')} />
|
|
105
|
+
<Dropdown arrow={false} menu={{ items }} trigger={['click']}>
|
|
106
|
+
{children}
|
|
107
|
+
</Dropdown>
|
|
108
|
+
</>
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
export default BottomAction;
|
|
@@ -1,106 +1,24 @@
|
|
|
1
|
-
import { ActionIcon,
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
Feather,
|
|
5
|
-
FileClock,
|
|
6
|
-
FolderInput,
|
|
7
|
-
FolderOutput,
|
|
8
|
-
Github,
|
|
9
|
-
Heart,
|
|
10
|
-
MessageSquare,
|
|
11
|
-
Settings,
|
|
12
|
-
Settings2,
|
|
13
|
-
Sticker,
|
|
14
|
-
} from 'lucide-react';
|
|
1
|
+
import { ActionIcon, SideNav } from '@lobehub/ui';
|
|
2
|
+
import { MessageSquare, Settings2, Sticker } from 'lucide-react';
|
|
15
3
|
import Router from 'next/router';
|
|
16
|
-
import { memo
|
|
17
|
-
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { memo } from 'react';
|
|
18
5
|
import { shallow } from 'zustand/shallow';
|
|
19
6
|
|
|
20
|
-
import pkg from '@/../package.json';
|
|
21
7
|
import AvatarWithUpload from '@/features/AvatarWithUpload';
|
|
22
8
|
import { useSettings } from '@/store/settings';
|
|
23
9
|
|
|
10
|
+
import BottomAction from './BottomAction';
|
|
11
|
+
|
|
24
12
|
export default memo(() => {
|
|
25
13
|
const [tab, setTab] = useSettings((s) => [s.sidebarKey, s.switchSideBar], shallow);
|
|
26
|
-
const { t } = useTranslation('common');
|
|
27
|
-
const items: MenuProps['items'] = useMemo(
|
|
28
|
-
() => [
|
|
29
|
-
{
|
|
30
|
-
icon: <Icon icon={FolderInput} />,
|
|
31
|
-
key: 'import',
|
|
32
|
-
label: <div>{t('import')}</div>,
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
children: [
|
|
36
|
-
{
|
|
37
|
-
key: 'allAgent',
|
|
38
|
-
label: <div>{t('exportType.allAgent')}</div>,
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
key: 'allAgentWithMessage',
|
|
42
|
-
label: <div>{t('exportType.allAgentWithMessage')}</div>,
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
key: 'globalSetting',
|
|
46
|
-
label: <div>{t('exportType.globalSetting')}</div>,
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
type: 'divider',
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
key: 'all',
|
|
53
|
-
label: <div>{t('exportType.all')}</div>,
|
|
54
|
-
},
|
|
55
|
-
],
|
|
56
|
-
icon: <Icon icon={FolderOutput} />,
|
|
57
|
-
key: 'export',
|
|
58
|
-
label: t('export'),
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
type: 'divider',
|
|
62
|
-
},
|
|
63
|
-
{
|
|
64
|
-
icon: <Icon icon={Feather} />,
|
|
65
|
-
key: 'feedback',
|
|
66
|
-
label: t('feedback'),
|
|
67
|
-
onClick: () => window.open(pkg.bugs.url, '__blank'),
|
|
68
|
-
},
|
|
69
|
-
{
|
|
70
|
-
icon: <Icon icon={FileClock} />,
|
|
71
|
-
key: 'changelog',
|
|
72
|
-
label: t('changelog'),
|
|
73
|
-
onClick: () => window.open(`${pkg.homepage}/blob/master/CHANGELOG.md`, '__blank'),
|
|
74
|
-
},
|
|
75
|
-
{
|
|
76
|
-
icon: <Icon icon={Heart} />,
|
|
77
|
-
key: 'about',
|
|
78
|
-
label: t('about'),
|
|
79
|
-
onClick: () => window.open(pkg.homepage, '__blank'),
|
|
80
|
-
},
|
|
81
|
-
{
|
|
82
|
-
type: 'divider',
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
icon: <Icon icon={Settings} />,
|
|
86
|
-
key: 'setting',
|
|
87
|
-
label: t('setting'),
|
|
88
|
-
onClick: () => Router.push('/setting'),
|
|
89
|
-
},
|
|
90
|
-
],
|
|
91
|
-
[],
|
|
92
|
-
);
|
|
93
14
|
|
|
94
15
|
return (
|
|
95
16
|
<SideNav
|
|
96
17
|
avatar={<AvatarWithUpload />}
|
|
97
18
|
bottomActions={
|
|
98
|
-
|
|
99
|
-
<ActionIcon
|
|
100
|
-
|
|
101
|
-
<ActionIcon icon={Settings2} />
|
|
102
|
-
</Dropdown>
|
|
103
|
-
</>
|
|
19
|
+
<BottomAction>
|
|
20
|
+
<ActionIcon active={tab === 'setting'} icon={Settings2} />
|
|
21
|
+
</BottomAction>
|
|
104
22
|
}
|
|
105
23
|
style={{ height: '100vh' }}
|
|
106
24
|
topActions={
|
|
@@ -108,7 +26,12 @@ export default memo(() => {
|
|
|
108
26
|
<ActionIcon
|
|
109
27
|
active={tab === 'chat'}
|
|
110
28
|
icon={MessageSquare}
|
|
111
|
-
onClick={() =>
|
|
29
|
+
onClick={() => {
|
|
30
|
+
// 如果已经在 chat 路径下了,那么就不用再跳转了
|
|
31
|
+
if (Router.asPath.startsWith('/chat')) return;
|
|
32
|
+
|
|
33
|
+
Router.push('/');
|
|
34
|
+
}}
|
|
112
35
|
size="large"
|
|
113
36
|
/>
|
|
114
37
|
<ActionIcon
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { sessionSelectors, useSessionStore } from '@/store/session';
|
|
2
|
+
import { settingsSelectors, useSettings } from '@/store/settings';
|
|
3
|
+
import { createConfigFile, exportConfigFile } from '@/utils/config';
|
|
4
|
+
|
|
5
|
+
const getAgents = () => sessionSelectors.exportAgents(useSessionStore.getState());
|
|
6
|
+
const getAgent = (id: string) => sessionSelectors.getExportAgent(id)(useSessionStore.getState());
|
|
7
|
+
|
|
8
|
+
const getSessions = () => sessionSelectors.exportSessions(useSessionStore.getState());
|
|
9
|
+
const getSession = (id: string) => sessionSelectors.getSessionById(id)(useSessionStore.getState());
|
|
10
|
+
|
|
11
|
+
const getSettings = () => settingsSelectors.exportSettings(useSettings.getState());
|
|
12
|
+
|
|
13
|
+
export const exportAgents = () => {
|
|
14
|
+
const sessions = getAgents();
|
|
15
|
+
|
|
16
|
+
const config = createConfigFile('agents', { sessions });
|
|
17
|
+
|
|
18
|
+
exportConfigFile(config, 'agents');
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const exportSingleAgent = (id: string) => {
|
|
22
|
+
const agent = getAgent(id);
|
|
23
|
+
if (!agent) return;
|
|
24
|
+
|
|
25
|
+
const config = createConfigFile('agents', { sessions: { [id]: agent } });
|
|
26
|
+
|
|
27
|
+
exportConfigFile(config, agent.meta?.title || 'agent');
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const exportSessions = () => {
|
|
31
|
+
const sessions = getSessions();
|
|
32
|
+
const config = createConfigFile('sessions', { sessions });
|
|
33
|
+
|
|
34
|
+
exportConfigFile(config, 'sessions');
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const exportSingleSession = (id: string) => {
|
|
38
|
+
const session = getSession(id);
|
|
39
|
+
if (!session) return;
|
|
40
|
+
const sessions = { [id]: session };
|
|
41
|
+
|
|
42
|
+
const config = createConfigFile('sessions', { sessions });
|
|
43
|
+
|
|
44
|
+
exportConfigFile(config, `${session.meta?.title}-session`);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const exportSettings = () => {
|
|
48
|
+
const settings = getSettings();
|
|
49
|
+
|
|
50
|
+
const config = createConfigFile('settings', { settings });
|
|
51
|
+
|
|
52
|
+
exportConfigFile(config, 'settings');
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const exportAll = () => {
|
|
56
|
+
const sessions = getSessions();
|
|
57
|
+
const settings = getSettings();
|
|
58
|
+
|
|
59
|
+
const config = createConfigFile('all', { sessions, settings });
|
|
60
|
+
|
|
61
|
+
exportConfigFile(config, 'config');
|
|
62
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
|
|
3
|
+
import { useSessionStore } from '@/store/session';
|
|
4
|
+
import { useSettings } from '@/store/settings';
|
|
5
|
+
import { importConfigFile } from '@/utils/config';
|
|
6
|
+
|
|
7
|
+
export const useImportConfig = () => {
|
|
8
|
+
const importSessions = useSessionStore((s) => s.importSessions);
|
|
9
|
+
const importSettings = useSettings((s) => s.importSettings);
|
|
10
|
+
|
|
11
|
+
const importConfig = (info: any) => {
|
|
12
|
+
importConfigFile(info, (config) => {
|
|
13
|
+
switch (config.exportType) {
|
|
14
|
+
case 'settings': {
|
|
15
|
+
importSettings(config.state.settings);
|
|
16
|
+
break;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
case 'sessions':
|
|
20
|
+
case 'agents': {
|
|
21
|
+
importSessions(config.state.sessions);
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
case 'all': {
|
|
26
|
+
importSessions(config.state.sessions);
|
|
27
|
+
importSettings(config.state.settings);
|
|
28
|
+
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
return useMemo(() => ({ importConfig }), []);
|
|
36
|
+
};
|
package/src/migrations/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ConfigStateAll } from '@/types/exportConfig';
|
|
2
2
|
import { VersionController } from '@/utils/VersionController';
|
|
3
3
|
|
|
4
4
|
// 当前最新的版本号
|
|
@@ -7,7 +7,7 @@ export const CURRENT_CONFIG_VERSION = 1;
|
|
|
7
7
|
// 历史记录版本升级模块
|
|
8
8
|
export const ConfigMigrations = [];
|
|
9
9
|
|
|
10
|
-
export const Migration = new VersionController<
|
|
10
|
+
export const Migration = new VersionController<ConfigStateAll>(
|
|
11
11
|
ConfigMigrations,
|
|
12
12
|
CURRENT_CONFIG_VERSION,
|
|
13
13
|
);
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { ActionIcon, Avatar, Icon, List } from '@lobehub/ui';
|
|
2
2
|
import { useHover } from 'ahooks';
|
|
3
|
-
import { Dropdown, type MenuProps,
|
|
3
|
+
import { App, Dropdown, type MenuProps, Tag } from 'antd';
|
|
4
4
|
import { FolderOutput, MoreVertical, Pin, PinOff, Trash } from 'lucide-react';
|
|
5
|
-
import { FC, memo, useMemo, useRef
|
|
5
|
+
import { FC, memo, useMemo, useRef } from 'react';
|
|
6
6
|
import { useTranslation } from 'react-i18next';
|
|
7
7
|
import { Flexbox } from 'react-layout-kit';
|
|
8
8
|
import { shallow } from 'zustand/shallow';
|
|
9
9
|
|
|
10
|
+
import { exportSingleAgent, exportSingleSession } from '@/helpers/export';
|
|
10
11
|
import { agentSelectors, chatSelectors, sessionSelectors, useSessionStore } from '@/store/session';
|
|
11
12
|
import { useSettings } from '@/store/settings';
|
|
12
13
|
|
|
@@ -24,10 +25,10 @@ interface SessionItemProps {
|
|
|
24
25
|
const SessionItem: FC<SessionItemProps> = memo(({ id, active = true, loading, pin }) => {
|
|
25
26
|
const ref = useRef(null);
|
|
26
27
|
const isHovering = useHover(ref);
|
|
27
|
-
|
|
28
|
-
const [dropdownOpen, setDropdownOpen] = useState(false);
|
|
28
|
+
|
|
29
29
|
const { t } = useTranslation('common');
|
|
30
|
-
const isHighlight = isHovering
|
|
30
|
+
const isHighlight = isHovering;
|
|
31
|
+
|
|
31
32
|
const { styles, theme, cx } = useStyles(isHighlight);
|
|
32
33
|
const [defaultModel] = useSettings((s) => [s.settings.model], shallow);
|
|
33
34
|
|
|
@@ -58,23 +59,31 @@ const SessionItem: FC<SessionItemProps> = memo(({ id, active = true, loading, pi
|
|
|
58
59
|
];
|
|
59
60
|
}, shallow);
|
|
60
61
|
|
|
61
|
-
|
|
62
|
+
const { modal } = App.useApp();
|
|
62
63
|
const items: MenuProps['items'] = useMemo(
|
|
63
64
|
() => [
|
|
64
65
|
{
|
|
65
66
|
icon: <Icon icon={pin ? PinOff : Pin} />,
|
|
66
67
|
key: 'pin',
|
|
67
68
|
label: t(pin ? 'pinOff' : 'pin'),
|
|
69
|
+
// TODO: 动作绑定
|
|
70
|
+
onClick: () => {},
|
|
68
71
|
},
|
|
69
72
|
{
|
|
70
73
|
children: [
|
|
71
74
|
{
|
|
72
75
|
key: 'agent',
|
|
73
76
|
label: <div>{t('exportType.agent')}</div>,
|
|
77
|
+
onClick: () => {
|
|
78
|
+
exportSingleAgent(id);
|
|
79
|
+
},
|
|
74
80
|
},
|
|
75
81
|
{
|
|
76
82
|
key: 'agentWithMessage',
|
|
77
83
|
label: <div>{t('exportType.agentWithMessage')}</div>,
|
|
84
|
+
onClick: () => {
|
|
85
|
+
exportSingleSession(id);
|
|
86
|
+
},
|
|
78
87
|
},
|
|
79
88
|
],
|
|
80
89
|
icon: <Icon icon={FolderOutput} />,
|
|
@@ -82,10 +91,24 @@ const SessionItem: FC<SessionItemProps> = memo(({ id, active = true, loading, pi
|
|
|
82
91
|
label: t('export'),
|
|
83
92
|
},
|
|
84
93
|
{
|
|
94
|
+
danger: true,
|
|
85
95
|
icon: <Icon icon={Trash} />,
|
|
86
96
|
key: 'delete',
|
|
87
97
|
label: t('delete'),
|
|
88
|
-
onClick: () =>
|
|
98
|
+
onClick: ({ domEvent }) => {
|
|
99
|
+
domEvent.stopPropagation();
|
|
100
|
+
|
|
101
|
+
modal.confirm({
|
|
102
|
+
centered: true,
|
|
103
|
+
okButtonProps: { danger: true },
|
|
104
|
+
okText: t('ok'),
|
|
105
|
+
onOk: () => {
|
|
106
|
+
removeSession(id);
|
|
107
|
+
},
|
|
108
|
+
rootClassName: styles.modalRoot,
|
|
109
|
+
title: t('confirmRemoveSessionItemAlert'),
|
|
110
|
+
});
|
|
111
|
+
},
|
|
89
112
|
},
|
|
90
113
|
],
|
|
91
114
|
[id],
|
|
@@ -123,15 +146,6 @@ const SessionItem: FC<SessionItemProps> = memo(({ id, active = true, loading, pi
|
|
|
123
146
|
{model}
|
|
124
147
|
</Tag>
|
|
125
148
|
)}
|
|
126
|
-
{/*{showChatLength && (*/}
|
|
127
|
-
{/* <Tag*/}
|
|
128
|
-
{/* bordered={false}*/}
|
|
129
|
-
{/* style={{ color: theme.colorTextSecondary, display: 'flex', gap: 4 }}*/}
|
|
130
|
-
{/* >*/}
|
|
131
|
-
{/* <Icon icon={LucideMessageCircle} />*/}
|
|
132
|
-
{/* {chatLength}*/}
|
|
133
|
-
{/* </Tag>*/}
|
|
134
|
-
{/*)}*/}
|
|
135
149
|
</Flexbox>
|
|
136
150
|
)}
|
|
137
151
|
</Flexbox>
|
|
@@ -140,42 +154,20 @@ const SessionItem: FC<SessionItemProps> = memo(({ id, active = true, loading, pi
|
|
|
140
154
|
style={{ color: theme.colorText }}
|
|
141
155
|
title={title}
|
|
142
156
|
/>
|
|
143
|
-
<
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
>
|
|
158
|
-
<Dropdown
|
|
159
|
-
arrow={false}
|
|
160
|
-
menu={{ items }}
|
|
161
|
-
onOpenChange={setDropdownOpen}
|
|
162
|
-
open={dropdownOpen}
|
|
163
|
-
trigger={['click']}
|
|
164
|
-
>
|
|
165
|
-
<ActionIcon
|
|
166
|
-
className="session-remove"
|
|
167
|
-
icon={MoreVertical}
|
|
168
|
-
onClick={(e) => {
|
|
169
|
-
e.preventDefault();
|
|
170
|
-
e.stopPropagation();
|
|
171
|
-
}}
|
|
172
|
-
size={{
|
|
173
|
-
blockSize: 28,
|
|
174
|
-
fontSize: 16,
|
|
175
|
-
}}
|
|
176
|
-
/>
|
|
177
|
-
</Dropdown>
|
|
178
|
-
</Popconfirm>
|
|
157
|
+
<Dropdown arrow={false} menu={{ items }} trigger={['click']}>
|
|
158
|
+
<ActionIcon
|
|
159
|
+
className="session-remove"
|
|
160
|
+
icon={MoreVertical}
|
|
161
|
+
onClick={(e) => {
|
|
162
|
+
e.preventDefault();
|
|
163
|
+
e.stopPropagation();
|
|
164
|
+
}}
|
|
165
|
+
size={{
|
|
166
|
+
blockSize: 28,
|
|
167
|
+
fontSize: 16,
|
|
168
|
+
}}
|
|
169
|
+
/>
|
|
170
|
+
</Dropdown>
|
|
179
171
|
</Flexbox>
|
|
180
172
|
</div>
|
|
181
173
|
);
|
|
@@ -52,6 +52,9 @@ export const useStyles = createStyles(({ css, token }, isHighlight: boolean) =>
|
|
|
52
52
|
hover: css`
|
|
53
53
|
background-color: ${token.colorFillSecondary};
|
|
54
54
|
`,
|
|
55
|
+
modalRoot: css`
|
|
56
|
+
z-index: 2000;
|
|
57
|
+
`,
|
|
55
58
|
pin: css`
|
|
56
59
|
background-color: ${token.colorFillTertiary};
|
|
57
60
|
`,
|
|
@@ -6,19 +6,32 @@ import { memo, useMemo } from 'react';
|
|
|
6
6
|
import { useTranslation } from 'react-i18next';
|
|
7
7
|
|
|
8
8
|
import HeaderTitle from '@/components/HeaderTitle';
|
|
9
|
+
import { exportSingleAgent, exportSingleSession } from '@/helpers/export';
|
|
10
|
+
import { useSessionStore } from '@/store/session';
|
|
9
11
|
|
|
10
12
|
const Header = memo(() => {
|
|
11
13
|
const { t } = useTranslation('setting');
|
|
14
|
+
const id = useSessionStore((s) => s.activeId);
|
|
12
15
|
|
|
13
|
-
const items
|
|
16
|
+
const items = useMemo<MenuProps['items']>(
|
|
14
17
|
() => [
|
|
15
18
|
{
|
|
16
19
|
key: 'agent',
|
|
17
20
|
label: <div>{t('exportType.agent', { ns: 'common' })}</div>,
|
|
21
|
+
onClick: () => {
|
|
22
|
+
if (!id) return;
|
|
23
|
+
|
|
24
|
+
exportSingleAgent(id);
|
|
25
|
+
},
|
|
18
26
|
},
|
|
19
27
|
{
|
|
20
28
|
key: 'agentWithMessage',
|
|
21
29
|
label: <div>{t('exportType.agentWithMessage', { ns: 'common' })}</div>,
|
|
30
|
+
onClick: () => {
|
|
31
|
+
if (!id) return;
|
|
32
|
+
|
|
33
|
+
exportSingleSession(id);
|
|
34
|
+
},
|
|
22
35
|
},
|
|
23
36
|
],
|
|
24
37
|
[],
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Form, type ItemGroup, ThemeSwitch } from '@lobehub/ui';
|
|
2
|
-
import { Form as AntForm, Button, Input,
|
|
2
|
+
import { Form as AntForm, App, Button, Input, Select, Switch } from 'antd';
|
|
3
3
|
import isEqual from 'fast-deep-equal';
|
|
4
4
|
import { debounce } from 'lodash-es';
|
|
5
5
|
import { AppWindow, BrainCog, MessagesSquare, Palette, Webhook } from 'lucide-react';
|
|
@@ -11,10 +11,11 @@ import { shallow } from 'zustand/shallow';
|
|
|
11
11
|
import { FORM_STYLE } from '@/const/layoutTokens';
|
|
12
12
|
import AvatarWithUpload from '@/features/AvatarWithUpload';
|
|
13
13
|
import { options } from '@/locales/options';
|
|
14
|
+
import { useSessionStore } from '@/store/session';
|
|
14
15
|
import { settingsSelectors, useSettings } from '@/store/settings';
|
|
15
16
|
import { DEFAULT_SETTINGS } from '@/store/settings/initialState';
|
|
16
|
-
import { ConfigKeys } from '@/types/exportConfig';
|
|
17
17
|
import { LanguageModel } from '@/types/llm';
|
|
18
|
+
import { ConfigKeys } from '@/types/settings';
|
|
18
19
|
|
|
19
20
|
import { ThemeSwatchesNeutral, ThemeSwatchesPrimary } from './ThemeSwatches';
|
|
20
21
|
|
|
@@ -27,6 +28,7 @@ type SettingItemGroup = ItemGroup & {
|
|
|
27
28
|
const SettingForm = memo(() => {
|
|
28
29
|
const { t } = useTranslation('setting');
|
|
29
30
|
const [form] = AntForm.useForm();
|
|
31
|
+
const clearSessions = useSessionStore((s) => s.clearSessions);
|
|
30
32
|
const settings = useSettings(settingsSelectors.currentSettings, isEqual);
|
|
31
33
|
const { setThemeMode, setSettings, resetSettings } = useSettings(
|
|
32
34
|
(s) => ({
|
|
@@ -37,14 +39,35 @@ const SettingForm = memo(() => {
|
|
|
37
39
|
shallow,
|
|
38
40
|
);
|
|
39
41
|
|
|
42
|
+
const { message, modal } = App.useApp();
|
|
40
43
|
const handleReset = useCallback(() => {
|
|
41
|
-
|
|
42
|
-
|
|
44
|
+
modal.confirm({
|
|
45
|
+
cancelText: t('cancel', { ns: 'common' }),
|
|
46
|
+
centered: true,
|
|
47
|
+
okButtonProps: { danger: true },
|
|
48
|
+
okText: t('ok', { ns: 'common' }),
|
|
49
|
+
onOk: () => {
|
|
50
|
+
resetSettings();
|
|
51
|
+
form.setFieldsValue(DEFAULT_SETTINGS);
|
|
52
|
+
},
|
|
53
|
+
title: t('danger.reset.confirm'),
|
|
54
|
+
});
|
|
43
55
|
}, []);
|
|
44
56
|
|
|
45
57
|
const handleClear = useCallback(() => {
|
|
46
|
-
|
|
47
|
-
|
|
58
|
+
modal.confirm({
|
|
59
|
+
cancelText: t('cancel', { ns: 'common' }),
|
|
60
|
+
centered: true,
|
|
61
|
+
okButtonProps: {
|
|
62
|
+
danger: true,
|
|
63
|
+
},
|
|
64
|
+
okText: t('ok', { ns: 'common' }),
|
|
65
|
+
onOk: () => {
|
|
66
|
+
clearSessions();
|
|
67
|
+
message.success(t('danger.clear.success'));
|
|
68
|
+
},
|
|
69
|
+
title: t('danger.clear.confirm'),
|
|
70
|
+
});
|
|
48
71
|
}, []);
|
|
49
72
|
|
|
50
73
|
const theme: SettingItemGroup = useMemo(
|
|
@@ -240,18 +263,9 @@ const SettingForm = memo(() => {
|
|
|
240
263
|
},
|
|
241
264
|
{
|
|
242
265
|
children: (
|
|
243
|
-
<
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
okButtonProps={{ danger: true }}
|
|
247
|
-
okText={t('ok', { ns: 'common' })}
|
|
248
|
-
onConfirm={handleReset}
|
|
249
|
-
title={t('danger.reset.confirm')}
|
|
250
|
-
>
|
|
251
|
-
<Button danger type="primary">
|
|
252
|
-
{t('danger.reset.action')}
|
|
253
|
-
</Button>
|
|
254
|
-
</Popconfirm>
|
|
266
|
+
<Button danger onClick={handleReset} type="primary">
|
|
267
|
+
{t('danger.reset.action')}
|
|
268
|
+
</Button>
|
|
255
269
|
),
|
|
256
270
|
desc: t('danger.reset.title'),
|
|
257
271
|
label: t('danger.reset.desc'),
|
|
@@ -259,18 +273,9 @@ const SettingForm = memo(() => {
|
|
|
259
273
|
},
|
|
260
274
|
{
|
|
261
275
|
children: (
|
|
262
|
-
<
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
okButtonProps={{ danger: true }}
|
|
266
|
-
okText={t('ok', { ns: 'common' })}
|
|
267
|
-
onConfirm={handleClear}
|
|
268
|
-
title={t('danger.clear.confirm')}
|
|
269
|
-
>
|
|
270
|
-
<Button danger type="primary">
|
|
271
|
-
{t('danger.clear.action')}
|
|
272
|
-
</Button>
|
|
273
|
-
</Popconfirm>
|
|
276
|
+
<Button danger onClick={handleClear} type="primary">
|
|
277
|
+
{t('danger.clear.action')}
|
|
278
|
+
</Button>
|
|
274
279
|
),
|
|
275
280
|
desc: t('danger.clear.title'),
|
|
276
281
|
label: t('danger.clear.desc'),
|
|
@@ -5,6 +5,7 @@ import { Flexbox } from 'react-layout-kit';
|
|
|
5
5
|
|
|
6
6
|
import SideBar from '@/features/SideBar';
|
|
7
7
|
import { createI18nNext } from '@/locales/create';
|
|
8
|
+
import { useSettings } from '@/store/settings';
|
|
8
9
|
import { genSiteHeadTitle } from '@/utils/genSiteHeadTitle';
|
|
9
10
|
|
|
10
11
|
const initI18n = createI18nNext('setting');
|
|
@@ -17,6 +18,13 @@ const SettingLayout = memo<{ children: ReactNode }>(({ children }) => {
|
|
|
17
18
|
initI18n.finally();
|
|
18
19
|
}, []);
|
|
19
20
|
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
const hasRehydrated = useSettings.persist.hasHydrated();
|
|
23
|
+
if (hasRehydrated) {
|
|
24
|
+
useSettings.setState({ sidebarKey: 'setting' });
|
|
25
|
+
}
|
|
26
|
+
}, []);
|
|
27
|
+
|
|
20
28
|
return (
|
|
21
29
|
<>
|
|
22
30
|
<Head>
|
|
@@ -1,46 +1,53 @@
|
|
|
1
|
+
import { produce } from 'immer';
|
|
1
2
|
import { merge } from 'lodash-es';
|
|
2
3
|
import Router from 'next/router';
|
|
3
4
|
import { StateCreator } from 'zustand/vanilla';
|
|
4
5
|
|
|
5
6
|
import { SessionStore, initLobeSession } from '@/store/session';
|
|
6
|
-
import { LobeAgentSession } from '@/types/session';
|
|
7
|
+
import { LobeAgentSession, LobeSessions } from '@/types/session';
|
|
7
8
|
import { uuid } from '@/utils/uuid';
|
|
8
9
|
|
|
9
10
|
import { SessionDispatch, sessionsReducer } from './reducers/session';
|
|
10
11
|
|
|
11
12
|
export interface SessionAction {
|
|
12
13
|
activeSession: (sessionId: string) => void;
|
|
14
|
+
clearSessions: () => void;
|
|
15
|
+
|
|
13
16
|
/**
|
|
14
17
|
* @title 添加会话
|
|
15
18
|
* @param session - 会话信息
|
|
16
19
|
* @returns void
|
|
17
20
|
*/
|
|
18
21
|
createSession: () => Promise<string>;
|
|
19
|
-
|
|
20
22
|
/**
|
|
21
|
-
*
|
|
23
|
+
* 变更session
|
|
22
24
|
* @param payload - 聊天记录
|
|
23
25
|
*/
|
|
24
26
|
dispatchSession: (payload: SessionDispatch) => void;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 导入会话
|
|
30
|
+
* @param sessions
|
|
31
|
+
*/
|
|
32
|
+
importSessions: (sessions: LobeSessions) => void;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 生成压缩后的消息
|
|
36
|
+
* @returns 压缩后的消息
|
|
37
|
+
*/
|
|
38
|
+
// genShareUrl: () => string;
|
|
25
39
|
/**
|
|
26
40
|
* @title 删除会话
|
|
27
41
|
* @param index - 会话索引
|
|
28
42
|
* @returns void
|
|
29
43
|
*/
|
|
30
44
|
removeSession: (sessionId: string) => void;
|
|
31
|
-
|
|
32
45
|
/**
|
|
33
46
|
* @title 切换会话
|
|
34
47
|
* @param sessionId - 会话索引
|
|
35
48
|
* @returns void
|
|
36
49
|
*/
|
|
37
50
|
switchSession: (sessionId?: string | 'new') => Promise<void>;
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* 生成压缩后的消息
|
|
41
|
-
* @returns 压缩后的消息
|
|
42
|
-
*/
|
|
43
|
-
// genShareUrl: () => string;
|
|
44
51
|
}
|
|
45
52
|
|
|
46
53
|
export const createSessionSlice: StateCreator<
|
|
@@ -53,6 +60,10 @@ export const createSessionSlice: StateCreator<
|
|
|
53
60
|
set({ activeId: sessionId });
|
|
54
61
|
},
|
|
55
62
|
|
|
63
|
+
clearSessions: () => {
|
|
64
|
+
set({ sessions: {} });
|
|
65
|
+
},
|
|
66
|
+
|
|
56
67
|
createSession: async () => {
|
|
57
68
|
const { dispatchSession, switchSession } = get();
|
|
58
69
|
|
|
@@ -80,6 +91,19 @@ export const createSessionSlice: StateCreator<
|
|
|
80
91
|
});
|
|
81
92
|
},
|
|
82
93
|
|
|
94
|
+
importSessions: (importSessions) => {
|
|
95
|
+
const { sessions } = get();
|
|
96
|
+
set({
|
|
97
|
+
sessions: produce(sessions, (draft) => {
|
|
98
|
+
for (const [id, session] of Object.entries(importSessions)) {
|
|
99
|
+
// 如果已经存在,则跳过
|
|
100
|
+
if (draft[id]) continue;
|
|
101
|
+
|
|
102
|
+
draft[id] = session;
|
|
103
|
+
}
|
|
104
|
+
}),
|
|
105
|
+
});
|
|
106
|
+
},
|
|
83
107
|
removeSession: (sessionId) => {
|
|
84
108
|
get().dispatchSession({ id: sessionId, type: 'removeSession' });
|
|
85
109
|
|
|
@@ -98,7 +122,6 @@ export const createSessionSlice: StateCreator<
|
|
|
98
122
|
// 新会话
|
|
99
123
|
await Router.push(`/chat/${sessionId}`);
|
|
100
124
|
},
|
|
101
|
-
|
|
102
125
|
// genShareUrl: () => {
|
|
103
126
|
// const session = sessionSelectors.currentSession(get());
|
|
104
127
|
// if (!session) return '';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { transform } from 'lodash-es';
|
|
2
|
+
|
|
3
|
+
import { SessionStore } from '@/store/session';
|
|
4
|
+
import { LobeAgentSession, LobeSessions } from '@/types/session';
|
|
5
|
+
|
|
6
|
+
import { getSessionById } from './list';
|
|
7
|
+
|
|
8
|
+
export const exportSessions = (s: SessionStore) => s.sessions;
|
|
9
|
+
|
|
10
|
+
// 排除 chats
|
|
11
|
+
export const exportAgents = (s: SessionStore) => {
|
|
12
|
+
return transform(s.sessions, (result: LobeSessions, value, key) => {
|
|
13
|
+
// 移除 chats 和 topics
|
|
14
|
+
result[key] = { ...value, chats: {}, topics: {} } as LobeAgentSession;
|
|
15
|
+
});
|
|
16
|
+
};
|
|
17
|
+
// 排除 chats
|
|
18
|
+
export const getExportAgent =
|
|
19
|
+
(id: string) =>
|
|
20
|
+
(s: SessionStore): LobeAgentSession => {
|
|
21
|
+
const session = getSessionById(id)(s);
|
|
22
|
+
return { ...session, chats: {}, topics: {} };
|
|
23
|
+
};
|
|
@@ -1,14 +1,18 @@
|
|
|
1
|
+
import { exportAgents, exportSessions, getExportAgent } from './export';
|
|
1
2
|
import {
|
|
3
|
+
currentSession,
|
|
2
4
|
currentSessionSafe,
|
|
3
|
-
currentSessionSel,
|
|
4
5
|
getSessionById,
|
|
5
6
|
getSessionMetaById,
|
|
6
7
|
sessionList,
|
|
7
8
|
} from './list';
|
|
8
9
|
|
|
9
10
|
export const sessionSelectors = {
|
|
10
|
-
currentSession
|
|
11
|
+
currentSession,
|
|
11
12
|
currentSessionSafe,
|
|
13
|
+
exportAgents,
|
|
14
|
+
exportSessions,
|
|
15
|
+
getExportAgent,
|
|
12
16
|
getSessionById,
|
|
13
17
|
getSessionMetaById,
|
|
14
18
|
sessionList,
|
|
@@ -5,13 +5,14 @@ import { filterWithKeywords } from '@/utils/filter';
|
|
|
5
5
|
|
|
6
6
|
import { initLobeSession } from '../initialState';
|
|
7
7
|
|
|
8
|
-
export const
|
|
8
|
+
export const currentSession = (s: SessionStore): LobeAgentSession | undefined => {
|
|
9
9
|
if (!s.activeId) return;
|
|
10
10
|
|
|
11
11
|
return s.sessions[s.activeId];
|
|
12
12
|
};
|
|
13
|
+
|
|
13
14
|
export const currentSessionSafe = (s: SessionStore): LobeAgentSession => {
|
|
14
|
-
return
|
|
15
|
+
return currentSession(s) || initLobeSession;
|
|
15
16
|
};
|
|
16
17
|
|
|
17
18
|
export const sessionList = (s: SessionStore) => {
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { ThemeMode } from 'antd-style';
|
|
2
2
|
import type { StateCreator } from 'zustand/vanilla';
|
|
3
3
|
|
|
4
|
-
import type {
|
|
4
|
+
import type { GlobalSettings } from '@/types/settings';
|
|
5
5
|
|
|
6
6
|
import type { SidebarTabKey } from './initialState';
|
|
7
|
-
import { DEFAULT_SETTINGS,
|
|
7
|
+
import { DEFAULT_SETTINGS, SettingsState } from './initialState';
|
|
8
8
|
import type { SettingsStore } from './store';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* 设置操作
|
|
12
12
|
*/
|
|
13
13
|
export interface SettingsAction {
|
|
14
|
+
importSettings: (settings: GlobalSettings) => void;
|
|
14
15
|
/**
|
|
15
16
|
* 重置设置
|
|
16
17
|
*/
|
|
@@ -19,12 +20,12 @@ export interface SettingsAction {
|
|
|
19
20
|
* 设置全局设置
|
|
20
21
|
* @param settings - 全局设置
|
|
21
22
|
*/
|
|
22
|
-
setGlobalSettings: (settings:
|
|
23
|
+
setGlobalSettings: (settings: SettingsState) => void;
|
|
23
24
|
/**
|
|
24
25
|
* 设置部分配置设置
|
|
25
26
|
* @param settings - 部分配置设置
|
|
26
27
|
*/
|
|
27
|
-
setSettings: (settings: Partial<
|
|
28
|
+
setSettings: (settings: Partial<GlobalSettings>) => void;
|
|
28
29
|
/**
|
|
29
30
|
* 设置主题模式
|
|
30
31
|
* @param themeMode - 主题模式
|
|
@@ -43,6 +44,17 @@ export const createSettings: StateCreator<
|
|
|
43
44
|
[],
|
|
44
45
|
SettingsAction
|
|
45
46
|
> = (set, get) => ({
|
|
47
|
+
importSettings: (importSettings) => {
|
|
48
|
+
const { setSettings } = get();
|
|
49
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
50
|
+
const { OPENAI_API_KEY: _, password: __, ...settings } = importSettings;
|
|
51
|
+
|
|
52
|
+
setSettings({
|
|
53
|
+
...settings,
|
|
54
|
+
// 如果用户存在用户头像,那么不做导入
|
|
55
|
+
avatar: get().settings.avatar ?? settings.avatar,
|
|
56
|
+
});
|
|
57
|
+
},
|
|
46
58
|
resetSettings: () => {
|
|
47
59
|
set({ settings: DEFAULT_SETTINGS });
|
|
48
60
|
},
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type { ConfigSettings } from '@/types/exportConfig';
|
|
2
1
|
import { LanguageModel } from '@/types/llm';
|
|
2
|
+
import type { GlobalSettings } from '@/types/settings';
|
|
3
3
|
|
|
4
|
-
export type SidebarTabKey = 'chat' | 'market';
|
|
4
|
+
export type SidebarTabKey = 'chat' | 'market' | 'setting';
|
|
5
5
|
|
|
6
|
-
export const DEFAULT_SETTINGS:
|
|
6
|
+
export const DEFAULT_SETTINGS: GlobalSettings = {
|
|
7
7
|
OPENAI_API_KEY: '',
|
|
8
8
|
avatar: '',
|
|
9
9
|
compressThreshold: 24,
|
|
@@ -26,15 +26,15 @@ export const DEFAULT_SETTINGS: ConfigSettings = {
|
|
|
26
26
|
topP: 1,
|
|
27
27
|
};
|
|
28
28
|
|
|
29
|
-
export interface
|
|
29
|
+
export interface SettingsState {
|
|
30
30
|
inputHeight: number;
|
|
31
31
|
sessionExpandable?: boolean;
|
|
32
32
|
sessionsWidth: number;
|
|
33
|
-
settings:
|
|
33
|
+
settings: GlobalSettings;
|
|
34
34
|
sidebarKey: SidebarTabKey;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
export const initialState:
|
|
37
|
+
export const initialState: SettingsState = {
|
|
38
38
|
inputHeight: 200,
|
|
39
39
|
sessionExpandable: true,
|
|
40
40
|
sessionsWidth: 320,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { defaults } from 'lodash-es';
|
|
2
2
|
|
|
3
3
|
import { DEFAULT_SETTINGS } from '@/store/settings/initialState';
|
|
4
|
+
import { GlobalSettings } from '@/types/settings';
|
|
4
5
|
|
|
5
6
|
import { SettingsStore } from './store';
|
|
6
7
|
|
|
@@ -11,7 +12,16 @@ const selecThemeMode = (s: SettingsStore) => ({
|
|
|
11
12
|
themeMode: s.settings.themeMode,
|
|
12
13
|
});
|
|
13
14
|
|
|
15
|
+
export const exportSettings = (s: SettingsStore) => {
|
|
16
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
17
|
+
const { OPENAI_API_KEY: _, password: __, ...settings } = s.settings;
|
|
18
|
+
|
|
19
|
+
return settings as GlobalSettings;
|
|
20
|
+
};
|
|
21
|
+
|
|
14
22
|
export const settingsSelectors = {
|
|
15
23
|
currentSettings,
|
|
24
|
+
exportSettings,
|
|
25
|
+
|
|
16
26
|
selecThemeMode,
|
|
17
27
|
};
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { StateCreator } from 'zustand/vanilla';
|
|
2
2
|
|
|
3
3
|
import { type SettingsAction, createSettings } from './action';
|
|
4
|
-
import { type
|
|
4
|
+
import { type SettingsState, initialState } from './initialState';
|
|
5
5
|
|
|
6
|
-
export type SettingsStore = SettingsAction &
|
|
6
|
+
export type SettingsStore = SettingsAction & SettingsState;
|
|
7
7
|
|
|
8
8
|
export const createStore: StateCreator<SettingsStore, [['zustand/devtools', never]]> = (
|
|
9
9
|
...parameters
|
|
@@ -1,46 +1,61 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import { LobeSessions } from '@/types/session';
|
|
2
|
+
import { GlobalSettings } from '@/types/settings';
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
// 存在4种导出方式
|
|
5
|
+
export type ExportType = 'agents' | 'sessions' | 'settings' | 'all';
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
*/
|
|
11
|
-
export interface ConfigSettings {
|
|
12
|
-
OPENAI_API_KEY: string;
|
|
13
|
-
avatar: string;
|
|
14
|
-
compressThreshold: number;
|
|
15
|
-
enableCompressThreshold: boolean;
|
|
16
|
-
enableHistoryCount: boolean;
|
|
17
|
-
enableMaxTokens: boolean;
|
|
18
|
-
endpoint: string;
|
|
19
|
-
fontSize: number;
|
|
20
|
-
frequencyPenalty: number;
|
|
21
|
-
historyCount: number;
|
|
22
|
-
language: Locales;
|
|
23
|
-
maxTokens: number;
|
|
24
|
-
model: LanguageModel;
|
|
25
|
-
neutralColor: NeutralColors | '';
|
|
26
|
-
password: string;
|
|
27
|
-
presencePenalty: number;
|
|
28
|
-
primaryColor: PrimaryColors | '';
|
|
29
|
-
temperature: number;
|
|
30
|
-
themeMode: ThemeMode;
|
|
31
|
-
topP: number;
|
|
7
|
+
// 4种方式对应的 state
|
|
8
|
+
export interface ConfigStateAll {
|
|
9
|
+
sessions: LobeSessions;
|
|
10
|
+
settings: GlobalSettings;
|
|
32
11
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
export interface
|
|
37
|
-
|
|
12
|
+
export interface ConfigStateSettings {
|
|
13
|
+
settings: GlobalSettings;
|
|
14
|
+
}
|
|
15
|
+
export interface ConfigStateSessions {
|
|
16
|
+
sessions: LobeSessions;
|
|
38
17
|
}
|
|
39
18
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
*/
|
|
19
|
+
// 4种方式对应的 file
|
|
20
|
+
export interface ConfigFileSettings {
|
|
21
|
+
exportType: 'settings';
|
|
22
|
+
state: ConfigStateSettings;
|
|
45
23
|
version: number;
|
|
46
24
|
}
|
|
25
|
+
export interface ConfigFileSessions {
|
|
26
|
+
exportType: 'sessions';
|
|
27
|
+
state: ConfigStateSessions;
|
|
28
|
+
version: number;
|
|
29
|
+
}
|
|
30
|
+
export interface ConfigFileAgents {
|
|
31
|
+
exportType: 'agents';
|
|
32
|
+
state: ConfigStateSessions;
|
|
33
|
+
version: number;
|
|
34
|
+
}
|
|
35
|
+
export interface ConfigFileAll {
|
|
36
|
+
exportType: 'all';
|
|
37
|
+
state: ConfigStateAll;
|
|
38
|
+
version: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type ConfigFile = ConfigFileSettings | ConfigFileSessions | ConfigFileAll | ConfigFileAgents;
|
|
42
|
+
|
|
43
|
+
// 用于 map 收集类型的 map
|
|
44
|
+
export interface ConfigModelMap {
|
|
45
|
+
agents: {
|
|
46
|
+
file: ConfigFileAgents;
|
|
47
|
+
state: ConfigStateSessions;
|
|
48
|
+
};
|
|
49
|
+
all: {
|
|
50
|
+
file: ConfigFileAll;
|
|
51
|
+
state: ConfigStateAll;
|
|
52
|
+
};
|
|
53
|
+
sessions: {
|
|
54
|
+
file: ConfigFileSessions;
|
|
55
|
+
state: ConfigStateSessions;
|
|
56
|
+
};
|
|
57
|
+
settings: {
|
|
58
|
+
file: ConfigFileSettings;
|
|
59
|
+
state: ConfigStateSettings;
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { NeutralColors, PrimaryColors } from '@lobehub/ui';
|
|
2
|
+
import { ThemeMode } from 'antd-style';
|
|
3
|
+
|
|
4
|
+
import { Locales } from '@/locales/resources';
|
|
5
|
+
import { LanguageModel } from '@/types/llm';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 配置设置
|
|
9
|
+
*/
|
|
10
|
+
export interface GlobalSettings {
|
|
11
|
+
OPENAI_API_KEY: string;
|
|
12
|
+
avatar: string;
|
|
13
|
+
compressThreshold: number;
|
|
14
|
+
enableCompressThreshold: boolean;
|
|
15
|
+
enableHistoryCount: boolean;
|
|
16
|
+
enableMaxTokens: boolean;
|
|
17
|
+
endpoint: string;
|
|
18
|
+
fontSize: number;
|
|
19
|
+
frequencyPenalty: number;
|
|
20
|
+
historyCount: number;
|
|
21
|
+
language: Locales;
|
|
22
|
+
maxTokens: number;
|
|
23
|
+
model: LanguageModel;
|
|
24
|
+
neutralColor: NeutralColors | '';
|
|
25
|
+
password: string;
|
|
26
|
+
presencePenalty: number;
|
|
27
|
+
primaryColor: PrimaryColors | '';
|
|
28
|
+
temperature: number;
|
|
29
|
+
themeMode: ThemeMode;
|
|
30
|
+
topP: number;
|
|
31
|
+
}
|
|
32
|
+
export type ConfigKeys = keyof GlobalSettings;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { notification } from 'antd';
|
|
2
|
+
|
|
3
|
+
import { CURRENT_CONFIG_VERSION, Migration } from '@/migrations';
|
|
4
|
+
import {
|
|
5
|
+
ConfigFile,
|
|
6
|
+
ConfigFileAgents,
|
|
7
|
+
ConfigFileAll,
|
|
8
|
+
ConfigFileSessions,
|
|
9
|
+
ConfigFileSettings,
|
|
10
|
+
ConfigModelMap,
|
|
11
|
+
ExportType,
|
|
12
|
+
} from '@/types/exportConfig';
|
|
13
|
+
|
|
14
|
+
export const exportConfigFile = (config: object, fileName?: string) => {
|
|
15
|
+
const file = `LobeChat-${fileName || '-config'}-v${CURRENT_CONFIG_VERSION}.json`;
|
|
16
|
+
|
|
17
|
+
// 创建一个 Blob 对象
|
|
18
|
+
const blob = new Blob([JSON.stringify(config)], { type: 'application/json' });
|
|
19
|
+
|
|
20
|
+
// 创建一个 URL 对象,用于下载
|
|
21
|
+
const url = URL.createObjectURL(blob);
|
|
22
|
+
|
|
23
|
+
// 创建一个 <a> 元素,设置下载链接和文件名
|
|
24
|
+
const a = document.createElement('a');
|
|
25
|
+
a.href = url;
|
|
26
|
+
a.download = file;
|
|
27
|
+
|
|
28
|
+
// 触发 <a> 元素的点击事件,开始下载
|
|
29
|
+
document.body.append(a);
|
|
30
|
+
a.click();
|
|
31
|
+
|
|
32
|
+
// 下载完成后,清除 URL 对象
|
|
33
|
+
URL.revokeObjectURL(url);
|
|
34
|
+
a.remove();
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const importConfigFile = (info: any, onConfigImport: (config: ConfigFile) => void) => {
|
|
38
|
+
const reader = new FileReader();
|
|
39
|
+
//读取完文件之后的回调函数
|
|
40
|
+
reader.onloadend = function (evt) {
|
|
41
|
+
const fileString = evt.target?.result;
|
|
42
|
+
const fileJson = fileString as string;
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const config = JSON.parse(fileJson);
|
|
46
|
+
const { state } = Migration.migrate(config);
|
|
47
|
+
|
|
48
|
+
onConfigImport({ ...config, state });
|
|
49
|
+
} catch (error) {
|
|
50
|
+
notification.error({
|
|
51
|
+
description: `出错原因: ${(error as Error).message}`,
|
|
52
|
+
message: '导入失败',
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
//@ts-ignore file 类型不明确
|
|
57
|
+
reader.readAsText(info.file.originFileObj, 'utf8');
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
type CreateConfigFileState<T extends ExportType> = ConfigModelMap[T]['state'];
|
|
61
|
+
|
|
62
|
+
type CreateConfigFile<T extends ExportType> = ConfigModelMap[T]['file'];
|
|
63
|
+
|
|
64
|
+
export const createConfigFile = <T extends ExportType>(
|
|
65
|
+
type: T,
|
|
66
|
+
state: CreateConfigFileState<T>,
|
|
67
|
+
): CreateConfigFile<T> => {
|
|
68
|
+
switch (type) {
|
|
69
|
+
case 'agents': {
|
|
70
|
+
return {
|
|
71
|
+
exportType: 'agents',
|
|
72
|
+
state,
|
|
73
|
+
version: Migration.targetVersion,
|
|
74
|
+
} as ConfigFileAgents;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
case 'sessions': {
|
|
78
|
+
return {
|
|
79
|
+
exportType: 'sessions',
|
|
80
|
+
state,
|
|
81
|
+
version: Migration.targetVersion,
|
|
82
|
+
} as ConfigFileSessions;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
case 'settings': {
|
|
86
|
+
return {
|
|
87
|
+
exportType: 'settings',
|
|
88
|
+
state,
|
|
89
|
+
version: Migration.targetVersion,
|
|
90
|
+
} as ConfigFileSettings;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
case 'all': {
|
|
94
|
+
return {
|
|
95
|
+
exportType: 'all',
|
|
96
|
+
state,
|
|
97
|
+
version: Migration.targetVersion,
|
|
98
|
+
} as ConfigFileAll;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
throw new Error('缺少正确的导出类型,请检查实现...');
|
|
103
|
+
};
|