@lobehub/chat 1.111.2 → 1.111.3

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 (35) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/changelog/v1.json +9 -0
  3. package/locales/ar/models.json +21 -3
  4. package/locales/bg-BG/models.json +21 -3
  5. package/locales/de-DE/models.json +21 -3
  6. package/locales/en-US/models.json +21 -3
  7. package/locales/es-ES/models.json +21 -3
  8. package/locales/fa-IR/models.json +21 -3
  9. package/locales/fr-FR/models.json +21 -3
  10. package/locales/it-IT/models.json +21 -3
  11. package/locales/ja-JP/models.json +21 -3
  12. package/locales/ko-KR/models.json +21 -3
  13. package/locales/nl-NL/models.json +21 -3
  14. package/locales/pl-PL/models.json +21 -3
  15. package/locales/pt-BR/models.json +21 -3
  16. package/locales/ru-RU/models.json +21 -3
  17. package/locales/tr-TR/models.json +21 -3
  18. package/locales/vi-VN/models.json +21 -3
  19. package/locales/zh-CN/models.json +21 -3
  20. package/locales/zh-TW/models.json +21 -3
  21. package/package.json +1 -1
  22. package/packages/types/src/hotkey.ts +2 -0
  23. package/src/app/[variants]/(main)/_layout/Desktop/SideBar/PinList/index.tsx +3 -3
  24. package/src/app/[variants]/(main)/_layout/Desktop/SideBar/TopActions.test.tsx +1 -0
  25. package/src/app/[variants]/(main)/_layout/Desktop/SideBar/TopActions.tsx +11 -2
  26. package/src/app/[variants]/(main)/_layout/Desktop/SideBar/index.tsx +2 -2
  27. package/src/app/[variants]/(main)/chat/(workspace)/_layout/Desktop/ChatHeader/Main.tsx +2 -2
  28. package/src/app/[variants]/(main)/chat/_layout/Desktop/SessionPanel.tsx +2 -2
  29. package/src/components/Thinking/index.tsx +53 -13
  30. package/src/const/hotkeys.ts +6 -0
  31. package/src/hooks/useHotkeys/chatScope.ts +2 -2
  32. package/src/hooks/useHotkeys/globalScope.ts +16 -4
  33. package/src/hooks/usePinnedAgentState.ts +21 -0
  34. package/src/hooks/useSwitchSession.ts +1 -1
  35. package/src/locales/default/hotkey.ts +4 -0
@@ -1,4 +1,4 @@
1
- import { ActionIcon, ActionIconProps } from '@lobehub/ui';
1
+ import { ActionIcon, ActionIconProps, Hotkey } from '@lobehub/ui';
2
2
  import { Compass, FolderClosed, MessageSquare, Palette } from 'lucide-react';
3
3
  import Link from 'next/link';
4
4
  import { memo } from 'react';
@@ -9,6 +9,9 @@ import { useGlobalStore } from '@/store/global';
9
9
  import { SidebarTabKey } from '@/store/global/initialState';
