@lobehub/lobehub 2.0.0-next.285 → 2.0.0-next.287
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 +58 -0
- package/apps/desktop/src/main/const/theme.ts +0 -3
- package/apps/desktop/src/main/core/browser/Browser.ts +1 -1
- package/apps/desktop/src/main/core/browser/WindowThemeManager.ts +3 -2
- package/apps/desktop/src/main/core/browser/__tests__/Browser.test.ts +0 -1
- package/apps/desktop/src/main/core/browser/__tests__/WindowThemeManager.test.ts +8 -5
- package/changelog/v1.json +14 -0
- package/package.json +1 -1
- package/packages/business/const/src/index.ts +0 -3
- package/packages/desktop-bridge/src/index.ts +3 -0
- package/src/app/[variants]/(main)/agent/features/Conversation/AgentWelcome/AddButton.tsx +2 -1
- package/src/app/[variants]/(main)/agent/features/Conversation/Header/ShareButton/index.tsx +5 -3
- package/src/app/[variants]/(main)/community/(detail)/provider/features/Details/Nav.tsx +27 -18
- package/src/app/[variants]/(main)/group/features/Conversation/Header/ShareButton/index.tsx +5 -3
- package/src/app/[variants]/(main)/settings/hooks/useCategory.tsx +15 -2
- package/src/app/[variants]/onboarding/_layout/style.ts +10 -20
- package/src/features/ElectronTitlebar/Connection/ConnectionMode.tsx +2 -2
- package/src/features/ElectronTitlebar/SimpleTitleBar.tsx +1 -2
- package/src/features/ElectronTitlebar/index.tsx +2 -2
- package/src/hooks/useUserAvatar.test.ts +23 -4
- package/src/store/electron/selectors/sync.ts +17 -1
- package/src/store/user/slices/settings/action.test.ts +25 -0
- package/src/store/user/slices/settings/action.ts +11 -0
- package/src/features/ElectronTitlebar/const.ts +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,64 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
## [Version 2.0.0-next.287](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.286...v2.0.0-next.287)
|
|
6
|
+
|
|
7
|
+
<sup>Released on **2026-01-14**</sup>
|
|
8
|
+
|
|
9
|
+
#### ♻ Code Refactoring
|
|
10
|
+
|
|
11
|
+
- **desktop**: Unify TITLE_BAR_HEIGHT constant to desktop-bridge.
|
|
12
|
+
|
|
13
|
+
#### 🐛 Bug Fixes
|
|
14
|
+
|
|
15
|
+
- **desktop**: Return OFFICIAL_URL in cloud mode for remoteServerUrl selector.
|
|
16
|
+
|
|
17
|
+
<br/>
|
|
18
|
+
|
|
19
|
+
<details>
|
|
20
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
21
|
+
|
|
22
|
+
#### Code refactoring
|
|
23
|
+
|
|
24
|
+
- **desktop**: Unify TITLE_BAR_HEIGHT constant to desktop-bridge, closes [#11496](https://github.com/lobehub/lobe-chat/issues/11496) ([e7739e5](https://github.com/lobehub/lobe-chat/commit/e7739e5))
|
|
25
|
+
|
|
26
|
+
#### What's fixed
|
|
27
|
+
|
|
28
|
+
- **desktop**: Return OFFICIAL_URL in cloud mode for remoteServerUrl selector, closes [#11502](https://github.com/lobehub/lobe-chat/issues/11502) ([1d11fac](https://github.com/lobehub/lobe-chat/commit/1d11fac))
|
|
29
|
+
|
|
30
|
+
</details>
|
|
31
|
+
|
|
32
|
+
<div align="right">
|
|
33
|
+
|
|
34
|
+
[](#readme-top)
|
|
35
|
+
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
## [Version 2.0.0-next.286](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.285...v2.0.0-next.286)
|
|
39
|
+
|
|
40
|
+
<sup>Released on **2026-01-14**</sup>
|
|
41
|
+
|
|
42
|
+
#### 🐛 Bug Fixes
|
|
43
|
+
|
|
44
|
+
- **misc**: Prevent auto navigation to profile when clicking topic.
|
|
45
|
+
|
|
46
|
+
<br/>
|
|
47
|
+
|
|
48
|
+
<details>
|
|
49
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
50
|
+
|
|
51
|
+
#### What's fixed
|
|
52
|
+
|
|
53
|
+
- **misc**: Prevent auto navigation to profile when clicking topic, closes [#11500](https://github.com/lobehub/lobe-chat/issues/11500) ([1e03005](https://github.com/lobehub/lobe-chat/commit/1e03005))
|
|
54
|
+
|
|
55
|
+
</details>
|
|
56
|
+
|
|
57
|
+
<div align="right">
|
|
58
|
+
|
|
59
|
+
[](#readme-top)
|
|
60
|
+
|
|
61
|
+
</div>
|
|
62
|
+
|
|
5
63
|
## [Version 2.0.0-next.285](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.284...v2.0.0-next.285)
|
|
6
64
|
|
|
7
65
|
<sup>Released on **2026-01-14**</sup>
|
|
@@ -4,8 +4,5 @@ export const BACKGROUND_LIGHT = '#f8f8f8';
|
|
|
4
4
|
export const SYMBOL_COLOR_DARK = '#ffffff80';
|
|
5
5
|
export const SYMBOL_COLOR_LIGHT = '#00000080';
|
|
6
6
|
|
|
7
|
-
// Window dimensions and constraints
|
|
8
|
-
export const TITLE_BAR_HEIGHT = 29;
|
|
9
|
-
|
|
10
7
|
// Default window configuration
|
|
11
8
|
export const THEME_CHANGE_DELAY = 100;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { TITLE_BAR_HEIGHT } from '@lobechat/desktop-bridge';
|
|
1
2
|
import { MainBroadcastEventKey, MainBroadcastParams } from '@lobechat/electron-client-ipc';
|
|
2
3
|
import {
|
|
3
4
|
BrowserWindow,
|
|
@@ -12,7 +13,6 @@ import { join } from 'node:path';
|
|
|
12
13
|
import { preloadDir, resourcesDir } from '@/const/dir';
|
|
13
14
|
import { isMac } from '@/const/env';
|
|
14
15
|
import { ELECTRON_BE_PROTOCOL_SCHEME } from '@/const/protocol';
|
|
15
|
-
import { TITLE_BAR_HEIGHT } from '@/const/theme';
|
|
16
16
|
import RemoteServerConfigCtr from '@/controllers/RemoteServerConfigCtr';
|
|
17
17
|
import { backendProxyProtocolManager } from '@/core/infrastructure/BackendProxyProtocolManager';
|
|
18
18
|
import { setResponseHeader } from '@/utils/http-headers';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { TITLE_BAR_HEIGHT } from '@lobechat/desktop-bridge';
|
|
1
2
|
import { BrowserWindow, nativeTheme } from 'electron';
|
|
2
3
|
import { join } from 'node:path';
|
|
3
4
|
|
|
@@ -9,7 +10,6 @@ import {
|
|
|
9
10
|
SYMBOL_COLOR_DARK,
|
|
10
11
|
SYMBOL_COLOR_LIGHT,
|
|
11
12
|
THEME_CHANGE_DELAY,
|
|
12
|
-
TITLE_BAR_HEIGHT,
|
|
13
13
|
} from '@/const/theme';
|
|
14
14
|
import { createLogger } from '@/utils/logger';
|
|
15
15
|
|
|
@@ -91,7 +91,8 @@ export class WindowThemeManager {
|
|
|
91
91
|
icon: isDev ? join(buildDir, 'icon-dev.ico') : undefined,
|
|
92
92
|
titleBarOverlay: {
|
|
93
93
|
color: isDarkMode ? BACKGROUND_DARK : BACKGROUND_LIGHT,
|
|
94
|
-
|
|
94
|
+
// Reduce 2px to prevent blocking the container border edge
|
|
95
|
+
height: TITLE_BAR_HEIGHT - 2,
|
|
95
96
|
symbolColor: isDarkMode ? SYMBOL_COLOR_DARK : SYMBOL_COLOR_LIGHT,
|
|
96
97
|
},
|
|
97
98
|
titleBarStyle: 'hidden',
|
|
@@ -38,13 +38,16 @@ vi.mock('@/const/env', () => ({
|
|
|
38
38
|
isWindows: true,
|
|
39
39
|
}));
|
|
40
40
|
|
|
41
|
+
vi.mock('@lobechat/desktop-bridge', () => ({
|
|
42
|
+
TITLE_BAR_HEIGHT: 38,
|
|
43
|
+
}));
|
|
44
|
+
|
|
41
45
|
vi.mock('@/const/theme', () => ({
|
|
42
46
|
BACKGROUND_DARK: '#1a1a1a',
|
|
43
47
|
BACKGROUND_LIGHT: '#ffffff',
|
|
44
48
|
SYMBOL_COLOR_DARK: '#ffffff',
|
|
45
49
|
SYMBOL_COLOR_LIGHT: '#000000',
|
|
46
50
|
THEME_CHANGE_DELAY: 0,
|
|
47
|
-
TITLE_BAR_HEIGHT: 32,
|
|
48
51
|
}));
|
|
49
52
|
|
|
50
53
|
describe('WindowThemeManager', () => {
|
|
@@ -89,7 +92,7 @@ describe('WindowThemeManager', () => {
|
|
|
89
92
|
icon: undefined,
|
|
90
93
|
titleBarOverlay: {
|
|
91
94
|
color: '#1a1a1a',
|
|
92
|
-
height:
|
|
95
|
+
height: 36,
|
|
93
96
|
symbolColor: '#ffffff',
|
|
94
97
|
},
|
|
95
98
|
titleBarStyle: 'hidden',
|
|
@@ -106,7 +109,7 @@ describe('WindowThemeManager', () => {
|
|
|
106
109
|
icon: undefined,
|
|
107
110
|
titleBarOverlay: {
|
|
108
111
|
color: '#ffffff',
|
|
109
|
-
height:
|
|
112
|
+
height: 36,
|
|
110
113
|
symbolColor: '#000000',
|
|
111
114
|
},
|
|
112
115
|
titleBarStyle: 'hidden',
|
|
@@ -183,7 +186,7 @@ describe('WindowThemeManager', () => {
|
|
|
183
186
|
expect(mockBrowserWindow.setBackgroundColor).toHaveBeenCalledWith('#1a1a1a');
|
|
184
187
|
expect(mockBrowserWindow.setTitleBarOverlay).toHaveBeenCalledWith({
|
|
185
188
|
color: '#1a1a1a',
|
|
186
|
-
height:
|
|
189
|
+
height: 36,
|
|
187
190
|
symbolColor: '#ffffff',
|
|
188
191
|
});
|
|
189
192
|
});
|
|
@@ -195,7 +198,7 @@ describe('WindowThemeManager', () => {
|
|
|
195
198
|
expect(mockBrowserWindow.setBackgroundColor).toHaveBeenCalledWith('#ffffff');
|
|
196
199
|
expect(mockBrowserWindow.setTitleBarOverlay).toHaveBeenCalledWith({
|
|
197
200
|
color: '#ffffff',
|
|
198
|
-
height:
|
|
201
|
+
height: 36,
|
|
199
202
|
symbolColor: '#000000',
|
|
200
203
|
});
|
|
201
204
|
});
|
package/changelog/v1.json
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"children": {},
|
|
4
|
+
"date": "2026-01-14",
|
|
5
|
+
"version": "2.0.0-next.287"
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
"children": {
|
|
9
|
+
"fixes": [
|
|
10
|
+
"Prevent auto navigation to profile when clicking topic."
|
|
11
|
+
]
|
|
12
|
+
},
|
|
13
|
+
"date": "2026-01-14",
|
|
14
|
+
"version": "2.0.0-next.286"
|
|
15
|
+
},
|
|
2
16
|
{
|
|
3
17
|
"children": {
|
|
4
18
|
"features": [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobehub/lobehub",
|
|
3
|
-
"version": "2.0.0-next.
|
|
3
|
+
"version": "2.0.0-next.287",
|
|
4
4
|
"description": "LobeHub - an open-source,comprehensive AI Agent 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",
|
|
@@ -9,7 +9,8 @@ import { useAgentStore } from '@/store/agent';
|
|
|
9
9
|
const AddButton = memo(() => {
|
|
10
10
|
const navigate = useNavigate();
|
|
11
11
|
const createAgent = useAgentStore((s) => s.createAgent);
|
|
12
|
-
|
|
12
|
+
// Use a unique SWR key to avoid conflicts with useCreateMenuItems which uses 'agent.createAgent'
|
|
13
|
+
const { mutate, isValidating } = useActionSWR('agent.createAgentFromWelcome', async () => {
|
|
13
14
|
const result = await createAgent({});
|
|
14
15
|
navigate(`/agent/${result.agentId}/profile`);
|
|
15
16
|
return result;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { ENABLE_TOPIC_LINK_SHARE } from '@lobechat/business-const';
|
|
4
3
|
import { ActionIcon } from '@lobehub/ui';
|
|
5
4
|
import { Share2 } from 'lucide-react';
|
|
6
5
|
import dynamic from 'next/dynamic';
|
|
@@ -10,6 +9,8 @@ import { useTranslation } from 'react-i18next';
|
|
|
10
9
|
import { DESKTOP_HEADER_ICON_SIZE, MOBILE_HEADER_ICON_SIZE } from '@/const/layoutTokens';
|
|
11
10
|
import { useWorkspaceModal } from '@/hooks/useWorkspaceModal';
|
|
12
11
|
import { useChatStore } from '@/store/chat';
|
|
12
|
+
import { useServerConfigStore } from '@/store/serverConfig';
|
|
13
|
+
import { serverConfigSelectors } from '@/store/serverConfig/selectors';
|
|
13
14
|
|
|
14
15
|
const ShareModal = dynamic(() => import('@/features/ShareModal'));
|
|
15
16
|
const SharePopover = dynamic(() => import('@/features/SharePopover'));
|
|
@@ -24,6 +25,7 @@ const ShareButton = memo<ShareButtonProps>(({ mobile, setOpen, open }) => {
|
|
|
24
25
|
const [isModalOpen, setIsModalOpen] = useWorkspaceModal(open, setOpen);
|
|
25
26
|
const { t } = useTranslation('common');
|
|
26
27
|
const activeTopicId = useChatStore((s) => s.activeTopicId);
|
|
28
|
+
const enableTopicLinkShare = useServerConfigStore(serverConfigSelectors.enableBusinessFeatures);
|
|
27
29
|
|
|
28
30
|
// Hide share button when no topic exists (no messages sent yet)
|
|
29
31
|
if (!activeTopicId) return null;
|
|
@@ -31,7 +33,7 @@ const ShareButton = memo<ShareButtonProps>(({ mobile, setOpen, open }) => {
|
|
|
31
33
|
const iconButton = (
|
|
32
34
|
<ActionIcon
|
|
33
35
|
icon={Share2}
|
|
34
|
-
onClick={
|
|
36
|
+
onClick={enableTopicLinkShare ? undefined : () => setIsModalOpen(true)}
|
|
35
37
|
size={mobile ? MOBILE_HEADER_ICON_SIZE : DESKTOP_HEADER_ICON_SIZE}
|
|
36
38
|
title={t('share')}
|
|
37
39
|
tooltipProps={{
|
|
@@ -42,7 +44,7 @@ const ShareButton = memo<ShareButtonProps>(({ mobile, setOpen, open }) => {
|
|
|
42
44
|
|
|
43
45
|
return (
|
|
44
46
|
<>
|
|
45
|
-
{
|
|
47
|
+
{enableTopicLinkShare ? (
|
|
46
48
|
<SharePopover onOpenModal={() => setIsModalOpen(true)}>{iconButton}</SharePopover>
|
|
47
49
|
) : (
|
|
48
50
|
iconButton
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { SOCIAL_URL } from '@lobechat/business-const';
|
|
3
|
+
import { BRANDING_PROVIDER, SOCIAL_URL } from '@lobechat/business-const';
|
|
4
4
|
import { Flexbox, Icon, Tabs } from '@lobehub/ui';
|
|
5
5
|
import { createStaticStyles } from 'antd-style';
|
|
6
6
|
import { BookOpenIcon, BrainCircuitIcon, ListIcon } from 'lucide-react';
|
|
@@ -38,27 +38,36 @@ const Nav = memo<NavProps>(({ mobile, setActiveTab, activeTab = ProviderNavKey.O
|
|
|
38
38
|
const { t } = useTranslation('discover');
|
|
39
39
|
const { identifier } = useDetailContext();
|
|
40
40
|
|
|
41
|
+
// Hide Guide tab for branding provider as it doesn't have integration docs
|
|
42
|
+
const showGuideTab = identifier !== BRANDING_PROVIDER;
|
|
43
|
+
|
|
44
|
+
const items = [
|
|
45
|
+
{
|
|
46
|
+
icon: <Icon icon={BookOpenIcon} size={16} />,
|
|
47
|
+
key: ProviderNavKey.Overview,
|
|
48
|
+
label: t('providers.details.overview.title'),
|
|
49
|
+
},
|
|
50
|
+
...(showGuideTab
|
|
51
|
+
? [
|
|
52
|
+
{
|
|
53
|
+
icon: <Icon icon={BrainCircuitIcon} size={16} />,
|
|
54
|
+
key: ProviderNavKey.Guide,
|
|
55
|
+
label: t('providers.details.guide.title'),
|
|
56
|
+
},
|
|
57
|
+
]
|
|
58
|
+
: []),
|
|
59
|
+
{
|
|
60
|
+
icon: <Icon icon={ListIcon} size={16} />,
|
|
61
|
+
key: ProviderNavKey.Related,
|
|
62
|
+
label: t('providers.details.related.title'),
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
|
|
41
66
|
const nav = (
|
|
42
67
|
<Tabs
|
|
43
68
|
activeKey={activeTab}
|
|
44
69
|
compact={mobile}
|
|
45
|
-
items={
|
|
46
|
-
{
|
|
47
|
-
icon: <Icon icon={BookOpenIcon} size={16} />,
|
|
48
|
-
key: ProviderNavKey.Overview,
|
|
49
|
-
label: t('providers.details.overview.title'),
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
icon: <Icon icon={BrainCircuitIcon} size={16} />,
|
|
53
|
-
key: ProviderNavKey.Guide,
|
|
54
|
-
label: t('providers.details.guide.title'),
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
icon: <Icon icon={ListIcon} size={16} />,
|
|
58
|
-
key: ProviderNavKey.Related,
|
|
59
|
-
label: t('providers.details.related.title'),
|
|
60
|
-
},
|
|
61
|
-
]}
|
|
70
|
+
items={items}
|
|
62
71
|
onChange={(key) => setActiveTab?.(key as ProviderNavKey)}
|
|
63
72
|
/>
|
|
64
73
|
);
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { ENABLE_TOPIC_LINK_SHARE } from '@lobechat/business-const';
|
|
4
3
|
import { ActionIcon } from '@lobehub/ui';
|
|
5
4
|
import { Share2 } from 'lucide-react';
|
|
6
5
|
import dynamic from 'next/dynamic';
|
|
@@ -10,6 +9,8 @@ import { useTranslation } from 'react-i18next';
|
|
|
10
9
|
import { DESKTOP_HEADER_ICON_SIZE, MOBILE_HEADER_ICON_SIZE } from '@/const/layoutTokens';
|
|
11
10
|
import { useWorkspaceModal } from '@/hooks/useWorkspaceModal';
|
|
12
11
|
import { useChatStore } from '@/store/chat';
|
|
12
|
+
import { useServerConfigStore } from '@/store/serverConfig';
|
|
13
|
+
import { serverConfigSelectors } from '@/store/serverConfig/selectors';
|
|
13
14
|
|
|
14
15
|
const ShareModal = dynamic(() => import('@/features/ShareModal'));
|
|
15
16
|
const SharePopover = dynamic(() => import('@/features/SharePopover'));
|
|
@@ -24,6 +25,7 @@ const ShareButton = memo<ShareButtonProps>(({ mobile, setOpen, open }) => {
|
|
|
24
25
|
const [isModalOpen, setIsModalOpen] = useWorkspaceModal(open, setOpen);
|
|
25
26
|
const { t } = useTranslation('common');
|
|
26
27
|
const activeTopicId = useChatStore((s) => s.activeTopicId);
|
|
28
|
+
const enableTopicLinkShare = useServerConfigStore(serverConfigSelectors.enableBusinessFeatures);
|
|
27
29
|
|
|
28
30
|
// Hide share button when no topic exists (no messages sent yet)
|
|
29
31
|
if (!activeTopicId) return null;
|
|
@@ -31,7 +33,7 @@ const ShareButton = memo<ShareButtonProps>(({ mobile, setOpen, open }) => {
|
|
|
31
33
|
const iconButton = (
|
|
32
34
|
<ActionIcon
|
|
33
35
|
icon={Share2}
|
|
34
|
-
onClick={
|
|
36
|
+
onClick={enableTopicLinkShare ? undefined : () => setIsModalOpen(true)}
|
|
35
37
|
size={mobile ? MOBILE_HEADER_ICON_SIZE : DESKTOP_HEADER_ICON_SIZE}
|
|
36
38
|
title={t('share')}
|
|
37
39
|
tooltipProps={{
|
|
@@ -42,7 +44,7 @@ const ShareButton = memo<ShareButtonProps>(({ mobile, setOpen, open }) => {
|
|
|
42
44
|
|
|
43
45
|
return (
|
|
44
46
|
<>
|
|
45
|
-
{
|
|
47
|
+
{enableTopicLinkShare ? (
|
|
46
48
|
<SharePopover onOpenModal={() => setIsModalOpen(true)}>{iconButton}</SharePopover>
|
|
47
49
|
) : (
|
|
48
50
|
iconButton
|
|
@@ -26,6 +26,8 @@ import {
|
|
|
26
26
|
import { useMemo } from 'react';
|
|
27
27
|
import { useTranslation } from 'react-i18next';
|
|
28
28
|
|
|
29
|
+
import { useElectronStore } from '@/store/electron';
|
|
30
|
+
import { electronSyncSelectors } from '@/store/electron/selectors';
|
|
29
31
|
import { SettingsTabs } from '@/store/global/initialState';
|
|
30
32
|
import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig';
|
|
31
33
|
import { useUserStore } from '@/store/user';
|
|
@@ -63,13 +65,24 @@ export const useCategory = () => {
|
|
|
63
65
|
userProfileSelectors.userAvatar(s),
|
|
64
66
|
userProfileSelectors.nickName(s),
|
|
65
67
|
]);
|
|
68
|
+
const remoteServerUrl = useElectronStore(electronSyncSelectors.remoteServerUrl);
|
|
69
|
+
|
|
70
|
+
// Process avatar URL for desktop environment
|
|
71
|
+
const avatarUrl = useMemo(() => {
|
|
72
|
+
if (!avatar) return undefined;
|
|
73
|
+
if (isDesktop && avatar.startsWith('/') && remoteServerUrl) {
|
|
74
|
+
return remoteServerUrl + avatar;
|
|
75
|
+
}
|
|
76
|
+
return avatar;
|
|
77
|
+
}, [avatar, remoteServerUrl]);
|
|
78
|
+
|
|
66
79
|
const categoryGroups: CategoryGroup[] = useMemo(() => {
|
|
67
80
|
const groups: CategoryGroup[] = [];
|
|
68
81
|
|
|
69
82
|
// 个人资料组 - Profile 相关设置
|
|
70
83
|
const profileItems: CategoryItem[] = [
|
|
71
84
|
{
|
|
72
|
-
icon:
|
|
85
|
+
icon: avatarUrl ? <Avatar avatar={avatarUrl} shape={'square'} size={26} /> : UserCircle,
|
|
73
86
|
key: SettingsTabs.Profile,
|
|
74
87
|
label: username ? username : tAuth('tab.profile'),
|
|
75
88
|
},
|
|
@@ -227,7 +240,7 @@ export const useCategory = () => {
|
|
|
227
240
|
showAiImage,
|
|
228
241
|
showApiKeyManage,
|
|
229
242
|
isLoginWithClerk,
|
|
230
|
-
|
|
243
|
+
avatarUrl,
|
|
231
244
|
username,
|
|
232
245
|
]);
|
|
233
246
|
|
|
@@ -1,21 +1,16 @@
|
|
|
1
1
|
import { createStaticStyles } from 'antd-style';
|
|
2
2
|
|
|
3
3
|
export const styles = createStaticStyles(({ css, cssVar }) => ({
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
// Divider 样式
|
|
7
|
-
divider: css`
|
|
4
|
+
// Divider 样式
|
|
5
|
+
divider: css`
|
|
8
6
|
height: 24px;
|
|
9
7
|
`,
|
|
10
8
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
// 内层容器 - 深色模式
|
|
15
|
-
innerContainerDark: css`
|
|
9
|
+
// 内层容器 - 深色模式
|
|
10
|
+
innerContainerDark: css`
|
|
16
11
|
position: relative;
|
|
17
12
|
|
|
18
|
-
overflow: hidden;
|
|
13
|
+
overflow: hidden auto;
|
|
19
14
|
|
|
20
15
|
border: 1px solid ${cssVar.colorBorderSecondary};
|
|
21
16
|
border-radius: ${cssVar.borderRadius};
|
|
@@ -23,14 +18,11 @@ innerContainerDark: css`
|
|
|
23
18
|
background: ${cssVar.colorBgContainer};
|
|
24
19
|
`,
|
|
25
20
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
// 内层容器 - 浅色模式
|
|
30
|
-
innerContainerLight: css`
|
|
21
|
+
// 内层容器 - 浅色模式
|
|
22
|
+
innerContainerLight: css`
|
|
31
23
|
position: relative;
|
|
32
24
|
|
|
33
|
-
overflow: hidden;
|
|
25
|
+
overflow: hidden auto;
|
|
34
26
|
|
|
35
27
|
border: 1px solid ${cssVar.colorBorder};
|
|
36
28
|
border-radius: ${cssVar.borderRadius};
|
|
@@ -38,10 +30,8 @@ innerContainerLight: css`
|
|
|
38
30
|
background: ${cssVar.colorBgContainer};
|
|
39
31
|
`,
|
|
40
32
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
// 外层容器
|
|
44
|
-
outerContainer: css`
|
|
33
|
+
// 外层容器
|
|
34
|
+
outerContainer: css`
|
|
45
35
|
position: relative;
|
|
46
36
|
`,
|
|
47
37
|
}));
|
|
@@ -82,12 +82,12 @@ const ConnectionMode = memo<ConnectionModeProps>(({ setWaiting }) => {
|
|
|
82
82
|
|
|
83
83
|
const connect = useElectronStore((s) => s.connectRemoteServer);
|
|
84
84
|
const storageMode = useElectronStore(electronSyncSelectors.storageMode);
|
|
85
|
-
const
|
|
85
|
+
const rawRemoteServerUrl = useElectronStore(electronSyncSelectors.rawRemoteServerUrl);
|
|
86
86
|
|
|
87
87
|
const [selectedOption, setSelectedOption] = useState<RemoteStorageMode>(
|
|
88
88
|
storageMode === StorageModeEnum.SelfHost ? StorageModeEnum.SelfHost : StorageModeEnum.Cloud,
|
|
89
89
|
);
|
|
90
|
-
const [selfHostedUrl, setSelfHostedUrl] = useState(
|
|
90
|
+
const [selfHostedUrl, setSelfHostedUrl] = useState(rawRemoteServerUrl);
|
|
91
91
|
|
|
92
92
|
const validateUrl = useCallback((url: string) => {
|
|
93
93
|
if (!url) {
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import { TITLE_BAR_HEIGHT } from '@lobechat/desktop-bridge';
|
|
3
4
|
import { Flexbox } from '@lobehub/ui';
|
|
4
5
|
import { type FC } from 'react';
|
|
5
6
|
|
|
6
7
|
import { ProductLogo } from '@/components/Branding/ProductLogo';
|
|
7
8
|
import { electronStylish } from '@/styles/electron';
|
|
8
9
|
|
|
9
|
-
import { TITLE_BAR_HEIGHT } from './const';
|
|
10
|
-
|
|
11
10
|
/**
|
|
12
11
|
* A simple, minimal TitleBar for Electron windows.
|
|
13
12
|
* Provides draggable area without business logic (navigation, updates, etc.)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { TITLE_BAR_HEIGHT } from '@lobechat/desktop-bridge';
|
|
1
2
|
import { Flexbox } from '@lobehub/ui';
|
|
2
3
|
import { Divider } from 'antd';
|
|
3
4
|
import { memo, useMemo } from 'react';
|
|
@@ -11,7 +12,6 @@ import NavigationBar from './NavigationBar';
|
|
|
11
12
|
import { UpdateModal } from './UpdateModal';
|
|
12
13
|
import { UpdateNotification } from './UpdateNotification';
|
|
13
14
|
import WinControl from './WinControl';
|
|
14
|
-
import { TITLE_BAR_HEIGHT } from './const';
|
|
15
15
|
import { useWatchThemeUpdate } from './hooks/useWatchThemeUpdate';
|
|
16
16
|
|
|
17
17
|
const isMac = isMacOS();
|
|
@@ -66,5 +66,5 @@ const TitleBar = memo(() => {
|
|
|
66
66
|
|
|
67
67
|
export default TitleBar;
|
|
68
68
|
|
|
69
|
-
export { TITLE_BAR_HEIGHT } from './const';
|
|
70
69
|
export { default as SimpleTitleBar } from './SimpleTitleBar';
|
|
70
|
+
export { TITLE_BAR_HEIGHT } from '@lobechat/desktop-bridge';
|
|
@@ -19,6 +19,7 @@ vi.mock('@lobechat/const', async (importOriginal) => {
|
|
|
19
19
|
return mockIsDesktop;
|
|
20
20
|
},
|
|
21
21
|
DEFAULT_USER_AVATAR: 'default-avatar.png',
|
|
22
|
+
OFFICIAL_URL: 'https://app.lobehub.com',
|
|
22
23
|
};
|
|
23
24
|
});
|
|
24
25
|
|
|
@@ -77,7 +78,7 @@ describe('useUserAvatar', () => {
|
|
|
77
78
|
expect(result.current).toBe(mockAvatar);
|
|
78
79
|
});
|
|
79
80
|
|
|
80
|
-
it('should prepend remote server URL when avatar starts with / in desktop environment', () => {
|
|
81
|
+
it('should prepend remote server URL when avatar starts with / in desktop environment (selfHost mode)', () => {
|
|
81
82
|
mockIsDesktop = true;
|
|
82
83
|
const mockAvatar = '/api/avatar.png';
|
|
83
84
|
const mockServerUrl = 'https://server.com';
|
|
@@ -85,7 +86,7 @@ describe('useUserAvatar', () => {
|
|
|
85
86
|
act(() => {
|
|
86
87
|
useUserStore.setState({ user: { avatar: mockAvatar } as any });
|
|
87
88
|
useElectronStore.setState({
|
|
88
|
-
dataSyncConfig: { remoteServerUrl: mockServerUrl, storageMode: '
|
|
89
|
+
dataSyncConfig: { remoteServerUrl: mockServerUrl, storageMode: 'selfHost' },
|
|
89
90
|
});
|
|
90
91
|
});
|
|
91
92
|
|
|
@@ -102,7 +103,7 @@ describe('useUserAvatar', () => {
|
|
|
102
103
|
act(() => {
|
|
103
104
|
useUserStore.setState({ user: { avatar: mockAvatar } as any });
|
|
104
105
|
useElectronStore.setState({
|
|
105
|
-
dataSyncConfig: { remoteServerUrl: mockServerUrl, storageMode: '
|
|
106
|
+
dataSyncConfig: { remoteServerUrl: mockServerUrl, storageMode: 'selfHost' },
|
|
106
107
|
});
|
|
107
108
|
});
|
|
108
109
|
|
|
@@ -111,7 +112,7 @@ describe('useUserAvatar', () => {
|
|
|
111
112
|
expect(result.current).toBe(mockAvatar);
|
|
112
113
|
});
|
|
113
114
|
|
|
114
|
-
it('should
|
|
115
|
+
it('should use OFFICIAL_URL when storageMode is cloud in desktop environment', () => {
|
|
115
116
|
mockIsDesktop = true;
|
|
116
117
|
const mockAvatar = '/api/avatar.png';
|
|
117
118
|
|
|
@@ -124,6 +125,24 @@ describe('useUserAvatar', () => {
|
|
|
124
125
|
|
|
125
126
|
const { result } = renderHook(() => useUserAvatar());
|
|
126
127
|
|
|
128
|
+
// In cloud mode, selector returns OFFICIAL_URL regardless of remoteServerUrl config
|
|
129
|
+
expect(result.current).toBe('https://app.lobehub.com/api/avatar.png');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should return original avatar when storageMode is selfHost but no URL configured', () => {
|
|
133
|
+
mockIsDesktop = true;
|
|
134
|
+
const mockAvatar = '/api/avatar.png';
|
|
135
|
+
|
|
136
|
+
act(() => {
|
|
137
|
+
useUserStore.setState({ user: { avatar: mockAvatar } as any });
|
|
138
|
+
useElectronStore.setState({
|
|
139
|
+
dataSyncConfig: { remoteServerUrl: '', storageMode: 'selfHost' },
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const { result } = renderHook(() => useUserAvatar());
|
|
144
|
+
|
|
145
|
+
// In selfHost mode with empty URL, avatar is not prepended
|
|
127
146
|
expect(result.current).toBe(mockAvatar);
|
|
128
147
|
});
|
|
129
148
|
});
|
|
@@ -1,12 +1,28 @@
|
|
|
1
|
+
import { OFFICIAL_URL } from '@lobechat/const';
|
|
2
|
+
|
|
1
3
|
import { type ElectronState } from '../initialState';
|
|
2
4
|
|
|
3
5
|
const isSyncActive = (s: ElectronState) => s.dataSyncConfig.active;
|
|
4
6
|
|
|
5
7
|
const storageMode = (s: ElectronState) => s.dataSyncConfig.storageMode;
|
|
6
|
-
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Returns the effective remote server URL based on storage mode:
|
|
11
|
+
* - Cloud mode: returns OFFICIAL_URL
|
|
12
|
+
* - SelfHost mode: returns the configured remoteServerUrl
|
|
13
|
+
*/
|
|
14
|
+
const remoteServerUrl = (s: ElectronState) =>
|
|
15
|
+
s.dataSyncConfig.storageMode === 'cloud' ? OFFICIAL_URL : s.dataSyncConfig.remoteServerUrl || '';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Returns the raw remoteServerUrl from config without transformation.
|
|
19
|
+
* Use this when you need the original configured value (e.g., for editing forms).
|
|
20
|
+
*/
|
|
21
|
+
const rawRemoteServerUrl = (s: ElectronState) => s.dataSyncConfig.remoteServerUrl || '';
|
|
7
22
|
|
|
8
23
|
export const electronSyncSelectors = {
|
|
9
24
|
isSyncActive,
|
|
25
|
+
rawRemoteServerUrl,
|
|
10
26
|
remoteServerUrl,
|
|
11
27
|
storageMode,
|
|
12
28
|
};
|
|
@@ -84,6 +84,31 @@ describe('SettingsAction', () => {
|
|
|
84
84
|
expect.any(AbortSignal),
|
|
85
85
|
);
|
|
86
86
|
});
|
|
87
|
+
|
|
88
|
+
it('should include field in diffs when user resets it to default value', async () => {
|
|
89
|
+
const { result } = renderHook(() => useUserStore());
|
|
90
|
+
|
|
91
|
+
// First, set memory.enabled to false (non-default value)
|
|
92
|
+
await act(async () => {
|
|
93
|
+
await result.current.setSettings({ memory: { enabled: false } });
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
expect(userService.updateUserSettings).toHaveBeenLastCalledWith(
|
|
97
|
+
expect.objectContaining({ memory: { enabled: false } }),
|
|
98
|
+
expect.any(AbortSignal),
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
// Then, reset memory.enabled back to true (default value)
|
|
102
|
+
// This should still include memory in the diffs to override the previously saved value
|
|
103
|
+
await act(async () => {
|
|
104
|
+
await result.current.setSettings({ memory: { enabled: true } });
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
expect(userService.updateUserSettings).toHaveBeenLastCalledWith(
|
|
108
|
+
expect.objectContaining({ memory: { enabled: true } }),
|
|
109
|
+
expect.any(AbortSignal),
|
|
110
|
+
);
|
|
111
|
+
});
|
|
87
112
|
});
|
|
88
113
|
|
|
89
114
|
describe('updateDefaultAgent', () => {
|
|
@@ -103,6 +103,17 @@ export const createSettingsSlice: StateCreator<
|
|
|
103
103
|
if (isEqual(prevSetting, nextSettings)) return;
|
|
104
104
|
|
|
105
105
|
const diffs = difference(nextSettings, defaultSettings);
|
|
106
|
+
|
|
107
|
+
// When user resets a field to default value, we need to explicitly include it in diffs
|
|
108
|
+
// to override the previously saved non-default value in the backend
|
|
109
|
+
const changedFields = difference(nextSettings, prevSetting);
|
|
110
|
+
for (const key of Object.keys(changedFields)) {
|
|
111
|
+
// Only handle fields that were previously set by user (exist in prevSetting)
|
|
112
|
+
if (key in prevSetting && !(key in diffs)) {
|
|
113
|
+
(diffs as any)[key] = (nextSettings as any)[key];
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
106
117
|
set({ settings: diffs }, false, 'optimistic_updateSettings');
|
|
107
118
|
|
|
108
119
|
const abortController = get().internal_createSignal();
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export const TITLE_BAR_HEIGHT = 30;
|