@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
@@ -6,6 +6,7 @@
6
6
  "confirmClearCurrentMessages": "即將清空當前對話,清空後將無法找回,請確認你的操作",
7
7
  "confirmRemoveSessionItemAlert": "即將刪除該助手,刪除後將無法找回,請確認你的操作",
8
8
  "defaultAgent": "自定義助手",
9
+ "defaultList": "預設清單",
9
10
  "defaultSession": "自定義助手",
10
11
  "duplicateTitle": "{{title}} 副本",
11
12
  "historyRange": "歷史範圍",
@@ -34,7 +35,18 @@
34
35
  "roleAndArchive": "角色與記錄",
35
36
  "searchAgentPlaceholder": "搜索助手和對話...",
36
37
  "sendPlaceholder": "輸入聊天內容...",
37
- "sessionList": "助手列表",
38
+ "sessionGroup": {
39
+ "config": "分組管理",
40
+ "confirmRemoveGroupAlert": "即將刪除該分組,刪除後該分組的助手將移動到預設清單,請確認你的操作",
41
+ "createGroup": "新增新分組",
42
+ "createSuccess": "創建成功",
43
+ "inputPlaceholder": "請輸入分組名稱...",
44
+ "moveGroup": "移動到分組",
45
+ "newGroup": "新分組",
46
+ "rename": "重新命名分組",
47
+ "renameSuccess": "重新命名成功",
48
+ "tooLong": "分組名稱長度需在 1-20 之內"
49
+ },
38
50
  "shareModal": {
39
51
  "download": "下載截圖",
40
52
  "imageType": "圖片格式",
@@ -44,6 +44,7 @@
44
44
  "added": "匯入成功",
45
45
  "errors": "匯入出錯",
46
46
  "messages": "訊息",
47
+ "sessionGroups": "會話分組",
47
48
  "sessions": "助手",
48
49
  "skips": "重複跳過",
49
50
  "topics": "話題",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "0.120.6",
3
+ "version": "0.121.1",
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",
@@ -44,11 +44,8 @@
44
44
  "release": "semantic-release",
45
45
  "start": "next start",
46
46
  "stylelint": "stylelint \"src/**/*.{js,jsx,ts,tsx}\" --fix",
47
- "test": "npm run test-temp-fixture-ci && vitest",
48
- "test-a-local": "vitest",
49
- "test-temp-fixture-ci": "echo 'please remove this script after new bun@1.0.24 released' && rm -rf node_modules && npx bun@1.0.22 install",
50
- "test:coverage": "npm run test-temp-fixture-ci && vitest run --coverage",
51
- "test:coverage-local": "vitest run --coverage",
47
+ "test": "vitest",
48
+ "test:coverage": "vitest run --coverage",
52
49
  "test:update": "vitest -u",
53
50
  "type-check": "tsc --noEmit",
54
51
  "workflow:docs": "tsx scripts/docsWorkflow/index.ts",
@@ -161,6 +158,7 @@
161
158
  "@types/uuid": "^9",
162
159
  "@umijs/lint": "^4",
163
160
  "@vitest/coverage-v8": "^1",
161
+ "ajv-keywords": "^5.1.0",
164
162
  "commitlint": "^18",
165
163
  "consola": "^3",
166
164
  "dpdm": "^3",
@@ -4,12 +4,13 @@ import { createStyles } from 'antd-style';
4
4
  import {
5
5
  ChevronUp,
6
6
  CornerDownLeft,
7
- Loader2,
8
7
  LucideCheck,
9
8
  LucideChevronDown,
10
9
  LucideCommand,
11
10
  LucidePlus,
11
+ StopCircle,
12
12
  } from 'lucide-react';
13
+ import { rgba } from 'polished';
13
14
  import { memo } from 'react';
14
15
  import { useTranslation } from 'react-i18next';
15
16
  import { Center, Flexbox } from 'react-layout-kit';
@@ -25,7 +26,7 @@ import { isMacOS } from '@/utils/platform';
25
26
 
26
27
  import { LocalFiles } from './LocalFiles';
27
28
 
28
- const useStyles = createStyles(({ css, prefixCls }) => {
29
+ const useStyles = createStyles(({ css, prefixCls, token }) => {
29
30
  return {
30
31
  arrow: css`
31
32
  &.${prefixCls}-btn.${prefixCls}-btn-icon-only {
@@ -38,13 +39,19 @@ const useStyles = createStyles(({ css, prefixCls }) => {
38
39
  align-items: center;
39
40
  justify-content: center;
40
41
  }
42
+
43
+ .${prefixCls}-btn.${prefixCls}-dropdown-trigger {
44
+ &::before {
45
+ background-color: ${rgba(token.colorBgLayout, 0.1)} !important;
46
+ }
47
+ }
41
48
  `,
42
49
  };
43
50
  });
44
51
 
45
52
  const isMac = isMacOS();
46
53
 
47
- const Footer = memo(() => {
54
+ const Footer = memo<{ setExpand?: (expand: boolean) => void }>(({ setExpand }) => {
48
55
  const { t } = useTranslation('chat');
49
56
 
50
57
  const { theme, styles } = useStyles();
@@ -104,12 +111,18 @@ const Footer = memo(() => {
104
111
  <SaveTopic />
105
112
  <Flexbox style={{ minWidth: 92 }}>
106
113
  {loading ? (
107
- <Button icon={loading && <Icon icon={Loader2} spin />} onClick={stopGenerateMessage}>
114
+ <Button icon={loading && <Icon icon={StopCircle} />} onClick={stopGenerateMessage}>
108
115
  {t('input.stop')}
109
116
  </Button>
110
117
  ) : (
111
118
  <Space.Compact>
112
- <Button onClick={() => sendMessage()} type={'primary'}>
119
+ <Button
120
+ onClick={() => {
121
+ sendMessage();
122
+ setExpand?.(false);
123
+ }}
124
+ type={'primary'}
125
+ >
113
126
  {t('input.send')}
114
127
  </Button>
115
128
  <Dropdown
@@ -46,7 +46,7 @@ const ChatInput = memo(() => {
46
46
  >
47
47
  <Head expand={expand} setExpand={setExpand} />
48
48
  <TextArea setExpand={setExpand} />
49
- <Footer />
49
+ <Footer setExpand={setExpand} />
50
50
  </Flexbox>
51
51
  </DraggablePanel>
52
52
  );
@@ -33,7 +33,6 @@ const ShareButton = memo<ShareButtonProps>(({ mobile, setOpen, open }) => {
33
33
  size={mobile ? MOBILE_HEADER_ICON_SIZE : DESKTOP_HEADER_ICON_SIZE}
34
34
  title={t('share')}
35
35
  />
36
-
37
36
  <ShareModal onCancel={() => setIsModalOpen(false)} open={isModalOpen} />
38
37
  </>
39
38
  );
@@ -63,8 +63,10 @@ const Failed = memo<FailedProps>(({ error, state, setUpgradeStatus, setError, up
63
63
  >
64
64
  <ExportConfigButton primary state={state} />
65
65
  <Popconfirm
66
+ arrow={false}
66
67
  okButtonProps={{
67
68
  danger: true,
69
+ type: 'primary',
68
70
  }}
69
71
  onConfirm={clearLocal}
70
72
  overlayInnerStyle={{ background: lighten(0.03, theme.colorBgElevated) }}
@@ -0,0 +1,115 @@
1
+ import { ActionIcon, Icon } from '@lobehub/ui';
2
+ import { App, Dropdown, DropdownProps, MenuProps } from 'antd';
3
+ import { createStyles } from 'antd-style';
4
+ import { MoreVertical, PencilLine, Settings2, Trash } from 'lucide-react';
5
+ import { memo, useMemo } from 'react';
6
+ import { useTranslation } from 'react-i18next';
7
+
8
+ import { useSessionStore } from '@/store/session';
9
+
10
+ const useStyles = createStyles(({ css }) => ({
11
+ modalRoot: css`
12
+ z-index: 2000;
13
+ `,
14
+ }));
15
+ interface ActionsProps extends Pick<DropdownProps, 'onOpenChange'> {
16
+ id?: string;
17
+ isCustomGroup?: boolean;
18
+ openConfigModal: () => void;
19
+ openRenameModal?: () => void;
20
+ }
21
+
22
+ const Actions = memo<ActionsProps>(
23
+ ({ id, openRenameModal, openConfigModal, onOpenChange, isCustomGroup }) => {
24
+ const { t } = useTranslation('chat');
25
+ const { styles } = useStyles();
26
+ const { modal } = App.useApp();
27
+
28
+ const [removeSessionGroup] = useSessionStore((s) => [s.removeSessionGroup]);
29
+ const customGroupItems: MenuProps['items'] = useMemo(
30
+ () => [
31
+ {
32
+ icon: <Icon icon={PencilLine} />,
33
+ key: 'rename',
34
+ label: t('sessionGroup.rename'),
35
+ onClick: ({ domEvent }) => {
36
+ domEvent.stopPropagation();
37
+ openRenameModal?.();
38
+ },
39
+ },
40
+ {
41
+ icon: <Icon icon={Settings2} />,
42
+ key: 'config',
43
+ label: t('sessionGroup.config'),
44
+ onClick: ({ domEvent }) => {
45
+ domEvent.stopPropagation();
46
+ openConfigModal();
47
+ },
48
+ },
49
+ {
50
+ type: 'divider',
51
+ },
52
+ {
53
+ danger: true,
54
+ icon: <Icon icon={Trash} />,
55
+ key: 'delete',
56
+ label: t('delete', { ns: 'common' }),
57
+ onClick: ({ domEvent }) => {
58
+ domEvent.stopPropagation();
59
+ modal.confirm({
60
+ centered: true,
61
+ okButtonProps: { danger: true },
62
+ onOk: () => {
63
+ if (!id) return;
64
+ removeSessionGroup(id);
65
+ },
66
+ rootClassName: styles.modalRoot,
67
+ title: t('sessionGroup.confirmRemoveGroupAlert'),
68
+ });
69
+ },
70
+ },
71
+ ],
72
+ [],
73
+ );
74
+
75
+ const defaultItems: MenuProps['items'] = useMemo(
76
+ () => [
77
+ {
78
+ icon: <Icon icon={Settings2} />,
79
+ key: 'config',
80
+ label: t('sessionGroup.config'),
81
+ onClick: ({ domEvent }) => {
82
+ domEvent.stopPropagation();
83
+ openConfigModal();
84
+ },
85
+ },
86
+ ],
87
+ [],
88
+ );
89
+
90
+ return (
91
+ <Dropdown
92
+ arrow={false}
93
+ menu={{
94
+ items: isCustomGroup ? customGroupItems : defaultItems,
95
+ onClick: ({ domEvent }) => {
96
+ domEvent.stopPropagation();
97
+ },
98
+ }}
99
+ onOpenChange={onOpenChange}
100
+ trigger={['click']}
101
+ >
102
+ <ActionIcon
103
+ icon={MoreVertical}
104
+ onClick={(e) => {
105
+ e.stopPropagation();
106
+ }}
107
+ size={{ blockSize: 22, fontSize: 16 }}
108
+ style={{ marginRight: -8 }}
109
+ />
110
+ </Dropdown>
111
+ );
112
+ },
113
+ );
114
+
115
+ export default Actions;
@@ -18,8 +18,14 @@ const useStyles = createStyles(({ css, prefixCls, token, responsive }) => ({
18
18
  &:hover {
19
19
  color: ${token.colorText} !important;
20
20
  background: ${token.colorFillTertiary};
21
+ .${prefixCls}-collapse-extra {
22
+ display: block;
23
+ }
21
24
  }
22
25
  }
26
+ .${prefixCls}-collapse-extra {
27
+ display: none;
28
+ }
23
29
  .${prefixCls}-collapse-content {
24
30
  border-radius: 0 !important;
25
31
  }
@@ -1,45 +1,74 @@
1
1
  import { CollapseProps } from 'antd';
2
2
  import isEqual from 'fast-deep-equal';
3
- import { memo } from 'react';
3
+ import { memo, useMemo, useState } from 'react';
4
4
  import { useTranslation } from 'react-i18next';
5
5
 
6
6
  import { useGlobalStore } from '@/store/global';
7
7
  import { preferenceSelectors } from '@/store/global/selectors';
8
8
  import { useSessionStore } from '@/store/session';
9
9
  import { sessionSelectors } from '@/store/session/selectors';
10
+ import { SessionDefaultGroup } from '@/types/session';
10
11
 
12
+ import Actions from '../SessionListContent/CollapseGroup/Actions';
11
13
  import CollapseGroup from './CollapseGroup';
12
14
  import Inbox from './Inbox';
13
15
  import SessionList from './List';
16
+ import ConfigGroupModal from './Modals/ConfigGroupModal';
17
+ import RenameGroupModal from './Modals/RenameGroupModal';
14
18
 
15
19
  const SessionListContent = memo(() => {
16
20
  const { t } = useTranslation('chat');
17
- const unpinnedSessionList = useSessionStore(sessionSelectors.unpinnedSessionList, isEqual);
18
- const pinnedList = useSessionStore(sessionSelectors.pinnedSessionList, isEqual);
19
- const [hasPinnedSessionList, useFetchSessions] = useSessionStore((s) => [
20
- sessionSelectors.hasPinnedSessionList(s),
21
- s.useFetchSessions,
22
- ]);
21
+
22
+ const [activeGroupId, setActiveGroupId] = useState<string>();
23
+ const [renameGroupModalOpen, setRenameGroupModalOpen] = useState(false);
24
+ const [configGroupModalOpen, setConfigGroupModalOpen] = useState(false);
25
+
26
+ const [useFetchSessions] = useSessionStore((s) => [s.useFetchSessions]);
27
+ useFetchSessions();
28
+
29
+ const pinnedSessions = useSessionStore(sessionSelectors.pinnedSessions, isEqual);
30
+ const defaultSessions = useSessionStore(sessionSelectors.defaultSessions, isEqual);
31
+ const customSessionGroups = useSessionStore(sessionSelectors.customSessionGroups, isEqual);
23
32
 
24
33
  const [sessionGroupKeys, updatePreference] = useGlobalStore((s) => [
25
34
  preferenceSelectors.sessionGroupKeys(s),
26
35
  s.updatePreference,
27
36
  ]);
28
37
 
29
- useFetchSessions();
30
-
31
- const items = [
32
- hasPinnedSessionList && {
33
- children: <SessionList dataSource={pinnedList} />,
34
- key: 'pinned',
35
- label: t('pin'),
36
- },
37
- {
38
- children: <SessionList dataSource={unpinnedSessionList} />,
39
- key: 'sessionList',
40
- label: t('sessionList'),
41
- },
42
- ].filter(Boolean) as CollapseProps['items'];
38
+ const items = useMemo(
39
+ () =>
40
+ [
41
+ pinnedSessions.length > 0 && {
42
+ children: <SessionList dataSource={pinnedSessions} />,
43
+ extra: <Actions openConfigModal={() => setConfigGroupModalOpen(true)} />,
44
+ key: SessionDefaultGroup.Pinned,
45
+ label: t('pin'),
46
+ },
47
+ ...customSessionGroups.map(({ id, name, children }) => ({
48
+ children: <SessionList dataSource={children} groupId={id} />,
49
+ extra: (
50
+ <Actions
51
+ id={id}
52
+ isCustomGroup
53
+ onOpenChange={(isOpen) => {
54
+ if (isOpen) setActiveGroupId(id);
55
+ }}
56
+ openConfigModal={() => setConfigGroupModalOpen(true)}
57
+ openRenameModal={() => setRenameGroupModalOpen(true)}
58
+ />
59
+ ),
60
+ key: id,
61
+ label: name,
62
+ })),
63
+ {
64
+ children: <SessionList dataSource={defaultSessions} />,
65
+ extra: <Actions openConfigModal={() => setConfigGroupModalOpen(true)} />,
66
+ key: SessionDefaultGroup.Default,
67
+ label: t('defaultList'),
68
+ },
69
+ ].filter(Boolean) as CollapseProps['items'],
70
+ [customSessionGroups, pinnedSessions, defaultSessions],
71
+ );
43
72
 
44
73
  return (
45
74
  <>
@@ -48,11 +77,22 @@ const SessionListContent = memo(() => {
48
77
  activeKey={sessionGroupKeys}
49
78
  items={items}
50
79
  onChange={(keys) => {
51
- const sessionGroupKeys = typeof keys === 'string' ? [keys] : keys;
80
+ const expandSessionGroupKeys = typeof keys === 'string' ? [keys] : keys;
52
81
 
53
- updatePreference({ sessionGroupKeys });
82
+ updatePreference({ expandSessionGroupKeys });
54
83
  }}
55
84
  />
85
+ {activeGroupId && (
86
+ <RenameGroupModal
87
+ id={activeGroupId}
88
+ onCancel={() => setRenameGroupModalOpen(false)}
89
+ open={renameGroupModalOpen}
90
+ />
91
+ )}
92
+ <ConfigGroupModal
93
+ onCancel={() => setConfigGroupModalOpen(false)}
94
+ open={configGroupModalOpen}
95
+ />
56
96
  </>
57
97
  );
58
98
  });
@@ -1,5 +1,5 @@
1
1
  import { Icon } from '@lobehub/ui';
2
- import { Button, type ButtonProps } from 'antd';
2
+ import { Button } from 'antd';
3
3
  import { Plus } from 'lucide-react';
4
4
  import { memo } from 'react';
5
5
  import { useTranslation } from 'react-i18next';
@@ -7,12 +7,12 @@ import { Flexbox } from 'react-layout-kit';
7
7
 
8
8
  import { useSessionStore } from '@/store/session';
9
9
 
10
- const AddButton = memo<ButtonProps>((props) => {
10
+ const AddButton = memo<{ groupId?: string }>(({ groupId }) => {
11
11
  const { t } = useTranslation('chat');
12
12
  const createSession = useSessionStore((s) => s.createSession);
13
13
  return (
14
14
  <Flexbox style={{ margin: '12px 16px' }}>
15
- <Button block icon={<Icon icon={Plus} />} onClick={() => createSession()} {...props}>
15
+ <Button block icon={<Icon icon={Plus} />} onClick={() => createSession({ group: groupId })}>
16
16
  {t('newAgent')}
17
17
  </Button>
18
18
  </Flexbox>
@@ -1,14 +1,26 @@
1
1
  import { ActionIcon, Icon } from '@lobehub/ui';
2
2
  import { App, Dropdown, type MenuProps } from 'antd';
3
3
  import { createStyles } from 'antd-style';
4
- import { HardDriveDownload, LucideCopy, MoreVertical, Pin, PinOff, Trash } from 'lucide-react';
4
+ import isEqual from 'fast-deep-equal';
5
+ import {
6
+ Check,
7
+ HardDriveDownload,
8
+ ListTree,
9
+ LucideCopy,
10
+ LucidePlus,
11
+ MoreVertical,
12
+ Pin,
13
+ PinOff,
14
+ Trash,
15
+ } from 'lucide-react';
5
16
  import { memo, useMemo } from 'react';
6
17
  import { useTranslation } from 'react-i18next';
7
18
 
8
19
  import { configService } from '@/services/config';
9
20
  import { useSessionStore } from '@/store/session';
10
21
  import { sessionHelpers } from '@/store/session/helpers';
11
- import { sessionSelectors } from '@/store/session/selectors';
22
+ import { sessionGroupSelectors, sessionSelectors } from '@/store/session/selectors';
23
+ import { SessionDefaultGroup } from '@/types/session';
12
24
 
13
25
  const useStyles = createStyles(({ css }) => ({
14
26
  modalRoot: css`
@@ -17,27 +29,35 @@ const useStyles = createStyles(({ css }) => ({
17
29
  }));
18
30
 
19
31
  interface ActionProps {
32
+ group: string | undefined;
20
33
  id: string;
34
+ openCreateGroupModal: () => void;
21
35
  setOpen: (open: boolean) => void;
22
36
  }
23
37
 
24
- const Actions = memo<ActionProps>(({ id, setOpen }) => {
25
- const { t } = useTranslation('common');
26
-
38
+ const Actions = memo<ActionProps>(({ group, id, openCreateGroupModal, setOpen }) => {
27
39
  const { styles } = useStyles();
40
+ const { t } = useTranslation('chat');
28
41
 
29
- const [pin, removeSession, pinSession, duplicateSession] = useSessionStore((s) => {
30
- const session = sessionSelectors.getSessionById(id)(s);
31
- return [
32
- sessionHelpers.getSessionPinned(session),
33
- s.removeSession,
34
- s.pinSession,
35
- s.duplicateSession,
36
- ];
37
- });
42
+ const sessionCustomGroups = useSessionStore(sessionGroupSelectors.sessionGroupItems, isEqual);
43
+ const [pin, removeSession, pinSession, duplicateSession, updateSessionGroup] = useSessionStore(
44
+ (s) => {
45
+ const session = sessionSelectors.getSessionById(id)(s);
46
+ return [
47
+ sessionHelpers.getSessionPinned(session),
48
+ s.removeSession,
49
+ s.pinSession,
50
+ s.duplicateSession,
51
+ s.updateSessionGroupId,
52
+ ];
53
+ },
54
+ );
38
55
 
39
56
  const { modal } = App.useApp();
40
57
 
58
+ const isDefault = group === SessionDefaultGroup.Default;
59
+ // const hasDivider = !isDefault || Object.keys(sessionByGroup).length > 0;
60
+
41
61
  const items: MenuProps['items'] = useMemo(
42
62
  () => [
43
63
  {
@@ -48,18 +68,69 @@ const Actions = memo<ActionProps>(({ id, setOpen }) => {
48
68
  pinSession(id, !pin);
49
69
  },
50
70
  },
71
+ {
72
+ icon: <Icon icon={LucideCopy} />,
73
+ key: 'duplicate',
74
+ label: t('duplicate', { ns: 'common' }),
75
+ onClick: ({ domEvent }) => {
76
+ domEvent.stopPropagation();
77
+
78
+ duplicateSession(id);
79
+ },
80
+ },
81
+ {
82
+ type: 'divider',
83
+ },
84
+ {
85
+ children: [
86
+ ...sessionCustomGroups.map(({ id: groupId, name }) => ({
87
+ icon: group === groupId ? <Icon icon={Check} /> : <div />,
88
+ key: groupId,
89
+ label: name,
90
+ onClick: () => {
91
+ updateSessionGroup(id, groupId);
92
+ },
93
+ })),
94
+ {
95
+ icon: isDefault ? <Icon icon={Check} /> : <div />,
96
+ key: 'defaultList',
97
+ label: t('defaultList'),
98
+ onClick: () => {
99
+ updateSessionGroup(id, SessionDefaultGroup.Default);
100
+ },
101
+ },
102
+ {
103
+ type: 'divider',
104
+ },
105
+ {
106
+ icon: <Icon icon={LucidePlus} />,
107
+ key: 'createGroup',
108
+ label: <div>{t('sessionGroup.createGroup')}</div>,
109
+ onClick: ({ domEvent }) => {
110
+ domEvent.stopPropagation();
111
+ openCreateGroupModal();
112
+ },
113
+ },
114
+ ],
115
+ icon: <Icon icon={ListTree} />,
116
+ key: 'moveGroup',
117
+ label: t('sessionGroup.moveGroup'),
118
+ },
119
+ {
120
+ type: 'divider',
121
+ },
51
122
  {
52
123
  children: [
53
124
  {
54
125
  key: 'agent',
55
- label: <div>{t('exportType.agent')}</div>,
126
+ label: t('exportType.agent', { ns: 'common' }),
56
127
  onClick: () => {
57
128
  configService.exportSingleAgent(id);
58
129
  },
59
130
  },
60
131
  {
61
132
  key: 'agentWithMessage',
62
- label: <div>{t('exportType.agentWithMessage')}</div>,
133
+ label: t('exportType.agentWithMessage', { ns: 'common' }),
63
134
  onClick: () => {
64
135
  configService.exportSingleSession(id);
65
136
  },
@@ -67,32 +138,15 @@ const Actions = memo<ActionProps>(({ id, setOpen }) => {
67
138
  ],
68
139
  icon: <Icon icon={HardDriveDownload} />,
69
140
  key: 'export',
70
- label: t('export'),
71
- },
72
- {
73
- type: 'divider',
74
- },
75
- {
76
- icon: <Icon icon={LucideCopy} />,
77
- key: 'duplicate',
78
- label: t('duplicate'),
79
- onClick: ({ domEvent }) => {
80
- domEvent.stopPropagation();
81
-
82
- duplicateSession(id);
83
- },
84
- },
85
- {
86
- type: 'divider',
141
+ label: t('export', { ns: 'common' }),
87
142
  },
88
143
  {
89
144
  danger: true,
90
145
  icon: <Icon icon={Trash} />,
91
146
  key: 'delete',
92
- label: t('delete'),
147
+ label: t('delete', { ns: 'common' }),
93
148
  onClick: ({ domEvent }) => {
94
149
  domEvent.stopPropagation();
95
-
96
150
  modal.confirm({
97
151
  centered: true,
98
152
  okButtonProps: { danger: true },