@lobehub/chat 1.75.5 → 1.76.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 (123) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/Dockerfile +3 -2
  3. package/Dockerfile.database +3 -1
  4. package/Dockerfile.pglite +3 -1
  5. package/changelog/v1.json +18 -0
  6. package/docs/developer/database-schema.dbml +1 -0
  7. package/locales/ar/hotkey.json +46 -0
  8. package/locales/ar/models.json +3 -0
  9. package/locales/ar/setting.json +12 -0
  10. package/locales/bg-BG/hotkey.json +46 -0
  11. package/locales/bg-BG/models.json +3 -0
  12. package/locales/bg-BG/setting.json +12 -0
  13. package/locales/de-DE/hotkey.json +46 -0
  14. package/locales/de-DE/models.json +3 -0
  15. package/locales/de-DE/setting.json +12 -0
  16. package/locales/en-US/hotkey.json +46 -0
  17. package/locales/en-US/models.json +3 -0
  18. package/locales/en-US/setting.json +12 -0
  19. package/locales/es-ES/hotkey.json +46 -0
  20. package/locales/es-ES/models.json +3 -0
  21. package/locales/es-ES/setting.json +12 -0
  22. package/locales/fa-IR/hotkey.json +46 -0
  23. package/locales/fa-IR/models.json +3 -0
  24. package/locales/fa-IR/setting.json +12 -0
  25. package/locales/fr-FR/hotkey.json +46 -0
  26. package/locales/fr-FR/models.json +3 -0
  27. package/locales/fr-FR/setting.json +12 -0
  28. package/locales/it-IT/hotkey.json +46 -0
  29. package/locales/it-IT/models.json +3 -0
  30. package/locales/it-IT/setting.json +12 -0
  31. package/locales/ja-JP/hotkey.json +46 -0
  32. package/locales/ja-JP/models.json +3 -0
  33. package/locales/ja-JP/setting.json +12 -0
  34. package/locales/ko-KR/hotkey.json +46 -0
  35. package/locales/ko-KR/models.json +3 -0
  36. package/locales/ko-KR/setting.json +12 -0
  37. package/locales/nl-NL/hotkey.json +46 -0
  38. package/locales/nl-NL/models.json +3 -0
  39. package/locales/nl-NL/setting.json +12 -0
  40. package/locales/pl-PL/hotkey.json +46 -0
  41. package/locales/pl-PL/models.json +3 -0
  42. package/locales/pl-PL/setting.json +12 -0
  43. package/locales/pt-BR/hotkey.json +46 -0
  44. package/locales/pt-BR/models.json +3 -0
  45. package/locales/pt-BR/setting.json +12 -0
  46. package/locales/ru-RU/hotkey.json +46 -0
  47. package/locales/ru-RU/models.json +3 -0
  48. package/locales/ru-RU/setting.json +12 -0
  49. package/locales/tr-TR/hotkey.json +46 -0
  50. package/locales/tr-TR/models.json +3 -0
  51. package/locales/tr-TR/setting.json +12 -0
  52. package/locales/vi-VN/hotkey.json +46 -0
  53. package/locales/vi-VN/models.json +3 -0
  54. package/locales/vi-VN/setting.json +12 -0
  55. package/locales/zh-CN/hotkey.json +46 -0
  56. package/locales/zh-CN/models.json +3 -0
  57. package/locales/zh-CN/setting.json +12 -0
  58. package/locales/zh-TW/hotkey.json +46 -0
  59. package/locales/zh-TW/models.json +3 -0
  60. package/locales/zh-TW/setting.json +12 -0
  61. package/package.json +3 -3
  62. package/src/app/[variants]/(main)/(mobile)/me/(home)/features/Category.tsx +1 -1
  63. package/src/app/[variants]/(main)/(mobile)/me/(home)/layout.tsx +3 -2
  64. package/src/app/[variants]/(main)/(mobile)/me/data/features/Category.tsx +1 -1
  65. package/src/app/[variants]/(main)/(mobile)/me/profile/features/Category.tsx +1 -1
  66. package/src/app/[variants]/(main)/(mobile)/me/settings/features/Category.tsx +1 -1
  67. package/src/app/[variants]/(main)/_layout/Desktop/RegisterHotkeys.tsx +11 -0
  68. package/src/app/[variants]/(main)/_layout/Desktop/SideBar/PinList/index.tsx +6 -23
  69. package/src/app/[variants]/(main)/_layout/Desktop/SideBar/TopActions.test.tsx +2 -0
  70. package/src/app/[variants]/(main)/_layout/Desktop/index.tsx +11 -4
  71. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/Footer/SendMore.tsx +6 -21
  72. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/Footer/ShortcutHint.tsx +13 -34
  73. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/Footer/index.tsx +1 -1
  74. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ZenModeToast/Toast.tsx +7 -4
  75. package/src/app/[variants]/(main)/chat/(workspace)/_layout/Desktop/ChatHeader/HeaderAction.tsx +12 -8
  76. package/src/app/[variants]/(main)/chat/(workspace)/_layout/Desktop/ChatHeader/Main.tsx +24 -30
  77. package/src/app/[variants]/(main)/chat/(workspace)/_layout/Desktop/index.tsx +0 -2
  78. package/src/app/[variants]/(main)/chat/(workspace)/features/SettingButton.tsx +12 -7
  79. package/src/app/[variants]/(main)/chat/@session/features/SessionSearchBar.tsx +5 -1
  80. package/src/app/[variants]/(main)/chat/_layout/Desktop/RegisterHotkeys.tsx +10 -0
  81. package/src/app/[variants]/(main)/chat/_layout/Desktop/index.tsx +5 -0
  82. package/src/app/[variants]/(main)/chat/_layout/Mobile.tsx +1 -1
  83. package/src/app/[variants]/(main)/discover/features/StoreSearchBar.tsx +5 -1
  84. package/src/app/[variants]/(main)/settings/hooks/useCategory.tsx +31 -21
  85. package/src/app/[variants]/(main)/settings/hotkey/features/HotkeySetting.tsx +80 -0
  86. package/src/app/[variants]/(main)/settings/hotkey/index.tsx +9 -0
  87. package/src/app/[variants]/(main)/settings/hotkey/page.tsx +15 -0
  88. package/src/app/[variants]/(main)/settings/tts/features/const.tsx +4 -0
  89. package/src/app/[variants]/layout.tsx +16 -13
  90. package/src/config/aiModels/openai.ts +10 -0
  91. package/src/const/hotkeys.ts +80 -10
  92. package/src/const/settings/hotkey.ts +10 -0
  93. package/src/const/settings/index.ts +3 -0
  94. package/src/database/client/migrations.json +46 -32
  95. package/src/database/migrations/0019_add_hotkey_user_settings.sql +2 -0
  96. package/src/database/migrations/meta/0019_snapshot.json +4218 -0
  97. package/src/database/migrations/meta/_journal.json +7 -0
  98. package/src/database/schemas/user.ts +1 -0
  99. package/src/database/server/models/user.ts +2 -0
  100. package/src/features/ChatInput/Desktop/InputArea/index.tsx +8 -0
  101. package/src/features/ChatInput/Desktop/index.tsx +0 -1
  102. package/src/features/ChatInput/Topic/index.tsx +10 -15
  103. package/src/features/FileManager/Header/FilesSearchBar.tsx +6 -2
  104. package/src/features/HotkeyHelperPanel/HotkeyContent.tsx +62 -0
  105. package/src/features/HotkeyHelperPanel/index.tsx +59 -0
  106. package/src/hooks/useHotkeys/chatScope.ts +105 -0
  107. package/src/hooks/useHotkeys/globalScope.ts +69 -0
  108. package/src/hooks/useHotkeys/index.ts +2 -0
  109. package/src/hooks/useHotkeys/useHotkeyById.test.ts +194 -0
  110. package/src/hooks/useHotkeys/useHotkeyById.ts +57 -0
  111. package/src/locales/default/hotkey.ts +50 -0
  112. package/src/locales/default/index.ts +2 -0
  113. package/src/locales/default/setting.ts +12 -0
  114. package/src/store/global/initialState.ts +3 -0
  115. package/src/store/user/slices/settings/selectors/__snapshots__/settings.test.ts.snap +79 -0
  116. package/src/store/user/slices/settings/selectors/settings.test.ts +131 -0
  117. package/src/store/user/slices/settings/selectors/settings.ts +6 -0
  118. package/src/types/hotkey.ts +59 -0
  119. package/src/types/user/settings/hotkey.ts +3 -0
  120. package/src/types/user/settings/index.ts +3 -0
  121. package/src/types/user/settings/tts.ts +1 -1
  122. package/src/app/[variants]/(main)/chat/(workspace)/_layout/Desktop/HotKeys.tsx +0 -44
  123. 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';
@@ -4,6 +4,10 @@ import { SelectProps } from 'antd';
4
4
  import { LabelRenderer } from '@/components/ModelSelect';
5
5
 
6
6
  export const opeanaiTTSOptions: SelectProps['options'] = [
7
+ {
8
+ label: <LabelRenderer Icon={OpenAI.Avatar} label={'gpt-4o-mini-tts'} />,
9
+ value: 'gpt-4o-mini-tts',
10
+ },
7
11
  {
8
12
  label: <LabelRenderer Icon={OpenAI.Avatar} label={'tts-1'} />,
9
13
  value: 'tts-1',
@@ -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>