@lobehub/chat 0.120.6 → 0.121.1
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 +58 -0
- package/README.md +14 -14
- package/README.zh-CN.md +14 -14
- 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/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 +11 -2
- package/src/app/settings/llm/LLM/index.tsx +1 -0
- 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/Error/ApiKeyForm.tsx +1 -0
- package/src/features/Conversation/Error/InvalidAccess.tsx +1 -0
- package/src/features/Conversation/Plugins/Inspector/index.tsx +1 -1
- package/src/features/Conversation/Plugins/Render/DefaultType/SystemJsRender/index.tsx +7 -2
- package/src/features/Conversation/hooks/useChatListActionsBar.tsx +3 -3
- package/src/features/PluginDevModal/index.tsx +3 -0
- package/src/features/PluginSettings/PluginSettingRender.tsx +1 -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
|
},
|
|
@@ -182,7 +186,12 @@ const Common = memo<SettingsCommonProps>(({ showAccessCodeConfig }) => {
|
|
|
182
186
|
const system: SettingItemGroup = {
|
|
183
187
|
children: [
|
|
184
188
|
{
|
|
185
|
-
children:
|
|
189
|
+
children: (
|
|
190
|
+
<Input.Password
|
|
191
|
+
autoComplete={'new-password'}
|
|
192
|
+
placeholder={t('settingSystem.accessCode.placeholder')}
|
|
193
|
+
/>
|
|
194
|
+
),
|
|
186
195
|
desc: t('settingSystem.accessCode.desc'),
|
|
187
196
|
hidden: !showAccessCodeConfig,
|
|
188
197
|
label: t('settingSystem.accessCode.title'),
|
|
@@ -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
|
});
|