@lobehub/chat 1.70.4 → 1.70.6

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.70.6](https://github.com/lobehub/lobe-chat/compare/v1.70.5...v1.70.6)
6
+
7
+ <sup>Released on **2025-03-11**</sup>
8
+
9
+ #### 🐛 Bug Fixes
10
+
11
+ - **misc**: Link jump in mobile terminal data statistics.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### What's fixed
19
+
20
+ - **misc**: Link jump in mobile terminal data statistics, closes [#6893](https://github.com/lobehub/lobe-chat/issues/6893) ([505d24d](https://github.com/lobehub/lobe-chat/commit/505d24d))
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.70.5](https://github.com/lobehub/lobe-chat/compare/v1.70.4...v1.70.5)
31
+
32
+ <sup>Released on **2025-03-11**</sup>
33
+
34
+ #### 🐛 Bug Fixes
35
+
36
+ - **misc**: Refactor the theme implement.
37
+
38
+ <br/>
39
+
40
+ <details>
41
+ <summary><kbd>Improvements and Fixes</kbd></summary>
42
+
43
+ #### What's fixed
44
+
45
+ - **misc**: Refactor the theme implement, closes [#6844](https://github.com/lobehub/lobe-chat/issues/6844) ([e5c2161](https://github.com/lobehub/lobe-chat/commit/e5c2161))
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.70.4](https://github.com/lobehub/lobe-chat/compare/v1.70.3...v1.70.4)
6
56
 
7
57
  <sup>Released on **2025-03-11**</sup>
package/README.md CHANGED
@@ -364,12 +364,12 @@ Our marketplace is not just a showcase platform but also a collaborative space.
364
364
 
365
365
  | Recent Submits | Description |
366
366
  | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
367
+ | [审稿回复专家](https://lobechat.com/discover/assistant/academic-paper-overview)<br/><sup>By **[arvinxx](https://github.com/arvinxx)** on **2025-03-11**</sup> | 擅长高质量文献检索与分析的学术研究助手<br/>`学术研究` `文献检索` `数据分析` `信息提取` `咨询` |
367
368
  | [Cron Expression Assistant](https://lobechat.com/discover/assistant/crontab-generate)<br/><sup>By **[edgesider](https://github.com/edgesider)** on **2025-02-17**</sup> | Crontab Expression Generator<br/>`crontab` `time-expression` `trigger-time` `generator` `technical-assistance` |
368
369
  | [Xiao Zhi French Translation Assistant](https://lobechat.com/discover/assistant/xiao-zhi-french-translation-asst-v-1)<br/><sup>By **[WeR-Best](https://github.com/WeR-Best)** on **2025-02-10**</sup> | A friendly, professional, and empathetic AI assistant for French translation<br/>`ai-assistant` `french-translation` `cross-cultural-communication` `creativity` |
369
- | [Language Charm Learning Mentor](https://lobechat.com/discover/assistant/bad-language-helper)<br/><sup>By **[Guducat](https://github.com/Guducat)** on **2025-02-06**</sup> | Specializes in teaching the charm of language and witty responses<br/>`language-learning` `dialogue-examples` |
370
- | [Astrology Researcher](https://lobechat.com/discover/assistant/fate-researcher)<br/><sup>By **[Jack980506](https://github.com/Jack980506)** on **2025-02-06**</sup> | Expert in BaZi astrology<br/>`astrology` `ba-zi` `traditional-culture` |
370
+ | [Investment Assistant](https://lobechat.com/discover/assistant/graham-investmentassi)<br/><sup>By **[farsightlin](https://github.com/farsightlin)** on **2025-02-06**</sup> | Helps users calculate the data needed for valuation<br/>`investment` `valuation` `financial-analysis` `calculator` |
371
371
 
372
- > 📊 Total agents: [<kbd>**487**</kbd> ](https://lobechat.com/discover/assistants)
372
+ > 📊 Total agents: [<kbd>**488**</kbd> ](https://lobechat.com/discover/assistants)
373
373
 
374
374
  <!-- AGENT LIST -->
375
375
 
package/README.zh-CN.md CHANGED
@@ -351,14 +351,14 @@ LobeChat 的插件生态系统是其核心功能的重要扩展,它极大地
351
351
 
352
352
  <!-- AGENT LIST -->
353
353
 
354
- | 最近新增 | 描述 |
355
- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
356
- | [Cron 表达式助手](https://lobechat.com/discover/assistant/crontab-generate)<br/><sup>By **[edgesider](https://github.com/edgesider)** on **2025-02-17**</sup> | Crontab 表达式生成<br/>`crontab` `时间表达` `触发时间` `生成器` `技术辅助` |
357
- | [小智法语翻译助手](https://lobechat.com/discover/assistant/xiao-zhi-french-translation-asst-v-1)<br/><sup>By **[WeR-Best](https://github.com/WeR-Best)** on **2025-02-10**</sup> | 友好、专业、富有同理心的法语翻译 AI 助手<br/>`ai助手` `法语翻译` `跨文化交流` `创造力` |
358
- | [语言魅力学习导师](https://lobechat.com/discover/assistant/bad-language-helper)<br/><sup>By **[Guducat](https://github.com/Guducat)** on **2025-02-06**</sup> | 擅长教学语言的魅力与花样回复<br/>`语言学习` `对话示例` |
359
- | [命理研究员](https://lobechat.com/discover/assistant/fate-researcher)<br/><sup>By **[Jack980506](https://github.com/Jack980506)** on **2025-02-06**</sup> | 精通八字命<br/>`命理学` `八字` `传统文化` |
360
-
361
- > 📊 Total agents: [<kbd>**487**</kbd> ](https://lobechat.com/discover/assistants)
354
+ | 最近新增 | 描述 |
355
+ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
356
+ | [审稿回复专家](https://lobechat.com/discover/assistant/academic-paper-overview)<br/><sup>By **[arvinxx](https://github.com/arvinxx)** on **2025-03-11**</sup> | 擅长高质量文献检索与分析的学术研究助手<br/>`学术研究` `文献检索` `数据分析` `信息提取` `咨询` |
357
+ | [Cron 表达式助手](https://lobechat.com/discover/assistant/crontab-generate)<br/><sup>By **[edgesider](https://github.com/edgesider)** on **2025-02-17**</sup> | Crontab 表达式生成<br/>`crontab` `时间表达` `触发时间` `生成器` `技术辅助` |
358
+ | [小智法语翻译助手](https://lobechat.com/discover/assistant/xiao-zhi-french-translation-asst-v-1)<br/><sup>By **[WeR-Best](https://github.com/WeR-Best)** on **2025-02-10**</sup> | 友好、专业、富有同理心的法语翻译 AI 助手<br/>`ai助手` `法语翻译` `跨文化交流` `创造力` |
359
+ | [投资小助手](https://lobechat.com/discover/assistant/graham-investmentassi)<br/><sup>By **[farsightlin](https://github.com/farsightlin)** on **2025-02-06**</sup> | 帮助用户计算估值所需的一些数据<br/>`投资` `估值` `财务分析` `计算器` |
360
+
361
+ > 📊 Total agents: [<kbd>**488**</kbd> ](https://lobechat.com/discover/assistants)
362
362
 
363
363
  <!-- AGENT LIST -->
364
364
 
package/changelog/v1.json CHANGED
@@ -1,4 +1,22 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "fixes": [
5
+ "Link jump in mobile terminal data statistics."
6
+ ]
7
+ },
8
+ "date": "2025-03-11",
9
+ "version": "1.70.6"
10
+ },
11
+ {
12
+ "children": {
13
+ "fixes": [
14
+ "Refactor the theme implement."
15
+ ]
16
+ },
17
+ "date": "2025-03-11",
18
+ "version": "1.70.5"
19
+ },
2
20
  {
3
21
  "children": {
4
22
  "improvements": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "1.70.4",
3
+ "version": "1.70.6",
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",
@@ -7,12 +7,12 @@ import { Moon, Sun } from 'lucide-react';
7
7
  import { memo } from 'react';
8
8
 
9
9
  import { MOBILE_HEADER_ICON_SIZE } from '@/const/layoutTokens';
10
- import { useUserStore } from '@/store/user';
10
+ import { useGlobalStore } from '@/store/global';
11
11
  import { mobileHeaderSticky } from '@/styles/mobileHeader';
12
12
 
13
13
  const Header = memo(() => {
14
14
  const theme = useTheme();
15
- const switchThemeMode = useUserStore((s) => s.switchThemeMode);
15
+ const switchThemeMode = useGlobalStore((s) => s.switchThemeMode);
16
16
 
17
17
  return (
18
18
  <MobileNavBar
@@ -42,8 +42,8 @@ const Client = memo<{ mobile?: boolean }>(({ mobile }) => {
42
42
  <AiHeatmaps mobile={mobile} />
43
43
  <Grid gap={mobile ? 0 : 48} rows={3}>
44
44
  <ModelsRank />
45
- <AssistantsRank />
46
- <TopicsRank />
45
+ <AssistantsRank mobile={mobile} />
46
+ <TopicsRank mobile={mobile} />
47
47
  </Grid>
48
48
  </Flexbox>
49
49
  );
@@ -15,7 +15,7 @@ import { useClientDataSWR } from '@/libs/swr';
15
15
  import { sessionService } from '@/services/session';
16
16
  import { SessionRankItem } from '@/types/session';
17
17
 
18
- export const AssistantsRank = memo(() => {
18
+ export const AssistantsRank = memo<{ mobile?: boolean }>(({ mobile }) => {
19
19
  const [open, setOpen] = useState(false);
20
20
  const { t } = useTranslation(['auth', 'chat']);
21
21
  const router = useRouter();
@@ -29,6 +29,7 @@ export const AssistantsRank = memo(() => {
29
29
  const link = qs.stringifyUrl({
30
30
  query: {
31
31
  session: item.id,
32
+ ...(mobile ? { showMobileWorkspace: true } : {}),
32
33
  },
33
34
  url: '/chat',
34
35
  });
@@ -15,7 +15,7 @@ import { useClientDataSWR } from '@/libs/swr';
15
15
  import { topicService } from '@/services/topic';
16
16
  import { TopicRankItem } from '@/types/topic';
17
17
 
18
- export const TopicsRank = memo(() => {
18
+ export const TopicsRank = memo<{ mobile?: boolean }>(({ mobile }) => {
19
19
  const [open, setOpen] = useState(false);
20
20
  const { t } = useTranslation('auth');
21
21
  const theme = useTheme();
@@ -31,6 +31,7 @@ export const TopicsRank = memo(() => {
31
31
  query: {
32
32
  session: item.sessionId || INBOX_SESSION_ID,
33
33
  topic: item.id,
34
+ ...(mobile ? { showMobileWorkspace: true } : {}),
34
35
  },
35
36
  url: '/chat',
36
37
  });
@@ -12,8 +12,9 @@ import { FORM_STYLE } from '@/const/layoutTokens';
12
12
  import { imageUrl } from '@/const/url';
13
13
  import { Locales, localeOptions } from '@/locales/resources';
14
14
  import { useGlobalStore } from '@/store/global';
15
+ import { systemStatusSelectors } from '@/store/global/selectors';
15
16
  import { useUserStore } from '@/store/user';
16
- import { settingsSelectors, userGeneralSettingsSelectors } from '@/store/user/selectors';
17
+ import { settingsSelectors } from '@/store/user/selectors';
17
18
 
18
19
  import { ThemeSwatchesNeutral, ThemeSwatchesPrimary } from './ThemeSwatches';
19
20
 
@@ -24,8 +25,9 @@ const Theme = memo(() => {
24
25
 
25
26
  const [form] = Form.useForm();
26
27
  const settings = useUserStore(settingsSelectors.currentSettings, isEqual);
27
- const themeMode = useUserStore(userGeneralSettingsSelectors.currentThemeMode);
28
- const [setThemeMode, setSettings] = useUserStore((s) => [s.switchThemeMode, s.setSettings]);
28
+ const themeMode = useGlobalStore(systemStatusSelectors.themeMode);
29
+ const [setSettings] = useUserStore((s) => [s.setSettings]);
30
+ const [setThemeMode] = useGlobalStore((s) => [s.switchThemeMode]);
29
31
 
30
32
  useSyncSettings(form);
31
33
  const [switchLocale] = useGlobalStore((s) => [s.switchLocale]);
@@ -2,5 +2,4 @@ import { UserGeneralConfig } from '@/types/user/settings';
2
2
 
3
3
  export const DEFAULT_COMMON_SETTINGS: UserGeneralConfig = {
4
4
  fontSize: 14,
5
- themeMode: 'auto',
6
5
  };
@@ -6,8 +6,8 @@ import { memo, useMemo } from 'react';
6
6
  import { useTranslation } from 'react-i18next';
7
7
 
8
8
  import Menu, { type MenuProps } from '@/components/Menu';
9
- import { useUserStore } from '@/store/user';
10
- import { userGeneralSettingsSelectors } from '@/store/user/selectors';
9
+ import { useGlobalStore } from '@/store/global';
10
+ import { systemStatusSelectors } from '@/store/global/selectors';
11
11
 
12
12
  const themeIcons = {
13
13
  auto: Monitor,
@@ -17,8 +17,8 @@ const themeIcons = {
17
17
 
18
18
  const ThemeButton = memo<{ placement?: PopoverProps['placement'] }>(({ placement = 'right' }) => {
19
19
  const theme = useTheme();
20
- const [themeMode, switchThemeMode] = useUserStore((s) => [
21
- userGeneralSettingsSelectors.currentThemeMode(s),
20
+ const [themeMode, switchThemeMode] = useGlobalStore((s) => [
21
+ systemStatusSelectors.themeMode(s),
22
22
  s.switchThemeMode,
23
23
  ]);
24
24
 
@@ -14,11 +14,9 @@ import Link from 'next/link';
14
14
  import { ReactNode, memo, useEffect } from 'react';
15
15
 
16
16
  import AntdStaticMethods from '@/components/AntdStaticMethods';
17
- import {
18
- LOBE_THEME_APPEARANCE,
19
- LOBE_THEME_NEUTRAL_COLOR,
20
- LOBE_THEME_PRIMARY_COLOR,
21
- } from '@/const/theme';
17
+ import { LOBE_THEME_NEUTRAL_COLOR, LOBE_THEME_PRIMARY_COLOR } from '@/const/theme';
18
+ import { useGlobalStore } from '@/store/global';
19
+ import { systemStatusSelectors } from '@/store/global/selectors';
22
20
  import { useUserStore } from '@/store/user';
23
21
  import { userGeneralSettingsSelectors } from '@/store/user/selectors';
24
22
  import { GlobalStyle } from '@/styles';
@@ -103,7 +101,7 @@ const AppTheme = memo<AppThemeProps>(
103
101
  // console.debug('server:appearance', defaultAppearance);
104
102
  // console.debug('server:primaryColor', defaultPrimaryColor);
105
103
  // console.debug('server:neutralColor', defaultNeutralColor);
106
- const themeMode = useUserStore(userGeneralSettingsSelectors.currentThemeMode);
104
+ const themeMode = useGlobalStore(systemStatusSelectors.themeMode);
107
105
  const { styles, cx, theme } = useStyles();
108
106
  const [primaryColor, neutralColor] = useUserStore((s) => [
109
107
  userGeneralSettingsSelectors.primaryColor(s),
@@ -120,15 +118,13 @@ const AppTheme = memo<AppThemeProps>(
120
118
 
121
119
  return (
122
120
  <ThemeProvider
121
+ appearance={themeMode !== 'auto' ? themeMode : undefined}
123
122
  className={cx(styles.app, styles.scrollbar, styles.scrollbarPolyfill)}
124
123
  customTheme={{
125
124
  neutralColor: neutralColor ?? defaultNeutralColor,
126
125
  primaryColor: primaryColor ?? defaultPrimaryColor,
127
126
  }}
128
127
  defaultAppearance={defaultAppearance}
129
- onAppearanceChange={(appearance) => {
130
- setCookie(LOBE_THEME_APPEARANCE, appearance);
131
- }}
132
128
  theme={{
133
129
  cssVar: true,
134
130
  token: {
@@ -57,7 +57,7 @@ describe('ClientService', () => {
57
57
  });
58
58
 
59
59
  it('should update user settings correctly', async () => {
60
- const settingsPatch: DeepPartial<UserSettings> = { general: { themeMode: 'dark' } };
60
+ const settingsPatch: DeepPartial<UserSettings> = { general: { fontSize: 12 } };
61
61
  (UserModel.updateSettings as Mock).mockResolvedValue(undefined);
62
62
 
63
63
  await clientService.updateUserSettings(settingsPatch);
@@ -54,7 +54,7 @@ describe('ClientService', () => {
54
54
  });
55
55
 
56
56
  it('should update user settings correctly', async () => {
57
- const settingsPatch: DeepPartial<UserSettings> = { general: { themeMode: 'dark' } };
57
+ const settingsPatch: DeepPartial<UserSettings> = { general: { fontSize: 12 } };
58
58
 
59
59
  await clientService.updateUserSettings(settingsPatch);
60
60
 
@@ -408,4 +408,19 @@ describe('createPreferenceSlice', () => {
408
408
  expect(result.current.status.inputHeight).toEqual(300);
409
409
  });
410
410
  });
411
+
412
+ describe('switchThemeMode', () => {
413
+ it('should switch theme mode', async () => {
414
+ const { result } = renderHook(() => useGlobalStore());
415
+
416
+ // Perform the action
417
+ act(() => {
418
+ useGlobalStore.setState({ isStatusInit: true });
419
+ result.current.switchThemeMode('light');
420
+ });
421
+
422
+ // Assert that updateUserSettings was called with the correct theme mode
423
+ expect(result.current.status.themeMode).toEqual('light');
424
+ });
425
+ });
411
426
  });
@@ -0,0 +1,221 @@
1
+ import { act, renderHook } from '@testing-library/react';
2
+ import { ThemeMode } from 'antd-style';
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4
+ import { withSWR } from '~test-utils';
5
+
6
+ import { CURRENT_VERSION } from '@/const/version';
7
+ import { globalService } from '@/services/global';
8
+ import { useGlobalStore } from '@/store/global';
9
+ import { initialState } from '@/store/global/initialState';
10
+ import { switchLang } from '@/utils/client/switchLang';
11
+
12
+ vi.mock('@/utils/client/switchLang', () => ({
13
+ switchLang: vi.fn(),
14
+ }));
15
+
16
+ vi.mock('@/services/global', () => ({
17
+ globalService: {
18
+ getLatestVersion: vi.fn(),
19
+ },
20
+ }));
21
+
22
+ describe('generalActionSlice', () => {
23
+ beforeEach(() => {
24
+ vi.clearAllMocks();
25
+ useGlobalStore.setState(initialState);
26
+ });
27
+
28
+ afterEach(() => {
29
+ vi.restoreAllMocks();
30
+ });
31
+
32
+ describe('updateSystemStatus', () => {
33
+ it('should not update status when not initialized', () => {
34
+ const { result } = renderHook(() => useGlobalStore());
35
+
36
+ act(() => {
37
+ result.current.updateSystemStatus({ inputHeight: 200 });
38
+ });
39
+
40
+ expect(result.current.status).toEqual(initialState.status);
41
+ });
42
+
43
+ it('should update status when initialized', () => {
44
+ const { result } = renderHook(() => useGlobalStore());
45
+
46
+ act(() => {
47
+ useGlobalStore.setState({ isStatusInit: true });
48
+ result.current.updateSystemStatus({ inputHeight: 200 });
49
+ });
50
+
51
+ expect(result.current.status.inputHeight).toBe(200);
52
+ });
53
+
54
+ it('should not update if new status equals current status', () => {
55
+ const { result } = renderHook(() => useGlobalStore());
56
+ const saveToLocalStorageSpy = vi.spyOn(result.current.statusStorage, 'saveToLocalStorage');
57
+
58
+ act(() => {
59
+ useGlobalStore.setState({ isStatusInit: true });
60
+ result.current.updateSystemStatus({ inputHeight: initialState.status.inputHeight });
61
+ });
62
+
63
+ expect(saveToLocalStorageSpy).not.toHaveBeenCalled();
64
+ });
65
+
66
+ it('should save to localStorage when status is updated', () => {
67
+ const { result } = renderHook(() => useGlobalStore());
68
+ const saveToLocalStorageSpy = vi.spyOn(result.current.statusStorage, 'saveToLocalStorage');
69
+
70
+ act(() => {
71
+ useGlobalStore.setState({ isStatusInit: true });
72
+ result.current.updateSystemStatus({ inputHeight: 300 });
73
+ });
74
+
75
+ expect(saveToLocalStorageSpy).toHaveBeenCalledWith(
76
+ expect.objectContaining({ inputHeight: 300 }),
77
+ );
78
+ });
79
+
80
+ it('should merge nested objects correctly', () => {
81
+ const { result } = renderHook(() => useGlobalStore());
82
+
83
+ act(() => {
84
+ useGlobalStore.setState({ isStatusInit: true });
85
+ result.current.updateSystemStatus({
86
+ expandSessionGroupKeys: ['test1', 'test2'],
87
+ });
88
+ });
89
+
90
+ expect(result.current.status.expandSessionGroupKeys).toEqual(['test1', 'test2']);
91
+ });
92
+ });
93
+
94
+ describe('switchLocale', () => {
95
+ it('should update language in system status and call switchLang', () => {
96
+ const { result } = renderHook(() => useGlobalStore());
97
+ const locale = 'zh-CN';
98
+
99
+ act(() => {
100
+ useGlobalStore.setState({ isStatusInit: true });
101
+ result.current.switchLocale(locale);
102
+ });
103
+
104
+ expect(result.current.status.language).toBe(locale);
105
+ expect(switchLang).toHaveBeenCalledWith(locale);
106
+ });
107
+
108
+ it('should not update language if status is not initialized', () => {
109
+ const { result } = renderHook(() => useGlobalStore());
110
+ const locale = 'zh-CN';
111
+
112
+ act(() => {
113
+ result.current.switchLocale(locale);
114
+ });
115
+
116
+ expect(result.current.status.language).toBeUndefined();
117
+ });
118
+ });
119
+
120
+ describe('switchThemeMode', () => {
121
+ it('should update theme mode in system status', () => {
122
+ const { result } = renderHook(() => useGlobalStore());
123
+ const themeMode: ThemeMode = 'dark';
124
+
125
+ act(() => {
126
+ useGlobalStore.setState({ isStatusInit: true });
127
+ result.current.switchThemeMode(themeMode);
128
+ });
129
+
130
+ expect(result.current.status.themeMode).toBe(themeMode);
131
+ });
132
+
133
+ it('should not update theme mode if status is not initialized', () => {
134
+ const { result } = renderHook(() => useGlobalStore());
135
+ const themeMode: ThemeMode = 'dark';
136
+
137
+ act(() => {
138
+ result.current.switchThemeMode(themeMode);
139
+ });
140
+
141
+ expect(result.current.status.themeMode).toBe(initialState.status.themeMode);
142
+ });
143
+
144
+ it('should handle light theme mode', () => {
145
+ const { result } = renderHook(() => useGlobalStore());
146
+
147
+ act(() => {
148
+ useGlobalStore.setState({ isStatusInit: true });
149
+ result.current.switchThemeMode('light');
150
+ });
151
+
152
+ expect(result.current.status.themeMode).toBe('light');
153
+ });
154
+ });
155
+
156
+ describe('useCheckLatestVersion', () => {
157
+ it('should not fetch version when check is disabled', () => {
158
+ const getLatestVersionSpy = vi.spyOn(globalService, 'getLatestVersion');
159
+
160
+ renderHook(() => useGlobalStore().useCheckLatestVersion(false), {
161
+ wrapper: withSWR,
162
+ });
163
+
164
+ expect(getLatestVersionSpy).not.toHaveBeenCalled();
165
+ });
166
+
167
+ it('should set hasNewVersion when major version is newer', async () => {
168
+ const latestVersion = '999.0.0';
169
+ vi.spyOn(globalService, 'getLatestVersion').mockResolvedValueOnce(latestVersion);
170
+
171
+ const { result } = renderHook(() => useGlobalStore().useCheckLatestVersion(), {
172
+ wrapper: withSWR,
173
+ });
174
+
175
+ await act(async () => {
176
+ await result.current.data;
177
+ });
178
+
179
+ expect(useGlobalStore.getState().hasNewVersion).toBe(true);
180
+ expect(useGlobalStore.getState().latestVersion).toBe(latestVersion);
181
+ });
182
+
183
+ it('should not set hasNewVersion for same major.minor version', async () => {
184
+ const currentParts = CURRENT_VERSION.split('.');
185
+ const latestVersion = `${currentParts[0]}.${currentParts[1]}.999`;
186
+ vi.spyOn(globalService, 'getLatestVersion').mockResolvedValueOnce(latestVersion);
187
+
188
+ const { result } = renderHook(() => useGlobalStore().useCheckLatestVersion(), {
189
+ wrapper: withSWR,
190
+ });
191
+
192
+ await act(async () => {
193
+ await result.current.data;
194
+ });
195
+
196
+ // Reset hasNewVersion and latestVersion
197
+ useGlobalStore.setState({ hasNewVersion: undefined, latestVersion: undefined });
198
+
199
+ expect(useGlobalStore.getState().hasNewVersion).toBeUndefined();
200
+ expect(useGlobalStore.getState().latestVersion).toBeUndefined();
201
+ });
202
+
203
+ it('should not set hasNewVersion when version is invalid', async () => {
204
+ vi.spyOn(globalService, 'getLatestVersion').mockResolvedValueOnce('invalid.version');
205
+
206
+ const { result } = renderHook(() => useGlobalStore().useCheckLatestVersion(), {
207
+ wrapper: withSWR,
208
+ });
209
+
210
+ await act(async () => {
211
+ await result.current.data;
212
+ });
213
+
214
+ // Reset hasNewVersion and latestVersion
215
+ useGlobalStore.setState({ hasNewVersion: undefined, latestVersion: undefined });
216
+
217
+ expect(useGlobalStore.getState().hasNewVersion).toBeUndefined();
218
+ expect(useGlobalStore.getState().latestVersion).toBeUndefined();
219
+ });
220
+ });
221
+ });
@@ -1,13 +1,16 @@
1
+ import { ThemeMode } from 'antd-style';
1
2
  import isEqual from 'fast-deep-equal';
2
3
  import { gt, parse, valid } from 'semver';
3
4
  import { SWRResponse } from 'swr';
4
5
  import type { StateCreator } from 'zustand/vanilla';
5
6
 
7
+ import { LOBE_THEME_APPEARANCE } from '@/const/theme';
6
8
  import { CURRENT_VERSION } from '@/const/version';
7
9
  import { useOnlyFetchOnceSWR } from '@/libs/swr';
8
10
  import { globalService } from '@/services/global';
9
11
  import type { SystemStatus } from '@/store/global/initialState';
10
12
  import { LocaleMode } from '@/types/locale';
13
+ import { setCookie } from '@/utils/client/cookie';
11
14
  import { switchLang } from '@/utils/client/switchLang';
12
15
  import { merge } from '@/utils/merge';
13
16
  import { setNamespace } from '@/utils/storeDebug';
@@ -18,6 +21,7 @@ const n = setNamespace('g');
18
21
 
19
22
  export interface GlobalGeneralAction {
20
23
  switchLocale: (locale: LocaleMode) => void;
24
+ switchThemeMode: (themeMode: ThemeMode) => void;
21
25
  updateSystemStatus: (status: Partial<SystemStatus>, action?: any) => void;
22
26
  useCheckLatestVersion: (enabledCheck?: boolean) => SWRResponse<string>;
23
27
  useInitSystemStatus: () => SWRResponse;
@@ -34,15 +38,20 @@ export const generalActionSlice: StateCreator<
34
38
 
35
39
  switchLang(locale);
36
40
  },
41
+ switchThemeMode: (themeMode) => {
42
+ get().updateSystemStatus({ themeMode });
43
+
44
+ setCookie(LOBE_THEME_APPEARANCE, themeMode === 'auto' ? undefined : themeMode);
45
+ },
37
46
  updateSystemStatus: (status, action) => {
38
47
  // Status cannot be modified when it is not initialized
39
48
  if (!get().isStatusInit) return;
40
49
 
41
50
  const nextStatus = merge(get().status, status);
51
+
42
52
  if (isEqual(get().status, nextStatus)) return;
43
53
 
44
54
  set({ status: nextStatus }, false, action || n('updateSystemStatus'));
45
-
46
55
  get().statusStorage.saveToLocalStorage(nextStatus);
47
56
  },
48
57
 
@@ -1,3 +1,4 @@
1
+ import type { ThemeMode } from 'antd-style';
1
2
  import { AppRouterInstance } from 'next/dist/shared/lib/app-router-context.shared-runtime';
2
3
 
3
4
  import { DatabaseLoadingState } from '@/types/clientDB';
@@ -61,6 +62,10 @@ export interface SystemStatus {
61
62
  showFilePanel?: boolean;
62
63
  showSessionPanel?: boolean;
63
64
  showSystemRole?: boolean;
65
+ /**
66
+ * theme mode
67
+ */
68
+ themeMode?: ThemeMode;
64
69
  threadInputHeight: number;
65
70
  zenMode?: boolean;
66
71
  }
@@ -96,6 +101,7 @@ export const INITIAL_STATUS = {
96
101
  showFilePanel: true,
97
102
  showSessionPanel: true,
98
103
  showSystemRole: false,
104
+ themeMode: 'auto',
99
105
  threadInputHeight: 200,
100
106
  zenMode: false,
101
107
  } satisfies SystemStatus;
@@ -0,0 +1,209 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ import { DatabaseLoadingState } from '@/types/clientDB';
4
+ import { merge } from '@/utils/merge';
5
+
6
+ import { GlobalState, INITIAL_STATUS, initialState } from '../initialState';
7
+ import { systemStatusSelectors } from './systemStatus';
8
+
9
+ // Mock version constants
10
+ vi.mock('@/const/version', () => ({
11
+ isServerMode: false,
12
+ isUsePgliteDB: true,
13
+ }));
14
+
15
+ describe('systemStatusSelectors', () => {
16
+ describe('sessionGroupKeys', () => {
17
+ it('should return expandSessionGroupKeys from status', () => {
18
+ const s: GlobalState = merge(initialState, {
19
+ status: {
20
+ expandSessionGroupKeys: ['group1', 'group2'],
21
+ },
22
+ });
23
+ expect(systemStatusSelectors.sessionGroupKeys(s)).toEqual(['group1', 'group2']);
24
+ });
25
+
26
+ it('should return initial value if not set', () => {
27
+ const s: GlobalState = merge(initialState, {
28
+ status: {
29
+ expandSessionGroupKeys: undefined,
30
+ },
31
+ });
32
+ expect(systemStatusSelectors.sessionGroupKeys(s)).toEqual(
33
+ INITIAL_STATUS.expandSessionGroupKeys,
34
+ );
35
+ });
36
+ });
37
+
38
+ describe('basic selectors', () => {
39
+ const s: GlobalState = merge(initialState, {
40
+ status: {
41
+ showSystemRole: true,
42
+ mobileShowTopic: true,
43
+ mobileShowPortal: true,
44
+ showChatSideBar: true,
45
+ showSessionPanel: true,
46
+ showFilePanel: true,
47
+ hidePWAInstaller: true,
48
+ isShowCredit: true,
49
+ zenMode: false,
50
+ sessionsWidth: 300,
51
+ portalWidth: 500,
52
+ filePanelWidth: 400,
53
+ inputHeight: 150,
54
+ threadInputHeight: 100,
55
+ },
56
+ });
57
+
58
+ it('should return correct values for basic selectors', () => {
59
+ expect(systemStatusSelectors.showSystemRole(s)).toBe(true);
60
+ expect(systemStatusSelectors.mobileShowTopic(s)).toBe(true);
61
+ expect(systemStatusSelectors.mobileShowPortal(s)).toBe(true);
62
+ expect(systemStatusSelectors.showChatSideBar(s)).toBe(true);
63
+ expect(systemStatusSelectors.showSessionPanel(s)).toBe(true);
64
+ expect(systemStatusSelectors.showFilePanel(s)).toBe(true);
65
+ expect(systemStatusSelectors.hidePWAInstaller(s)).toBe(true);
66
+ expect(systemStatusSelectors.isShowCredit(s)).toBe(true);
67
+ expect(systemStatusSelectors.showChatHeader(s)).toBe(true);
68
+ expect(systemStatusSelectors.inZenMode(s)).toBe(false);
69
+ expect(systemStatusSelectors.sessionWidth(s)).toBe(300);
70
+ expect(systemStatusSelectors.portalWidth(s)).toBe(500);
71
+ expect(systemStatusSelectors.filePanelWidth(s)).toBe(400);
72
+ expect(systemStatusSelectors.inputHeight(s)).toBe(150);
73
+ expect(systemStatusSelectors.threadInputHeight(s)).toBe(100);
74
+ });
75
+
76
+ it('should handle zen mode effects', () => {
77
+ const zenState = merge(s, {
78
+ status: { zenMode: true },
79
+ });
80
+ expect(systemStatusSelectors.showChatSideBar(zenState)).toBe(false);
81
+ expect(systemStatusSelectors.showSessionPanel(zenState)).toBe(false);
82
+ expect(systemStatusSelectors.showChatHeader(zenState)).toBe(false);
83
+ });
84
+
85
+ it('should return default portal width if not set', () => {
86
+ const noPortalWidth = merge(initialState, {
87
+ status: { portalWidth: undefined },
88
+ });
89
+ expect(systemStatusSelectors.portalWidth(noPortalWidth)).toBe(400);
90
+ });
91
+ });
92
+
93
+ describe('theme mode', () => {
94
+ it('should return the correct theme', () => {
95
+ const s: GlobalState = merge(initialState, {
96
+ status: {
97
+ themeMode: 'light',
98
+ },
99
+ });
100
+ expect(systemStatusSelectors.themeMode(s)).toBe('light');
101
+ });
102
+
103
+ it('should return auto if not set', () => {
104
+ const s: GlobalState = merge(initialState, {
105
+ status: {
106
+ themeMode: undefined,
107
+ },
108
+ });
109
+ expect(systemStatusSelectors.themeMode(s)).toBe('auto');
110
+ });
111
+ });
112
+
113
+ describe('pglite status selectors', () => {
114
+ describe('isPgliteNotEnabled', () => {
115
+ it('should return true when conditions are met', () => {
116
+ const s: GlobalState = {
117
+ ...initialState,
118
+ isStatusInit: true,
119
+ status: {
120
+ ...initialState.status,
121
+ isEnablePglite: false,
122
+ },
123
+ };
124
+ expect(systemStatusSelectors.isPgliteNotEnabled(s)).toBe(true);
125
+ });
126
+
127
+ it('should return false when isStatusInit is false', () => {
128
+ const s: GlobalState = {
129
+ ...initialState,
130
+ isStatusInit: false,
131
+ status: {
132
+ ...initialState.status,
133
+ isEnablePglite: false,
134
+ },
135
+ };
136
+ expect(systemStatusSelectors.isPgliteNotEnabled(s)).toBe(false);
137
+ });
138
+ });
139
+
140
+ describe('isPgliteNotInited', () => {
141
+ it('should return true when pglite is enabled but not ready', () => {
142
+ const s: GlobalState = {
143
+ ...initialState,
144
+ isStatusInit: true,
145
+ status: {
146
+ ...initialState.status,
147
+ isEnablePglite: true,
148
+ },
149
+ initClientDBStage: DatabaseLoadingState.Initializing,
150
+ };
151
+ expect(systemStatusSelectors.isPgliteNotInited(s)).toBe(true);
152
+ });
153
+
154
+ it('should return false when pglite is ready', () => {
155
+ const s: GlobalState = {
156
+ ...initialState,
157
+ isStatusInit: true,
158
+ status: {
159
+ ...initialState.status,
160
+ isEnablePglite: true,
161
+ },
162
+ initClientDBStage: DatabaseLoadingState.Ready,
163
+ };
164
+ expect(systemStatusSelectors.isPgliteNotInited(s)).toBe(false);
165
+ });
166
+
167
+ it('should return false when pglite is not enabled', () => {
168
+ const s: GlobalState = {
169
+ ...initialState,
170
+ isStatusInit: true,
171
+ status: {
172
+ ...initialState.status,
173
+ isEnablePglite: false,
174
+ },
175
+ initClientDBStage: DatabaseLoadingState.Initializing,
176
+ };
177
+ expect(systemStatusSelectors.isPgliteNotInited(s)).toBe(false);
178
+ });
179
+ });
180
+
181
+ describe('isPgliteInited', () => {
182
+ it('should return true when pglite is enabled and ready', () => {
183
+ const s: GlobalState = {
184
+ ...initialState,
185
+ isStatusInit: true,
186
+ status: {
187
+ ...initialState.status,
188
+ isEnablePglite: true,
189
+ },
190
+ initClientDBStage: DatabaseLoadingState.Ready,
191
+ };
192
+ expect(systemStatusSelectors.isPgliteInited(s)).toBe(true);
193
+ });
194
+
195
+ it('should return false when not ready', () => {
196
+ const s: GlobalState = {
197
+ ...initialState,
198
+ isStatusInit: true,
199
+ status: {
200
+ ...initialState.status,
201
+ isEnablePglite: true,
202
+ },
203
+ initClientDBStage: DatabaseLoadingState.Initializing,
204
+ };
205
+ expect(systemStatusSelectors.isPgliteInited(s)).toBe(false);
206
+ });
207
+ });
208
+ });
209
+ });
@@ -1,38 +1,38 @@
1
1
  import { isServerMode, isUsePgliteDB } from '@/const/version';
2
- import { GlobalStore } from '@/store/global';
3
2
  import { DatabaseLoadingState } from '@/types/clientDB';
4
3
 
5
4
  import { GlobalState, INITIAL_STATUS } from '../initialState';
6
5
 
7
6
  export const systemStatus = (s: GlobalState) => s.status;
8
7
 
9
- const sessionGroupKeys = (s: GlobalStore): string[] =>
8
+ const sessionGroupKeys = (s: GlobalState): string[] =>
10
9
  s.status.expandSessionGroupKeys || INITIAL_STATUS.expandSessionGroupKeys;
11
10
 
12
- const showSystemRole = (s: GlobalStore) => s.status.showSystemRole;
13
- const mobileShowTopic = (s: GlobalStore) => s.status.mobileShowTopic;
14
- const mobileShowPortal = (s: GlobalStore) => s.status.mobileShowPortal;
15
- const showChatSideBar = (s: GlobalStore) => !s.status.zenMode && s.status.showChatSideBar;
16
- const showSessionPanel = (s: GlobalStore) => !s.status.zenMode && s.status.showSessionPanel;
17
- const showFilePanel = (s: GlobalStore) => s.status.showFilePanel;
18
- const hidePWAInstaller = (s: GlobalStore) => s.status.hidePWAInstaller;
19
- const isShowCredit = (s: GlobalStore) => s.status.isShowCredit;
11
+ const showSystemRole = (s: GlobalState) => s.status.showSystemRole;
12
+ const mobileShowTopic = (s: GlobalState) => s.status.mobileShowTopic;
13
+ const mobileShowPortal = (s: GlobalState) => s.status.mobileShowPortal;
14
+ const showChatSideBar = (s: GlobalState) => !s.status.zenMode && s.status.showChatSideBar;
15
+ const showSessionPanel = (s: GlobalState) => !s.status.zenMode && s.status.showSessionPanel;
16
+ const showFilePanel = (s: GlobalState) => s.status.showFilePanel;
17
+ const hidePWAInstaller = (s: GlobalState) => s.status.hidePWAInstaller;
18
+ const isShowCredit = (s: GlobalState) => s.status.isShowCredit;
19
+ const themeMode = (s: GlobalState) => s.status.themeMode || 'auto';
20
20
 
21
- const showChatHeader = (s: GlobalStore) => !s.status.zenMode;
22
- const inZenMode = (s: GlobalStore) => s.status.zenMode;
23
- const sessionWidth = (s: GlobalStore) => s.status.sessionsWidth;
24
- const portalWidth = (s: GlobalStore) => s.status.portalWidth || 400;
25
- const filePanelWidth = (s: GlobalStore) => s.status.filePanelWidth;
26
- const inputHeight = (s: GlobalStore) => s.status.inputHeight;
27
- const threadInputHeight = (s: GlobalStore) => s.status.threadInputHeight;
21
+ const showChatHeader = (s: GlobalState) => !s.status.zenMode;
22
+ const inZenMode = (s: GlobalState) => s.status.zenMode;
23
+ const sessionWidth = (s: GlobalState) => s.status.sessionsWidth;
24
+ const portalWidth = (s: GlobalState) => s.status.portalWidth || 400;
25
+ const filePanelWidth = (s: GlobalState) => s.status.filePanelWidth;
26
+ const inputHeight = (s: GlobalState) => s.status.inputHeight;
27
+ const threadInputHeight = (s: GlobalState) => s.status.threadInputHeight;
28
28
 
29
- const isPgliteNotEnabled = (s: GlobalStore) =>
29
+ const isPgliteNotEnabled = (s: GlobalState) =>
30
30
  isUsePgliteDB && !isServerMode && s.isStatusInit && !s.status.isEnablePglite;
31
31
 
32
32
  /**
33
33
  * 当且仅当 client db 模式,且 pglite 未初始化完成时返回 true
34
34
  */
35
- const isPgliteNotInited = (s: GlobalStore) =>
35
+ const isPgliteNotInited = (s: GlobalState) =>
36
36
  isUsePgliteDB &&
37
37
  s.isStatusInit &&
38
38
  s.status.isEnablePglite &&
@@ -41,14 +41,14 @@ const isPgliteNotInited = (s: GlobalStore) =>
41
41
  /**
42
42
  * 当且仅当 client db 模式,且 pglite 初始化完成时返回 true
43
43
  */
44
- const isPgliteInited = (s: GlobalStore): boolean =>
44
+ const isPgliteInited = (s: GlobalState): boolean =>
45
45
  (s.isStatusInit &&
46
46
  s.status.isEnablePglite &&
47
47
  s.initClientDBStage === DatabaseLoadingState.Ready) ||
48
48
  false;
49
49
 
50
50
  // 这个变量控制 clientdb 是否完成初始化,正常来说,只有 pgliteDB 模式下,才会存在变化,其他时候都是 true
51
- const isDBInited = (s: GlobalStore): boolean => (isUsePgliteDB ? isPgliteInited(s) : true);
51
+ const isDBInited = (s: GlobalState): boolean => (isUsePgliteDB ? isPgliteInited(s) : true);
52
52
 
53
53
  export const systemStatusSelectors = {
54
54
  filePanelWidth,
@@ -71,5 +71,6 @@ export const systemStatusSelectors = {
71
71
  showSessionPanel,
72
72
  showSystemRole,
73
73
  systemStatus,
74
+ themeMode,
74
75
  threadInputHeight,
75
76
  };
@@ -70,7 +70,7 @@ describe('SettingsAction', () => {
70
70
  describe('setSettings', () => {
71
71
  it('should set partial settings', async () => {
72
72
  const { result } = renderHook(() => useUserStore());
73
- const partialSettings: DeepPartial<UserSettings> = { general: { themeMode: 'dark' } };
73
+ const partialSettings: DeepPartial<UserSettings> = { general: { fontSize: 12 } };
74
74
 
75
75
  // Perform the action
76
76
  await act(async () => {
@@ -85,24 +85,6 @@ describe('SettingsAction', () => {
85
85
  });
86
86
  });
87
87
 
88
- describe('switchThemeMode', () => {
89
- it('should switch theme mode', async () => {
90
- const { result } = renderHook(() => useUserStore());
91
- const themeMode = 'light';
92
-
93
- // Perform the action
94
- await act(async () => {
95
- await result.current.switchThemeMode(themeMode);
96
- });
97
-
98
- // Assert that updateUserSettings was called with the correct theme mode
99
- expect(userService.updateUserSettings).toHaveBeenCalledWith(
100
- { general: { themeMode } },
101
- expect.any(AbortSignal),
102
- );
103
- });
104
- });
105
-
106
88
  describe('updateDefaultAgent', () => {
107
89
  it('should update default agent settings', async () => {
108
90
  const { result } = renderHook(() => useUserStore());
@@ -1,4 +1,3 @@
1
- import { ThemeMode } from 'antd-style';
2
1
  import isEqual from 'fast-deep-equal';
3
2
  import { DeepPartial } from 'utility-types';
4
3
  import type { StateCreator } from 'zustand/vanilla';
@@ -24,7 +23,6 @@ export interface UserSettingsAction {
24
23
  internal_createSignal: () => AbortController;
25
24
  resetSettings: () => Promise<void>;
26
25
  setSettings: (settings: DeepPartial<UserSettings>) => Promise<void>;
27
- switchThemeMode: (themeMode: ThemeMode) => Promise<void>;
28
26
  updateDefaultAgent: (agent: DeepPartial<LobeAgentSettings>) => Promise<void>;
29
27
  updateGeneralConfig: (settings: Partial<UserGeneralConfig>) => Promise<void>;
30
28
  updateKeyVaults: (settings: Partial<UserKeyVaults>) => Promise<void>;
@@ -92,9 +90,6 @@ export const createSettingsSlice: StateCreator<
92
90
  await userService.updateUserSettings(diffs, abortController.signal);
93
91
  await get().refreshUserState();
94
92
  },
95
- switchThemeMode: async (themeMode) => {
96
- await get().updateGeneralConfig({ themeMode });
97
- },
98
93
  updateDefaultAgent: async (defaultAgent) => {
99
94
  await get().setSettings({ defaultAgent });
100
95
  },
@@ -5,27 +5,17 @@ import { merge } from '@/utils/merge';
5
5
  import { userGeneralSettingsSelectors } from './general';
6
6
 
7
7
  describe('settingsSelectors', () => {
8
- describe('currentThemeMode', () => {
9
- it('should return the correct theme', () => {
8
+ describe('fontSize', () => {
9
+ it('should return the fontSize', () => {
10
10
  const s: UserState = merge(initialState, {
11
11
  settings: {
12
- general: { themeMode: 'light' },
12
+ general: { fontSize: 12 },
13
13
  },
14
14
  });
15
15
 
16
- const result = userGeneralSettingsSelectors.currentThemeMode(s as UserStore);
16
+ const result = userGeneralSettingsSelectors.fontSize(s as UserStore);
17
17
 
18
- expect(result).toBe('light');
19
- });
20
- it('should return the auto if not set the themeMode', () => {
21
- const s: UserState = merge(initialState, {
22
- settings: {
23
- general: { themeMode: undefined },
24
- },
25
- });
26
- const result = userGeneralSettingsSelectors.currentThemeMode(s as UserStore);
27
-
28
- expect(result).toBe('auto');
18
+ expect(result).toBe(12);
29
19
  });
30
20
  });
31
21
  });
@@ -3,18 +3,12 @@ import { currentSettings } from './settings';
3
3
 
4
4
  const generalConfig = (s: UserStore) => currentSettings(s).general || {};
5
5
 
6
- const currentThemeMode = (s: UserStore) => {
7
- const themeMode = generalConfig(s).themeMode;
8
- return themeMode || 'auto';
9
- };
10
-
11
6
  const neutralColor = (s: UserStore) => generalConfig(s).neutralColor;
12
7
  const primaryColor = (s: UserStore) => generalConfig(s).primaryColor;
13
8
  const fontSize = (s: UserStore) => generalConfig(s).fontSize;
14
9
 
15
10
  export const userGeneralSettingsSelectors = {
16
11
  config: generalConfig,
17
- currentThemeMode,
18
12
  fontSize,
19
13
  neutralColor,
20
14
  primaryColor,
@@ -1,9 +1,7 @@
1
1
  import type { NeutralColors, PrimaryColors } from '@lobehub/ui';
2
- import type { ThemeMode } from 'antd-style';
3
2
 
4
3
  export interface UserGeneralConfig {
5
4
  fontSize: number;
6
5
  neutralColor?: NeutralColors;
7
6
  primaryColor?: PrimaryColors;
8
- themeMode: ThemeMode;
9
7
  }