@lobehub/chat 1.25.3 → 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 +25 -0
- package/package.json +1 -1
- package/src/app/(main)/@nav/_layout/Desktop/PinList/index.tsx +91 -0
- package/src/app/(main)/@nav/_layout/Desktop/index.tsx +9 -2
- package/src/app/(main)/chat/(workspace)/_layout/Desktop/ChatHeader/Main.tsx +4 -2
- package/src/app/(main)/chat/_layout/Desktop/SessionPanel.tsx +6 -1
- package/src/config/featureFlags/schema.ts +3 -0
- package/src/store/serverConfig/selectors.test.ts +1 -0
- package/src/store/session/slices/session/helpers.ts +9 -0
package/CHANGELOG.md
CHANGED
@@ -2,6 +2,31 @@
|
|
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
|
+
[](#readme-top)
|
27
|
+
|
28
|
+
</div>
|
29
|
+
|
5
30
|
### [Version 1.25.3](https://github.com/lobehub/lobe-chat/compare/v1.25.2...v1.25.3)
|
6
31
|
|
7
32
|
<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.
|
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={
|
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}
|
@@ -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
|
-
|
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,
|
@@ -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
|
};
|