@lobehub/chat 0.148.8 → 0.148.9
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 +25 -0
- package/package.json +2 -2
- package/src/app/chat/features/SessionListContent/CollapseGroup/Actions.tsx +11 -5
- package/src/app/chat/features/SessionListContent/DefaultMode.tsx +6 -4
- package/src/app/chat/features/SessionListContent/List/AddButton.tsx +4 -9
- package/src/app/chat/features/SessionListContent/Modals/ConfigGroupModal/GroupItem.tsx +21 -16
- package/src/app/chat/features/SessionListContent/Modals/ConfigGroupModal/index.tsx +8 -2
- package/src/app/chat/features/SessionListContent/Modals/CreateGroupModal.tsx +9 -1
- package/src/app/chat/features/SessionListContent/Modals/RenameGroupModal.tsx +12 -3
- package/src/app/home/Redirect.tsx +2 -2
- package/src/app/welcome/(desktop)/features/Footer.tsx +1 -1
- package/src/app/welcome/features/Banner/index.tsx +5 -9
- package/src/database/client/models/__tests__/session.test.ts +2 -2
- package/src/database/client/models/session.ts +4 -14
- package/src/locales/default/chat.ts +6 -2
- package/src/locales/default/welcome.ts +1 -0
- package/src/services/config.ts +5 -21
- package/src/services/message/client.ts +10 -0
- package/src/services/message/index.ts +1 -13
- package/src/services/message/type.ts +3 -0
- package/src/services/session/client.ts +3 -0
- package/src/services/session/type.ts +8 -2
- package/src/services/topic/client.ts +1 -1
- package/src/services/topic/type.ts +3 -2
- package/src/store/session/slices/session/action.ts +74 -76
- package/src/store/session/slices/session/initialState.ts +8 -1
- package/src/store/session/slices/session/reducers.test.ts +79 -0
- package/src/store/session/slices/session/reducers.ts +61 -0
- package/src/store/session/slices/sessionGroup/action.test.ts +9 -0
- package/src/store/session/slices/sessionGroup/action.ts +25 -6
- package/src/store/session/slices/sessionGroup/reducer.test.ts +86 -0
- package/src/store/session/slices/sessionGroup/reducer.ts +56 -0
- package/src/store/session/slices/sessionGroup/selectors.ts +1 -5
- package/src/types/service.ts +7 -0
- package/src/types/session.ts +5 -7
- package/src/utils/uuid.ts +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
### [Version 0.148.9](https://github.com/lobehub/lobe-chat/compare/v0.148.8...v0.148.9)
|
|
6
|
+
|
|
7
|
+
<sup>Released on **2024-04-23**</sup>
|
|
8
|
+
|
|
9
|
+
#### ♻ Code Refactoring
|
|
10
|
+
|
|
11
|
+
- **misc**: Refactor for session server mode.
|
|
12
|
+
|
|
13
|
+
<br/>
|
|
14
|
+
|
|
15
|
+
<details>
|
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
17
|
+
|
|
18
|
+
#### Code refactoring
|
|
19
|
+
|
|
20
|
+
- **misc**: Refactor for session server mode, closes [#2163](https://github.com/lobehub/lobe-chat/issues/2163) ([e012597](https://github.com/lobehub/lobe-chat/commit/e012597))
|
|
21
|
+
|
|
22
|
+
</details>
|
|
23
|
+
|
|
24
|
+
<div align="right">
|
|
25
|
+
|
|
26
|
+
[](#readme-top)
|
|
27
|
+
|
|
28
|
+
</div>
|
|
29
|
+
|
|
5
30
|
### [Version 0.148.8](https://github.com/lobehub/lobe-chat/compare/v0.148.7...v0.148.8)
|
|
6
31
|
|
|
7
32
|
<sup>Released on **2024-04-23**</sup>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobehub/chat",
|
|
3
|
-
"version": "0.148.
|
|
3
|
+
"version": "0.148.9",
|
|
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",
|
|
@@ -173,7 +173,7 @@
|
|
|
173
173
|
"@next/eslint-plugin-next": "^14.2.2",
|
|
174
174
|
"@peculiar/webcrypto": "^1.4.6",
|
|
175
175
|
"@testing-library/jest-dom": "^6.4.2",
|
|
176
|
-
"@testing-library/react": "^15.0.
|
|
176
|
+
"@testing-library/react": "^15.0.4",
|
|
177
177
|
"@types/chroma-js": "^2.4.4",
|
|
178
178
|
"@types/debug": "^4.1.12",
|
|
179
179
|
"@types/diff": "^5.2.0",
|
|
@@ -27,7 +27,7 @@ const Actions = memo<ActionsProps>(
|
|
|
27
27
|
({ id, openRenameModal, openConfigModal, onOpenChange, isCustomGroup, isPinned }) => {
|
|
28
28
|
const { t } = useTranslation('chat');
|
|
29
29
|
const { styles } = useStyles();
|
|
30
|
-
const { modal } = App.useApp();
|
|
30
|
+
const { modal, message } = App.useApp();
|
|
31
31
|
|
|
32
32
|
const [createSession, removeSessionGroup] = useSessionStore((s) => [
|
|
33
33
|
s.createSession,
|
|
@@ -48,9 +48,15 @@ const Actions = memo<ActionsProps>(
|
|
|
48
48
|
icon: <Icon icon={Plus} />,
|
|
49
49
|
key: 'newAgent',
|
|
50
50
|
label: t('newAgent'),
|
|
51
|
-
onClick: ({ domEvent }) => {
|
|
51
|
+
onClick: async ({ domEvent }) => {
|
|
52
52
|
domEvent.stopPropagation();
|
|
53
|
-
|
|
53
|
+
const key = 'createNewAgentInGroup';
|
|
54
|
+
message.loading({ content: t('sessionGroup.creatingAgent'), duration: 0, key });
|
|
55
|
+
|
|
56
|
+
await createSession({ group: id, pinned: isPinned });
|
|
57
|
+
|
|
58
|
+
message.destroy(key);
|
|
59
|
+
message.success({ content: t('sessionGroup.createAgentSuccess') });
|
|
54
60
|
},
|
|
55
61
|
};
|
|
56
62
|
|
|
@@ -83,9 +89,9 @@ const Actions = memo<ActionsProps>(
|
|
|
83
89
|
modal.confirm({
|
|
84
90
|
centered: true,
|
|
85
91
|
okButtonProps: { danger: true },
|
|
86
|
-
onOk: () => {
|
|
92
|
+
onOk: async () => {
|
|
87
93
|
if (!id) return;
|
|
88
|
-
removeSessionGroup(id);
|
|
94
|
+
await removeSessionGroup(id);
|
|
89
95
|
},
|
|
90
96
|
rootClassName: styles.modalRoot,
|
|
91
97
|
title: t('sessionGroup.confirmRemoveGroupAlert'),
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { CollapseProps } from 'antd';
|
|
2
|
+
import isEqual from 'fast-deep-equal';
|
|
2
3
|
import { memo, useMemo, useState } from 'react';
|
|
3
4
|
import { useTranslation } from 'react-i18next';
|
|
4
5
|
|
|
5
6
|
import { useGlobalStore } from '@/store/global';
|
|
6
7
|
import { preferenceSelectors } from '@/store/global/selectors';
|
|
7
8
|
import { useSessionStore } from '@/store/session';
|
|
9
|
+
import { sessionSelectors } from '@/store/session/selectors';
|
|
8
10
|
import { SessionDefaultGroup } from '@/types/session';
|
|
9
11
|
|
|
10
12
|
import Actions from '../SessionListContent/CollapseGroup/Actions';
|
|
@@ -22,11 +24,11 @@ const SessionListContent = memo(() => {
|
|
|
22
24
|
const [configGroupModalOpen, setConfigGroupModalOpen] = useState(false);
|
|
23
25
|
|
|
24
26
|
const [useFetchSessions] = useSessionStore((s) => [s.useFetchSessions]);
|
|
25
|
-
|
|
27
|
+
useFetchSessions();
|
|
26
28
|
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
const
|
|
29
|
+
const defaultSessions = useSessionStore(sessionSelectors.defaultSessions, isEqual);
|
|
30
|
+
const customSessionGroups = useSessionStore(sessionSelectors.customSessionGroups, isEqual);
|
|
31
|
+
const pinnedSessions = useSessionStore(sessionSelectors.pinnedSessions, isEqual);
|
|
30
32
|
|
|
31
33
|
const [sessionGroupKeys, updatePreference] = useGlobalStore((s) => [
|
|
32
34
|
preferenceSelectors.sessionGroupKeys(s),
|
|
@@ -12,18 +12,13 @@ const AddButton = memo<{ groupId?: string }>(({ groupId }) => {
|
|
|
12
12
|
const { t } = useTranslation('chat');
|
|
13
13
|
const createSession = useSessionStore((s) => s.createSession);
|
|
14
14
|
|
|
15
|
-
const { mutate, isValidating } = useActionSWR('session.createSession', (
|
|
16
|
-
createSession({ group: groupId })
|
|
17
|
-
);
|
|
15
|
+
const { mutate, isValidating } = useActionSWR(['session.createSession', groupId], () => {
|
|
16
|
+
return createSession({ group: groupId });
|
|
17
|
+
});
|
|
18
18
|
|
|
19
19
|
return (
|
|
20
20
|
<Flexbox style={{ margin: '12px 16px' }}>
|
|
21
|
-
<Button
|
|
22
|
-
block
|
|
23
|
-
icon={<Icon icon={Plus} />}
|
|
24
|
-
loading={isValidating}
|
|
25
|
-
onClick={() => mutate(groupId)}
|
|
26
|
-
>
|
|
21
|
+
<Button block icon={<Icon icon={Plus} />} loading={isValidating} onClick={() => mutate()}>
|
|
27
22
|
{t('newAgent')}
|
|
28
23
|
</Button>
|
|
29
24
|
</Flexbox>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ActionIcon, EditableText, SortableList } from '@lobehub/ui';
|
|
2
|
-
import { App
|
|
2
|
+
import { App } from 'antd';
|
|
3
3
|
import { createStyles } from 'antd-style';
|
|
4
4
|
import { PencilLine, Trash } from 'lucide-react';
|
|
5
5
|
import { memo, useState } from 'react';
|
|
@@ -25,7 +25,7 @@ const useStyles = createStyles(({ css }) => ({
|
|
|
25
25
|
const GroupItem = memo<SessionGroupItem>(({ id, name }) => {
|
|
26
26
|
const { t } = useTranslation('chat');
|
|
27
27
|
const { styles } = useStyles();
|
|
28
|
-
const { message } = App.useApp();
|
|
28
|
+
const { message, modal } = App.useApp();
|
|
29
29
|
|
|
30
30
|
const [editing, setEditing] = useState(false);
|
|
31
31
|
const [updateSessionGroupName, removeSessionGroup] = useSessionStore((s) => [
|
|
@@ -40,29 +40,34 @@ const GroupItem = memo<SessionGroupItem>(({ id, name }) => {
|
|
|
40
40
|
<>
|
|
41
41
|
<span className={styles.title}>{name}</span>
|
|
42
42
|
<ActionIcon icon={PencilLine} onClick={() => setEditing(true)} size={'small'} />
|
|
43
|
-
<
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
<ActionIcon
|
|
44
|
+
icon={Trash}
|
|
45
|
+
onClick={() => {
|
|
46
|
+
modal.confirm({
|
|
47
|
+
centered: true,
|
|
48
|
+
okButtonProps: {
|
|
49
|
+
danger: true,
|
|
50
|
+
type: 'primary',
|
|
51
|
+
},
|
|
52
|
+
onOk: async () => {
|
|
53
|
+
await removeSessionGroup(id);
|
|
54
|
+
},
|
|
55
|
+
title: t('sessionGroup.confirmRemoveGroupAlert'),
|
|
56
|
+
});
|
|
48
57
|
}}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}}
|
|
52
|
-
title={t('sessionGroup.confirmRemoveGroupAlert')}
|
|
53
|
-
>
|
|
54
|
-
<ActionIcon icon={Trash} size={'small'} />
|
|
55
|
-
</Popconfirm>
|
|
58
|
+
size={'small'}
|
|
59
|
+
/>
|
|
56
60
|
</>
|
|
57
61
|
) : (
|
|
58
62
|
<EditableText
|
|
59
63
|
editing={editing}
|
|
60
|
-
onChangeEnd={(input) => {
|
|
64
|
+
onChangeEnd={async (input) => {
|
|
61
65
|
if (name !== input) {
|
|
62
66
|
if (!input) return;
|
|
63
67
|
if (input.length === 0 || input.length > 20)
|
|
64
68
|
return message.warning(t('sessionGroup.tooLong'));
|
|
65
|
-
|
|
69
|
+
|
|
70
|
+
await updateSessionGroupName(id, input);
|
|
66
71
|
message.success(t('sessionGroup.renameSuccess'));
|
|
67
72
|
}
|
|
68
73
|
setEditing(false);
|
|
@@ -3,7 +3,7 @@ import { Button } from 'antd';
|
|
|
3
3
|
import { createStyles } from 'antd-style';
|
|
4
4
|
import isEqual from 'fast-deep-equal';
|
|
5
5
|
import { Plus } from 'lucide-react';
|
|
6
|
-
import { memo } from 'react';
|
|
6
|
+
import { memo, useState } from 'react';
|
|
7
7
|
import { useTranslation } from 'react-i18next';
|
|
8
8
|
import { Flexbox } from 'react-layout-kit';
|
|
9
9
|
|
|
@@ -35,6 +35,7 @@ const ConfigGroupModal = memo<ModalProps>(({ open, onCancel }) => {
|
|
|
35
35
|
s.addSessionGroup,
|
|
36
36
|
s.updateSessionGroupSort,
|
|
37
37
|
]);
|
|
38
|
+
const [loading, setLoading] = useState(false);
|
|
38
39
|
|
|
39
40
|
return (
|
|
40
41
|
<Modal
|
|
@@ -67,7 +68,12 @@ const ConfigGroupModal = memo<ModalProps>(({ open, onCancel }) => {
|
|
|
67
68
|
<Button
|
|
68
69
|
block
|
|
69
70
|
icon={<Icon icon={Plus} />}
|
|
70
|
-
|
|
71
|
+
loading={loading}
|
|
72
|
+
onClick={async () => {
|
|
73
|
+
setLoading(true);
|
|
74
|
+
await addSessionGroup(t('sessionGroup.newGroup'));
|
|
75
|
+
setLoading(false);
|
|
76
|
+
}}
|
|
71
77
|
>
|
|
72
78
|
{t('sessionGroup.createGroup')}
|
|
73
79
|
</Button>
|
|
@@ -22,21 +22,29 @@ const CreateGroupModal = memo<CreateGroupModalProps>(
|
|
|
22
22
|
s.addSessionGroup,
|
|
23
23
|
]);
|
|
24
24
|
const [input, setInput] = useState('');
|
|
25
|
+
const [loading, setLoading] = useState(false);
|
|
25
26
|
|
|
26
27
|
return (
|
|
27
28
|
<div onClick={(e) => e.stopPropagation()}>
|
|
28
29
|
<Modal
|
|
29
30
|
allowFullscreen
|
|
30
|
-
|
|
31
|
+
destroyOnClose
|
|
32
|
+
okButtonProps={{ loading }}
|
|
33
|
+
onCancel={(e) => {
|
|
34
|
+
setInput('');
|
|
35
|
+
onCancel?.(e);
|
|
36
|
+
}}
|
|
31
37
|
onOk={async (e: MouseEvent<HTMLButtonElement>) => {
|
|
32
38
|
if (!input) return;
|
|
33
39
|
|
|
34
40
|
if (input.length === 0 || input.length > 20)
|
|
35
41
|
return message.warning(t('sessionGroup.tooLong'));
|
|
36
42
|
|
|
43
|
+
setLoading(true);
|
|
37
44
|
const groupId = await addCustomGroup(input);
|
|
38
45
|
await updateSessionGroup(id, groupId);
|
|
39
46
|
toggleExpandSessionGroup(groupId, true);
|
|
47
|
+
setLoading(false);
|
|
40
48
|
|
|
41
49
|
message.success(t('sessionGroup.createSuccess'));
|
|
42
50
|
onCancel?.(e);
|
|
@@ -18,18 +18,27 @@ const RenameGroupModal = memo<RenameGroupModalProps>(({ id, open, onCancel }) =>
|
|
|
18
18
|
const group = useSessionStore((s) => sessionGroupSelectors.getGroupById(id)(s), isEqual);
|
|
19
19
|
|
|
20
20
|
const [input, setInput] = useState<string>();
|
|
21
|
+
const [loading, setLoading] = useState(false);
|
|
21
22
|
|
|
22
23
|
const { message } = App.useApp();
|
|
23
24
|
return (
|
|
24
25
|
<Modal
|
|
25
26
|
allowFullscreen
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
destroyOnClose
|
|
28
|
+
okButtonProps={{ loading }}
|
|
29
|
+
onCancel={(e) => {
|
|
30
|
+
setInput(group?.name);
|
|
31
|
+
onCancel?.(e);
|
|
32
|
+
}}
|
|
33
|
+
onOk={async (e) => {
|
|
28
34
|
if (!input) return;
|
|
29
35
|
if (input.length === 0 || input.length > 20)
|
|
30
36
|
return message.warning(t('sessionGroup.tooLong'));
|
|
31
|
-
|
|
37
|
+
setLoading(true);
|
|
38
|
+
await updateSessionGroupName(id, input);
|
|
32
39
|
message.success(t('sessionGroup.renameSuccess'));
|
|
40
|
+
setLoading(false);
|
|
41
|
+
|
|
33
42
|
onCancel?.(e);
|
|
34
43
|
}}
|
|
35
44
|
open={open}
|
|
@@ -8,8 +8,8 @@ import { sessionService } from '@/services/session';
|
|
|
8
8
|
|
|
9
9
|
const checkHasConversation = async () => {
|
|
10
10
|
const hasMessages = await messageService.hasMessages();
|
|
11
|
-
const hasAgents = await sessionService.
|
|
12
|
-
return hasMessages || hasAgents
|
|
11
|
+
const hasAgents = await sessionService.hasSessions();
|
|
12
|
+
return hasMessages || hasAgents;
|
|
13
13
|
};
|
|
14
14
|
|
|
15
15
|
const Redirect = memo(() => {
|
|
@@ -16,7 +16,7 @@ const Footer = memo(() => {
|
|
|
16
16
|
return (
|
|
17
17
|
<Flexbox align={'center'} horizontal justify={'space-between'} style={{ padding: 16 }}>
|
|
18
18
|
<span style={{ color: theme.colorTextDescription }}>
|
|
19
|
-
©{new Date().getFullYear()} LobeHub
|
|
19
|
+
© 2023 - {new Date().getFullYear()} LobeHub, LLC
|
|
20
20
|
</span>
|
|
21
21
|
<Flexbox horizontal>
|
|
22
22
|
<ActionIcon
|
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
import { Icon } from '@lobehub/ui';
|
|
4
4
|
import { Button } from 'antd';
|
|
5
5
|
import { SendHorizonal } from 'lucide-react';
|
|
6
|
+
import Link from 'next/link';
|
|
6
7
|
import { useRouter } from 'next/navigation';
|
|
7
8
|
import { memo } from 'react';
|
|
8
9
|
import { useTranslation } from 'react-i18next';
|
|
9
10
|
import { Flexbox } from 'react-layout-kit';
|
|
10
11
|
|
|
11
|
-
import DataImporter from '@/features/DataImporter';
|
|
12
12
|
import { useGlobalStore } from '@/store/global';
|
|
13
13
|
|
|
14
14
|
import Hero from './Hero';
|
|
@@ -32,15 +32,11 @@ const Banner = memo<{ mobile?: boolean }>(({ mobile }) => {
|
|
|
32
32
|
justify={'center'}
|
|
33
33
|
width={'100%'}
|
|
34
34
|
>
|
|
35
|
-
<
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}}
|
|
39
|
-
>
|
|
40
|
-
<Button block={mobile} size={'large'}>
|
|
41
|
-
{t('button.import')}
|
|
35
|
+
<Link href={'/market'}>
|
|
36
|
+
<Button block={mobile} size={'large'} type={'default'}>
|
|
37
|
+
{t('button.market')}
|
|
42
38
|
</Button>
|
|
43
|
-
</
|
|
39
|
+
</Link>
|
|
44
40
|
<Button
|
|
45
41
|
block={mobile}
|
|
46
42
|
onClick={() => (isMobile ? router.push('/chat') : switchBackToChat())}
|
|
@@ -179,8 +179,8 @@ describe('SessionModel', () => {
|
|
|
179
179
|
await SessionModel.create('agent', sessionData);
|
|
180
180
|
|
|
181
181
|
const sessionsWithGroups = await SessionModel.queryWithGroups();
|
|
182
|
-
expect(sessionsWithGroups.
|
|
183
|
-
expect(sessionsWithGroups.
|
|
182
|
+
expect(sessionsWithGroups.sessions).toHaveLength(1);
|
|
183
|
+
expect(sessionsWithGroups.sessions[0]).toEqual(expect.objectContaining(sessionData));
|
|
184
184
|
});
|
|
185
185
|
});
|
|
186
186
|
|
|
@@ -43,21 +43,11 @@ class _SessionModel extends BaseModel {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
async queryWithGroups(): Promise<ChatSessionList> {
|
|
46
|
-
const
|
|
47
|
-
const customGroups = await this.queryByGroupIds(groups.map((item) => item.id));
|
|
48
|
-
const defaultItems = await this.querySessionsByGroupId(SessionDefaultGroup.Default);
|
|
49
|
-
const pinnedItems = await this.getPinnedSessions();
|
|
46
|
+
const sessionGroups = await SessionGroupModel.query();
|
|
50
47
|
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
customGroup: groups.map((group) => ({
|
|
55
|
-
...group,
|
|
56
|
-
children: customGroups[group.id],
|
|
57
|
-
})),
|
|
58
|
-
default: defaultItems,
|
|
59
|
-
pinned: pinnedItems,
|
|
60
|
-
};
|
|
48
|
+
const sessions = await this.query();
|
|
49
|
+
|
|
50
|
+
return { sessionGroups, sessions };
|
|
61
51
|
}
|
|
62
52
|
|
|
63
53
|
/**
|
|
@@ -50,13 +50,17 @@ export default {
|
|
|
50
50
|
sessionGroup: {
|
|
51
51
|
config: '分组管理',
|
|
52
52
|
confirmRemoveGroupAlert: '即将删除该分组,删除后该分组的助手将移动到默认列表,请确认你的操作',
|
|
53
|
+
createAgentSuccess: '助手创建成功',
|
|
53
54
|
createGroup: '添加新分组',
|
|
54
|
-
createSuccess: '
|
|
55
|
+
createSuccess: '分组创建成功',
|
|
56
|
+
creatingAgent: '助手创建中...',
|
|
55
57
|
inputPlaceholder: '请输入分组名称...',
|
|
56
58
|
moveGroup: '移动到分组',
|
|
57
59
|
newGroup: '新分组',
|
|
58
60
|
rename: '重命名分组',
|
|
59
61
|
renameSuccess: '重命名成功',
|
|
62
|
+
sortSuccess: '重新排序成功',
|
|
63
|
+
sorting: '分组排序更新中...',
|
|
60
64
|
tooLong: '分组名称长度需在 1-20 之内',
|
|
61
65
|
},
|
|
62
66
|
shareModal: {
|
|
@@ -126,6 +130,6 @@ export default {
|
|
|
126
130
|
dragDesc: '拖拽文件到这里,支持上传多个图片。按住 Shift 直接发送图片',
|
|
127
131
|
dragFileDesc: '拖拽图片和文件到这里,支持上传多个图片和文件。按住 Shift 直接发送图片或文件',
|
|
128
132
|
dragFileTitle: '上传文件',
|
|
129
|
-
dragTitle: '上传图片'
|
|
133
|
+
dragTitle: '上传图片',
|
|
130
134
|
},
|
|
131
135
|
};
|
package/src/services/config.ts
CHANGED
|
@@ -65,31 +65,15 @@ class ConfigService {
|
|
|
65
65
|
|
|
66
66
|
case 'all': {
|
|
67
67
|
await this.importSettings(config.state.settings);
|
|
68
|
-
|
|
69
|
-
const sessionGroups = await this.importSessionGroups(config.state.sessionGroups);
|
|
70
|
-
|
|
71
|
-
const [sessions, messages, topics] = await Promise.all([
|
|
72
|
-
this.importSessions(config.state.sessions),
|
|
73
|
-
this.importMessages(config.state.messages),
|
|
74
|
-
this.importTopics(config.state.topics),
|
|
75
|
-
]);
|
|
76
|
-
|
|
77
|
-
return {
|
|
78
|
-
messages: this.mapImportResult(messages),
|
|
79
|
-
sessionGroups: this.mapImportResult(sessionGroups),
|
|
80
|
-
sessions: this.mapImportResult(sessions),
|
|
81
|
-
topics: this.mapImportResult(topics),
|
|
82
|
-
};
|
|
83
68
|
}
|
|
69
|
+
// all and sessions have the same data process, so we can fall through
|
|
84
70
|
|
|
71
|
+
// eslint-disable-next-line no-fallthrough
|
|
85
72
|
case 'sessions': {
|
|
86
73
|
const sessionGroups = await this.importSessionGroups(config.state.sessionGroups);
|
|
87
|
-
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
this.importMessages(config.state.messages),
|
|
91
|
-
this.importTopics(config.state.topics),
|
|
92
|
-
]);
|
|
74
|
+
const sessions = await this.importSessions(config.state.sessions);
|
|
75
|
+
const topics = await this.importTopics(config.state.topics);
|
|
76
|
+
const messages = await this.importMessages(config.state.messages);
|
|
93
77
|
|
|
94
78
|
return {
|
|
95
79
|
messages: this.mapImportResult(messages),
|
|
@@ -62,4 +62,14 @@ export class ClientService implements IMessageService {
|
|
|
62
62
|
async removeAllMessages() {
|
|
63
63
|
return MessageModel.clearTable();
|
|
64
64
|
}
|
|
65
|
+
|
|
66
|
+
async hasMessages() {
|
|
67
|
+
const number = await this.countMessages();
|
|
68
|
+
return number > 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async messageCountToCheckTrace() {
|
|
72
|
+
const number = await this.countMessages();
|
|
73
|
+
return number >= 4;
|
|
74
|
+
}
|
|
65
75
|
}
|
|
@@ -9,16 +9,4 @@ import { ClientService } from './client';
|
|
|
9
9
|
|
|
10
10
|
export type { CreateMessageParams } from './type';
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
async hasMessages() {
|
|
14
|
-
const number = await this.countMessages();
|
|
15
|
-
return number > 0;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
async messageCountToCheckTrace() {
|
|
19
|
-
const number = await this.countMessages();
|
|
20
|
-
return number >= 4;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export const messageService = new MessageService();
|
|
12
|
+
export const messageService = new ClientService();
|
|
@@ -30,4 +30,7 @@ export interface IMessageService {
|
|
|
30
30
|
removeMessage(id: string): Promise<any>;
|
|
31
31
|
removeMessages(assistantId: string, topicId?: string): Promise<any>;
|
|
32
32
|
removeAllMessages(): Promise<any>;
|
|
33
|
+
|
|
34
|
+
hasMessages(): Promise<boolean>;
|
|
35
|
+
messageCountToCheckTrace(): Promise<boolean>;
|
|
33
36
|
}
|
|
@@ -64,6 +64,9 @@ export class ClientService implements ISessionService {
|
|
|
64
64
|
async countSessions() {
|
|
65
65
|
return SessionModel.count();
|
|
66
66
|
}
|
|
67
|
+
async hasSessions() {
|
|
68
|
+
return (await this.countSessions()) === 0;
|
|
69
|
+
}
|
|
67
70
|
|
|
68
71
|
async searchSessions(keyword: string) {
|
|
69
72
|
return SessionModel.queryByKeyword(keyword);
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
import { DeepPartial } from 'utility-types';
|
|
3
3
|
|
|
4
4
|
import { LobeAgentConfig } from '@/types/agent';
|
|
5
|
+
import { BatchTaskResult } from '@/types/service';
|
|
5
6
|
import {
|
|
6
7
|
ChatSessionList,
|
|
7
8
|
LobeAgentSession,
|
|
8
9
|
LobeSessionType,
|
|
9
10
|
LobeSessions,
|
|
11
|
+
SessionGroupId,
|
|
10
12
|
SessionGroupItem,
|
|
11
13
|
SessionGroups,
|
|
12
14
|
} from '@/types/session';
|
|
@@ -19,9 +21,13 @@ export interface ISessionService {
|
|
|
19
21
|
getGroupedSessions(): Promise<ChatSessionList>;
|
|
20
22
|
getSessionsByType(type: 'agent' | 'group' | 'all'): Promise<LobeSessions>;
|
|
21
23
|
countSessions(): Promise<number>;
|
|
24
|
+
hasSessions(): Promise<boolean>;
|
|
22
25
|
searchSessions(keyword: string): Promise<LobeSessions>;
|
|
23
26
|
|
|
24
|
-
updateSession(
|
|
27
|
+
updateSession(
|
|
28
|
+
id: string,
|
|
29
|
+
data: Partial<{ group?: SessionGroupId; pinned?: boolean }>,
|
|
30
|
+
): Promise<any>;
|
|
25
31
|
updateSessionConfig(id: string, config: DeepPartial<LobeAgentConfig>): Promise<any>;
|
|
26
32
|
|
|
27
33
|
removeSession(id: string): Promise<any>;
|
|
@@ -32,7 +38,7 @@ export interface ISessionService {
|
|
|
32
38
|
// ************************************** //
|
|
33
39
|
|
|
34
40
|
createSessionGroup(name: string, sort?: number): Promise<string>;
|
|
35
|
-
batchCreateSessionGroups(groups: SessionGroups): Promise<
|
|
41
|
+
batchCreateSessionGroups(groups: SessionGroups): Promise<BatchTaskResult>;
|
|
36
42
|
|
|
37
43
|
getSessionGroups(): Promise<SessionGroupItem[]>;
|
|
38
44
|
|
|
@@ -5,7 +5,7 @@ import { CreateTopicParams, ITopicService, QueryTopicParams } from './type';
|
|
|
5
5
|
|
|
6
6
|
export class ClientService implements ITopicService {
|
|
7
7
|
async createTopic(params: CreateTopicParams): Promise<string> {
|
|
8
|
-
const item = await TopicModel.create(params);
|
|
8
|
+
const item = await TopicModel.create(params as any);
|
|
9
9
|
|
|
10
10
|
if (!item) {
|
|
11
11
|
throw new Error('topic create Error');
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/* eslint-disable typescript-sort-keys/interface */
|
|
2
|
+
import { BatchTaskResult } from '@/types/service';
|
|
2
3
|
import { ChatTopic } from '@/types/topic';
|
|
3
4
|
|
|
4
5
|
export interface CreateTopicParams {
|
|
5
6
|
favorite?: boolean;
|
|
6
7
|
messages?: string[];
|
|
7
|
-
sessionId
|
|
8
|
+
sessionId?: string | null;
|
|
8
9
|
title: string;
|
|
9
10
|
}
|
|
10
11
|
|
|
@@ -16,7 +17,7 @@ export interface QueryTopicParams {
|
|
|
16
17
|
|
|
17
18
|
export interface ITopicService {
|
|
18
19
|
createTopic(params: CreateTopicParams): Promise<string>;
|
|
19
|
-
batchCreateTopics(importTopics: ChatTopic[]): Promise<
|
|
20
|
+
batchCreateTopics(importTopics: ChatTopic[]): Promise<BatchTaskResult>;
|
|
20
21
|
cloneTopic(id: string, newTitle?: string): Promise<string>;
|
|
21
22
|
|
|
22
23
|
getTopics(params: QueryTopicParams): Promise<ChatTopic[]>;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { t } from 'i18next';
|
|
2
|
-
import { produce } from 'immer';
|
|
3
2
|
import useSWR, { SWRResponse, mutate } from 'swr';
|
|
4
3
|
import { DeepPartial } from 'utility-types';
|
|
5
4
|
import { StateCreator } from 'zustand/vanilla';
|
|
@@ -11,12 +10,20 @@ import { sessionService } from '@/services/session';
|
|
|
11
10
|
import { useGlobalStore } from '@/store/global';
|
|
12
11
|
import { settingsSelectors } from '@/store/global/selectors';
|
|
13
12
|
import { SessionStore } from '@/store/session';
|
|
14
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
ChatSessionList,
|
|
15
|
+
LobeAgentSession,
|
|
16
|
+
LobeSessionGroups,
|
|
17
|
+
LobeSessionType,
|
|
18
|
+
LobeSessions,
|
|
19
|
+
SessionGroupId,
|
|
20
|
+
} from '@/types/session';
|
|
15
21
|
import { merge } from '@/utils/merge';
|
|
16
22
|
import { setNamespace } from '@/utils/storeDebug';
|
|
17
23
|
|
|
18
24
|
import { agentSelectors } from '../agent/selectors';
|
|
19
25
|
import { initLobeSession } from './initialState';
|
|
26
|
+
import { SessionDispatch, sessionsReducer } from './reducers';
|
|
20
27
|
import { sessionSelectors } from './selectors';
|
|
21
28
|
|
|
22
29
|
const n = setNamespace('session');
|
|
@@ -24,6 +31,7 @@ const n = setNamespace('session');
|
|
|
24
31
|
const FETCH_SESSIONS_KEY = 'fetchSessions';
|
|
25
32
|
const SEARCH_SESSIONS_KEY = 'searchSessions';
|
|
26
33
|
|
|
34
|
+
/* eslint-disable typescript-sort-keys/interface */
|
|
27
35
|
export interface SessionAction {
|
|
28
36
|
/**
|
|
29
37
|
* active the session
|
|
@@ -44,6 +52,8 @@ export interface SessionAction {
|
|
|
44
52
|
isSwitchSession?: boolean,
|
|
45
53
|
) => Promise<string>;
|
|
46
54
|
duplicateSession: (id: string) => Promise<void>;
|
|
55
|
+
updateSessionGroupId: (sessionId: string, groupId: string) => Promise<void>;
|
|
56
|
+
|
|
47
57
|
/**
|
|
48
58
|
* Pins or unpins a session.
|
|
49
59
|
*/
|
|
@@ -52,17 +62,26 @@ export interface SessionAction {
|
|
|
52
62
|
* re-fetch the data
|
|
53
63
|
*/
|
|
54
64
|
refreshSessions: (params?: SWRRefreshParams<ChatSessionList>) => Promise<void>;
|
|
55
|
-
|
|
56
65
|
/**
|
|
57
66
|
* remove session
|
|
58
67
|
* @param id - sessionId
|
|
59
68
|
*/
|
|
60
|
-
removeSession: (id: string) => void
|
|
61
|
-
|
|
62
|
-
* A custom hook that uses SWR to fetch sessions data.
|
|
63
|
-
*/
|
|
69
|
+
removeSession: (id: string) => Promise<void>;
|
|
70
|
+
|
|
64
71
|
useFetchSessions: () => SWRResponse<ChatSessionList>;
|
|
65
72
|
useSearchSessions: (keyword?: string) => SWRResponse<any>;
|
|
73
|
+
|
|
74
|
+
internal_dispatchSessions: (payload: SessionDispatch) => void;
|
|
75
|
+
internal_updateSession: (
|
|
76
|
+
id: string,
|
|
77
|
+
data: Partial<{ group?: SessionGroupId; meta?: any; pinned?: boolean }>,
|
|
78
|
+
) => Promise<void>;
|
|
79
|
+
internal_processSessions: (
|
|
80
|
+
sessions: LobeSessions,
|
|
81
|
+
customGroups: LobeSessionGroups,
|
|
82
|
+
actions?: string,
|
|
83
|
+
) => void;
|
|
84
|
+
/* eslint-enable */
|
|
66
85
|
}
|
|
67
86
|
|
|
68
87
|
export const createSessionSlice: StateCreator<
|
|
@@ -101,7 +120,6 @@ export const createSessionSlice: StateCreator<
|
|
|
101
120
|
|
|
102
121
|
return id;
|
|
103
122
|
},
|
|
104
|
-
|
|
105
123
|
duplicateSession: async (id) => {
|
|
106
124
|
const { activeSession, refreshSessions } = get();
|
|
107
125
|
const session = sessionSelectors.getSessionById(id)(get());
|
|
@@ -135,63 +153,12 @@ export const createSessionSlice: StateCreator<
|
|
|
135
153
|
activeSession(newId);
|
|
136
154
|
},
|
|
137
155
|
|
|
138
|
-
pinSession: async (
|
|
139
|
-
await get().
|
|
140
|
-
action: async () => {
|
|
141
|
-
await sessionService.updateSession(sessionId, { pinned });
|
|
142
|
-
},
|
|
143
|
-
// 乐观更新
|
|
144
|
-
optimisticData: produce((draft) => {
|
|
145
|
-
if (!draft) return;
|
|
146
|
-
|
|
147
|
-
const session = draft.all.find((i) => i.id === sessionId);
|
|
148
|
-
if (!session) return;
|
|
149
|
-
|
|
150
|
-
session.pinned = pinned;
|
|
151
|
-
|
|
152
|
-
if (pinned) {
|
|
153
|
-
draft.pinned.unshift(session);
|
|
154
|
-
|
|
155
|
-
if (session.group === 'default') {
|
|
156
|
-
const index = draft.default.findIndex((i) => i.id === sessionId);
|
|
157
|
-
draft.default.splice(index, 1);
|
|
158
|
-
} else {
|
|
159
|
-
const customGroup = draft.customGroup.find((group) => group.id === session.group);
|
|
160
|
-
|
|
161
|
-
if (customGroup) {
|
|
162
|
-
const index = customGroup.children.findIndex((i) => i.id === sessionId);
|
|
163
|
-
customGroup.children.splice(index, 1);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
} else {
|
|
167
|
-
const index = draft.pinned.findIndex((i) => i.id === sessionId);
|
|
168
|
-
if (index !== -1) {
|
|
169
|
-
draft.pinned.splice(index, 1);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (session.group === 'default') {
|
|
173
|
-
draft.default.push(session);
|
|
174
|
-
} else {
|
|
175
|
-
const customGroup = draft.customGroup.find((group) => group.id === session.group);
|
|
176
|
-
if (customGroup) {
|
|
177
|
-
customGroup.children.push(session);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}),
|
|
182
|
-
});
|
|
156
|
+
pinSession: async (id, pinned) => {
|
|
157
|
+
await get().internal_updateSession(id, { pinned });
|
|
183
158
|
},
|
|
184
159
|
|
|
185
|
-
refreshSessions: async (
|
|
186
|
-
|
|
187
|
-
// @ts-ignore
|
|
188
|
-
await mutate(FETCH_SESSIONS_KEY, params.action, {
|
|
189
|
-
optimisticData: params.optimisticData,
|
|
190
|
-
// we won't need to make the action's data go into cache ,or the display will be
|
|
191
|
-
// old -> optimistic -> undefined -> new
|
|
192
|
-
populateCache: false,
|
|
193
|
-
});
|
|
194
|
-
} else await mutate(FETCH_SESSIONS_KEY);
|
|
160
|
+
refreshSessions: async () => {
|
|
161
|
+
await mutate(FETCH_SESSIONS_KEY);
|
|
195
162
|
},
|
|
196
163
|
|
|
197
164
|
removeSession: async (sessionId) => {
|
|
@@ -204,8 +171,10 @@ export const createSessionSlice: StateCreator<
|
|
|
204
171
|
}
|
|
205
172
|
},
|
|
206
173
|
|
|
207
|
-
|
|
208
|
-
|
|
174
|
+
updateSessionGroupId: async (sessionId, group) => {
|
|
175
|
+
await get().internal_updateSession(sessionId, { group });
|
|
176
|
+
},
|
|
177
|
+
|
|
209
178
|
useFetchSessions: () =>
|
|
210
179
|
useClientDataSWR<ChatSessionList>(FETCH_SESSIONS_KEY, sessionService.getGroupedSessions, {
|
|
211
180
|
onSuccess: (data) => {
|
|
@@ -217,20 +186,14 @@ export const createSessionSlice: StateCreator<
|
|
|
217
186
|
// TODO:后续的根本解法应该是解除 inbox 和 session 的数据耦合
|
|
218
187
|
// 避免互相依赖的情况出现
|
|
219
188
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
isSessionsFirstFetchFinished: true,
|
|
225
|
-
pinnedSessions: data.pinned,
|
|
226
|
-
sessions: data.all,
|
|
227
|
-
},
|
|
228
|
-
false,
|
|
229
|
-
n('useFetchSessions/onSuccess', data),
|
|
189
|
+
get().internal_processSessions(
|
|
190
|
+
data.sessions,
|
|
191
|
+
data.sessionGroups,
|
|
192
|
+
n('useFetchSessions/updateData') as any,
|
|
230
193
|
);
|
|
194
|
+
set({ isSessionsFirstFetchFinished: true }, false, n('useFetchSessions/onSuccess', data));
|
|
231
195
|
},
|
|
232
196
|
}),
|
|
233
|
-
|
|
234
197
|
useSearchSessions: (keyword) =>
|
|
235
198
|
useSWR<LobeSessions>(
|
|
236
199
|
[SEARCH_SESSIONS_KEY, keyword],
|
|
@@ -241,4 +204,39 @@ export const createSessionSlice: StateCreator<
|
|
|
241
204
|
},
|
|
242
205
|
{ revalidateOnFocus: false, revalidateOnMount: false },
|
|
243
206
|
),
|
|
207
|
+
|
|
208
|
+
/* eslint-disable sort-keys-fix/sort-keys-fix */
|
|
209
|
+
internal_dispatchSessions: (payload) => {
|
|
210
|
+
const nextSessions = sessionsReducer(get().sessions, payload);
|
|
211
|
+
get().internal_processSessions(nextSessions, get().sessionGroups);
|
|
212
|
+
},
|
|
213
|
+
internal_updateSession: async (id, data) => {
|
|
214
|
+
get().internal_dispatchSessions({ type: 'updateSession', id, value: data });
|
|
215
|
+
|
|
216
|
+
await sessionService.updateSession(id, data);
|
|
217
|
+
await get().refreshSessions();
|
|
218
|
+
},
|
|
219
|
+
internal_processSessions: (sessions, sessionGroups) => {
|
|
220
|
+
const customGroups = sessionGroups.map((item) => ({
|
|
221
|
+
...item,
|
|
222
|
+
children: sessions.filter((i) => i.group === item.id && !i.pinned),
|
|
223
|
+
}));
|
|
224
|
+
|
|
225
|
+
const defaultGroup = sessions.filter(
|
|
226
|
+
(item) => (!item.group || item.group === 'default') && !item.pinned,
|
|
227
|
+
);
|
|
228
|
+
const pinnedGroup = sessions.filter((item) => item.pinned);
|
|
229
|
+
|
|
230
|
+
set(
|
|
231
|
+
{
|
|
232
|
+
customSessionGroups: customGroups,
|
|
233
|
+
defaultSessions: defaultGroup,
|
|
234
|
+
pinnedSessions: pinnedGroup,
|
|
235
|
+
sessionGroups,
|
|
236
|
+
sessions,
|
|
237
|
+
},
|
|
238
|
+
false,
|
|
239
|
+
n('processSessions'),
|
|
240
|
+
);
|
|
241
|
+
},
|
|
244
242
|
});
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { DEFAULT_AGENT_META } from '@/const/meta';
|
|
2
2
|
import { DEFAULT_AGENT_CONFIG } from '@/const/settings';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
CustomSessionGroup,
|
|
5
|
+
LobeAgentSession,
|
|
6
|
+
LobeSessionGroups,
|
|
7
|
+
LobeSessionType,
|
|
8
|
+
} from '@/types/session';
|
|
4
9
|
|
|
5
10
|
export const initLobeSession: LobeAgentSession = {
|
|
6
11
|
config: DEFAULT_AGENT_CONFIG,
|
|
@@ -24,6 +29,7 @@ export interface SessionState {
|
|
|
24
29
|
isSessionsFirstFetchFinished: boolean;
|
|
25
30
|
pinnedSessions: LobeAgentSession[];
|
|
26
31
|
searchKeywords: string;
|
|
32
|
+
sessionGroups: LobeSessionGroups;
|
|
27
33
|
sessionSearchKeywords?: string;
|
|
28
34
|
/**
|
|
29
35
|
* it means defaultSessions
|
|
@@ -40,5 +46,6 @@ export const initialSessionState: SessionState = {
|
|
|
40
46
|
isSessionsFirstFetchFinished: false,
|
|
41
47
|
pinnedSessions: [],
|
|
42
48
|
searchKeywords: '',
|
|
49
|
+
sessionGroups: [],
|
|
43
50
|
sessions: [],
|
|
44
51
|
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { nanoid } from 'nanoid';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { LobeAgentConfig } from '@/types/agent';
|
|
5
|
+
import { LobeAgentSession, LobeSessions } from '@/types/session';
|
|
6
|
+
|
|
7
|
+
import { SessionDispatch, sessionsReducer } from './reducers';
|
|
8
|
+
|
|
9
|
+
describe('sessionsReducer', () => {
|
|
10
|
+
const mockSession = {
|
|
11
|
+
id: nanoid(),
|
|
12
|
+
config: {
|
|
13
|
+
model: 'gpt-3.5-turbo',
|
|
14
|
+
} as any,
|
|
15
|
+
meta: {
|
|
16
|
+
title: 'Test Agent',
|
|
17
|
+
description: 'A test agent',
|
|
18
|
+
avatar: '',
|
|
19
|
+
},
|
|
20
|
+
} as any;
|
|
21
|
+
|
|
22
|
+
const initialState: LobeSessions = [];
|
|
23
|
+
|
|
24
|
+
it('should add a new session', () => {
|
|
25
|
+
const addAction: SessionDispatch = {
|
|
26
|
+
session: mockSession,
|
|
27
|
+
type: 'addSession',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const newState = sessionsReducer(initialState, addAction);
|
|
31
|
+
|
|
32
|
+
expect(newState).toHaveLength(1);
|
|
33
|
+
expect(newState[0]).toMatchObject({
|
|
34
|
+
...mockSession,
|
|
35
|
+
createdAt: expect.any(Date),
|
|
36
|
+
updatedAt: expect.any(Date),
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should remove an existing session', () => {
|
|
41
|
+
const state: LobeSessions = [mockSession];
|
|
42
|
+
const removeAction: SessionDispatch = {
|
|
43
|
+
id: mockSession.id,
|
|
44
|
+
type: 'removeSession',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const newState = sessionsReducer(state, removeAction);
|
|
48
|
+
|
|
49
|
+
expect(newState).toHaveLength(0);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should update an existing session', () => {
|
|
53
|
+
const state: LobeSessions = [mockSession];
|
|
54
|
+
const updateAction: SessionDispatch = {
|
|
55
|
+
id: mockSession.id,
|
|
56
|
+
type: 'updateSession',
|
|
57
|
+
value: { group: 'abc' },
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const newState = sessionsReducer(state, updateAction);
|
|
61
|
+
|
|
62
|
+
expect(newState).toHaveLength(1);
|
|
63
|
+
expect(newState[0]).toMatchObject({
|
|
64
|
+
...mockSession,
|
|
65
|
+
group: 'abc',
|
|
66
|
+
updatedAt: expect.any(Date),
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should return the same state for unknown action', () => {
|
|
71
|
+
const state: LobeSessions = [mockSession];
|
|
72
|
+
// @ts-ignore
|
|
73
|
+
const unknownAction: SessionDispatch = { type: 'unknown' };
|
|
74
|
+
|
|
75
|
+
const newState = sessionsReducer(state, unknownAction);
|
|
76
|
+
|
|
77
|
+
expect(newState).toEqual(state);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { produce } from 'immer';
|
|
2
|
+
|
|
3
|
+
import { LobeAgentSession, LobeSessions } from '@/types/session';
|
|
4
|
+
|
|
5
|
+
interface AddSession {
|
|
6
|
+
session: LobeAgentSession;
|
|
7
|
+
type: 'addSession';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface RemoveSession {
|
|
11
|
+
id: string;
|
|
12
|
+
type: 'removeSession';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface UpdateSession {
|
|
16
|
+
id: string;
|
|
17
|
+
type: 'updateSession';
|
|
18
|
+
value: Partial<LobeAgentSession>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type SessionDispatch = AddSession | RemoveSession | UpdateSession;
|
|
22
|
+
|
|
23
|
+
export const sessionsReducer = (state: LobeSessions, payload: SessionDispatch): LobeSessions => {
|
|
24
|
+
switch (payload.type) {
|
|
25
|
+
case 'addSession': {
|
|
26
|
+
return produce(state, (draft) => {
|
|
27
|
+
const { session } = payload;
|
|
28
|
+
if (!session) return;
|
|
29
|
+
|
|
30
|
+
// TODO: 后续将 Date 类型做个迁移,就可以移除这里的 ignore 了
|
|
31
|
+
// @ts-ignore
|
|
32
|
+
draft.unshift({ ...session, createdAt: new Date(), updatedAt: new Date() });
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
case 'removeSession': {
|
|
37
|
+
return produce(state, (draftState) => {
|
|
38
|
+
const index = draftState.findIndex((item) => item.id === payload.id);
|
|
39
|
+
if (index !== -1) {
|
|
40
|
+
draftState.splice(index, 1);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
case 'updateSession': {
|
|
46
|
+
return produce(state, (draftState) => {
|
|
47
|
+
const { value, id } = payload;
|
|
48
|
+
const index = draftState.findIndex((item) => item.id === id);
|
|
49
|
+
|
|
50
|
+
if (index !== -1) {
|
|
51
|
+
// @ts-ignore
|
|
52
|
+
draftState[index] = { ...draftState[index], ...value, updatedAt: new Date() };
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
default: {
|
|
58
|
+
return produce(state, () => {});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
@@ -8,6 +8,15 @@ afterEach(() => {
|
|
|
8
8
|
vi.restoreAllMocks();
|
|
9
9
|
});
|
|
10
10
|
|
|
11
|
+
vi.mock('@/components/AntdStaticMethods', () => ({
|
|
12
|
+
message: {
|
|
13
|
+
loading: vi.fn(),
|
|
14
|
+
success: vi.fn(),
|
|
15
|
+
error: vi.fn(),
|
|
16
|
+
destroy: vi.fn(),
|
|
17
|
+
},
|
|
18
|
+
}));
|
|
19
|
+
|
|
11
20
|
describe('createSessionGroupSlice', () => {
|
|
12
21
|
describe('addSessionGroup', () => {
|
|
13
22
|
it('should add a session group and refresh sessions', async () => {
|
|
@@ -1,17 +1,23 @@
|
|
|
1
|
+
import { t } from 'i18next';
|
|
1
2
|
import { StateCreator } from 'zustand/vanilla';
|
|
2
3
|
|
|
4
|
+
import { message } from '@/components/AntdStaticMethods';
|
|
3
5
|
import { sessionService } from '@/services/session';
|
|
4
6
|
import { SessionStore } from '@/store/session';
|
|
5
7
|
import { SessionGroupItem } from '@/types/session';
|
|
6
8
|
|
|
9
|
+
import { SessionGroupsDispatch, sessionGroupsReducer } from './reducer';
|
|
10
|
+
|
|
11
|
+
/* eslint-disable typescript-sort-keys/interface */
|
|
7
12
|
export interface SessionGroupAction {
|
|
8
13
|
addSessionGroup: (name: string) => Promise<string>;
|
|
9
14
|
clearSessionGroups: () => Promise<void>;
|
|
10
15
|
removeSessionGroup: (id: string) => Promise<void>;
|
|
11
|
-
updateSessionGroupId: (sessionId: string, groupId: string) => Promise<void>;
|
|
12
16
|
updateSessionGroupName: (id: string, name: string) => Promise<void>;
|
|
13
17
|
updateSessionGroupSort: (items: SessionGroupItem[]) => Promise<void>;
|
|
18
|
+
internal_dispatchSessionGroups: (payload: SessionGroupsDispatch) => void;
|
|
14
19
|
}
|
|
20
|
+
/* eslint-enable */
|
|
15
21
|
|
|
16
22
|
export const createSessionGroupSlice: StateCreator<
|
|
17
23
|
SessionStore,
|
|
@@ -36,11 +42,6 @@ export const createSessionGroupSlice: StateCreator<
|
|
|
36
42
|
await sessionService.removeSessionGroup(id);
|
|
37
43
|
await get().refreshSessions();
|
|
38
44
|
},
|
|
39
|
-
updateSessionGroupId: async (sessionId, group) => {
|
|
40
|
-
await sessionService.updateSession(sessionId, { group });
|
|
41
|
-
|
|
42
|
-
await get().refreshSessions();
|
|
43
|
-
},
|
|
44
45
|
|
|
45
46
|
updateSessionGroupName: async (id, name) => {
|
|
46
47
|
await sessionService.updateSessionGroup(id, { name });
|
|
@@ -48,7 +49,25 @@ export const createSessionGroupSlice: StateCreator<
|
|
|
48
49
|
},
|
|
49
50
|
updateSessionGroupSort: async (items) => {
|
|
50
51
|
const sortMap = items.map((item, index) => ({ id: item.id, sort: index }));
|
|
52
|
+
|
|
53
|
+
get().internal_dispatchSessionGroups({ sortMap, type: 'updateSessionGroupOrder' });
|
|
54
|
+
|
|
55
|
+
message.loading({
|
|
56
|
+
content: t('sessionGroup.sorting', { ns: 'chat' }),
|
|
57
|
+
duration: 0,
|
|
58
|
+
key: 'updateSessionGroupSort',
|
|
59
|
+
});
|
|
60
|
+
|
|
51
61
|
await sessionService.updateSessionGroupOrder(sortMap);
|
|
62
|
+
message.destroy('updateSessionGroupSort');
|
|
63
|
+
message.success(t('sessionGroup.sortSuccess', { ns: 'chat' }));
|
|
64
|
+
|
|
52
65
|
await get().refreshSessions();
|
|
53
66
|
},
|
|
67
|
+
|
|
68
|
+
/* eslint-disable sort-keys-fix/sort-keys-fix */
|
|
69
|
+
internal_dispatchSessionGroups: (payload) => {
|
|
70
|
+
const nextSessionGroups = sessionGroupsReducer(get().sessionGroups, payload);
|
|
71
|
+
get().internal_processSessions(get().sessions, nextSessionGroups, 'updateSessionGroups');
|
|
72
|
+
},
|
|
54
73
|
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { nanoid } from 'nanoid';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { SessionGroupItem } from '@/types/session';
|
|
5
|
+
|
|
6
|
+
import { sessionGroupsReducer } from './reducer';
|
|
7
|
+
|
|
8
|
+
describe('sessionGroupsReducer', () => {
|
|
9
|
+
const initialState: SessionGroupItem[] = [
|
|
10
|
+
{
|
|
11
|
+
id: nanoid(),
|
|
12
|
+
name: 'Group 1',
|
|
13
|
+
createdAt: Date.now(),
|
|
14
|
+
updatedAt: Date.now(),
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
id: nanoid(),
|
|
18
|
+
name: 'Group 2',
|
|
19
|
+
createdAt: Date.now(),
|
|
20
|
+
updatedAt: Date.now(),
|
|
21
|
+
sort: 1,
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
it('should add a new session group item', () => {
|
|
26
|
+
const newItem: SessionGroupItem = {
|
|
27
|
+
id: nanoid(),
|
|
28
|
+
name: 'New Group',
|
|
29
|
+
createdAt: Date.now(),
|
|
30
|
+
updatedAt: Date.now(),
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const result = sessionGroupsReducer(initialState, {
|
|
34
|
+
type: 'addSessionGroupItem',
|
|
35
|
+
item: newItem,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
expect(result).toHaveLength(3);
|
|
39
|
+
expect(result).toContainEqual(newItem);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should delete a session group item', () => {
|
|
43
|
+
const itemToDelete = initialState[0].id;
|
|
44
|
+
|
|
45
|
+
const result = sessionGroupsReducer(initialState, {
|
|
46
|
+
type: 'deleteSessionGroupItem',
|
|
47
|
+
id: itemToDelete,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
expect(result).toHaveLength(1);
|
|
51
|
+
expect(result).not.toContainEqual(expect.objectContaining({ id: itemToDelete }));
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should update a session group item', () => {
|
|
55
|
+
const itemToUpdate = initialState[0].id;
|
|
56
|
+
const updatedItem = { name: 'Updated Group' };
|
|
57
|
+
|
|
58
|
+
const result = sessionGroupsReducer(initialState, {
|
|
59
|
+
type: 'updateSessionGroupItem',
|
|
60
|
+
id: itemToUpdate,
|
|
61
|
+
item: updatedItem,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
expect(result).toHaveLength(2);
|
|
65
|
+
expect(result).toContainEqual(expect.objectContaining({ id: itemToUpdate, ...updatedItem }));
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should update session group order', () => {
|
|
69
|
+
const sortMap = [
|
|
70
|
+
{ id: initialState[1].id, sort: 0 },
|
|
71
|
+
{ id: initialState[0].id, sort: 1 },
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
const result = sessionGroupsReducer(initialState, { type: 'updateSessionGroupOrder', sortMap });
|
|
75
|
+
|
|
76
|
+
expect(result).toHaveLength(2);
|
|
77
|
+
expect(result[0].id).toBe(initialState[1].id);
|
|
78
|
+
expect(result[1].id).toBe(initialState[0].id);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should return the initial state for unknown action', () => {
|
|
82
|
+
const result = sessionGroupsReducer(initialState, { type: 'unknown' } as any);
|
|
83
|
+
|
|
84
|
+
expect(result).toEqual(initialState);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { SessionGroupItem } from '@/types/session';
|
|
2
|
+
|
|
3
|
+
export type AddSessionGroupAction = { item: SessionGroupItem; type: 'addSessionGroupItem' };
|
|
4
|
+
export type DeleteSessionGroupAction = { id: string; type: 'deleteSessionGroupItem' };
|
|
5
|
+
export type UpdateSessionGroupAction = {
|
|
6
|
+
id: string;
|
|
7
|
+
item: Partial<SessionGroupItem>;
|
|
8
|
+
type: 'updateSessionGroupItem';
|
|
9
|
+
};
|
|
10
|
+
export type UpdateSessionGroupOrderAction = {
|
|
11
|
+
sortMap: { id: string; sort?: number }[];
|
|
12
|
+
type: 'updateSessionGroupOrder';
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type SessionGroupsDispatch =
|
|
16
|
+
| AddSessionGroupAction
|
|
17
|
+
| DeleteSessionGroupAction
|
|
18
|
+
| UpdateSessionGroupAction
|
|
19
|
+
| UpdateSessionGroupOrderAction;
|
|
20
|
+
|
|
21
|
+
export const sessionGroupsReducer = (
|
|
22
|
+
state: SessionGroupItem[],
|
|
23
|
+
payload: SessionGroupsDispatch,
|
|
24
|
+
): SessionGroupItem[] => {
|
|
25
|
+
switch (payload.type) {
|
|
26
|
+
case 'addSessionGroupItem': {
|
|
27
|
+
return [...state, payload.item];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
case 'deleteSessionGroupItem': {
|
|
31
|
+
return state.filter((item) => item.id !== payload.id);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
case 'updateSessionGroupItem': {
|
|
35
|
+
return state.map((item) => {
|
|
36
|
+
if (item.id === payload.id) {
|
|
37
|
+
return { ...item, ...payload.item };
|
|
38
|
+
}
|
|
39
|
+
return item;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
case 'updateSessionGroupOrder': {
|
|
44
|
+
return state
|
|
45
|
+
.map((item) => {
|
|
46
|
+
const sort = payload.sortMap.find((i) => i.id === item.id)?.sort;
|
|
47
|
+
return { ...item, sort };
|
|
48
|
+
})
|
|
49
|
+
.sort((a, b) => (a.sort || 0) - (b.sort || 0));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
default: {
|
|
53
|
+
return state;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
};
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import { SessionStore } from '@/store/session';
|
|
2
2
|
|
|
3
|
-
const sessionGroupItems = (s: SessionStore) =>
|
|
4
|
-
s.customSessionGroups.map((group) => ({
|
|
5
|
-
id: group.id,
|
|
6
|
-
name: group.name,
|
|
7
|
-
}));
|
|
3
|
+
const sessionGroupItems = (s: SessionStore) => s.sessionGroups;
|
|
8
4
|
|
|
9
5
|
const getGroupById = (id: string) => (s: SessionStore) =>
|
|
10
6
|
sessionGroupItems(s).find((group) => group.id === id);
|
package/src/types/session.ts
CHANGED
|
@@ -44,15 +44,13 @@ export interface LobeAgentSettings {
|
|
|
44
44
|
|
|
45
45
|
export type LobeSessions = LobeAgentSession[];
|
|
46
46
|
|
|
47
|
-
export interface CustomSessionGroup {
|
|
47
|
+
export interface CustomSessionGroup extends SessionGroupItem {
|
|
48
48
|
children: LobeSessions;
|
|
49
|
-
id: SessionGroupId;
|
|
50
|
-
name: string;
|
|
51
49
|
}
|
|
52
50
|
|
|
51
|
+
export type LobeSessionGroups = SessionGroupItem[];
|
|
52
|
+
|
|
53
53
|
export interface ChatSessionList {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
default: LobeSessions;
|
|
57
|
-
pinned: LobeSessions;
|
|
54
|
+
sessionGroups: LobeSessionGroups;
|
|
55
|
+
sessions: LobeSessions;
|
|
58
56
|
}
|
package/src/utils/uuid.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// generate('1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', 16); //=> "4f90d13a42"
|
|
2
2
|
import { customAlphabet } from 'nanoid/non-secure';
|
|
3
3
|
|
|
4
|
-
export const
|
|
5
|
-
'1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',
|
|
6
|
-
|
|
7
|
-
);
|
|
4
|
+
export const createNanoId = (size = 8) =>
|
|
5
|
+
customAlphabet('1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', size);
|
|
6
|
+
|
|
7
|
+
export const nanoid = createNanoId();
|
|
8
8
|
|
|
9
9
|
export { v4 as uuid } from 'uuid';
|