@lobehub/chat 1.25.2 → 1.26.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 CHANGED
@@ -2,6 +2,56 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## [Version 1.26.0](https://github.com/lobehub/lobe-chat/compare/v1.25.3...v1.26.0)
6
+
7
+ <sup>Released on **2024-10-27**</sup>
8
+
9
+ #### ✨ Features
10
+
11
+ - **misc**: experimentally support to pin assistant to sidebar.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### What's improved
19
+
20
+ - **misc**: experimentally support to pin assistant to sidebar, closes [#4514](https://github.com/lobehub/lobe-chat/issues/4514) ([6e55865](https://github.com/lobehub/lobe-chat/commit/6e55865))
21
+
22
+ </details>
23
+
24
+ <div align="right">
25
+
26
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
27
+
28
+ </div>
29
+
30
+ ### [Version 1.25.3](https://github.com/lobehub/lobe-chat/compare/v1.25.2...v1.25.3)
31
+
32
+ <sup>Released on **2024-10-27**</sup>
33
+
34
+ #### 🐛 Bug Fixes
35
+
36
+ - **misc**: Fix the issue of the switch assistant portal not closing.
37
+
38
+ <br/>
39
+
40
+ <details>
41
+ <summary><kbd>Improvements and Fixes</kbd></summary>
42
+
43
+ #### What's fixed
44
+
45
+ - **misc**: Fix the issue of the switch assistant portal not closing, closes [#4500](https://github.com/lobehub/lobe-chat/issues/4500) ([83f896b](https://github.com/lobehub/lobe-chat/commit/83f896b))
46
+
47
+ </details>
48
+
49
+ <div align="right">
50
+
51
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
52
+
53
+ </div>
54
+
5
55
  ### [Version 1.25.2](https://github.com/lobehub/lobe-chat/compare/v1.25.1...v1.25.2)
6
56
 
7
57
  <sup>Released on **2024-10-27**</sup>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "1.25.2",
3
+ "version": "1.26.0",
4
4
  "description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
5
5
  "keywords": [
6
6
  "framework",
@@ -0,0 +1,91 @@
1
+ import { Avatar, Tooltip } from '@lobehub/ui';
2
+ import { Divider } from 'antd';
3
+ import { createStyles } from 'antd-style';
4
+ import isEqual from 'fast-deep-equal';
5
+ import { parseAsBoolean, useQueryState } from 'nuqs';
6
+ import { useHotkeys } from 'react-hotkeys-hook';
7
+ import { Flexbox } from 'react-layout-kit';
8
+
9
+ import HotKeys from '@/components/HotKeys';
10
+ import { useSessionStore } from '@/store/session';
11
+ import { sessionHelpers } from '@/store/session/helpers';
12
+ import { sessionSelectors } from '@/store/session/selectors';
13
+
14
+ const useStyles = createStyles(({ css, token }) => ({
15
+ avatar: css`
16
+ position: relative;
17
+ transition: all 200ms ease-out 0s;
18
+
19
+ &:hover {
20
+ box-shadow: 0 0 0 2px ${token.colorPrimary};
21
+ }
22
+ `,
23
+ avatarActive: css`
24
+ background: ${token.colorFillQuaternary};
25
+ box-shadow: 0 0 0 2px ${token.colorPrimaryBorder};
26
+ `,
27
+ }));
28
+
29
+ const PinList = () => {
30
+ const { styles, cx } = useStyles();
31
+ const list = useSessionStore(sessionSelectors.pinnedSessions, isEqual);
32
+ const [activeId, switchSession] = useSessionStore((s) => [s.activeId, s.switchSession]);
33
+
34
+ const hasList = list.length > 0;
35
+ const [isPinned, setPinned] = useQueryState('pinned', parseAsBoolean);
36
+
37
+ const switchAgent = (id: string) => {
38
+ switchSession(id);
39
+ setPinned(true);
40
+ };
41
+
42
+ useHotkeys(
43
+ list.slice(0, 9).map((e, i) => `ctrl+${i + 1}`),
44
+ (keyboardEvent, hotkeysEvent) => {
45
+ if (!hotkeysEvent.keys?.[0]) return;
46
+
47
+ const index = parseInt(hotkeysEvent.keys?.[0]) - 1;
48
+ const item = list[index];
49
+ if (!item) return;
50
+
51
+ switchAgent(item.id);
52
+ },
53
+ { enableOnFormTags: true, preventDefault: true },
54
+ );
55
+
56
+ return (
57
+ hasList && (
58
+ <>
59
+ <Divider style={{ margin: '8px 12px' }} />
60
+ <Flexbox flex={1} gap={12} height={'100%'}>
61
+ {list.slice(0, 9).map((item, index) => (
62
+ <Tooltip
63
+ key={item.id}
64
+ placement={'right'}
65
+ title={
66
+ <Flexbox gap={8} horizontal>
67
+ {sessionHelpers.getTitle(item.meta)}
68
+ <HotKeys inverseTheme keys={`ctrl+${index + 1}`} />
69
+ </Flexbox>
70
+ }
71
+ >
72
+ <Avatar
73
+ avatar={sessionHelpers.getAvatar(item.meta)}
74
+ background={item.meta.backgroundColor}
75
+ className={cx(
76
+ styles.avatar,
77
+ isPinned && activeId === item.id ? styles.avatarActive : undefined,
78
+ )}
79
+ onClick={() => {
80
+ switchAgent(item.id);
81
+ }}
82
+ />
83
+ </Tooltip>
84
+ ))}
85
+ </Flexbox>
86
+ </>
87
+ )
88
+ );
89
+ };
90
+
91
+ export default PinList;
@@ -6,22 +6,29 @@ import { memo } from 'react';
6
6
  import { useActiveTabKey } from '@/hooks/useActiveTabKey';
7
7
  import { useGlobalStore } from '@/store/global';
8
8
  import { systemStatusSelectors } from '@/store/global/selectors';
9
+ import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig';
9
10
 
10
11
  import Avatar from './Avatar';
11
12
  import BottomActions from './BottomActions';
13
+ import PinList from './PinList';
12
14
  import TopActions from './TopActions';
13
15
 
14
16
  const Nav = memo(() => {
15
17
  const sidebarKey = useActiveTabKey();
16
18
  const inZenMode = useGlobalStore(systemStatusSelectors.inZenMode);
17
-
19
+ const { showPinList } = useServerConfigStore(featureFlagsSelectors);
18
20
  return (
19
21
  !inZenMode && (
20
22
  <SideNav
21
23
  avatar={<Avatar />}
22
24
  bottomActions={<BottomActions />}
23
25
  style={{ height: '100%', zIndex: 100 }}
24
- topActions={<TopActions tab={sidebarKey} />}
26
+ topActions={
27
+ <>
28
+ <TopActions tab={sidebarKey} />
29
+ {showPinList && <PinList />}
30
+ </>
31
+ }
25
32
  />
26
33
  )
27
34
  );
@@ -3,6 +3,7 @@
3
3
  import { ActionIcon, Avatar, ChatHeaderTitle } from '@lobehub/ui';
4
4
  import { Skeleton } from 'antd';
5
5
  import { PanelLeftClose, PanelLeftOpen } from 'lucide-react';
6
+ import { parseAsBoolean, useQueryState } from 'nuqs';
6
7
  import { Suspense, memo } from 'react';
7
8
  import { useTranslation } from 'react-i18next';
8
9
  import { Flexbox } from 'react-layout-kit';
@@ -21,6 +22,7 @@ const Main = memo(() => {
21
22
  const { t } = useTranslation('chat');
22
23
 
23
24
  useInitAgentConfig();
25
+ const [isPinned] = useQueryState('pinned', parseAsBoolean);
24
26
 
25
27
  const [init, isInbox, title, description, avatar, backgroundColor] = useSessionStore((s) => [
26
28
  sessionSelectors.isSomeSessionActive(s),
@@ -49,7 +51,7 @@ const Main = memo(() => {
49
51
  </Flexbox>
50
52
  ) : (
51
53
  <Flexbox align={'center'} gap={4} horizontal>
52
- {
54
+ {!isPinned && (
53
55
  <ActionIcon
54
56
  aria-label={t('agents')}
55
57
  icon={showSessionPanel ? PanelLeftClose : PanelLeftOpen}
@@ -62,7 +64,7 @@ const Main = memo(() => {
62
64
  size={DESKTOP_HEADER_ICON_SIZE}
63
65
  title={t('agents')}
64
66
  />
65
- }
67
+ )}
66
68
  <Avatar
67
69
  avatar={avatar}
68
70
  background={backgroundColor}
@@ -1,17 +1,20 @@
1
1
  import { useCallback } from 'react';
2
2
 
3
3
  import { useQueryRoute } from '@/hooks/useQueryRoute';
4
+ import { useChatStore } from '@/store/chat';
4
5
  import { useServerConfigStore } from '@/store/serverConfig';
5
6
  import { useSessionStore } from '@/store/session';
6
7
 
7
8
  export const useSwitchSession = () => {
8
9
  const switchSession = useSessionStore((s) => s.switchSession);
10
+ const togglePortal = useChatStore((s) => s.togglePortal);
9
11
  const mobile = useServerConfigStore((s) => s.isMobile);
10
12
  const router = useQueryRoute();
11
13
 
12
14
  return useCallback(
13
15
  (id: string) => {
14
16
  switchSession(id);
17
+ togglePortal(false);
15
18
 
16
19
  if (mobile) {
17
20
  setTimeout(() => {
@@ -3,6 +3,7 @@
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';
6
7
  import { PropsWithChildren, memo, useEffect, useState } from 'react';
7
8
 
8
9
  import { FOLDER_WIDTH } from '@/const/layoutTokens';
@@ -20,6 +21,8 @@ export const useStyles = createStyles(({ css, token }) => ({
20
21
  const SessionPanel = memo<PropsWithChildren>(({ children }) => {
21
22
  const { md = true } = useResponsive();
22
23
 
24
+ const [isPinned] = useQueryState('pinned', parseAsBoolean);
25
+
23
26
  const { styles } = useStyles();
24
27
  const [sessionsWidth, sessionExpandable, updatePreference] = useGlobalStore((s) => [
25
28
  systemStatusSelectors.sessionWidth(s),
@@ -56,7 +59,9 @@ const SessionPanel = memo<PropsWithChildren>(({ children }) => {
56
59
  <DraggablePanel
57
60
  className={styles.panel}
58
61
  defaultSize={{ width: tmpWidth }}
59
- expand={sessionExpandable}
62
+ // 当进入 pin 模式下,不可展开
63
+ expand={!isPinned && sessionExpandable}
64
+ expandable={!isPinned}
60
65
  maxWidth={400}
61
66
  minWidth={FOLDER_WIDTH}
62
67
  mode={md ? 'fixed' : 'float'}
@@ -7,6 +7,7 @@ export const FeatureFlagsSchema = z.object({
7
7
  */
8
8
  webrtc_sync: z.boolean().optional(),
9
9
  check_updates: z.boolean().optional(),
10
+ pin_list: z.boolean().optional(),
10
11
 
11
12
  // settings
12
13
  language_model_settings: z.boolean().optional(),
@@ -45,6 +46,7 @@ export type IFeatureFlags = z.infer<typeof FeatureFlagsSchema>;
45
46
 
46
47
  export const DEFAULT_FEATURE_FLAGS: IFeatureFlags = {
47
48
  webrtc_sync: false,
49
+ pin_list: false,
48
50
 
49
51
  language_model_settings: true,
50
52
 
@@ -85,6 +87,7 @@ export const mapFeatureFlagsEnvToState = (config: IFeatureFlags) => {
85
87
 
86
88
  showCreateSession: config.create_session,
87
89
  showLLM: config.language_model_settings,
90
+ showPinList: config.pin_list,
88
91
 
89
92
  showOpenAIApiKey: config.openai_api_key,
90
93
  showOpenAIProxyUrl: config.openai_proxy_url,
@@ -34,6 +34,7 @@ describe('featureFlagsSelectors', () => {
34
34
  showWelcomeSuggest: true,
35
35
  enableClerkSignUp: true,
36
36
  showMarket: true,
37
+ showPinList: false,
37
38
  enableSTT: true,
38
39
  });
39
40
  });
@@ -1,8 +1,15 @@
1
+ import { t } from 'i18next';
2
+
3
+ import { DEFAULT_AVATAR } from '@/const/meta';
1
4
  import { DEFAULT_AGENT_LOBE_SESSION } from '@/const/session';
5
+ import { MetaData } from '@/types/meta';
2
6
  import { LobeAgentSession, LobeSessions } from '@/types/session';
3
7
 
4
8
  export const getSessionPinned = (session: LobeAgentSession) => session.pinned;
5
9
 
10
+ const getAvatar = (s: MetaData) => s.avatar || DEFAULT_AVATAR;
11
+ const getTitle = (s: MetaData) => s.title || t('defaultSession', { ns: 'common' });
12
+
6
13
  const getSessionById = (id: string, sessions: LobeSessions): LobeAgentSession => {
7
14
  const session = sessions.find((s) => s.id === id);
8
15
 
@@ -12,6 +19,8 @@ const getSessionById = (id: string, sessions: LobeSessions): LobeAgentSession =>
12
19
  };
13
20
 
14
21
  export const sessionHelpers = {
22
+ getAvatar,
15
23
  getSessionById,
16
24
  getSessionPinned,
25
+ getTitle,
17
26
  };