@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.
Files changed (147) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +1 -1
  3. package/README.zh-CN.md +1 -1
  4. package/changelog/v1.json +18 -0
  5. package/docs/developer/database-schema.dbml +1 -0
  6. package/docs/self-hosting/advanced/model-list.mdx +5 -3
  7. package/docs/self-hosting/advanced/model-list.zh-CN.mdx +5 -3
  8. package/docs/usage/providers/infiniai.zh-CN.mdx +4 -0
  9. package/locales/ar/hotkey.json +46 -0
  10. package/locales/ar/models.json +51 -54
  11. package/locales/ar/providers.json +3 -0
  12. package/locales/ar/setting.json +12 -0
  13. package/locales/bg-BG/hotkey.json +46 -0
  14. package/locales/bg-BG/models.json +51 -54
  15. package/locales/bg-BG/providers.json +3 -0
  16. package/locales/bg-BG/setting.json +12 -0
  17. package/locales/de-DE/hotkey.json +46 -0
  18. package/locales/de-DE/models.json +51 -54
  19. package/locales/de-DE/providers.json +3 -0
  20. package/locales/de-DE/setting.json +12 -0
  21. package/locales/en-US/hotkey.json +46 -0
  22. package/locales/en-US/models.json +51 -54
  23. package/locales/en-US/providers.json +3 -0
  24. package/locales/en-US/setting.json +12 -0
  25. package/locales/es-ES/hotkey.json +46 -0
  26. package/locales/es-ES/models.json +51 -54
  27. package/locales/es-ES/providers.json +3 -0
  28. package/locales/es-ES/setting.json +12 -0
  29. package/locales/fa-IR/hotkey.json +46 -0
  30. package/locales/fa-IR/models.json +51 -54
  31. package/locales/fa-IR/providers.json +3 -0
  32. package/locales/fa-IR/setting.json +12 -0
  33. package/locales/fr-FR/hotkey.json +46 -0
  34. package/locales/fr-FR/models.json +51 -54
  35. package/locales/fr-FR/providers.json +3 -0
  36. package/locales/fr-FR/setting.json +12 -0
  37. package/locales/it-IT/hotkey.json +46 -0
  38. package/locales/it-IT/models.json +51 -54
  39. package/locales/it-IT/providers.json +3 -0
  40. package/locales/it-IT/setting.json +12 -0
  41. package/locales/ja-JP/hotkey.json +46 -0
  42. package/locales/ja-JP/models.json +51 -54
  43. package/locales/ja-JP/providers.json +3 -0
  44. package/locales/ja-JP/setting.json +12 -0
  45. package/locales/ko-KR/hotkey.json +46 -0
  46. package/locales/ko-KR/models.json +51 -54
  47. package/locales/ko-KR/providers.json +3 -0
  48. package/locales/ko-KR/setting.json +12 -0
  49. package/locales/nl-NL/hotkey.json +46 -0
  50. package/locales/nl-NL/models.json +51 -54
  51. package/locales/nl-NL/providers.json +3 -0
  52. package/locales/nl-NL/setting.json +12 -0
  53. package/locales/pl-PL/hotkey.json +46 -0
  54. package/locales/pl-PL/models.json +51 -54
  55. package/locales/pl-PL/providers.json +3 -0
  56. package/locales/pl-PL/setting.json +12 -0
  57. package/locales/pt-BR/hotkey.json +46 -0
  58. package/locales/pt-BR/models.json +51 -54
  59. package/locales/pt-BR/providers.json +3 -0
  60. package/locales/pt-BR/setting.json +12 -0
  61. package/locales/ru-RU/hotkey.json +46 -0
  62. package/locales/ru-RU/models.json +51 -54
  63. package/locales/ru-RU/providers.json +3 -0
  64. package/locales/ru-RU/setting.json +12 -0
  65. package/locales/tr-TR/hotkey.json +46 -0
  66. package/locales/tr-TR/models.json +51 -54
  67. package/locales/tr-TR/providers.json +3 -0
  68. package/locales/tr-TR/setting.json +12 -0
  69. package/locales/vi-VN/hotkey.json +46 -0
  70. package/locales/vi-VN/models.json +51 -54
  71. package/locales/vi-VN/providers.json +3 -0
  72. package/locales/vi-VN/setting.json +12 -0
  73. package/locales/zh-CN/hotkey.json +46 -0
  74. package/locales/zh-CN/models.json +55 -58
  75. package/locales/zh-CN/providers.json +3 -0
  76. package/locales/zh-CN/setting.json +12 -0
  77. package/locales/zh-TW/hotkey.json +46 -0
  78. package/locales/zh-TW/models.json +51 -54
  79. package/locales/zh-TW/providers.json +3 -0
  80. package/locales/zh-TW/setting.json +12 -0
  81. package/package.json +3 -3
  82. package/src/app/[variants]/(main)/(mobile)/me/(home)/features/Category.tsx +1 -1
  83. package/src/app/[variants]/(main)/(mobile)/me/(home)/layout.tsx +3 -2
  84. package/src/app/[variants]/(main)/(mobile)/me/data/features/Category.tsx +1 -1
  85. package/src/app/[variants]/(main)/(mobile)/me/profile/features/Category.tsx +1 -1
  86. package/src/app/[variants]/(main)/(mobile)/me/settings/features/Category.tsx +1 -1
  87. package/src/app/[variants]/(main)/_layout/Desktop/RegisterHotkeys.tsx +11 -0
  88. package/src/app/[variants]/(main)/_layout/Desktop/SideBar/PinList/index.tsx +6 -23
  89. package/src/app/[variants]/(main)/_layout/Desktop/SideBar/TopActions.test.tsx +2 -0
  90. package/src/app/[variants]/(main)/_layout/Desktop/index.tsx +11 -4
  91. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/Footer/SendMore.tsx +6 -21
  92. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/Footer/ShortcutHint.tsx +13 -34
  93. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/Footer/index.tsx +1 -1
  94. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ZenModeToast/Toast.tsx +7 -4
  95. package/src/app/[variants]/(main)/chat/(workspace)/_layout/Desktop/ChatHeader/HeaderAction.tsx +12 -8
  96. package/src/app/[variants]/(main)/chat/(workspace)/_layout/Desktop/ChatHeader/Main.tsx +24 -30
  97. package/src/app/[variants]/(main)/chat/(workspace)/_layout/Desktop/index.tsx +0 -2
  98. package/src/app/[variants]/(main)/chat/(workspace)/features/SettingButton.tsx +12 -7
  99. package/src/app/[variants]/(main)/chat/@session/features/SessionSearchBar.tsx +5 -1
  100. package/src/app/[variants]/(main)/chat/_layout/Desktop/RegisterHotkeys.tsx +10 -0
  101. package/src/app/[variants]/(main)/chat/_layout/Desktop/index.tsx +5 -0
  102. package/src/app/[variants]/(main)/chat/_layout/Mobile.tsx +1 -1
  103. package/src/app/[variants]/(main)/discover/features/StoreSearchBar.tsx +5 -1
  104. package/src/app/[variants]/(main)/settings/hooks/useCategory.tsx +31 -21
  105. package/src/app/[variants]/(main)/settings/hotkey/features/HotkeySetting.tsx +80 -0
  106. package/src/app/[variants]/(main)/settings/hotkey/index.tsx +9 -0
  107. package/src/app/[variants]/(main)/settings/hotkey/page.tsx +15 -0
  108. package/src/app/[variants]/layout.tsx +16 -13
  109. package/src/config/aiModels/infiniai.ts +52 -55
  110. package/src/config/aiModels/siliconcloud.ts +17 -1
  111. package/src/config/aiModels/tencentcloud.ts +17 -0
  112. package/src/const/hotkeys.ts +80 -10
  113. package/src/const/settings/hotkey.ts +10 -0
  114. package/src/const/settings/index.ts +3 -0
  115. package/src/database/client/migrations.json +46 -32
  116. package/src/database/migrations/0019_add_hotkey_user_settings.sql +2 -0
  117. package/src/database/migrations/meta/0019_snapshot.json +4218 -0
  118. package/src/database/migrations/meta/_journal.json +7 -0
  119. package/src/database/schemas/user.ts +1 -0
  120. package/src/database/server/models/user.ts +2 -0
  121. package/src/features/ChatInput/Desktop/InputArea/index.tsx +8 -0
  122. package/src/features/ChatInput/Desktop/index.tsx +0 -1
  123. package/src/features/ChatInput/Topic/index.tsx +10 -15
  124. package/src/features/FileManager/Header/FilesSearchBar.tsx +6 -2
  125. package/src/features/HotkeyHelperPanel/HotkeyContent.tsx +62 -0
  126. package/src/features/HotkeyHelperPanel/index.tsx +59 -0
  127. package/src/hooks/useHotkeys/chatScope.ts +105 -0
  128. package/src/hooks/useHotkeys/globalScope.ts +69 -0
  129. package/src/hooks/useHotkeys/index.ts +2 -0
  130. package/src/hooks/useHotkeys/useHotkeyById.test.ts +194 -0
  131. package/src/hooks/useHotkeys/useHotkeyById.ts +57 -0
  132. package/src/libs/agent-runtime/infiniai/index.ts +38 -3
  133. package/src/locales/default/hotkey.ts +50 -0
  134. package/src/locales/default/index.ts +2 -0
  135. package/src/locales/default/setting.ts +12 -0
  136. package/src/store/global/initialState.ts +3 -0
  137. package/src/store/user/slices/settings/selectors/__snapshots__/settings.test.ts.snap +79 -0
  138. package/src/store/user/slices/settings/selectors/settings.test.ts +131 -0
  139. package/src/store/user/slices/settings/selectors/settings.ts +6 -0
  140. package/src/types/hotkey.ts +59 -0
  141. package/src/types/user/settings/hotkey.ts +3 -0
  142. package/src/types/user/settings/index.ts +3 -0
  143. package/src/utils/format.ts +1 -1
  144. package/src/utils/parseModels.test.ts +14 -0
  145. package/src/utils/parseModels.ts +4 -0
  146. package/src/app/[variants]/(main)/chat/(workspace)/_layout/Desktop/HotKeys.tsx +0 -44
  147. package/src/components/HotKeys/index.tsx +0 -77
