@lobehub/chat 0.120.5 → 0.121.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/.github/ISSUE_TEMPLATE/2_feature_request_cn.yml +1 -1
- package/CHANGELOG.md +50 -0
- package/README.md +9 -9
- package/README.zh-CN.md +9 -9
- package/docs/Development/Feature-Development-Frontend.zh-CN.md +126 -0
- package/docs/Development/Feature-Development.zh-CN.md +664 -83
- package/docs/Development/{State-Management/State-Management-Intro.zh-CN.md → State-Management-Intro.zh-CN.md} +9 -9
- package/docs/Home.md +1 -0
- package/docs/_Sidebar.md +1 -0
- package/locales/ar/chat.json +13 -1
- package/locales/ar/common.json +1 -0
- package/locales/de-DE/chat.json +13 -1
- package/locales/de-DE/common.json +1 -0
- package/locales/en-US/chat.json +13 -1
- package/locales/en-US/common.json +1 -0
- package/locales/es-ES/chat.json +13 -1
- package/locales/es-ES/common.json +1 -0
- package/locales/fr-FR/chat.json +13 -1
- package/locales/fr-FR/common.json +1 -0
- package/locales/it-IT/chat.json +13 -1
- package/locales/it-IT/common.json +1 -0
- package/locales/ja-JP/chat.json +13 -1
- package/locales/ja-JP/common.json +1 -0
- package/locales/ko-KR/chat.json +13 -1
- package/locales/ko-KR/common.json +1 -0
- package/locales/nl-NL/chat.json +13 -1
- package/locales/nl-NL/common.json +1 -0
- package/locales/pl-PL/chat.json +13 -1
- package/locales/pl-PL/common.json +1 -0
- package/locales/pt-BR/chat.json +13 -1
- package/locales/pt-BR/common.json +1 -0
- package/locales/ru-RU/chat.json +13 -1
- package/locales/ru-RU/common.json +1 -0
- package/locales/tr-TR/chat.json +13 -1
- package/locales/tr-TR/common.json +1 -0
- package/locales/vi-VN/chat.json +13 -1
- package/locales/vi-VN/common.json +1 -0
- package/locales/zh-CN/chat.json +13 -1
- package/locales/zh-CN/common.json +1 -0
- package/locales/zh-TW/chat.json +13 -1
- package/locales/zh-TW/common.json +1 -0
- package/package.json +4 -6
- package/src/app/chat/(desktop)/features/ChatInput/Footer/index.tsx +18 -5
- package/src/app/chat/(desktop)/features/ChatInput/index.tsx +1 -1
- package/src/app/chat/features/ChatHeader/ShareButton/index.tsx +0 -1
- package/src/app/chat/features/ChatHeader/ShareButton/style.ts +0 -1
- package/src/app/chat/features/Migration/Failed.tsx +2 -0
- package/src/app/chat/features/SessionListContent/CollapseGroup/Actions.tsx +115 -0
- package/src/app/chat/features/SessionListContent/CollapseGroup/index.tsx +6 -0
- package/src/app/chat/features/SessionListContent/DefaultMode.tsx +63 -23
- package/src/app/chat/features/SessionListContent/List/AddButton.tsx +3 -3
- package/src/app/chat/features/SessionListContent/List/Item/Actions.tsx +89 -35
- package/src/app/chat/features/SessionListContent/List/Item/index.tsx +35 -15
- package/src/app/chat/features/SessionListContent/List/index.tsx +4 -2
- package/src/app/chat/features/SessionListContent/Modals/ConfigGroupModal/GroupItem.tsx +84 -0
- package/src/app/chat/features/SessionListContent/Modals/ConfigGroupModal/index.tsx +79 -0
- package/src/app/chat/features/SessionListContent/Modals/CreateGroupModal.tsx +63 -0
- package/src/app/chat/features/SessionListContent/Modals/RenameGroupModal.tsx +50 -0
- package/src/app/chat/features/SessionListContent/SearchMode.tsx +1 -1
- package/src/app/settings/common/Common.tsx +5 -1
- package/src/components/FullscreenLoading/index.tsx +1 -1
- package/src/database/core/__tests__/db-upgrade.test.ts +42 -0
- package/src/database/core/db.ts +26 -4
- package/src/database/core/schemas.ts +14 -1
- package/src/database/core/types/db.ts +2 -0
- package/src/database/models/__tests__/message.test.ts +42 -0
- package/src/database/models/__tests__/plugin.test.ts +77 -0
- package/src/database/models/__tests__/session.test.ts +202 -63
- package/src/database/models/__tests__/sessionGroup.test.ts +195 -0
- package/src/database/models/__tests__/topic.test.ts +76 -1
- package/src/database/models/message.ts +7 -8
- package/src/database/models/session.ts +103 -7
- package/src/database/models/sessionGroup.ts +76 -0
- package/src/database/schemas/session.ts +1 -0
- package/src/database/schemas/sessionGroup.ts +8 -0
- package/src/features/ChatInput/ActionBar/Clear.tsx +2 -1
- package/src/features/Conversation/Actions/Assistant.tsx +1 -1
- package/src/features/Conversation/Actions/Function.tsx +2 -2
- package/src/features/Conversation/Actions/User.tsx +1 -1
- package/src/features/Conversation/hooks/useChatListActionsBar.tsx +3 -3
- package/src/features/PluginDevModal/index.tsx +3 -0
- package/src/features/PluginStore/PluginItem/Action.tsx +2 -0
- package/src/locales/default/chat.ts +13 -1
- package/src/locales/default/common.ts +1 -0
- package/src/migrations/FromV2ToV3/fixtures/input-v2-session.json +72 -0
- package/src/migrations/FromV2ToV3/fixtures/output-v3-from-v1.json +203 -0
- package/src/migrations/FromV2ToV3/fixtures/output-v3.json +74 -0
- package/src/migrations/FromV2ToV3/index.ts +30 -0
- package/src/migrations/FromV2ToV3/migrations.test.ts +42 -0
- package/src/migrations/FromV2ToV3/types/v3.ts +27 -0
- package/src/migrations/index.ts +7 -1
- package/src/services/__tests__/session.test.ts +198 -7
- package/src/services/config.ts +23 -7
- package/src/services/session.ts +67 -14
- package/src/store/global/slices/common/action.test.ts +130 -0
- package/src/store/global/slices/common/action.ts +14 -0
- package/src/store/global/slices/common/initialState.ts +6 -2
- package/src/store/global/slices/common/selectors.ts +1 -4
- package/src/store/global/slices/settings/__snapshots__/selectors.test.ts.snap +83 -0
- package/src/store/global/slices/settings/action.ts +2 -2
- package/src/store/global/slices/settings/selectors.test.ts +132 -182
- package/src/store/global/store.ts +7 -0
- package/src/store/session/initialState.ts +5 -30
- package/src/store/session/selectors.ts +2 -1
- package/src/store/session/slices/session/action.test.ts +56 -0
- package/src/store/session/slices/session/action.ts +13 -9
- package/src/store/session/slices/session/helpers.ts +2 -3
- package/src/store/session/slices/session/initialState.ts +31 -23
- package/src/store/session/slices/session/selectors/index.ts +0 -1
- package/src/store/session/slices/session/selectors/list.ts +11 -18
- package/src/store/session/slices/sessionGroup/action.ts +54 -0
- package/src/store/session/slices/sessionGroup/initialState.ts +5 -0
- package/src/store/session/slices/sessionGroup/selectors.ts +15 -0
- package/src/store/session/store.ts +9 -2
- package/src/styles/antdOverride.ts +0 -7
- package/src/types/exportConfig.ts +3 -1
- package/src/types/session.ts +27 -3
- package/src/store/session/slices/session/selectors/export.ts +0 -31
- /package/docs/Development/{Feature-Development.md → Feature-Development-Frontend.md} +0 -0
- /package/docs/Development/{State-Management/State-Management-Intro.md → State-Management-Intro.md} +0 -0
- /package/docs/Development/{State-Management/Selectors.md → State-Management-Selectors.md} +0 -0
- /package/docs/Development/{State-Management/Selectors.zh-CN.md → State-Management-Selectors.zh-CN.md} +0 -0
|
@@ -12,6 +12,7 @@ import { sessionHelpers } from '@/store/session/helpers';
|
|
|
12
12
|
import { agentSelectors, sessionSelectors } from '@/store/session/selectors';
|
|
13
13
|
|
|
14
14
|
import ListItem from '../../ListItem';
|
|
15
|
+
import CreateGroupModal from '../../Modals/CreateGroupModal';
|
|
15
16
|
import Actions from './Actions';
|
|
16
17
|
|
|
17
18
|
interface SessionItemProps {
|
|
@@ -20,12 +21,13 @@ interface SessionItemProps {
|
|
|
20
21
|
|
|
21
22
|
const SessionItem = memo<SessionItemProps>(({ id }) => {
|
|
22
23
|
const [open, setOpen] = useState(false);
|
|
24
|
+
const [createGroupModalOpen, setCreateGroupModalOpen] = useState(false);
|
|
23
25
|
|
|
24
26
|
const [defaultModel] = useGlobalStore((s) => [settingsSelectors.defaultAgentConfig(s).model]);
|
|
25
27
|
const [active] = useSessionStore((s) => [s.activeId === id]);
|
|
26
28
|
const [loading] = useChatStore((s) => [!!s.chatLoadingId && id === s.activeId]);
|
|
27
29
|
|
|
28
|
-
const [pin, title, description, systemRole, avatar, avatarBackground, updateAt, model] =
|
|
30
|
+
const [pin, title, description, systemRole, avatar, avatarBackground, updateAt, model, group] =
|
|
29
31
|
useSessionStore((s) => {
|
|
30
32
|
const session = sessionSelectors.getSessionById(id)(s);
|
|
31
33
|
const meta = session.meta;
|
|
@@ -40,12 +42,23 @@ const SessionItem = memo<SessionItemProps>(({ id }) => {
|
|
|
40
42
|
meta.backgroundColor,
|
|
41
43
|
session?.updatedAt,
|
|
42
44
|
session.config.model,
|
|
45
|
+
session?.group,
|
|
43
46
|
];
|
|
44
47
|
});
|
|
45
48
|
|
|
46
49
|
const showModel = model !== defaultModel;
|
|
47
50
|
|
|
48
|
-
const actions = useMemo(
|
|
51
|
+
const actions = useMemo(
|
|
52
|
+
() => (
|
|
53
|
+
<Actions
|
|
54
|
+
group={group}
|
|
55
|
+
id={id}
|
|
56
|
+
openCreateGroupModal={() => setCreateGroupModalOpen(true)}
|
|
57
|
+
setOpen={setOpen}
|
|
58
|
+
/>
|
|
59
|
+
),
|
|
60
|
+
[group, id],
|
|
61
|
+
);
|
|
49
62
|
|
|
50
63
|
const addon = useMemo(
|
|
51
64
|
() =>
|
|
@@ -58,19 +71,26 @@ const SessionItem = memo<SessionItemProps>(({ id }) => {
|
|
|
58
71
|
);
|
|
59
72
|
|
|
60
73
|
return (
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
+
<>
|
|
75
|
+
<ListItem
|
|
76
|
+
actions={actions}
|
|
77
|
+
active={active}
|
|
78
|
+
addon={addon}
|
|
79
|
+
avatar={avatar}
|
|
80
|
+
avatarBackground={avatarBackground}
|
|
81
|
+
date={updateAt}
|
|
82
|
+
description={description || systemRole}
|
|
83
|
+
loading={loading}
|
|
84
|
+
pin={pin}
|
|
85
|
+
showAction={open}
|
|
86
|
+
title={title}
|
|
87
|
+
/>
|
|
88
|
+
<CreateGroupModal
|
|
89
|
+
id={id}
|
|
90
|
+
onCancel={() => setCreateGroupModalOpen(false)}
|
|
91
|
+
open={createGroupModalOpen}
|
|
92
|
+
/>
|
|
93
|
+
</>
|
|
74
94
|
);
|
|
75
95
|
}, shallow);
|
|
76
96
|
|
|
@@ -20,8 +20,10 @@ const useStyles = createStyles(
|
|
|
20
20
|
|
|
21
21
|
interface SessionListProps {
|
|
22
22
|
dataSource: LobeAgentSession[];
|
|
23
|
+
groupId?: string;
|
|
24
|
+
showAddButton?: boolean;
|
|
23
25
|
}
|
|
24
|
-
const SessionList = memo<SessionListProps>(({ dataSource }) => {
|
|
26
|
+
const SessionList = memo<SessionListProps>(({ dataSource, groupId, showAddButton = true }) => {
|
|
25
27
|
const [activeSession, switchSession, isInit] = useSessionStore((s) => [
|
|
26
28
|
s.activeSession,
|
|
27
29
|
s.switchSession,
|
|
@@ -50,7 +52,7 @@ const SessionList = memo<SessionListProps>(({ dataSource }) => {
|
|
|
50
52
|
</LazyLoad>
|
|
51
53
|
))
|
|
52
54
|
) : (
|
|
53
|
-
<AddButton />
|
|
55
|
+
showAddButton && <AddButton groupId={groupId} />
|
|
54
56
|
);
|
|
55
57
|
});
|
|
56
58
|
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { ActionIcon, EditableText, SortableList } from '@lobehub/ui';
|
|
2
|
+
import { App, Popconfirm } from 'antd';
|
|
3
|
+
import { createStyles } from 'antd-style';
|
|
4
|
+
import { PencilLine, Trash } from 'lucide-react';
|
|
5
|
+
import { memo, useState } from 'react';
|
|
6
|
+
import { useTranslation } from 'react-i18next';
|
|
7
|
+
|
|
8
|
+
import { useSessionStore } from '@/store/session';
|
|
9
|
+
import { SessionGroupItem } from '@/types/session';
|
|
10
|
+
|
|
11
|
+
const useStyles = createStyles(({ css }) => ({
|
|
12
|
+
content: css`
|
|
13
|
+
position: relative;
|
|
14
|
+
overflow: hidden;
|
|
15
|
+
flex: 1;
|
|
16
|
+
`,
|
|
17
|
+
title: css`
|
|
18
|
+
flex: 1;
|
|
19
|
+
height: 28px;
|
|
20
|
+
line-height: 28px;
|
|
21
|
+
text-align: start;
|
|
22
|
+
`,
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
const GroupItem = memo<SessionGroupItem>(({ id, name }) => {
|
|
26
|
+
const { t } = useTranslation('chat');
|
|
27
|
+
const { styles } = useStyles();
|
|
28
|
+
const { message } = App.useApp();
|
|
29
|
+
|
|
30
|
+
const [editing, setEditing] = useState(false);
|
|
31
|
+
const [updateSessionGroupName, removeSessionGroup] = useSessionStore((s) => [
|
|
32
|
+
s.updateSessionGroupName,
|
|
33
|
+
s.removeSessionGroup,
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<>
|
|
38
|
+
<SortableList.DragHandle />
|
|
39
|
+
{!editing ? (
|
|
40
|
+
<>
|
|
41
|
+
<span className={styles.title}>{name}</span>
|
|
42
|
+
<ActionIcon icon={PencilLine} onClick={() => setEditing(true)} size={'small'} />
|
|
43
|
+
<Popconfirm
|
|
44
|
+
arrow={false}
|
|
45
|
+
okButtonProps={{
|
|
46
|
+
danger: true,
|
|
47
|
+
type: 'primary',
|
|
48
|
+
}}
|
|
49
|
+
onConfirm={() => {
|
|
50
|
+
removeSessionGroup(id);
|
|
51
|
+
}}
|
|
52
|
+
title={t('sessionGroup.confirmRemoveGroupAlert')}
|
|
53
|
+
>
|
|
54
|
+
<ActionIcon icon={Trash} size={'small'} />
|
|
55
|
+
</Popconfirm>
|
|
56
|
+
</>
|
|
57
|
+
) : (
|
|
58
|
+
<EditableText
|
|
59
|
+
editing={editing}
|
|
60
|
+
onChangeEnd={(input) => {
|
|
61
|
+
if (name !== input) {
|
|
62
|
+
if (!input) return;
|
|
63
|
+
if (input.length === 0 || input.length > 20)
|
|
64
|
+
return message.warning(t('sessionGroup.tooLong'));
|
|
65
|
+
updateSessionGroupName(id, input);
|
|
66
|
+
message.success(t('sessionGroup.renameSuccess'));
|
|
67
|
+
}
|
|
68
|
+
setEditing(false);
|
|
69
|
+
}}
|
|
70
|
+
onEditingChange={(e) => setEditing(e)}
|
|
71
|
+
showEditIcon={false}
|
|
72
|
+
size={'small'}
|
|
73
|
+
style={{
|
|
74
|
+
height: 28,
|
|
75
|
+
}}
|
|
76
|
+
type={'pure'}
|
|
77
|
+
value={name}
|
|
78
|
+
/>
|
|
79
|
+
)}
|
|
80
|
+
</>
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
export default GroupItem;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Icon, Modal, type ModalProps, SortableList } from '@lobehub/ui';
|
|
2
|
+
import { Button } from 'antd';
|
|
3
|
+
import { createStyles } from 'antd-style';
|
|
4
|
+
import isEqual from 'fast-deep-equal';
|
|
5
|
+
import { Plus } from 'lucide-react';
|
|
6
|
+
import { memo } from 'react';
|
|
7
|
+
import { useTranslation } from 'react-i18next';
|
|
8
|
+
import { Flexbox } from 'react-layout-kit';
|
|
9
|
+
|
|
10
|
+
import { useSessionStore } from '@/store/session';
|
|
11
|
+
import { sessionGroupSelectors } from '@/store/session/selectors';
|
|
12
|
+
import { SessionGroupItem } from '@/types/session';
|
|
13
|
+
|
|
14
|
+
import GroupItem from './GroupItem';
|
|
15
|
+
|
|
16
|
+
const useStyles = createStyles(({ css, token, stylish }) => ({
|
|
17
|
+
container: css`
|
|
18
|
+
height: 36px;
|
|
19
|
+
padding-inline: 8px;
|
|
20
|
+
border-radius: ${token.borderRadius}px;
|
|
21
|
+
transition: background 0.2s ease-in-out;
|
|
22
|
+
|
|
23
|
+
&:hover {
|
|
24
|
+
${stylish.blur};
|
|
25
|
+
background: ${token.colorFillTertiary};
|
|
26
|
+
}
|
|
27
|
+
`,
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
const ConfigGroupModal = memo<ModalProps>(({ open, onCancel }) => {
|
|
31
|
+
const { t } = useTranslation('chat');
|
|
32
|
+
const { styles } = useStyles();
|
|
33
|
+
const sessionGroupItems = useSessionStore(sessionGroupSelectors.sessionGroupItems, isEqual);
|
|
34
|
+
const [addSessionGroup, updateSessionGroupSort] = useSessionStore((s) => [
|
|
35
|
+
s.addSessionGroup,
|
|
36
|
+
s.updateSessionGroupSort,
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<Modal
|
|
41
|
+
allowFullscreen
|
|
42
|
+
footer={null}
|
|
43
|
+
onCancel={onCancel}
|
|
44
|
+
open={open}
|
|
45
|
+
title={t('sessionGroup.config')}
|
|
46
|
+
width={400}
|
|
47
|
+
>
|
|
48
|
+
<Flexbox>
|
|
49
|
+
<SortableList
|
|
50
|
+
items={sessionGroupItems}
|
|
51
|
+
onChange={(items: SessionGroupItem[]) => {
|
|
52
|
+
updateSessionGroupSort(items);
|
|
53
|
+
}}
|
|
54
|
+
renderItem={(item: SessionGroupItem) => (
|
|
55
|
+
<SortableList.Item
|
|
56
|
+
align={'center'}
|
|
57
|
+
className={styles.container}
|
|
58
|
+
gap={4}
|
|
59
|
+
horizontal
|
|
60
|
+
id={item.id}
|
|
61
|
+
justify={'space-between'}
|
|
62
|
+
>
|
|
63
|
+
<GroupItem {...item} />
|
|
64
|
+
</SortableList.Item>
|
|
65
|
+
)}
|
|
66
|
+
/>
|
|
67
|
+
<Button
|
|
68
|
+
block
|
|
69
|
+
icon={<Icon icon={Plus} />}
|
|
70
|
+
onClick={() => addSessionGroup(t('sessionGroup.newGroup'))}
|
|
71
|
+
>
|
|
72
|
+
{t('sessionGroup.createGroup')}
|
|
73
|
+
</Button>
|
|
74
|
+
</Flexbox>
|
|
75
|
+
</Modal>
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
export default ConfigGroupModal;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Input, Modal, type ModalProps } from '@lobehub/ui';
|
|
2
|
+
import { App } from 'antd';
|
|
3
|
+
import { MouseEvent, memo, useState } from 'react';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
import { Flexbox } from 'react-layout-kit';
|
|
6
|
+
|
|
7
|
+
import { useGlobalStore } from '@/store/global';
|
|
8
|
+
import { useSessionStore } from '@/store/session';
|
|
9
|
+
|
|
10
|
+
interface CreateGroupModalProps extends ModalProps {
|
|
11
|
+
id: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const CreateGroupModal = memo<CreateGroupModalProps>(
|
|
15
|
+
({ id, open, onCancel }: CreateGroupModalProps) => {
|
|
16
|
+
const { t } = useTranslation('chat');
|
|
17
|
+
|
|
18
|
+
const toggleExpandSessionGroup = useGlobalStore((s) => s.toggleExpandSessionGroup);
|
|
19
|
+
const { message } = App.useApp();
|
|
20
|
+
const [updateSessionGroup, addCustomGroup] = useSessionStore((s) => [
|
|
21
|
+
s.updateSessionGroupId,
|
|
22
|
+
s.addSessionGroup,
|
|
23
|
+
]);
|
|
24
|
+
const [input, setInput] = useState('');
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div onClick={(e) => e.stopPropagation()}>
|
|
28
|
+
<Modal
|
|
29
|
+
allowFullscreen
|
|
30
|
+
centered
|
|
31
|
+
onCancel={onCancel}
|
|
32
|
+
onOk={async (e: MouseEvent<HTMLButtonElement>) => {
|
|
33
|
+
if (!input) return;
|
|
34
|
+
|
|
35
|
+
if (input.length === 0 || input.length > 20)
|
|
36
|
+
return message.warning(t('sessionGroup.tooLong'));
|
|
37
|
+
|
|
38
|
+
const groupId = await addCustomGroup(input);
|
|
39
|
+
await updateSessionGroup(id, groupId);
|
|
40
|
+
toggleExpandSessionGroup(groupId, true);
|
|
41
|
+
|
|
42
|
+
message.success(t('sessionGroup.createSuccess'));
|
|
43
|
+
onCancel?.(e);
|
|
44
|
+
}}
|
|
45
|
+
open={open}
|
|
46
|
+
title={t('sessionGroup.createGroup')}
|
|
47
|
+
width={400}
|
|
48
|
+
>
|
|
49
|
+
<Flexbox paddingBlock={16}>
|
|
50
|
+
<Input
|
|
51
|
+
autoFocus
|
|
52
|
+
onChange={(e) => setInput(e.target.value)}
|
|
53
|
+
placeholder={t('sessionGroup.inputPlaceholder')}
|
|
54
|
+
value={input}
|
|
55
|
+
/>
|
|
56
|
+
</Flexbox>
|
|
57
|
+
</Modal>
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
},
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
export default CreateGroupModal;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Input, Modal, type ModalProps } from '@lobehub/ui';
|
|
2
|
+
import { App } from 'antd';
|
|
3
|
+
import isEqual from 'fast-deep-equal';
|
|
4
|
+
import { memo, useState } from 'react';
|
|
5
|
+
import { useTranslation } from 'react-i18next';
|
|
6
|
+
|
|
7
|
+
import { useSessionStore } from '@/store/session';
|
|
8
|
+
import { sessionGroupSelectors } from '@/store/session/selectors';
|
|
9
|
+
|
|
10
|
+
interface RenameGroupModalProps extends ModalProps {
|
|
11
|
+
id: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const RenameGroupModal = memo<RenameGroupModalProps>(({ id, open, onCancel }) => {
|
|
15
|
+
const { t } = useTranslation('chat');
|
|
16
|
+
|
|
17
|
+
const updateSessionGroupName = useSessionStore((s) => s.updateSessionGroupName);
|
|
18
|
+
const group = useSessionStore((s) => sessionGroupSelectors.getGroupById(id)(s), isEqual);
|
|
19
|
+
|
|
20
|
+
const [input, setInput] = useState<string>();
|
|
21
|
+
|
|
22
|
+
const { message } = App.useApp();
|
|
23
|
+
return (
|
|
24
|
+
<Modal
|
|
25
|
+
allowFullscreen
|
|
26
|
+
onCancel={onCancel}
|
|
27
|
+
onOk={(e) => {
|
|
28
|
+
if (!input) return;
|
|
29
|
+
if (input.length === 0 || input.length > 20)
|
|
30
|
+
return message.warning(t('sessionGroup.tooLong'));
|
|
31
|
+
updateSessionGroupName(id, input);
|
|
32
|
+
message.success(t('sessionGroup.renameSuccess'));
|
|
33
|
+
onCancel?.(e);
|
|
34
|
+
}}
|
|
35
|
+
open={open}
|
|
36
|
+
title={t('sessionGroup.rename')}
|
|
37
|
+
width={400}
|
|
38
|
+
>
|
|
39
|
+
<Input
|
|
40
|
+
autoFocus
|
|
41
|
+
defaultValue={group?.name}
|
|
42
|
+
onChange={(e) => setInput(e.target.value)}
|
|
43
|
+
placeholder={t('sessionGroup.inputPlaceholder')}
|
|
44
|
+
value={input}
|
|
45
|
+
/>
|
|
46
|
+
</Modal>
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
export default RenameGroupModal;
|
|
@@ -9,7 +9,7 @@ import SessionList from './List';
|
|
|
9
9
|
const SessionListContent = memo(() => {
|
|
10
10
|
const searchSessions = useSessionStore(sessionSelectors.searchSessions, isEqual);
|
|
11
11
|
|
|
12
|
-
return <SessionList dataSource={searchSessions} />;
|
|
12
|
+
return <SessionList dataSource={searchSessions} showAddButton={false} />;
|
|
13
13
|
});
|
|
14
14
|
|
|
15
15
|
export default SessionListContent;
|
|
@@ -30,7 +30,10 @@ const Common = memo<SettingsCommonProps>(({ showAccessCodeConfig }) => {
|
|
|
30
30
|
const { t } = useTranslation('setting');
|
|
31
31
|
const [form] = AntForm.useForm();
|
|
32
32
|
|
|
33
|
-
const clearSessions = useSessionStore((s) =>
|
|
33
|
+
const [clearSessions, clearSessionGroups] = useSessionStore((s) => [
|
|
34
|
+
s.clearSessions,
|
|
35
|
+
s.clearSessionGroups,
|
|
36
|
+
]);
|
|
34
37
|
const [clearTopics, clearAllMessages] = useChatStore((s) => [
|
|
35
38
|
s.removeAllTopics,
|
|
36
39
|
s.clearAllMessages,
|
|
@@ -71,6 +74,7 @@ const Common = memo<SettingsCommonProps>(({ showAccessCodeConfig }) => {
|
|
|
71
74
|
await clearTopics();
|
|
72
75
|
await removeAllFiles();
|
|
73
76
|
await clearAllMessages();
|
|
77
|
+
await clearSessionGroups();
|
|
74
78
|
|
|
75
79
|
message.success(t('danger.clear.success'));
|
|
76
80
|
},
|
|
@@ -5,7 +5,7 @@ import { Center, Flexbox } from 'react-layout-kit';
|
|
|
5
5
|
|
|
6
6
|
const FullscreenLoading = memo<{ title?: string }>(({ title }) => {
|
|
7
7
|
return (
|
|
8
|
-
<Flexbox height={'100%'} width={'100%'}>
|
|
8
|
+
<Flexbox height={'100%'} style={{ userSelect: 'none' }} width={'100%'}>
|
|
9
9
|
<Center flex={1} gap={12} width={'100%'}>
|
|
10
10
|
<Logo extra={'Chat'} size={48} type={'combine'} />
|
|
11
11
|
<Center gap={16} horizontal>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import Dexie from 'dexie';
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { LocalDB } from '../db';
|
|
5
|
+
import { dbSchemaV3 } from '../schemas';
|
|
6
|
+
import { LOBE_CHAT_LOCAL_DB_NAME } from '../types/db';
|
|
7
|
+
|
|
8
|
+
beforeEach(async () => {
|
|
9
|
+
// 确保在测试开始前删除数据库
|
|
10
|
+
await Dexie.delete(LOBE_CHAT_LOCAL_DB_NAME);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
afterEach(async () => {
|
|
14
|
+
// 确保在测试结束后删除数据库
|
|
15
|
+
await Dexie.delete(LOBE_CHAT_LOCAL_DB_NAME);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('LocalDB migration', () => {
|
|
19
|
+
it('should upgrade from version 3 to version 4 correctly', async () => {
|
|
20
|
+
// 使用版本3的schema创建数据库实例并填充测试数据
|
|
21
|
+
const dbV3 = new Dexie(LOBE_CHAT_LOCAL_DB_NAME);
|
|
22
|
+
dbV3.version(3).stores(dbSchemaV3);
|
|
23
|
+
await dbV3.open();
|
|
24
|
+
await dbV3.table('sessions').bulkAdd([
|
|
25
|
+
{ id: 's1', group: 'pinned' },
|
|
26
|
+
{ id: 's2', group: 'default' },
|
|
27
|
+
]);
|
|
28
|
+
dbV3.close();
|
|
29
|
+
|
|
30
|
+
// 创建新的数据库实例,包含版本4的迁移逻辑
|
|
31
|
+
const dbV4 = new LocalDB();
|
|
32
|
+
await dbV4.open();
|
|
33
|
+
|
|
34
|
+
// 验证迁移后的数据
|
|
35
|
+
const updatedSession1 = await dbV4.sessions.get('s1');
|
|
36
|
+
const updatedSession2 = await dbV4.sessions.get('s2');
|
|
37
|
+
expect(updatedSession1).toEqual({ id: 's1', pinned: 1, group: 'default' });
|
|
38
|
+
expect(updatedSession2).toEqual({ id: 's2', pinned: 0, group: 'default' });
|
|
39
|
+
|
|
40
|
+
dbV4.close();
|
|
41
|
+
});
|
|
42
|
+
});
|
package/src/database/core/db.ts
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
|
-
import Dexie from 'dexie';
|
|
1
|
+
import Dexie, { Transaction } from 'dexie';
|
|
2
2
|
|
|
3
|
-
import { DBModel } from '@/database/core/types/db';
|
|
3
|
+
import { DBModel, LOBE_CHAT_LOCAL_DB_NAME } from '@/database/core/types/db';
|
|
4
4
|
import { DB_File } from '@/database/schemas/files';
|
|
5
5
|
import { DB_Message } from '@/database/schemas/message';
|
|
6
6
|
import { DB_Plugin } from '@/database/schemas/plugin';
|
|
7
7
|
import { DB_Session } from '@/database/schemas/session';
|
|
8
|
+
import { DB_SessionGroup } from '@/database/schemas/sessionGroup';
|
|
8
9
|
import { DB_Topic } from '@/database/schemas/topic';
|
|
9
10
|
|
|
10
|
-
import { dbSchemaV1, dbSchemaV2, dbSchemaV3 } from './schemas';
|
|
11
|
+
import { dbSchemaV1, dbSchemaV2, dbSchemaV3, dbSchemaV4 } from './schemas';
|
|
11
12
|
|
|
12
13
|
interface LobeDBSchemaMap {
|
|
13
14
|
files: DB_File;
|
|
14
15
|
messages: DB_Message;
|
|
15
16
|
plugins: DB_Plugin;
|
|
17
|
+
sessionGroups: DB_SessionGroup;
|
|
16
18
|
sessions: DB_Session;
|
|
17
19
|
topics: DB_Topic;
|
|
18
20
|
}
|
|
@@ -24,19 +26,39 @@ export class LocalDB extends Dexie {
|
|
|
24
26
|
public messages: LobeDBTable<'messages'>;
|
|
25
27
|
public topics: LobeDBTable<'topics'>;
|
|
26
28
|
public plugins: LobeDBTable<'plugins'>;
|
|
29
|
+
public sessionGroups: LobeDBTable<'sessionGroups'>;
|
|
27
30
|
|
|
28
31
|
constructor() {
|
|
29
|
-
super(
|
|
32
|
+
super(LOBE_CHAT_LOCAL_DB_NAME);
|
|
30
33
|
this.version(1).stores(dbSchemaV1);
|
|
31
34
|
this.version(2).stores(dbSchemaV2);
|
|
32
35
|
this.version(3).stores(dbSchemaV3);
|
|
36
|
+
this.version(4)
|
|
37
|
+
.stores(dbSchemaV4)
|
|
38
|
+
.upgrade((trans) => this.upgradeToV4(trans));
|
|
33
39
|
|
|
34
40
|
this.files = this.table('files');
|
|
35
41
|
this.sessions = this.table('sessions');
|
|
36
42
|
this.messages = this.table('messages');
|
|
37
43
|
this.topics = this.table('topics');
|
|
38
44
|
this.plugins = this.table('plugins');
|
|
45
|
+
this.sessionGroups = this.table('sessionGroups');
|
|
39
46
|
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 2024.01.22
|
|
50
|
+
*
|
|
51
|
+
* DB V3 to V4
|
|
52
|
+
* from `group = pinned` to `pinned:true`
|
|
53
|
+
*/
|
|
54
|
+
upgradeToV4 = async (trans: Transaction) => {
|
|
55
|
+
const sessions = trans.table('sessions');
|
|
56
|
+
await sessions.toCollection().modify((session) => {
|
|
57
|
+
// translate boolean to number
|
|
58
|
+
session.pinned = session.group === 'pinned' ? 1 : 0;
|
|
59
|
+
session.group = 'default';
|
|
60
|
+
});
|
|
61
|
+
};
|
|
40
62
|
}
|
|
41
63
|
|
|
42
64
|
export const LocalDBInstance = new LocalDB();
|
|
@@ -24,10 +24,23 @@ export const dbSchemaV2 = {
|
|
|
24
24
|
// ************************************** //
|
|
25
25
|
// ******* Version 3 - 2023-12-06 ******* //
|
|
26
26
|
// ************************************** //
|
|
27
|
-
// - Added `
|
|
27
|
+
// - Added `plugins` table
|
|
28
28
|
|
|
29
29
|
export const dbSchemaV3 = {
|
|
30
30
|
...dbSchemaV2,
|
|
31
31
|
plugins:
|
|
32
32
|
'&identifier, type, manifest.type, manifest.meta.title, manifest.meta.description, manifest.meta.author, createdAt, updatedAt',
|
|
33
33
|
};
|
|
34
|
+
|
|
35
|
+
// ************************************** //
|
|
36
|
+
// ******* Version 4 - 2024-01-21 ******* //
|
|
37
|
+
// ************************************** //
|
|
38
|
+
// - Added `sessionGroups` table
|
|
39
|
+
// - Add `pinned` to sessions table
|
|
40
|
+
|
|
41
|
+
export const dbSchemaV4 = {
|
|
42
|
+
...dbSchemaV3,
|
|
43
|
+
sessionGroups: '&id, name, sort, createdAt, updatedAt',
|
|
44
|
+
sessions:
|
|
45
|
+
'&id, type, group, pinned, meta.title, meta.description, meta.tags, createdAt, updatedAt',
|
|
46
|
+
};
|
|
@@ -373,4 +373,46 @@ describe('MessageModel', () => {
|
|
|
373
373
|
}
|
|
374
374
|
});
|
|
375
375
|
});
|
|
376
|
+
|
|
377
|
+
describe('clearTable', () => {
|
|
378
|
+
it('should clear the table', async () => {
|
|
379
|
+
await MessageModel.create(messageData);
|
|
380
|
+
await MessageModel.clearTable();
|
|
381
|
+
const messages = await MessageModel.queryAll();
|
|
382
|
+
expect(messages).toHaveLength(0);
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
describe('updatePluginState', () => {
|
|
387
|
+
it('should update plugin state', async () => {
|
|
388
|
+
const createdMessage = await MessageModel.create(messageData);
|
|
389
|
+
await MessageModel.updatePluginState(createdMessage.id, 'testKey', 'testValue');
|
|
390
|
+
const updatedMessage = await MessageModel.findById(createdMessage.id);
|
|
391
|
+
expect(updatedMessage.pluginState).toHaveProperty('testKey', 'testValue');
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
describe('clearTable', () => {
|
|
395
|
+
it('should clear the table', async () => {
|
|
396
|
+
await MessageModel.create(messageData);
|
|
397
|
+
await MessageModel.clearTable();
|
|
398
|
+
const messages = await MessageModel.queryAll();
|
|
399
|
+
expect(messages).toHaveLength(0);
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
describe('updatePluginState', () => {
|
|
404
|
+
it('should update plugin state', async () => {
|
|
405
|
+
const createdMessage = await MessageModel.create(messageData);
|
|
406
|
+
await MessageModel.updatePluginState(createdMessage.id, 'testKey', 'testValue');
|
|
407
|
+
const updatedMessage = await MessageModel.findById(createdMessage.id);
|
|
408
|
+
expect(updatedMessage.pluginState).toHaveProperty('testKey', 'testValue');
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
describe('isEmpty', () => {
|
|
413
|
+
it('should return true if table is empty', async () => {
|
|
414
|
+
const isEmpty = await MessageModel.isEmpty();
|
|
415
|
+
expect(isEmpty).toBeTruthy();
|
|
416
|
+
});
|
|
417
|
+
});
|
|
376
418
|
});
|