@lobehub/chat 1.75.4 → 1.76.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/CHANGELOG.md +52 -0
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/changelog/v1.json +18 -0
- package/docs/developer/database-schema.dbml +1 -0
- package/docs/self-hosting/advanced/model-list.mdx +5 -3
- package/docs/self-hosting/advanced/model-list.zh-CN.mdx +5 -3
- package/docs/usage/providers/infiniai.zh-CN.mdx +4 -0
- package/locales/ar/hotkey.json +46 -0
- package/locales/ar/models.json +51 -54
- package/locales/ar/providers.json +3 -0
- package/locales/ar/setting.json +12 -0
- package/locales/bg-BG/hotkey.json +46 -0
- package/locales/bg-BG/models.json +51 -54
- package/locales/bg-BG/providers.json +3 -0
- package/locales/bg-BG/setting.json +12 -0
- package/locales/de-DE/hotkey.json +46 -0
- package/locales/de-DE/models.json +51 -54
- package/locales/de-DE/providers.json +3 -0
- package/locales/de-DE/setting.json +12 -0
- package/locales/en-US/hotkey.json +46 -0
- package/locales/en-US/models.json +51 -54
- package/locales/en-US/providers.json +3 -0
- package/locales/en-US/setting.json +12 -0
- package/locales/es-ES/hotkey.json +46 -0
- package/locales/es-ES/models.json +51 -54
- package/locales/es-ES/providers.json +3 -0
- package/locales/es-ES/setting.json +12 -0
- package/locales/fa-IR/hotkey.json +46 -0
- package/locales/fa-IR/models.json +51 -54
- package/locales/fa-IR/providers.json +3 -0
- package/locales/fa-IR/setting.json +12 -0
- package/locales/fr-FR/hotkey.json +46 -0
- package/locales/fr-FR/models.json +51 -54
- package/locales/fr-FR/providers.json +3 -0
- package/locales/fr-FR/setting.json +12 -0
- package/locales/it-IT/hotkey.json +46 -0
- package/locales/it-IT/models.json +51 -54
- package/locales/it-IT/providers.json +3 -0
- package/locales/it-IT/setting.json +12 -0
- package/locales/ja-JP/hotkey.json +46 -0
- package/locales/ja-JP/models.json +51 -54
- package/locales/ja-JP/providers.json +3 -0
- package/locales/ja-JP/setting.json +12 -0
- package/locales/ko-KR/hotkey.json +46 -0
- package/locales/ko-KR/models.json +51 -54
- package/locales/ko-KR/providers.json +3 -0
- package/locales/ko-KR/setting.json +12 -0
- package/locales/nl-NL/hotkey.json +46 -0
- package/locales/nl-NL/models.json +51 -54
- package/locales/nl-NL/providers.json +3 -0
- package/locales/nl-NL/setting.json +12 -0
- package/locales/pl-PL/hotkey.json +46 -0
- package/locales/pl-PL/models.json +51 -54
- package/locales/pl-PL/providers.json +3 -0
- package/locales/pl-PL/setting.json +12 -0
- package/locales/pt-BR/hotkey.json +46 -0
- package/locales/pt-BR/models.json +51 -54
- package/locales/pt-BR/providers.json +3 -0
- package/locales/pt-BR/setting.json +12 -0
- package/locales/ru-RU/hotkey.json +46 -0
- package/locales/ru-RU/models.json +51 -54
- package/locales/ru-RU/providers.json +3 -0
- package/locales/ru-RU/setting.json +12 -0
- package/locales/tr-TR/hotkey.json +46 -0
- package/locales/tr-TR/models.json +51 -54
- package/locales/tr-TR/providers.json +3 -0
- package/locales/tr-TR/setting.json +12 -0
- package/locales/vi-VN/hotkey.json +46 -0
- package/locales/vi-VN/models.json +51 -54
- package/locales/vi-VN/providers.json +3 -0
- package/locales/vi-VN/setting.json +12 -0
- package/locales/zh-CN/hotkey.json +46 -0
- package/locales/zh-CN/models.json +55 -58
- package/locales/zh-CN/providers.json +3 -0
- package/locales/zh-CN/setting.json +12 -0
- package/locales/zh-TW/hotkey.json +46 -0
- package/locales/zh-TW/models.json +51 -54
- package/locales/zh-TW/providers.json +3 -0
- package/locales/zh-TW/setting.json +12 -0
- package/package.json +3 -3
- package/src/app/[variants]/(main)/(mobile)/me/(home)/features/Category.tsx +1 -1
- package/src/app/[variants]/(main)/(mobile)/me/(home)/layout.tsx +3 -2
- package/src/app/[variants]/(main)/(mobile)/me/data/features/Category.tsx +1 -1
- package/src/app/[variants]/(main)/(mobile)/me/profile/features/Category.tsx +1 -1
- package/src/app/[variants]/(main)/(mobile)/me/settings/features/Category.tsx +1 -1
- package/src/app/[variants]/(main)/_layout/Desktop/RegisterHotkeys.tsx +11 -0
- package/src/app/[variants]/(main)/_layout/Desktop/SideBar/PinList/index.tsx +6 -23
- package/src/app/[variants]/(main)/_layout/Desktop/SideBar/TopActions.test.tsx +2 -0
- package/src/app/[variants]/(main)/_layout/Desktop/index.tsx +11 -4
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/Footer/SendMore.tsx +6 -21
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/Footer/ShortcutHint.tsx +13 -34
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/Footer/index.tsx +1 -1
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ZenModeToast/Toast.tsx +7 -4
- package/src/app/[variants]/(main)/chat/(workspace)/_layout/Desktop/ChatHeader/HeaderAction.tsx +12 -8
- package/src/app/[variants]/(main)/chat/(workspace)/_layout/Desktop/ChatHeader/Main.tsx +24 -30
- package/src/app/[variants]/(main)/chat/(workspace)/_layout/Desktop/index.tsx +0 -2
- package/src/app/[variants]/(main)/chat/(workspace)/features/SettingButton.tsx +12 -7
- package/src/app/[variants]/(main)/chat/@session/features/SessionSearchBar.tsx +5 -1
- package/src/app/[variants]/(main)/chat/_layout/Desktop/RegisterHotkeys.tsx +10 -0
- package/src/app/[variants]/(main)/chat/_layout/Desktop/index.tsx +5 -0
- package/src/app/[variants]/(main)/chat/_layout/Mobile.tsx +1 -1
- package/src/app/[variants]/(main)/discover/features/StoreSearchBar.tsx +5 -1
- package/src/app/[variants]/(main)/settings/hooks/useCategory.tsx +31 -21
- package/src/app/[variants]/(main)/settings/hotkey/features/HotkeySetting.tsx +80 -0
- package/src/app/[variants]/(main)/settings/hotkey/index.tsx +9 -0
- package/src/app/[variants]/(main)/settings/hotkey/page.tsx +15 -0
- package/src/app/[variants]/layout.tsx +16 -13
- package/src/config/aiModels/infiniai.ts +52 -55
- package/src/config/aiModels/siliconcloud.ts +17 -1
- package/src/config/aiModels/tencentcloud.ts +17 -0
- package/src/const/hotkeys.ts +80 -10
- package/src/const/settings/hotkey.ts +10 -0
- package/src/const/settings/index.ts +3 -0
- package/src/database/client/migrations.json +46 -32
- package/src/database/migrations/0019_add_hotkey_user_settings.sql +2 -0
- package/src/database/migrations/meta/0019_snapshot.json +4218 -0
- package/src/database/migrations/meta/_journal.json +7 -0
- package/src/database/schemas/user.ts +1 -0
- package/src/database/server/models/user.ts +2 -0
- package/src/features/ChatInput/Desktop/InputArea/index.tsx +8 -0
- package/src/features/ChatInput/Desktop/index.tsx +0 -1
- package/src/features/ChatInput/Topic/index.tsx +10 -15
- package/src/features/FileManager/Header/FilesSearchBar.tsx +6 -2
- package/src/features/HotkeyHelperPanel/HotkeyContent.tsx +62 -0
- package/src/features/HotkeyHelperPanel/index.tsx +59 -0
- package/src/hooks/useHotkeys/chatScope.ts +105 -0
- package/src/hooks/useHotkeys/globalScope.ts +69 -0
- package/src/hooks/useHotkeys/index.ts +2 -0
- package/src/hooks/useHotkeys/useHotkeyById.test.ts +194 -0
- package/src/hooks/useHotkeys/useHotkeyById.ts +57 -0
- package/src/libs/agent-runtime/infiniai/index.ts +38 -3
- package/src/locales/default/hotkey.ts +50 -0
- package/src/locales/default/index.ts +2 -0
- package/src/locales/default/setting.ts +12 -0
- package/src/store/global/initialState.ts +3 -0
- package/src/store/user/slices/settings/selectors/__snapshots__/settings.test.ts.snap +79 -0
- package/src/store/user/slices/settings/selectors/settings.test.ts +131 -0
- package/src/store/user/slices/settings/selectors/settings.ts +6 -0
- package/src/types/hotkey.ts +59 -0
- package/src/types/user/settings/hotkey.ts +3 -0
- package/src/types/user/settings/index.ts +3 -0
- package/src/utils/format.ts +1 -1
- package/src/utils/parseModels.test.ts +14 -0
- package/src/utils/parseModels.ts +4 -0
- package/src/app/[variants]/(main)/chat/(workspace)/_layout/Desktop/HotKeys.tsx +0 -44
- package/src/components/HotKeys/index.tsx +0 -77
@@ -133,6 +133,13 @@
|
|
133
133
|
"when": 1742616026643,
|
134
134
|
"tag": "0018_add_client_id_for_entities",
|
135
135
|
"breakpoints": true
|
136
|
+
},
|
137
|
+
{
|
138
|
+
"idx": 19,
|
139
|
+
"version": "7",
|
140
|
+
"when": 1742806552131,
|
141
|
+
"tag": "0019_add_hotkey_user_settings",
|
142
|
+
"breakpoints": true
|
136
143
|
}
|
137
144
|
],
|
138
145
|
"version": "6"
|
@@ -67,6 +67,7 @@ export class UserModel {
|
|
67
67
|
|
68
68
|
settingsDefaultAgent: userSettings.defaultAgent,
|
69
69
|
settingsGeneral: userSettings.general,
|
70
|
+
settingsHotkey: userSettings.hotkey,
|
70
71
|
settingsKeyVaults: userSettings.keyVaults,
|
71
72
|
settingsLanguageModel: userSettings.languageModel,
|
72
73
|
settingsSystemAgent: userSettings.systemAgent,
|
@@ -89,6 +90,7 @@ export class UserModel {
|
|
89
90
|
const settings: DeepPartial<UserSettings> = {
|
90
91
|
defaultAgent: state.settingsDefaultAgent || {},
|
91
92
|
general: state.settingsGeneral || {},
|
93
|
+
hotkey: state.settingsHotkey || {},
|
92
94
|
keyVaults: decryptKeyVaults,
|
93
95
|
languageModel: state.settingsLanguageModel || {},
|
94
96
|
systemAgent: state.settingsSystemAgent || {},
|
@@ -2,10 +2,12 @@ import { TextArea } from '@lobehub/ui';
|
|
2
2
|
import { createStyles } from 'antd-style';
|
3
3
|
import { TextAreaRef } from 'antd/es/input/TextArea';
|
4
4
|
import { RefObject, memo, useEffect, useRef } from 'react';
|
5
|
+
import { useHotkeysContext } from 'react-hotkeys-hook';
|
5
6
|
import { useTranslation } from 'react-i18next';
|
6
7
|
|
7
8
|
import { useUserStore } from '@/store/user';
|
8
9
|
import { preferenceSelectors } from '@/store/user/selectors';
|
10
|
+
import { HotkeyEnum } from '@/types/hotkey';
|
9
11
|
import { isCommandPressed } from '@/utils/keyboard';
|
10
12
|
|
11
13
|
import { useAutoFocus } from '../useAutoFocus';
|
@@ -38,8 +40,10 @@ interface InputAreaProps {
|
|
38
40
|
}
|
39
41
|
|
40
42
|
const InputArea = memo<InputAreaProps>(({ onSend, value, loading, onChange }) => {
|
43
|
+
const { enableScope, disableScope } = useHotkeysContext();
|
41
44
|
const { t } = useTranslation('chat');
|
42
45
|
const { styles } = useStyles();
|
46
|
+
|
43
47
|
const ref = useRef<TextAreaRef>(null);
|
44
48
|
const isChineseInput = useRef(false);
|
45
49
|
|
@@ -71,6 +75,7 @@ const InputArea = memo<InputAreaProps>(({ onSend, value, loading, onChange }) =>
|
|
71
75
|
className={styles.textarea}
|
72
76
|
onBlur={(e) => {
|
73
77
|
onChange?.(e.target.value);
|
78
|
+
disableScope(HotkeyEnum.AddUserMessage);
|
74
79
|
}}
|
75
80
|
onChange={(e) => {
|
76
81
|
onChange?.(e.target.value);
|
@@ -81,6 +86,9 @@ const InputArea = memo<InputAreaProps>(({ onSend, value, loading, onChange }) =>
|
|
81
86
|
onCompositionStart={() => {
|
82
87
|
isChineseInput.current = true;
|
83
88
|
}}
|
89
|
+
onFocus={() => {
|
90
|
+
enableScope(HotkeyEnum.AddUserMessage);
|
91
|
+
}}
|
84
92
|
onPressEnter={(e) => {
|
85
93
|
if (loading || e.altKey || e.shiftKey || isChineseInput.current) return;
|
86
94
|
|
@@ -1,17 +1,19 @@
|
|
1
|
-
import { ActionIcon, Icon, Tooltip } from '@lobehub/ui';
|
1
|
+
import { ActionIcon, Hotkey, Icon, Tooltip } from '@lobehub/ui';
|
2
2
|
import { Button, Popconfirm } from 'antd';
|
3
3
|
import { LucideGalleryVerticalEnd, LucideMessageSquarePlus } from 'lucide-react';
|
4
4
|
import { memo, useState } from 'react';
|
5
|
-
import { useHotkeys } from 'react-hotkeys-hook';
|
6
5
|
import { useTranslation } from 'react-i18next';
|
6
|
+
import { Flexbox } from 'react-layout-kit';
|
7
7
|
|
8
|
-
import HotKeys from '@/components/HotKeys';
|
9
|
-
import { ALT_KEY, SAVE_TOPIC_KEY } from '@/const/hotkeys';
|
10
8
|
import { useActionSWR } from '@/libs/swr';
|
11
9
|
import { useChatStore } from '@/store/chat';
|
10
|
+
import { useUserStore } from '@/store/user';
|
11
|
+
import { settingsSelectors } from '@/store/user/selectors';
|
12
|
+
import { HotkeyEnum } from '@/types/hotkey';
|
12
13
|
|
13
14
|
const SaveTopic = memo<{ mobile?: boolean }>(({ mobile }) => {
|
14
15
|
const { t } = useTranslation('chat');
|
16
|
+
const hotkey = useUserStore(settingsSelectors.getHotkeyById(HotkeyEnum.SaveTopic));
|
15
17
|
const [hasTopic, openNewTopicOrSaveTopic] = useChatStore((s) => [
|
16
18
|
!!s.activeTopicId,
|
17
19
|
s.openNewTopicOrSaveTopic,
|
@@ -24,13 +26,6 @@ const SaveTopic = memo<{ mobile?: boolean }>(({ mobile }) => {
|
|
24
26
|
const icon = hasTopic ? LucideMessageSquarePlus : LucideGalleryVerticalEnd;
|
25
27
|
const desc = t(hasTopic ? 'topic.openNewTopic' : 'topic.saveCurrentMessages');
|
26
28
|
|
27
|
-
const hotkeys = [ALT_KEY, SAVE_TOPIC_KEY].join('+');
|
28
|
-
|
29
|
-
useHotkeys(hotkeys, () => mutate(), {
|
30
|
-
enableOnFormTags: true,
|
31
|
-
preventDefault: true,
|
32
|
-
});
|
33
|
-
|
34
29
|
if (mobile) {
|
35
30
|
return (
|
36
31
|
<Popconfirm
|
@@ -41,12 +36,12 @@ const SaveTopic = memo<{ mobile?: boolean }>(({ mobile }) => {
|
|
41
36
|
open={confirmOpened}
|
42
37
|
placement={'top'}
|
43
38
|
title={
|
44
|
-
<
|
39
|
+
<Flexbox align={'center'} horizontal style={{ marginBottom: 8 }}>
|
45
40
|
<div style={{ marginRight: '16px', whiteSpace: 'pre-line', wordBreak: 'break-word' }}>
|
46
41
|
{t(hasTopic ? 'topic.checkOpenNewTopic' : 'topic.checkSaveCurrentMessages')}
|
47
42
|
</div>
|
48
|
-
<
|
49
|
-
</
|
43
|
+
<Hotkey keys={hotkey} />
|
44
|
+
</Flexbox>
|
50
45
|
}
|
51
46
|
>
|
52
47
|
<ActionIcon
|
@@ -59,7 +54,7 @@ const SaveTopic = memo<{ mobile?: boolean }>(({ mobile }) => {
|
|
59
54
|
);
|
60
55
|
} else {
|
61
56
|
return (
|
62
|
-
<Tooltip
|
57
|
+
<Tooltip hotkey={hotkey} title={desc}>
|
63
58
|
<Button
|
64
59
|
aria-label={desc}
|
65
60
|
icon={<Icon icon={icon} />}
|
@@ -5,9 +5,13 @@ import { useQueryState } from 'nuqs';
|
|
5
5
|
import { memo, useState } from 'react';
|
6
6
|
import { useTranslation } from 'react-i18next';
|
7
7
|
|
8
|
+
import { useUserStore } from '@/store/user';
|
9
|
+
import { settingsSelectors } from '@/store/user/selectors';
|
10
|
+
import { HotkeyEnum } from '@/types/hotkey';
|
11
|
+
|
8
12
|
const FilesSearchBar = memo<{ mobile?: boolean }>(({ mobile }) => {
|
9
13
|
const { t } = useTranslation('file');
|
10
|
-
|
14
|
+
const hotkey = useUserStore(settingsSelectors.getHotkeyById(HotkeyEnum.Search));
|
11
15
|
const [keywords, setKeywords] = useState<string>('');
|
12
16
|
|
13
17
|
const [, setQuery] = useQueryState('q', {
|
@@ -30,7 +34,7 @@ const FilesSearchBar = memo<{ mobile?: boolean }>(({ mobile }) => {
|
|
30
34
|
setQuery(keywords);
|
31
35
|
}}
|
32
36
|
placeholder={t('searchFilePlaceholder')}
|
33
|
-
shortKey={
|
37
|
+
shortKey={hotkey}
|
34
38
|
spotlight={!mobile}
|
35
39
|
style={{ width: 320 }}
|
36
40
|
value={keywords}
|
@@ -0,0 +1,62 @@
|
|
1
|
+
import { Hotkey } from '@lobehub/ui';
|
2
|
+
import { createStyles } from 'antd-style';
|
3
|
+
import isEqual from 'fast-deep-equal';
|
4
|
+
import { memo } from 'react';
|
5
|
+
import { useTranslation } from 'react-i18next';
|
6
|
+
import { Flexbox } from 'react-layout-kit';
|
7
|
+
|
8
|
+
import { HOTKEYS_REGISTRATION } from '@/const/hotkeys';
|
9
|
+
import hotkeyMeta from '@/locales/default/hotkey';
|
10
|
+
import { useUserStore } from '@/store/user';
|
11
|
+
import { settingsSelectors } from '@/store/user/slices/settings/selectors';
|
12
|
+
import { HotkeyGroupId } from '@/types/hotkey';
|
13
|
+
|
14
|
+
const useStyle = createStyles(({ css, token }) => ({
|
15
|
+
desc: css`
|
16
|
+
font-size: 12px;
|
17
|
+
line-height: 1.3;
|
18
|
+
color: ${token.colorTextDescription};
|
19
|
+
`,
|
20
|
+
hotkey: css`
|
21
|
+
gap: 4px;
|
22
|
+
|
23
|
+
kbd {
|
24
|
+
min-width: 26px;
|
25
|
+
height: 26px;
|
26
|
+
border: 1px solid ${token.colorBorder};
|
27
|
+
}
|
28
|
+
`,
|
29
|
+
}));
|
30
|
+
|
31
|
+
interface HotkeyContentProps {
|
32
|
+
groupId: HotkeyGroupId;
|
33
|
+
}
|
34
|
+
|
35
|
+
const HotkeyContent = memo<HotkeyContentProps>(({ groupId }) => {
|
36
|
+
const settings = useUserStore(settingsSelectors.currentSettings, isEqual);
|
37
|
+
const { t } = useTranslation('hotkey');
|
38
|
+
const { styles } = useStyle();
|
39
|
+
return (
|
40
|
+
<>
|
41
|
+
{HOTKEYS_REGISTRATION.filter((item) => item.group === groupId).map((item) => (
|
42
|
+
<Flexbox align={'flex-start'} gap={16} horizontal key={item.id} width={'100%'}>
|
43
|
+
<Flexbox flex={1} gap={4} justify={'space-between'}>
|
44
|
+
<span>{t(`${item.id}.title`)}</span>
|
45
|
+
{hotkeyMeta[item.id].desc ? (
|
46
|
+
<span className={styles.desc}>{t(`${item.id}.desc`)}</span>
|
47
|
+
) : null}
|
48
|
+
</Flexbox>
|
49
|
+
<Hotkey
|
50
|
+
className={styles.hotkey}
|
51
|
+
keys={settings.hotkey[item.id]}
|
52
|
+
style={{
|
53
|
+
zoom: 1.1,
|
54
|
+
}}
|
55
|
+
/>
|
56
|
+
</Flexbox>
|
57
|
+
))}
|
58
|
+
</>
|
59
|
+
);
|
60
|
+
});
|
61
|
+
|
62
|
+
export default HotkeyContent;
|
@@ -0,0 +1,59 @@
|
|
1
|
+
'use client';
|
2
|
+
|
3
|
+
import { Drawer, Grid, TabsNav } from '@lobehub/ui';
|
4
|
+
import { memo, useState } from 'react';
|
5
|
+
import { useTranslation } from 'react-i18next';
|
6
|
+
|
7
|
+
import { useGlobalStore } from '@/store/global';
|
8
|
+
import { HotkeyGroupEnum, HotkeyGroupId } from '@/types/hotkey';
|
9
|
+
|
10
|
+
import HotkeyContent from './HotkeyContent';
|
11
|
+
|
12
|
+
const HotkeyHelperPanel = memo(() => {
|
13
|
+
const [open, updateSystemStatus] = useGlobalStore((s) => [
|
14
|
+
s.status.showHotkeyHelper,
|
15
|
+
s.updateSystemStatus,
|
16
|
+
]);
|
17
|
+
const [active, setActive] = useState<HotkeyGroupId>(HotkeyGroupEnum.Essential);
|
18
|
+
const { t } = useTranslation('setting');
|
19
|
+
|
20
|
+
const handleClose = () => updateSystemStatus({ showHotkeyHelper: false });
|
21
|
+
|
22
|
+
return (
|
23
|
+
<Drawer
|
24
|
+
height={240}
|
25
|
+
mask={false}
|
26
|
+
maskClosable={false}
|
27
|
+
onClose={handleClose}
|
28
|
+
open={open}
|
29
|
+
placement={'bottom'}
|
30
|
+
styles={{
|
31
|
+
bodyContent: { paddingBlock: 24 },
|
32
|
+
title: { paddingBlock: 0 },
|
33
|
+
}}
|
34
|
+
title={
|
35
|
+
<TabsNav
|
36
|
+
activeKey={active}
|
37
|
+
items={[
|
38
|
+
{
|
39
|
+
key: HotkeyGroupEnum.Essential,
|
40
|
+
label: t('hotkey.group.essential'),
|
41
|
+
},
|
42
|
+
{
|
43
|
+
key: HotkeyGroupEnum.Conversation,
|
44
|
+
label: t('hotkey.group.conversation'),
|
45
|
+
},
|
46
|
+
]}
|
47
|
+
onChange={(key) => setActive(key as HotkeyGroupId)}
|
48
|
+
variant={'compact'}
|
49
|
+
/>
|
50
|
+
}
|
51
|
+
>
|
52
|
+
<Grid gap={32}>
|
53
|
+
<HotkeyContent groupId={active} />
|
54
|
+
</Grid>
|
55
|
+
</Drawer>
|
56
|
+
);
|
57
|
+
});
|
58
|
+
|
59
|
+
export default HotkeyHelperPanel;
|
@@ -0,0 +1,105 @@
|
|
1
|
+
import isEqual from 'fast-deep-equal';
|
2
|
+
import { parseAsBoolean, useQueryState } from 'nuqs';
|
3
|
+
import { useEffect } from 'react';
|
4
|
+
import { useHotkeysContext } from 'react-hotkeys-hook';
|
5
|
+
|
6
|
+
import { useSendMessage } from '@/features/ChatInput/useSend';
|
7
|
+
import { useOpenChatSettings } from '@/hooks/useInterceptingRoutes';
|
8
|
+
import { useActionSWR } from '@/libs/swr';
|
9
|
+
import { useChatStore } from '@/store/chat';
|
10
|
+
import { chatSelectors } from '@/store/chat/selectors';
|
11
|
+
import { useGlobalStore } from '@/store/global';
|
12
|
+
import { systemStatusSelectors } from '@/store/global/selectors';
|
13
|
+
import { HotkeyEnum, HotkeyScopeEnum } from '@/types/hotkey';
|
14
|
+
|
15
|
+
import { useHotkeyById } from './useHotkeyById';
|
16
|
+
|
17
|
+
export const useSaveTopicHotkey = () => {
|
18
|
+
const openNewTopicOrSaveTopic = useChatStore((s) => s.openNewTopicOrSaveTopic);
|
19
|
+
const { mutate } = useActionSWR('openNewTopicOrSaveTopic', openNewTopicOrSaveTopic);
|
20
|
+
return useHotkeyById(HotkeyEnum.SaveTopic, () => mutate());
|
21
|
+
};
|
22
|
+
|
23
|
+
export const useToggleZenModeHotkey = () => {
|
24
|
+
const toggleZenMode = useGlobalStore((s) => s.toggleZenMode);
|
25
|
+
return useHotkeyById(HotkeyEnum.ToggleZenMode, toggleZenMode);
|
26
|
+
};
|
27
|
+
|
28
|
+
export const useOpenChatSettingsHotkey = () => {
|
29
|
+
const openChatSettings = useOpenChatSettings();
|
30
|
+
return useHotkeyById(HotkeyEnum.OpenChatSettings, openChatSettings);
|
31
|
+
};
|
32
|
+
|
33
|
+
export const useRegenerateMessageHotkey = () => {
|
34
|
+
const regenerateMessage = useChatStore((s) => s.regenerateMessage);
|
35
|
+
const lastMessage = useChatStore(chatSelectors.latestMessage, isEqual);
|
36
|
+
|
37
|
+
const disable = !lastMessage || lastMessage.id === 'default' || lastMessage.role === 'system';
|
38
|
+
|
39
|
+
return useHotkeyById(
|
40
|
+
HotkeyEnum.RegenerateMessage,
|
41
|
+
() => !disable && regenerateMessage(lastMessage.id),
|
42
|
+
{
|
43
|
+
enabled: !disable,
|
44
|
+
},
|
45
|
+
);
|
46
|
+
};
|
47
|
+
|
48
|
+
export const useToggleLeftPanelHotkey = () => {
|
49
|
+
const isZenMode = useGlobalStore((s) => s.status.zenMode);
|
50
|
+
const [isPinned] = useQueryState('pinned', parseAsBoolean);
|
51
|
+
const showSessionPanel = useGlobalStore(systemStatusSelectors.showSessionPanel);
|
52
|
+
const updateSystemStatus = useGlobalStore((s) => s.updateSystemStatus);
|
53
|
+
|
54
|
+
return useHotkeyById(
|
55
|
+
HotkeyEnum.ToggleLeftPanel,
|
56
|
+
() =>
|
57
|
+
updateSystemStatus({
|
58
|
+
sessionsWidth: showSessionPanel ? 0 : 320,
|
59
|
+
showSessionPanel: !showSessionPanel,
|
60
|
+
}),
|
61
|
+
{
|
62
|
+
enabled: !isZenMode && !isPinned,
|
63
|
+
},
|
64
|
+
);
|
65
|
+
};
|
66
|
+
|
67
|
+
export const useToggleRightPanelHotkey = () => {
|
68
|
+
const isZenMode = useGlobalStore((s) => s.status.zenMode);
|
69
|
+
const toggleConfig = useGlobalStore((s) => s.toggleChatSideBar);
|
70
|
+
|
71
|
+
return useHotkeyById(HotkeyEnum.ToggleRightPanel, () => toggleConfig(), {
|
72
|
+
enabled: !isZenMode,
|
73
|
+
});
|
74
|
+
};
|
75
|
+
|
76
|
+
export const useAddUserMessageHotkey = () => {
|
77
|
+
const { send } = useSendMessage();
|
78
|
+
return useHotkeyById(HotkeyEnum.AddUserMessage, () => send({ onlyAddUserMessage: true }));
|
79
|
+
};
|
80
|
+
|
81
|
+
// 注册聚合
|
82
|
+
|
83
|
+
export const useRegisterChatHotkeys = () => {
|
84
|
+
const { enableScope, disableScope } = useHotkeysContext();
|
85
|
+
|
86
|
+
// System
|
87
|
+
useOpenChatSettingsHotkey();
|
88
|
+
|
89
|
+
// Layout
|
90
|
+
useToggleLeftPanelHotkey();
|
91
|
+
useToggleRightPanelHotkey();
|
92
|
+
useToggleZenModeHotkey();
|
93
|
+
|
94
|
+
// Conversation
|
95
|
+
useRegenerateMessageHotkey();
|
96
|
+
useSaveTopicHotkey();
|
97
|
+
useAddUserMessageHotkey();
|
98
|
+
|
99
|
+
useEffect(() => {
|
100
|
+
enableScope(HotkeyScopeEnum.Chat);
|
101
|
+
return () => disableScope(HotkeyScopeEnum.Chat);
|
102
|
+
}, []);
|
103
|
+
|
104
|
+
return null;
|
105
|
+
};
|
@@ -0,0 +1,69 @@
|
|
1
|
+
import isEqual from 'fast-deep-equal';
|
2
|
+
import { parseAsBoolean, useQueryState } from 'nuqs';
|
3
|
+
import { useHotkeys } from 'react-hotkeys-hook';
|
4
|
+
|
5
|
+
import { useSwitchSession } from '@/hooks/useSwitchSession';
|
6
|
+
import { useGlobalStore } from '@/store/global';
|
7
|
+
import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig';
|
8
|
+
import { useSessionStore } from '@/store/session';
|
9
|
+
import { sessionSelectors } from '@/store/session/selectors';
|
10
|
+
import { useUserStore } from '@/store/user';
|
11
|
+
import { settingsSelectors } from '@/store/user/selectors';
|
12
|
+
import { HotkeyEnum, HotkeyScopeEnum, KeyEnum } from '@/types/hotkey';
|
13
|
+
|
14
|
+
import { useHotkeyById } from './useHotkeyById';
|
15
|
+
|
16
|
+
export const useSwitchAgentHotkey = () => {
|
17
|
+
const { showPinList } = useServerConfigStore(featureFlagsSelectors);
|
18
|
+
const list = useSessionStore(sessionSelectors.pinnedSessions, isEqual);
|
19
|
+
const hotkey = useUserStore(settingsSelectors.getHotkeyById(HotkeyEnum.SwitchAgent));
|
20
|
+
const switchSession = useSwitchSession();
|
21
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
22
|
+
const [_, setPinned] = useQueryState('pinned', parseAsBoolean);
|
23
|
+
|
24
|
+
const switchAgent = (id: string) => {
|
25
|
+
switchSession(id);
|
26
|
+
setPinned(true);
|
27
|
+
};
|
28
|
+
|
29
|
+
const ref = useHotkeys(
|
30
|
+
list.slice(0, 9).map((e, i) => hotkey.replaceAll(KeyEnum.Number, String(i + 1))),
|
31
|
+
(_, hotkeysEvent) => {
|
32
|
+
if (!hotkeysEvent.keys?.[0]) return;
|
33
|
+
const index = parseInt(hotkeysEvent.keys?.[0]) - 1;
|
34
|
+
const item = list[index];
|
35
|
+
if (!item) return;
|
36
|
+
switchAgent(item.id);
|
37
|
+
},
|
38
|
+
{
|
39
|
+
enableOnFormTags: true,
|
40
|
+
enabled: showPinList,
|
41
|
+
preventDefault: true,
|
42
|
+
scopes: [HotkeyScopeEnum.Global, HotkeyEnum.SwitchAgent],
|
43
|
+
},
|
44
|
+
);
|
45
|
+
|
46
|
+
return {
|
47
|
+
id: HotkeyEnum.SwitchAgent,
|
48
|
+
ref,
|
49
|
+
};
|
50
|
+
};
|
51
|
+
|
52
|
+
export const useOpenHotkeyHelperHotkey = () => {
|
53
|
+
const [open, updateSystemStatus] = useGlobalStore((s) => [
|
54
|
+
s.status.showHotkeyHelper,
|
55
|
+
s.updateSystemStatus,
|
56
|
+
]);
|
57
|
+
|
58
|
+
return useHotkeyById(HotkeyEnum.OpenHotkeyHelper, () =>
|
59
|
+
updateSystemStatus({ showHotkeyHelper: !open }),
|
60
|
+
);
|
61
|
+
};
|
62
|
+
|
63
|
+
// 注册聚合
|
64
|
+
|
65
|
+
export const useRegisterGlobalHotkeys = () => {
|
66
|
+
// 全局自动注册不需要 enableScope
|
67
|
+
useSwitchAgentHotkey();
|
68
|
+
useOpenHotkeyHelperHotkey();
|
69
|
+
};
|
@@ -0,0 +1,194 @@
|
|
1
|
+
import { renderHook } from '@testing-library/react';
|
2
|
+
import { uniq } from 'lodash-es';
|
3
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
4
|
+
|
5
|
+
import { HOTKEYS_REGISTRATION } from '@/const/hotkeys';
|
6
|
+
import { HotkeyEnum, HotkeyScopeEnum } from '@/types/hotkey';
|
7
|
+
|
8
|
+
import { useHotkeyById } from './useHotkeyById';
|
9
|
+
|
10
|
+
// Mock dependencies
|
11
|
+
vi.mock('lodash-es', async (importOriginal) => {
|
12
|
+
const actual: any = await importOriginal();
|
13
|
+
return {
|
14
|
+
...actual,
|
15
|
+
uniq: vi.fn((arr) => [...new Set(arr)]),
|
16
|
+
};
|
17
|
+
});
|
18
|
+
|
19
|
+
// Mock react-hotkeys-hook
|
20
|
+
const mockRef = { current: document.createElement('div') };
|
21
|
+
const mockUseHotkeys = vi.fn(() => mockRef);
|
22
|
+
|
23
|
+
vi.mock('react-hotkeys-hook', () => ({
|
24
|
+
// @ts-ignore
|
25
|
+
useHotkeys: (...args: any[]) => mockUseHotkeys(...args),
|
26
|
+
}));
|
27
|
+
|
28
|
+
// Mock store and environment
|
29
|
+
let isMobileValue = false;
|
30
|
+
const mockHotkey = 'mod+k';
|
31
|
+
|
32
|
+
vi.mock('@/store/serverConfig', () => ({
|
33
|
+
useServerConfigStore: (selector: (state: any) => any) => selector({ isMobile: isMobileValue }),
|
34
|
+
}));
|
35
|
+
|
36
|
+
vi.mock('@/store/user', () => ({
|
37
|
+
useUserStore: (selector: any) => selector(),
|
38
|
+
}));
|
39
|
+
|
40
|
+
vi.mock('@/store/user/selectors', () => ({
|
41
|
+
settingsSelectors: {
|
42
|
+
getHotkeyById: () => () => mockHotkey,
|
43
|
+
},
|
44
|
+
}));
|
45
|
+
|
46
|
+
vi.mock('@/utils/env', () => ({
|
47
|
+
isDev: false,
|
48
|
+
}));
|
49
|
+
|
50
|
+
describe('useHotkeyById', () => {
|
51
|
+
const mockCallback = vi.fn();
|
52
|
+
|
53
|
+
beforeEach(() => {
|
54
|
+
vi.clearAllMocks();
|
55
|
+
isMobileValue = false;
|
56
|
+
});
|
57
|
+
|
58
|
+
it('should register hotkey with correct parameters', () => {
|
59
|
+
const { result } = renderHook(() => useHotkeyById(HotkeyEnum.Search, mockCallback));
|
60
|
+
|
61
|
+
// 验证 useHotkeys 被调用,但不检查具体函数引用
|
62
|
+
expect(mockUseHotkeys).toHaveBeenCalled();
|
63
|
+
|
64
|
+
// 获取实际调用参数
|
65
|
+
const callArgs: any = mockUseHotkeys.mock.calls[0];
|
66
|
+
|
67
|
+
// 验证第一个参数是正确的热键
|
68
|
+
expect(callArgs[0]).toBe(mockHotkey);
|
69
|
+
|
70
|
+
// 验证第二个参数是函数
|
71
|
+
expect(typeof callArgs[1]).toBe('function');
|
72
|
+
|
73
|
+
// 验证第三个参数包含预期的属性
|
74
|
+
expect(callArgs[2]).toMatchObject({
|
75
|
+
enableOnFormTags: true,
|
76
|
+
preventDefault: true,
|
77
|
+
scopes: [HotkeyEnum.Search, HotkeyScopeEnum.Global],
|
78
|
+
});
|
79
|
+
|
80
|
+
// 验证第四个参数是 undefined
|
81
|
+
expect(callArgs[3]).toBeUndefined();
|
82
|
+
|
83
|
+
// 验证返回值
|
84
|
+
expect(result.current).toEqual({
|
85
|
+
id: HotkeyEnum.Search,
|
86
|
+
ref: mockRef,
|
87
|
+
});
|
88
|
+
});
|
89
|
+
|
90
|
+
it('should disable hotkey on mobile devices', () => {
|
91
|
+
// Set as mobile
|
92
|
+
isMobileValue = true;
|
93
|
+
|
94
|
+
renderHook(() => useHotkeyById(HotkeyEnum.Search, mockCallback, { enabled: true }));
|
95
|
+
|
96
|
+
expect(mockUseHotkeys).toHaveBeenCalledWith(
|
97
|
+
mockHotkey,
|
98
|
+
expect.any(Function),
|
99
|
+
expect.objectContaining({
|
100
|
+
enabled: false, // Should be disabled on mobile
|
101
|
+
}),
|
102
|
+
undefined,
|
103
|
+
);
|
104
|
+
});
|
105
|
+
|
106
|
+
it('should handle options parameter correctly', () => {
|
107
|
+
const options = {
|
108
|
+
enabled: true,
|
109
|
+
scopes: ['customScope'],
|
110
|
+
};
|
111
|
+
|
112
|
+
renderHook(() => useHotkeyById(HotkeyEnum.Search, mockCallback, options));
|
113
|
+
|
114
|
+
expect(mockUseHotkeys).toHaveBeenCalledWith(
|
115
|
+
mockHotkey,
|
116
|
+
expect.any(Function),
|
117
|
+
expect.objectContaining({
|
118
|
+
enabled: true,
|
119
|
+
scopes: expect.arrayContaining([HotkeyEnum.Search, HotkeyScopeEnum.Global, 'customScope']),
|
120
|
+
}),
|
121
|
+
undefined,
|
122
|
+
);
|
123
|
+
|
124
|
+
expect(uniq).toHaveBeenCalled();
|
125
|
+
});
|
126
|
+
|
127
|
+
it('should handle dependencies parameter correctly', () => {
|
128
|
+
const deps = [1, 2, 3];
|
129
|
+
|
130
|
+
renderHook(() => useHotkeyById(HotkeyEnum.Search, mockCallback, deps));
|
131
|
+
|
132
|
+
expect(mockUseHotkeys).toHaveBeenCalledWith(
|
133
|
+
mockHotkey,
|
134
|
+
expect.any(Function),
|
135
|
+
expect.any(Object),
|
136
|
+
deps,
|
137
|
+
);
|
138
|
+
});
|
139
|
+
|
140
|
+
it('should handle both options and dependencies correctly', () => {
|
141
|
+
const options = { enabled: true };
|
142
|
+
const deps = [1, 2, 3];
|
143
|
+
|
144
|
+
renderHook(() => useHotkeyById(HotkeyEnum.Search, mockCallback, options, deps));
|
145
|
+
|
146
|
+
expect(mockUseHotkeys).toHaveBeenCalledWith(
|
147
|
+
mockHotkey,
|
148
|
+
expect.any(Function),
|
149
|
+
expect.objectContaining({
|
150
|
+
enabled: true,
|
151
|
+
}),
|
152
|
+
deps,
|
153
|
+
);
|
154
|
+
});
|
155
|
+
|
156
|
+
it('should combine scopes from registration and options', () => {
|
157
|
+
const testHotkeyId = HotkeyEnum.ToggleLeftPanel;
|
158
|
+
const registrationItem = HOTKEYS_REGISTRATION.find((item) => item.id === testHotkeyId);
|
159
|
+
const options = {
|
160
|
+
scopes: ['customScope'],
|
161
|
+
};
|
162
|
+
|
163
|
+
renderHook(() => useHotkeyById(testHotkeyId, mockCallback, options));
|
164
|
+
|
165
|
+
expect(mockUseHotkeys).toHaveBeenCalledWith(
|
166
|
+
mockHotkey,
|
167
|
+
expect.any(Function),
|
168
|
+
expect.objectContaining({
|
169
|
+
scopes: expect.arrayContaining([
|
170
|
+
testHotkeyId,
|
171
|
+
...(registrationItem?.scopes || []),
|
172
|
+
'customScope',
|
173
|
+
]),
|
174
|
+
}),
|
175
|
+
undefined,
|
176
|
+
);
|
177
|
+
});
|
178
|
+
|
179
|
+
it('should handle case when no registration item is found', () => {
|
180
|
+
// Using a non-existent ID to test this case
|
181
|
+
const nonExistentId = 'nonExistentId' as any;
|
182
|
+
|
183
|
+
renderHook(() => useHotkeyById(nonExistentId, mockCallback));
|
184
|
+
|
185
|
+
expect(mockUseHotkeys).toHaveBeenCalledWith(
|
186
|
+
mockHotkey,
|
187
|
+
expect.any(Function),
|
188
|
+
expect.objectContaining({
|
189
|
+
scopes: expect.arrayContaining([nonExistentId]),
|
190
|
+
}),
|
191
|
+
undefined,
|
192
|
+
);
|
193
|
+
});
|
194
|
+
});
|