@@ -1,58 +1,37 @@
1
- import { Icon } from '@lobehub/ui';
2
- import { Skeleton } from 'antd';
1
+ import { Hotkey, combineKeys } from '@lobehub/ui';
3
2
  import { useTheme } from 'antd-style';
4
- import { ChevronUp, CornerDownLeft, LucideCommand } from 'lucide-react';
5
- import { memo, useEffect, useState } from 'react';
3
+ import { memo } from 'react';
6
4
  import { useTranslation } from 'react-i18next';
7
- import { Center, Flexbox } from 'react-layout-kit';
5
+ import { Flexbox } from 'react-layout-kit';
8
6
 
9
7
  import { useUserStore } from '@/store/user';
10
8
  import { preferenceSelectors } from '@/store/user/selectors';
11
- import { isMacOS } from '@/utils/platform';
9
+ import { KeyEnum } from '@/types/hotkey';
12
10
 
13
11
  const ShortcutHint = memo(() => {
14
12
  const { t } = useTranslation('chat');
15
13
  const theme = useTheme();
16
14
  const useCmdEnterToSend = useUserStore(preferenceSelectors.useCmdEnterToSend);
17
- const [isMac, setIsMac] = useState<boolean>();
18
15
 
19
- useEffect(() => {
20
- setIsMac(isMacOS());
21
- }, []);
16
+ const sendShortcut = useCmdEnterToSend
17
+ ? combineKeys([KeyEnum.Mod, KeyEnum.Enter])
18
+ : KeyEnum.Enter;
22
19
 
23
- const cmdEnter = (
24
- <Flexbox gap={2} horizontal>
25
- {typeof isMac === 'boolean' ? (
26
- <Icon icon={isMac ? LucideCommand : ChevronUp} />
27
- ) : (
28
- <Skeleton.Node active style={{ height: '100%', width: 12 }}>
29
- {' '}
30
- </Skeleton.Node>
31
- )}
32
- <Icon icon={CornerDownLeft} />
33
- </Flexbox>
34
- );
35
-
36
- const enter = (
37
- <Center>
38
- <Icon icon={CornerDownLeft} />
39
- </Center>
40
- );
41
-
42
- const sendShortcut = useCmdEnterToSend ? cmdEnter : enter;
43
-
44
- const wrapperShortcut = useCmdEnterToSend ? enter : cmdEnter;
20
+ const wrapperShortcut = useCmdEnterToSend
21
+ ? KeyEnum.Enter
22
+ : combineKeys([KeyEnum.Mod, KeyEnum.Enter]);
45
23
 
46
24
  return (
47
25
  <Flexbox
26
+ align={'center'}
48
27
  gap={4}
49
28
  horizontal
50
29
  style={{ color: theme.colorTextDescription, fontSize: 12, marginRight: 12 }}
51
30
  >
52
- {sendShortcut}
31
+ <Hotkey keys={sendShortcut} style={{ color: 'inherit' }} variant={'pure'} />
53
32
  <span>{t('input.send')}</span>
54
33
  <span>/</span>
55
- {wrapperShortcut}
34
+ <Hotkey keys={wrapperShortcut} style={{ color: 'inherit' }} variant={'pure'} />
56
35
  <span>{t('input.warp')}</span>
57
36
  </Flexbox>
58
37
  );
@@ -69,7 +69,7 @@ const Footer = memo<FooterProps>(({ onExpandChange, expand }) => {
69
69
 
70
70
  return (
71
71
  <>
72
- <Suspense fallback={null}>
72
+ <Suspense>
73
73
  <MessageFromUrl />
74
74
  </Suspense>
75
75
  <Flexbox
@@ -1,12 +1,14 @@
1
1
  'use client';
2
2
 
3
+ import { Hotkey } from '@lobehub/ui';
3
4
  import { createStyles } from 'antd-style';
4
5
  import { useEffect, useState } from 'react';
5
6
  import { useTranslation } from 'react-i18next';
6
7
  import { Flexbox } from 'react-layout-kit';
7
8
 
8
- import HotKeys from '@/components/HotKeys';
9
- import { HOTKEYS } from '@/const/hotkeys';
9
+ import { useUserStore } from '@/store/user';
10
+ import { settingsSelectors } from '@/store/user/selectors';
11
+ import { HotkeyEnum } from '@/types/hotkey';
10
12
 
11
13
  const useStyles = createStyles(({ css, token }) => ({
12
14
  closeButton: css`
@@ -62,6 +64,7 @@ const Toast = () => {
62
64
  const { t } = useTranslation('chat');
63
65
  const { styles } = useStyles();
64
66
  const [isVisible, setIsVisible] = useState(true);
67
+ const hotkey = useUserStore(settingsSelectors.getHotkeyById(HotkeyEnum.ToggleZenMode));
65
68
 
66
69
  useEffect(() => {
67
70
  const timer = setTimeout(() => {
@@ -76,8 +79,8 @@ const Toast = () => {
76
79
  return (
77
80
  <div className={styles.container}>
78
81
  <div className={styles.toast}>
79
- <Flexbox className={styles.text} gap={12} horizontal>
80
- {t('zenMode')} <HotKeys inverseTheme keys={HOTKEYS.zenMode} />
82
+ <Flexbox align={'center'} className={styles.text} gap={8} horizontal>
83
+ {t('zenMode')} <Hotkey inverseTheme keys={hotkey} />
81
84
  </Flexbox>
82
85
  </div>
83
86
  </div>
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { ActionIcon } from '@lobehub/ui';
3
+ import { ActionIcon, Tooltip } from '@lobehub/ui';
4
4
  import { PanelRightClose, PanelRightOpen } from 'lucide-react';
5
5
  import { memo } from 'react';
6
6
  import { useTranslation } from 'react-i18next';
@@ -10,13 +10,16 @@ import { DESKTOP_HEADER_ICON_SIZE } from '@/const/layoutTokens';
10
10
  import { useGlobalStore } from '@/store/global';
11
11
  import { systemStatusSelectors } from '@/store/global/selectors';
12
12
  import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig';
13
+ import { useUserStore } from '@/store/user';
14
+ import { settingsSelectors } from '@/store/user/selectors';
15
+ import { HotkeyEnum } from '@/types/hotkey';
13
16
 
14
17
  import SettingButton from '../../../features/SettingButton';
15
18
  import ShareButton from '../../../features/ShareButton';
16
19
 
17
20
  const HeaderAction = memo(() => {
18
21
  const { t } = useTranslation('chat');
19
-
22
+ const hotkey = useUserStore(settingsSelectors.getHotkeyById(HotkeyEnum.ToggleRightPanel));
20
23
  const [showAgentSettings, toggleConfig] = useGlobalStore((s) => [
21
24
  systemStatusSelectors.showChatSideBar(s),
22
25
  s.toggleChatSideBar,
@@ -27,12 +30,13 @@ const HeaderAction = memo(() => {
27
30
  return (
28
31
  <Flexbox gap={4} horizontal>
29
32
  <ShareButton />
30
- <ActionIcon
31
- icon={showAgentSettings ? PanelRightClose : PanelRightOpen}
32
- onClick={() => toggleConfig()}
33
- size={DESKTOP_HEADER_ICON_SIZE}
34
- title={t('roleAndArchive')}
35
- />
33
+ <Tooltip hotkey={hotkey} title={t('toggleRightPanel.title', { ns: 'hotkey' })}>
34
+ <ActionIcon
35
+ icon={showAgentSettings ? PanelRightClose : PanelRightOpen}
36
+ onClick={() => toggleConfig()}
37
+ size={DESKTOP_HEADER_ICON_SIZE}
38
+ />
39
+ </Tooltip>
36
40
  {isAgentEditable && <SettingButton />}
37
41
  </Flexbox>
38
42
  );
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { ActionIcon, Avatar } from '@lobehub/ui';
3
+ import { ActionIcon, Avatar, Tooltip } from '@lobehub/ui';
4
4
  import { Skeleton } from 'antd';
5
5
  import { createStyles } from 'antd-style';
6
6
  import { PanelLeftClose, PanelLeftOpen } from 'lucide-react';
@@ -16,6 +16,9 @@ import { useGlobalStore } from '@/store/global';
16
16
  import { systemStatusSelectors } from '@/store/global/selectors';
17
17
  import { useSessionStore } from '@/store/session';
18
18
  import { sessionMetaSelectors, sessionSelectors } from '@/store/session/selectors';
19
+ import { useUserStore } from '@/store/user';
20
+ import { settingsSelectors } from '@/store/user/selectors';
21
+ import { HotkeyEnum } from '@/types/hotkey';
19
22
 
20
23
  import Tags from './Tags';
21
24
 
@@ -42,7 +45,9 @@ const useStyles = createStyles(({ css }) => ({
42
45
  }));
43
46
 
44
47
  const Main = memo(() => {
45
- const { t } = useTranslation('chat');
48
+ const hotkey = useUserStore(settingsSelectors.getHotkeyById(HotkeyEnum.ToggleLeftPanel));
49
+
50
+ const { t } = useTranslation(['chat', 'hotkey']);
46
51
  const { styles } = useStyles();
47
52
  useInitAgentConfig();
48
53
  const [isPinned] = useQueryState('pinned', parseAsBoolean);
@@ -61,23 +66,25 @@ const Main = memo(() => {
61
66
  const showSessionPanel = useGlobalStore(systemStatusSelectors.showSessionPanel);
62
67
  const updateSystemStatus = useGlobalStore((s) => s.updateSystemStatus);
63
68
 
69
+ const ToggleAction = (
70
+ <Tooltip hotkey={hotkey} title={t('toggleLeftPanel.title', { ns: 'hotkey' })}>
71
+ <ActionIcon
72
+ icon={showSessionPanel ? PanelLeftClose : PanelLeftOpen}
73
+ onClick={() => {
74
+ updateSystemStatus({
75
+ sessionsWidth: showSessionPanel ? 0 : 320,
76
+ showSessionPanel: !showSessionPanel,
77
+ });
78
+ }}
79
+ size={DESKTOP_HEADER_ICON_SIZE}
80
+ />
81
+ </Tooltip>
82
+ );
83
+
64
84
  if (!init)
65
85
  return (
66
86
  <Flexbox align={'center'} gap={8} horizontal>
67
- {!isPinned && (
68
- <ActionIcon
69
- aria-label={t('agents')}
70
- icon={showSessionPanel ? PanelLeftClose : PanelLeftOpen}
71
- onClick={() => {
72
- updateSystemStatus({
73
- sessionsWidth: showSessionPanel ? 0 : 320,
74
- showSessionPanel: !showSessionPanel,
75
- });
76
- }}
77
- size={DESKTOP_HEADER_ICON_SIZE}
78
- title={t('agents')}
79
- />
80
- )}
87
+ {!isPinned && ToggleAction}
81
88
  <Skeleton
82
89
  active
83
90
  avatar={{ shape: 'circle', size: 28 }}
@@ -89,20 +96,7 @@ const Main = memo(() => {
89
96
 
90
97
  return (
91
98
  <Flexbox align={'center'} gap={4} horizontal>
92
- {!isPinned && (
93
- <ActionIcon
94
- aria-label={t('agents')}
95
- icon={showSessionPanel ? PanelLeftClose : PanelLeftOpen}
96
- onClick={() => {
97
- updateSystemStatus({
98
- sessionsWidth: showSessionPanel ? 0 : 320,
99
- showSessionPanel: !showSessionPanel,
100
- });
101
- }}
102
- size={DESKTOP_HEADER_ICON_SIZE}
103
- title={t('agents')}
104
- />
105
- )}
99
+ {!isPinned && ToggleAction}
106
100
  <Avatar
107
101
  avatar={avatar}
108
102
  background={backgroundColor}
@@ -5,7 +5,6 @@ import BrandTextLoading from '@/components/Loading/BrandTextLoading';
5
5
 
6
6
  import { LayoutProps } from '../type';
7
7
  import ChatHeader from './ChatHeader';
8
- import HotKeys from './HotKeys';
9
8
  import Portal from './Portal';
10
9
  import TopicPanel from './TopicPanel';
11
10
 
@@ -32,7 +31,6 @@ const Layout = ({ children, topic, conversation, portal }: LayoutProps) => {
32
31
  </Portal>
33
32
  <TopicPanel>{topic}</TopicPanel>
34
33
  </Flexbox>
35
- <HotKeys />
36
34
  </>
37
35
  );
38
36
  };
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { ActionIcon } from '@lobehub/ui';
3
+ import { ActionIcon, Tooltip } from '@lobehub/ui';
4
4
  import { AlignJustify } from 'lucide-react';
5
5
  import dynamic from 'next/dynamic';
6
6
  import { memo } from 'react';
@@ -9,24 +9,29 @@ import { useTranslation } from 'react-i18next';
9
9
  import { DESKTOP_HEADER_ICON_SIZE, MOBILE_HEADER_ICON_SIZE } from '@/const/layoutTokens';
10
10
  import { useOpenChatSettings } from '@/hooks/useInterceptingRoutes';
11
11
  import { useSessionStore } from '@/store/session';
12
+ import { useUserStore } from '@/store/user';
13
+ import { settingsSelectors } from '@/store/user/selectors';
14
+ import { HotkeyEnum } from '@/types/hotkey';
12
15
 
13
16
  const AgentSettings = dynamic(() => import('./AgentSettings'), {
14
17
  ssr: false,
15
18
  });
16
19
 
17
20
  const SettingButton = memo<{ mobile?: boolean }>(({ mobile }) => {
21
+ const hotkey = useUserStore(settingsSelectors.getHotkeyById(HotkeyEnum.OpenChatSettings));
18
22
  const { t } = useTranslation('common');
19
23
  const openChatSettings = useOpenChatSettings();
20
24
  const id = useSessionStore((s) => s.activeId);
21
25
 
22
26
  return (
23
27
  <>
24
- <ActionIcon
25
- icon={AlignJustify}
26
- onClick={() => openChatSettings()}
27
- size={mobile ? MOBILE_HEADER_ICON_SIZE : DESKTOP_HEADER_ICON_SIZE}
28
- title={t('header.session', { ns: 'setting' })}
29
- />
28
+ <Tooltip hotkey={hotkey} title={t('openChatSettings.title', { ns: 'hotkey' })}>
29
+ <ActionIcon
30
+ icon={AlignJustify}
31
+ onClick={() => openChatSettings()}
32
+ size={mobile ? MOBILE_HEADER_ICON_SIZE : DESKTOP_HEADER_ICON_SIZE}
33
+ />
34
+ </Tooltip>
30
35
  <AgentSettings key={id} />
31
36
  </>
32
37
  );
@@ -5,9 +5,13 @@ import { memo } from 'react';
5
5
  import { useTranslation } from 'react-i18next';
6
6
 
7
7
  import { useSessionStore } from '@/store/session';
8
+ import { useUserStore } from '@/store/user';
9
+ import { settingsSelectors } from '@/store/user/selectors';
10
+ import { HotkeyEnum } from '@/types/hotkey';
8
11
 
9
12
  const SessionSearchBar = memo<{ mobile?: boolean }>(({ mobile }) => {
10
13
  const { t } = useTranslation('chat');
14
+ const hotkey = useUserStore(settingsSelectors.getHotkeyById(HotkeyEnum.Search));
11
15
 
12
16
  const [keywords, useSearchSessions, updateSearchKeywords] = useSessionStore((s) => [
13
17
  s.sessionSearchKeywords,
@@ -26,7 +30,7 @@ const SessionSearchBar = memo<{ mobile?: boolean }>(({ mobile }) => {
26
30
  updateSearchKeywords(e.target.value);
27
31
  }}
28
32
  placeholder={t('searchAgentPlaceholder')}
29
- shortKey={'k'}
33
+ shortKey={hotkey}
30
34
  spotlight={!mobile}
31
35
  type={mobile ? 'block' : 'ghost'}
32
36
  value={keywords}
@@ -0,0 +1,10 @@
1
+ 'use client';
2
+
3
+ import { useRegisterChatHotkeys } from '@/hooks/useHotkeys';
4
+
5
+ const RegisterHotkeys = () => {
6
+ useRegisterChatHotkeys();
7
+ return null;
8
+ };
9
+
10
+ export default RegisterHotkeys;
@@ -1,8 +1,10 @@
1
+ import { Suspense } from 'react';
1
2
  import { Flexbox } from 'react-layout-kit';
2
3
 
3
4
  import InitClientDB from '@/features/InitClientDB';
4
5
 
5
6
  import { LayoutProps } from '../type';
7
+ import RegisterHotkeys from './RegisterHotkeys';
6
8
  import SessionPanel from './SessionPanel';
7
9
 
8
10
  const Layout = ({ children, session }: LayoutProps) => {
@@ -23,6 +25,9 @@ const Layout = ({ children, session }: LayoutProps) => {
23
25
  {/* ↓ cloud slot ↓ */}
24
26
 
25
27
  {/* ↑ cloud slot ↑ */}
28
+ <Suspense>
29
+ <RegisterHotkeys />
30
+ </Suspense>
26
31
  </>
27
32
  );
28
33
  };
@@ -40,7 +40,7 @@ const Layout = memo<LayoutProps>(({ children, session }) => {
40
40
  >
41
41
  {children}
42
42
  </Flexbox>
43
- <Suspense fallback={null}>
43
+ <Suspense>
44
44
  <InitClientDB bottom={100} />
45
45
  </Suspense>
46
46
  </>
@@ -10,7 +10,10 @@ import urlJoin from 'url-join';
10
10
 
11
11
  import { withSuspense } from '@/components/withSuspense';
12
12
  import { useQueryRoute } from '@/hooks/useQueryRoute';
13
+ import { useUserStore } from '@/store/user';
14
+ import { settingsSelectors } from '@/store/user/selectors';
13
15
  import { DiscoverTab } from '@/types/discover';
16
+ import { HotkeyEnum } from '@/types/hotkey';
14
17
 
15
18
  import { useNav } from './useNav';
16
19
 
@@ -34,6 +37,7 @@ const StoreSearchBar = memo<StoreSearchBarProps>(({ mobile, onBlur, onFocus, ...
34
37
  const pathname = usePathname();
35
38
  const { activeKey } = useNav();
36
39
  const [searchKey, setSearchKey] = useQueryState('q', { clearOnDefault: true, defaultValue: '' });
40
+ const hotkey = useUserStore(settingsSelectors.getHotkeyById(HotkeyEnum.Search));
37
41
 
38
42
  const { t } = useTranslation('discover');
39
43
  const { cx, styles } = useStyles();
@@ -69,7 +73,7 @@ const StoreSearchBar = memo<StoreSearchBarProps>(({ mobile, onBlur, onFocus, ...
69
73
  }}
70
74
  onSearch={handleSearch}
71
75
  placeholder={t('search.placeholder')}
72
- shortKey={'k'}
76
+ shortKey={hotkey}
73
77
  spotlight={!mobile}
74
78
  style={{ width: mobile || active ? '100%' : 'min(480px,100%)' }}
75
79
  styles={{ input: { width: '100%' } }}
@@ -1,6 +1,6 @@
1
1
  import { Icon } from '@lobehub/ui';
2
2
  import { Tag } from 'antd';
3
- import { Bot, Brain, Cloudy, Info, Mic2, Settings2, Sparkles } from 'lucide-react';
3
+ import { Bot, Brain, Cloudy, Info, KeyboardIcon, Mic2, Settings2, Sparkles } from 'lucide-react';
4
4
  import Link from 'next/link';
5
5
  import { useMemo } from 'react';
6
6
  import { useTranslation } from 'react-i18next';
@@ -13,6 +13,7 @@ import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfi
13
13
 
14
14
  export const useCategory = () => {
15
15
  const { t } = useTranslation('setting');
16
+ const mobile = useServerConfigStore((s) => s.isMobile);
16
17
  const { enableWebrtc, showLLM, enableSTT, hideDocs } =
17
18
  useServerConfigStore(featureFlagsSelectors);
18
19
 
@@ -52,26 +53,26 @@ export const useCategory = () => {
52
53
  ),
53
54
  },
54
55
  showLLM &&
55
- // TODO: Remove /llm when v2.0
56
- (isDeprecatedEdition
57
- ? {
58
- icon: <Icon icon={Brain} />,
59
- key: SettingsTabs.LLM,
60
- label: (
61
- <Link href={'/settings/llm'} onClick={(e) => e.preventDefault()}>
62
- {t('tab.llm')}
63
- </Link>
64
- ),
65
- }
66
- : {
67
- icon: <Icon icon={Brain} />,
68
- key: SettingsTabs.Provider,
69
- label: (
70
- <Link href={'/settings/provider'} onClick={(e) => e.preventDefault()}>
71
- {t('tab.provider')}
72
- </Link>
73
- ),
74
- }),
56
+ // TODO: Remove /llm when v2.0
57
+ (isDeprecatedEdition
58
+ ? {
59
+ icon: <Icon icon={Brain} />,
60
+ key: SettingsTabs.LLM,
61
+ label: (
62
+ <Link href={'/settings/llm'} onClick={(e) => e.preventDefault()}>
63
+ {t('tab.llm')}
64
+ </Link>
65
+ ),
66
+ }
67
+ : {
68
+ icon: <Icon icon={Brain} />,
69
+ key: SettingsTabs.Provider,
70
+ label: (
71
+ <Link href={'/settings/provider'} onClick={(e) => e.preventDefault()}>
72
+ {t('tab.provider')}
73
+ </Link>
74
+ ),
75
+ }),
75
76
 
76
77
  enableSTT && {
77
78
  icon: <Icon icon={Mic2} />,
@@ -91,6 +92,15 @@ export const useCategory = () => {
91
92
  </Link>
92
93
  ),
93
94
  },
95
+ !mobile && {
96
+ icon: <Icon icon={KeyboardIcon} />,
97
+ key: SettingsTabs.Hotkey,
98
+ label: (
99
+ <Link href={'/settings/hotkey'} onClick={(e) => e.preventDefault()}>
100
+ {t('tab.hotkey')}
101
+ </Link>
102
+ ),
103
+ },
94
104
  !hideDocs && {
95
105
  icon: <Icon icon={Info} />,
96
106
  key: SettingsTabs.About,
@@ -0,0 +1,80 @@
1
+ 'use client';
2
+
3
+ import { Form, HotkeyInput, type ItemGroup } from '@lobehub/ui';
4
+ import isEqual from 'fast-deep-equal';
5
+ import { memo } from 'react';
6
+ import { useTranslation } from 'react-i18next';
7
+
8
+ import { HOTKEYS_REGISTRATION } from '@/const/hotkeys';
9
+ import { FORM_STYLE } from '@/const/layoutTokens';
10
+ import hotkeyMeta from '@/locales/default/hotkey';
11
+ import { useUserStore } from '@/store/user';
12
+ import { settingsSelectors } from '@/store/user/selectors';
13
+ import { HotkeyGroupEnum, HotkeyItem } from '@/types/hotkey';
14
+
15
+ type SettingItemGroup = ItemGroup;
16
+
17
+ const HOTKEY_SETTING_KEY = 'hotkey';
18
+
19
+ const HotkeySetting = memo(() => {
20
+ const { t } = useTranslation(['setting', 'hotkey']);
21
+ const [form] = Form.useForm();
22
+
23
+ const settings = useUserStore(settingsSelectors.currentSettings, isEqual);
24
+ const [setSettings] = useUserStore((s) => [s.setSettings]);
25
+
26
+ const mapHotkeyItem = (item: HotkeyItem) => {
27
+ const hotkeyConflicts = Object.entries(settings.hotkey)
28
+ .map(([key, value]) => {
29
+ if (key === item.id) return false;
30
+ return value;
31
+ })
32
+ .filter(Boolean) as string[];
33
+ return {
34
+ children: (
35
+ <HotkeyInput
36
+ disabled={item.nonEditable}
37
+ hotkeyConflicts={hotkeyConflicts}
38
+ placeholder={t('hotkey.record')}
39
+ resetValue={item.keys}
40
+ texts={{
41
+ conflicts: t('hotkey.conflicts'),
42
+ invalidCombination: t('hotkey.invalidCombination'),
43
+ reset: t('hotkey.reset'),
44
+ }}
45
+ />
46
+ ),
47
+ desc: hotkeyMeta[item.id].desc ? t(`${item.id}.desc`, { ns: 'hotkey' }) : undefined,
48
+ label: t(`${item.id}.title`, { ns: 'hotkey' }),
49
+ name: [HOTKEY_SETTING_KEY, item.id],
50
+ };
51
+ };
52
+
53
+ const essential: SettingItemGroup = {
54
+ children: HOTKEYS_REGISTRATION.filter((item) => item.group === HotkeyGroupEnum.Essential).map(
55
+ (item) => mapHotkeyItem(item),
56
+ ),
57
+ title: t('hotkey.group.essential'),
58
+ };
59
+
60
+ const conversation: SettingItemGroup = {
61
+ children: HOTKEYS_REGISTRATION.filter(
62
+ (item) => item.group === HotkeyGroupEnum.Conversation,
63
+ ).map((item) => mapHotkeyItem(item)),
64
+ title: t('hotkey.group.conversation'),
65
+ };
66
+
67
+ return (
68
+ <Form
69
+ form={form}
70
+ initialValues={settings}
71
+ items={[essential, conversation]}
72
+ itemsType={'group'}
73
+ onValuesChange={setSettings}
74
+ variant={'pure'}
75
+ {...FORM_STYLE}
76
+ />
77
+ );
78
+ });
79
+
80
+ export default HotkeySetting;
@@ -0,0 +1,9 @@
1
+ import HotkeySetting from './features/HotkeySetting';
2
+
3
+ const Page = () => {
4
+ return <HotkeySetting />;
5
+ };
6
+
7
+ Page.displayName = 'HotkeySetting';
8
+
9
+ export default Page;
@@ -0,0 +1,15 @@
1
+ import { metadataModule } from '@/server/metadata';
2
+ import { translation } from '@/server/translation';
3
+ import { DynamicLayoutProps } from '@/types/next';
4
+ import { RouteVariants } from '@/utils/server/routeVariants';
5
+
6
+ export const generateMetadata = async (props: DynamicLayoutProps) => {
7
+ const locale = await RouteVariants.getLocale(props);
8
+ const { t } = await translation('setting', locale);
9
+ return metadataModule.generate({
10
+ description: t('header.desc'),
11
+ title: t('tab.hotkey'),
12
+ url: '/settings/hotkey',
13
+ });
14
+ };
15
+ export { default } from './index';
@@ -1,6 +1,7 @@
1
1
  import { SpeedInsights } from '@vercel/speed-insights/next';
2
2
  import { ThemeAppearance } from 'antd-style';
3
3
  import { ResolvingViewport } from 'next';
4
+ import { NuqsAdapter } from 'nuqs/adapters/next/app';
4
5
  import { ReactNode } from 'react';
5
6
  import { isRtlLang } from 'rtl-detect';
6
7
 
@@ -31,19 +32,21 @@ const RootLayout = async ({ children, params, modal }: RootLayoutProps) => {
31
32
  return (
32
33
  <html dir={direction} lang={locale} suppressHydrationWarning>
33
34
  <body>
34
- <GlobalProvider
35
- appearance={theme}
36
- isMobile={isMobile}
37
- locale={locale}
38
- neutralColor={neutralColor}
39
- primaryColor={primaryColor}
40
- >
41
- <AuthProvider>
42
- {children}
43
- {!isMobile && modal}
44
- </AuthProvider>
45
- <PWAInstall />
46
- </GlobalProvider>
35
+ <NuqsAdapter>
36
+ <GlobalProvider
37
+ appearance={theme}
38
+ isMobile={isMobile}
39
+ locale={locale}
40
+ neutralColor={neutralColor}
41
+ primaryColor={primaryColor}
42
+ >
43
+ <AuthProvider>
44
+ {children}
45
+ {!isMobile && modal}
46
+ </AuthProvider>
47
+ <PWAInstall />
48
+ </GlobalProvider>
49
+ </NuqsAdapter>
47
50
  <Analytics />
48
51
  {inVercel && <SpeedInsights />}
49
52
  </body>