10
10
  import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig';
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 ICON_SIZE: ActionIconProps['size'] = {
14
17
  blockSize: 40,
@@ -25,6 +28,7 @@ const TopActions = memo<TopActionProps>(({ tab, isPinned }) => {
25
28
  const { t } = useTranslation('common');
26
29
  const switchBackToChat = useGlobalStore((s) => s.switchBackToChat);
27
30
  const { showMarket, enableKnowledgeBase } = useServerConfigStore(featureFlagsSelectors);
31
+ const hotkey = useUserStore(settingsSelectors.getHotkeyById(HotkeyEnum.NavigateToChat));
28
32
 
29
33
  const isChatActive = tab === SidebarTabKey.Chat && !isPinned;
30
34
  const isFilesActive = tab === SidebarTabKey.Files;
@@ -51,7 +55,12 @@ const TopActions = memo<TopActionProps>(({ tab, isPinned }) => {
51
55
  active={isChatActive}
52
56
  icon={MessageSquare}
53
57
  size={ICON_SIZE}
54
- title={t('tab.chat')}
58
+ title={
59
+ <Flexbox align={'center'} gap={8} horizontal justify={'space-between'}>
60
+ <span>{t('tab.chat')}</span>
61
+ <Hotkey inverseTheme keys={hotkey} />
62
+ </Flexbox>
63
+ }
55
64
  tooltipProps={{ placement: 'right' }}
56
65
  />
57
66
  </Link>
@@ -2,11 +2,11 @@
2
2
 
3
3
  import { SideNav } from '@lobehub/ui';
4
4
  import { useTheme } from 'antd-style';
5
- import { parseAsBoolean, useQueryState } from 'nuqs';
6
5
  import { Suspense, memo } from 'react';
7
6
 
8
7
  import { isDesktop } from '@/const/version';
9
8
  import { useActiveTabKey } from '@/hooks/useActiveTabKey';
9
+ import { usePinnedAgentState } from '@/hooks/usePinnedAgentState';
10
10
  import { useGlobalStore } from '@/store/global';
11
11
  import { systemStatusSelectors } from '@/store/global/selectors';
12
12
  import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig';
@@ -18,7 +18,7 @@ import PinList from './PinList';
18
18
  import TopActions from './TopActions';
19
19
 
20
20
  const Top = () => {
21
- const [isPinned] = useQueryState('pinned', parseAsBoolean);
21
+ const [isPinned] = usePinnedAgentState();
22
22
  const sidebarKey = useActiveTabKey();
23
23
 
24
24
  return <TopActions isPinned={isPinned} tab={sidebarKey} />;
@@ -3,13 +3,13 @@
3
3
  import { Avatar } from '@lobehub/ui';
4
4
  import { Skeleton } from 'antd';
5
5
  import { createStyles } from 'antd-style';
6
- import { parseAsBoolean, useQueryState } from 'nuqs';
7
6
  import { Suspense, memo } from 'react';
8
7
  import { useTranslation } from 'react-i18next';
9
8
  import { Flexbox } from 'react-layout-kit';
10
9
 
11
10
  import { useInitAgentConfig } from '@/hooks/useInitAgentConfig';
12
11
  import { useOpenChatSettings } from '@/hooks/useInterceptingRoutes';
12
+ import { usePinnedAgentState } from '@/hooks/usePinnedAgentState';
13
13
  import { useGlobalStore } from '@/store/global';
14
14
  import { systemStatusSelectors } from '@/store/global/selectors';
15
15
  import { useSessionStore } from '@/store/session';
@@ -44,7 +44,7 @@ const Main = memo<{ className?: string }>(({ className }) => {
44
44
  const { t } = useTranslation(['chat', 'hotkey']);
45
45
  const { styles } = useStyles();
46
46
  useInitAgentConfig();
47
- const [isPinned] = useQueryState('pinned', parseAsBoolean);
47
+ const [isPinned] = usePinnedAgentState();
48
48
 
49
49
  const [init, isInbox, title, avatar, backgroundColor] = useSessionStore((s) => [
50
50
  sessionSelectors.isSomeSessionActive(s),
@@ -3,11 +3,11 @@
3
3
  import { DraggablePanel, DraggablePanelContainer, type DraggablePanelProps } from '@lobehub/ui';
4
4
  import { createStyles, useResponsive } from 'antd-style';
5
5
  import isEqual from 'fast-deep-equal';
6
- import { parseAsBoolean, useQueryState } from 'nuqs';
7
6
  import { PropsWithChildren, memo, useEffect, useState } from 'react';
8
7
 
9
8
  import { withSuspense } from '@/components/withSuspense';
10
9
  import { FOLDER_WIDTH } from '@/const/layoutTokens';
10
+ import { usePinnedAgentState } from '@/hooks/usePinnedAgentState';
11
11
  import { useGlobalStore } from '@/store/global';
12
12
  import { systemStatusSelectors } from '@/store/global/selectors';
13
13
 
@@ -35,7 +35,7 @@ export const useStyles = createStyles(({ css, token }) => ({
35
35
  const SessionPanel = memo<PropsWithChildren>(({ children }) => {
36
36
  const { md = true } = useResponsive();
37
37
 
38
- const [isPinned] = useQueryState('pinned', parseAsBoolean);
38
+ const [isPinned] = usePinnedAgentState();
39
39
 
40
40
  const { styles } = useStyles();
41
41
  const [sessionsWidth, sessionExpandable, updatePreference] = useGlobalStore((s) => [
@@ -3,7 +3,7 @@ import { createStyles } from 'antd-style';
3
3
  import { AnimatePresence, motion } from 'framer-motion';
4
4
  import { AtomIcon, ChevronDown, ChevronRight } from 'lucide-react';
5
5
  import { rgba } from 'polished';
6
- import { CSSProperties, memo, useEffect, useState } from 'react';
6
+ import { CSSProperties, RefObject, memo, useEffect, useRef, useState } from 'react';
7
7
  import { useTranslation } from 'react-i18next';
8
8
  import { Flexbox } from 'react-layout-kit';
9
9
 
@@ -16,11 +16,19 @@ const useStyles = createStyles(({ css, token }) => ({
16
16
  color: ${token.colorTextTertiary};
17
17
  transition: all 0.2s ${token.motionEaseOut};
18
18
  `,
19
+ contentScroll: css`
20
+ scroll-behavior: auto;
21
+
22
+ overflow-y: auto;
23
+ overscroll-behavior: contain;
24
+
25
+ max-height: 40vh;
26
+ padding-block-end: 12px;
27
+ padding-inline: 12px;
28
+ `,
19
29
  expand: css`
20
- color: ${token.colorTextSecondary};
21
- background: ${token.colorFillTertiary};
30
+ color: ${token.colorTextTertiary};
22
31
  `,
23
-
24
32
  header: css`
25
33
  padding-block: 4px;
26
34
  padding-inline: 8px 4px;
@@ -34,7 +42,6 @@ const useStyles = createStyles(({ css, token }) => ({
34
42
 
35
43
  headerExpand: css`
36
44
  color: ${token.colorTextSecondary};
37
- background: ${token.colorFillQuaternary};
38
45
  `,
39
46
  shinyText: css`
40
47
  color: ${rgba(token.colorText, 0.45)};
@@ -86,11 +93,39 @@ const Thinking = memo<ThinkingProps>((props) => {
86
93
  const { styles, cx, theme } = useStyles();
87
94
 
88
95
  const [showDetail, setShowDetail] = useState(false);
96
+ const contentRef = useRef<HTMLDivElement | null>(null);
89
97
 
90
98
  useEffect(() => {
91
99
  setShowDetail(!!thinking);
92
100
  }, [thinking]);
93
101
 
102
+ // 当内容变更且正在思考时,如果用户接近底部则自动滚动到底部
103
+ useEffect(() => {
104
+ if (!thinking || !showDetail) return;
105
+ const container = contentRef.current;
106
+ if (!container) return;
107
+
108
+ // 仅当用户接近底部时才自动滚动,避免打断用户查看上方内容
109
+ const distanceToBottom = container.scrollHeight - container.scrollTop - container.clientHeight;
110
+ const isNearBottom = distanceToBottom < 60;
111
+
112
+ if (isNearBottom) {
113
+ requestAnimationFrame(() => {
114
+ container.scrollTop = container.scrollHeight;
115
+ });
116
+ }
117
+ }, [content, thinking, showDetail]);
118
+
119
+ // 展开时滚动到底部,便于查看最新内容
120
+ useEffect(() => {
121
+ if (!showDetail) return;
122
+ const container = contentRef.current;
123
+ if (!container) return;
124
+ requestAnimationFrame(() => {
125
+ container.scrollTop = container.scrollHeight;
126
+ });
127
+ }, [showDetail]);
128
+
94
129
  return (
95
130
  <Flexbox
96
131
  className={cx(styles.container, showDetail && styles.expand)}
@@ -145,7 +180,7 @@ const Thinking = memo<ThinkingProps>((props) => {
145
180
  animate="open"
146
181
  exit="collapsed"
147
182
  initial="collapsed"
148
- style={{ overflow: 'hidden', padding: 12 }}
183
+ style={{ overflow: 'hidden' }}
149
184
  transition={{
150
185
  duration: 0.2,
151
186
  ease: [0.4, 0, 0.2, 1], // 使用 ease-out 缓动函数
@@ -155,13 +190,18 @@ const Thinking = memo<ThinkingProps>((props) => {
155
190
  open: { opacity: 1, width: 'auto' },
156
191
  }}
157
192
  >
158
- {typeof content === 'string' ? (
159
- <Markdown animated={thinkingAnimated} citations={citations} variant={'chat'}>
160
- {content}
161
- </Markdown>
162
- ) : (
163
- content
164
- )}
193
+ <div
194
+ className={styles.contentScroll}
195
+ ref={contentRef as unknown as RefObject<HTMLDivElement>}
196
+ >
197
+ {typeof content === 'string' ? (
198
+ <Markdown animated={thinkingAnimated} citations={citations} variant={'chat'}>
199
+ {content}
200
+ </Markdown>
201
+ ) : (
202
+ content
203
+ )}
204
+ </div>
165
205
  </motion.div>
166
206
  )}
167
207
  </AnimatePresence>
@@ -28,6 +28,12 @@ export const HOTKEYS_REGISTRATION: HotkeyRegistration = [
28
28
  nonEditable: true,
29
29
  scopes: [HotkeyScopeEnum.Global],
30
30
  },
31
+ {
32
+ group: HotkeyGroupEnum.Essential,
33
+ id: HotkeyEnum.NavigateToChat,
34
+ keys: combineKeys([KeyEnum.Ctrl, KeyEnum.Backquote]),
35
+ scopes: [HotkeyScopeEnum.Global],
36
+ },
31
37
  {
32
38
  group: HotkeyGroupEnum.Essential,
33
39
  id: HotkeyEnum.ToggleZenMode,
@@ -1,5 +1,4 @@
1
1
  import isEqual from 'fast-deep-equal';
2
- import { parseAsBoolean, useQueryState } from 'nuqs';
3
2
  import { useEffect } from 'react';
4
3
  import { useHotkeysContext } from 'react-hotkeys-hook';
5
4
 
@@ -13,6 +12,7 @@ import { useGlobalStore } from '@/store/global';
13
12
  import { systemStatusSelectors } from '@/store/global/selectors';
14
13
  import { HotkeyEnum, HotkeyScopeEnum } from '@/types/hotkey';
15
14
 
15
+ import { usePinnedAgentState } from '../usePinnedAgentState';
16
16
  import { useHotkeyById } from './useHotkeyById';
17
17
 
18
18
  export const useSaveTopicHotkey = () => {
@@ -48,7 +48,7 @@ export const useRegenerateMessageHotkey = () => {
48
48
 
49
49
  export const useToggleLeftPanelHotkey = () => {
50
50
  const isZenMode = useGlobalStore((s) => s.status.zenMode);
51
- const [isPinned] = useQueryState('pinned', parseAsBoolean);
51
+ const [isPinned] = usePinnedAgentState();
52
52
  const showSessionPanel = useGlobalStore(systemStatusSelectors.showSessionPanel);
53
53
  const updateSystemStatus = useGlobalStore((s) => s.updateSystemStatus);
54
54
 
@@ -1,7 +1,8 @@
1
1
  import isEqual from 'fast-deep-equal';
2
- import { parseAsBoolean, useQueryState } from 'nuqs';
3
2
  import { useHotkeys } from 'react-hotkeys-hook';
4
3
 
4
+ import { INBOX_SESSION_ID } from '@/const/session';
5
+ import { usePinnedAgentState } from '@/hooks/usePinnedAgentState';
5
6
  import { useSwitchSession } from '@/hooks/useSwitchSession';
6
7
  import { useGlobalStore } from '@/store/global';
7
8
  import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig';
@@ -18,12 +19,11 @@ export const useSwitchAgentHotkey = () => {
18
19
  const list = useSessionStore(sessionSelectors.pinnedSessions, isEqual);
19
20
  const hotkey = useUserStore(settingsSelectors.getHotkeyById(HotkeyEnum.SwitchAgent));
20
21
  const switchSession = useSwitchSession();
21
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
22
- const [_, setPinned] = useQueryState('pinned', parseAsBoolean);
22
+ const [, { pinAgent }] = usePinnedAgentState();
23
23
 
24
24
  const switchAgent = (id: string) => {
25
25
  switchSession(id);
26
- setPinned(true);
26
+ pinAgent();
27
27
  };
28
28
 
29
29
  const ref = useHotkeys(
@@ -49,6 +49,17 @@ export const useSwitchAgentHotkey = () => {
49
49
  };
50
50
  };
51
51
 
52
+ // 切换到会话标签(并聚焦到随便聊聊)
53
+ export const useNavigateToChatHotkey = () => {
54
+ const switchSession = useSwitchSession();
55
+ const [, { unpinAgent }] = usePinnedAgentState();
56
+
57
+ return useHotkeyById(HotkeyEnum.NavigateToChat, () => {
58
+ switchSession(INBOX_SESSION_ID);
59
+ unpinAgent();
60
+ });
61
+ };
62
+
52
63
  export const useOpenHotkeyHelperHotkey = () => {
53
64
  const [open, updateSystemStatus] = useGlobalStore((s) => [
54
65
  s.status.showHotkeyHelper,
@@ -65,5 +76,6 @@ export const useOpenHotkeyHelperHotkey = () => {
65
76
  export const useRegisterGlobalHotkeys = () => {
66
77
  // 全局自动注册不需要 enableScope
67
78
  useSwitchAgentHotkey();
79
+ useNavigateToChatHotkey();
68
80
  useOpenHotkeyHelperHotkey();
69
81
  };
@@ -0,0 +1,21 @@
1
+ import { parseAsBoolean, useQueryState } from 'nuqs';
2
+ import { useMemo } from 'react';
3
+
4
+ export const usePinnedAgentState = () => {
5
+ const [isPinned, setIsPinned] = useQueryState(
6
+ 'pinned',
7
+ parseAsBoolean.withDefault(false).withOptions({ clearOnDefault: true }),
8
+ );
9
+
10
+ const actions = useMemo(
11
+ () => ({
12
+ pinAgent: () => setIsPinned(true),
13
+ setIsPinned,
14
+ togglePinAgent: () => setIsPinned((prev) => !prev),
15
+ unpinAgent: () => setIsPinned(false),
16
+ }),
17
+ [],
18
+ );
19
+
20
+ return [isPinned, actions] as const;
21
+ };
@@ -27,6 +27,6 @@ export const useSwitchSession = () => {
27
27
  }, 50);
28
28
  }
29
29
  },
30
- [mobile],
30
+ [mobile, pathname],
31
31
  );
32
32
  };
@@ -25,6 +25,10 @@ const hotkey: HotkeyI18nTranslations & {
25
25
  desc: '通过按住 Alt 并双击消息进入编辑模式',
26
26
  title: '编辑消息',
27
27
  },
28
+ navigateToChat: {
29
+ desc: '切换至会话标签并进入随便聊聊',
30
+ title: '切换至默认会话',
31
+ },
28
32
  openChatSettings: {
29
33
  desc: '查看和修改当前会话的设置',
30
34
  title: '打开会话设置',