@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.
Files changed (127) hide show
  1. package/.github/ISSUE_TEMPLATE/2_feature_request_cn.yml +1 -1
  2. package/CHANGELOG.md +58 -0
  3. package/README.md +14 -14
  4. package/README.zh-CN.md +14 -14
  5. package/docs/Development/Feature-Development-Frontend.zh-CN.md +126 -0
  6. package/docs/Development/Feature-Development.zh-CN.md +664 -83
  7. package/docs/Development/{State-Management/State-Management-Intro.zh-CN.md → State-Management-Intro.zh-CN.md} +9 -9
  8. package/docs/Home.md +1 -0
  9. package/docs/_Sidebar.md +1 -0
  10. package/locales/ar/chat.json +13 -1
  11. package/locales/ar/common.json +1 -0
  12. package/locales/de-DE/chat.json +13 -1
  13. package/locales/de-DE/common.json +1 -0
  14. package/locales/en-US/chat.json +13 -1
  15. package/locales/en-US/common.json +1 -0
  16. package/locales/es-ES/chat.json +13 -1
  17. package/locales/es-ES/common.json +1 -0
  18. package/locales/fr-FR/chat.json +13 -1
  19. package/locales/fr-FR/common.json +1 -0
  20. package/locales/it-IT/chat.json +13 -1
  21. package/locales/it-IT/common.json +1 -0
  22. package/locales/ja-JP/chat.json +13 -1
  23. package/locales/ja-JP/common.json +1 -0
  24. package/locales/ko-KR/chat.json +13 -1
  25. package/locales/ko-KR/common.json +1 -0
  26. package/locales/nl-NL/chat.json +13 -1
  27. package/locales/nl-NL/common.json +1 -0
  28. package/locales/pl-PL/chat.json +13 -1
  29. package/locales/pl-PL/common.json +1 -0
  30. package/locales/pt-BR/chat.json +13 -1
  31. package/locales/pt-BR/common.json +1 -0
  32. package/locales/ru-RU/chat.json +13 -1
  33. package/locales/ru-RU/common.json +1 -0
  34. package/locales/tr-TR/chat.json +13 -1
  35. package/locales/tr-TR/common.json +1 -0
  36. package/locales/vi-VN/chat.json +13 -1
  37. package/locales/vi-VN/common.json +1 -0
  38. package/locales/zh-CN/chat.json +13 -1
  39. package/locales/zh-CN/common.json +1 -0
  40. package/locales/zh-TW/chat.json +13 -1
  41. package/locales/zh-TW/common.json +1 -0
  42. package/package.json +4 -6
  43. package/src/app/chat/(desktop)/features/ChatInput/Footer/index.tsx +18 -5
  44. package/src/app/chat/(desktop)/features/ChatInput/index.tsx +1 -1
  45. package/src/app/chat/features/ChatHeader/ShareButton/index.tsx +0 -1
  46. package/src/app/chat/features/Migration/Failed.tsx +2 -0
  47. package/src/app/chat/features/SessionListContent/CollapseGroup/Actions.tsx +115 -0
  48. package/src/app/chat/features/SessionListContent/CollapseGroup/index.tsx +6 -0
  49. package/src/app/chat/features/SessionListContent/DefaultMode.tsx +63 -23
  50. package/src/app/chat/features/SessionListContent/List/AddButton.tsx +3 -3
  51. package/src/app/chat/features/SessionListContent/List/Item/Actions.tsx +89 -35
  52. package/src/app/chat/features/SessionListContent/List/Item/index.tsx +35 -15
  53. package/src/app/chat/features/SessionListContent/List/index.tsx +4 -2
  54. package/src/app/chat/features/SessionListContent/Modals/ConfigGroupModal/GroupItem.tsx +84 -0
  55. package/src/app/chat/features/SessionListContent/Modals/ConfigGroupModal/index.tsx +79 -0
  56. package/src/app/chat/features/SessionListContent/Modals/CreateGroupModal.tsx +63 -0
  57. package/src/app/chat/features/SessionListContent/Modals/RenameGroupModal.tsx +50 -0
  58. package/src/app/chat/features/SessionListContent/SearchMode.tsx +1 -1
  59. package/src/app/settings/common/Common.tsx +11 -2
  60. package/src/app/settings/llm/LLM/index.tsx +1 -0
  61. package/src/components/FullscreenLoading/index.tsx +1 -1
  62. package/src/database/core/__tests__/db-upgrade.test.ts +42 -0
  63. package/src/database/core/db.ts +26 -4
  64. package/src/database/core/schemas.ts +14 -1
  65. package/src/database/core/types/db.ts +2 -0
  66. package/src/database/models/__tests__/message.test.ts +42 -0
  67. package/src/database/models/__tests__/plugin.test.ts +77 -0
  68. package/src/database/models/__tests__/session.test.ts +202 -63
  69. package/src/database/models/__tests__/sessionGroup.test.ts +195 -0
  70. package/src/database/models/__tests__/topic.test.ts +76 -1
  71. package/src/database/models/message.ts +7 -8
  72. package/src/database/models/session.ts +103 -7
  73. package/src/database/models/sessionGroup.ts +76 -0
  74. package/src/database/schemas/session.ts +1 -0
  75. package/src/database/schemas/sessionGroup.ts +8 -0
  76. package/src/features/ChatInput/ActionBar/Clear.tsx +2 -1
  77. package/src/features/Conversation/Actions/Assistant.tsx +1 -1
  78. package/src/features/Conversation/Actions/Function.tsx +2 -2
  79. package/src/features/Conversation/Actions/User.tsx +1 -1
  80. package/src/features/Conversation/Error/ApiKeyForm.tsx +1 -0
  81. package/src/features/Conversation/Error/InvalidAccess.tsx +1 -0
  82. package/src/features/Conversation/Plugins/Inspector/index.tsx +1 -1
  83. package/src/features/Conversation/Plugins/Render/DefaultType/SystemJsRender/index.tsx +7 -2
  84. package/src/features/Conversation/hooks/useChatListActionsBar.tsx +3 -3
  85. package/src/features/PluginDevModal/index.tsx +3 -0
  86. package/src/features/PluginSettings/PluginSettingRender.tsx +1 -0
  87. package/src/features/PluginStore/PluginItem/Action.tsx +2 -0
  88. package/src/locales/default/chat.ts +13 -1
  89. package/src/locales/default/common.ts +1 -0
  90. package/src/migrations/FromV2ToV3/fixtures/input-v2-session.json +72 -0
  91. package/src/migrations/FromV2ToV3/fixtures/output-v3-from-v1.json +203 -0
  92. package/src/migrations/FromV2ToV3/fixtures/output-v3.json +74 -0
  93. package/src/migrations/FromV2ToV3/index.ts +30 -0
  94. package/src/migrations/FromV2ToV3/migrations.test.ts +42 -0
  95. package/src/migrations/FromV2ToV3/types/v3.ts +27 -0
  96. package/src/migrations/index.ts +7 -1
  97. package/src/services/__tests__/session.test.ts +198 -7
  98. package/src/services/config.ts +23 -7
  99. package/src/services/session.ts +67 -14
  100. package/src/store/global/slices/common/action.test.ts +130 -0
  101. package/src/store/global/slices/common/action.ts +14 -0
  102. package/src/store/global/slices/common/initialState.ts +6 -2
  103. package/src/store/global/slices/common/selectors.ts +1 -4
  104. package/src/store/global/slices/settings/__snapshots__/selectors.test.ts.snap +83 -0
  105. package/src/store/global/slices/settings/action.ts +2 -2
  106. package/src/store/global/slices/settings/selectors.test.ts +132 -182
  107. package/src/store/global/store.ts +7 -0
  108. package/src/store/session/initialState.ts +5 -30
  109. package/src/store/session/selectors.ts +2 -1
  110. package/src/store/session/slices/session/action.test.ts +56 -0
  111. package/src/store/session/slices/session/action.ts +13 -9
  112. package/src/store/session/slices/session/helpers.ts +2 -3
  113. package/src/store/session/slices/session/initialState.ts +31 -23
  114. package/src/store/session/slices/session/selectors/index.ts +0 -1
  115. package/src/store/session/slices/session/selectors/list.ts +11 -18
  116. package/src/store/session/slices/sessionGroup/action.ts +54 -0
  117. package/src/store/session/slices/sessionGroup/initialState.ts +5 -0
  118. package/src/store/session/slices/sessionGroup/selectors.ts +15 -0
  119. package/src/store/session/store.ts +9 -2
  120. package/src/styles/antdOverride.ts +0 -7
  121. package/src/types/exportConfig.ts +3 -1
  122. package/src/types/session.ts +27 -3
  123. package/src/store/session/slices/session/selectors/export.ts +0 -31
  124. /package/docs/Development/{Feature-Development.md → Feature-Development-Frontend.md} +0 -0
  125. /package/docs/Development/{State-Management/State-Management-Intro.md → State-Management-Intro.md} +0 -0
  126. /package/docs/Development/{State-Management/Selectors.md → State-Management-Selectors.md} +0 -0
  127. /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(() => <Actions id={id} setOpen={setOpen} />, [id]);
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
- <ListItem
62
- actions={actions}
63
- active={active}
64
- addon={addon}
65
- avatar={avatar}
66
- avatarBackground={avatarBackground}
67
- date={updateAt}
68
- description={description || systemRole}
69
- loading={loading}
70
- pin={pin}
71
- showAction={open}
72
- title={title}
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) => s.clearSessions);
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: <Input.Password placeholder={t('settingSystem.accessCode.placeholder')} />,
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'),
@@ -50,6 +50,7 @@ const LLM = memo(() => {
50
50
  {
51
51
  children: (
52
52
  <Input.Password
53
+ autoComplete={'new-password'}
53
54
  placeholder={
54
55
  useAzure ? t('llm.AzureOpenAI.token.placeholder') : t('llm.OpenAI.token.placeholder')
55
56
  }
@@ -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
+ });
@@ -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('LOBE_CHAT_DB');
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 `plugin` table
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
+ };
@@ -11,3 +11,5 @@ export const DBBaseFieldsSchema = z.object({
11
11
  id: z.string(),
12
12
  updatedAt: z.number(),
13
13
  });
14
+
15
+ export const LOBE_CHAT_LOCAL_DB_NAME = 'LOBE_CHAT_DB';
@@ -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
  });