@lobehub/chat 0.122.1 → 0.122.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +8 -8
  3. package/README.zh-CN.md +8 -8
  4. package/docs/Deployment/Docker-Deployment.md +118 -0
  5. package/docs/Deployment/Docker-Deployment.zh-CN.md +118 -0
  6. package/package.json +1 -4
  7. package/src/app/api/config/route.ts +3 -1
  8. package/src/app/chat/(mobile)/features/SessionHeader.tsx +2 -1
  9. package/src/app/chat/features/ChatHeader/ShareButton/ShareModal.tsx +2 -1
  10. package/src/app/chat/features/TopicListContent/Topic/TopicContent.tsx +2 -1
  11. package/src/app/chat/settings/features/SubmitAgentButton/SubmitAgentModal.tsx +2 -1
  12. package/src/app/settings/(mobile)/mobile/index.tsx +2 -1
  13. package/src/app/settings/common/Common.tsx +14 -3
  14. package/src/app/settings/features/ThemeSwatches/ThemeSwatchesNeutral.tsx +2 -1
  15. package/src/app/settings/features/ThemeSwatches/ThemeSwatchesPrimary.tsx +2 -1
  16. package/src/app/settings/llm/LLM/index.tsx +17 -10
  17. package/src/const/settings.ts +0 -1
  18. package/src/database/core/db.ts +41 -2
  19. package/src/database/core/migrations/migrateSettingsToUser/fixtures/input.json +55 -0
  20. package/src/database/core/migrations/migrateSettingsToUser/fixtures/output.json +60 -0
  21. package/src/database/core/migrations/migrateSettingsToUser/index.test.ts +13 -0
  22. package/src/database/core/migrations/migrateSettingsToUser/index.ts +39 -0
  23. package/src/database/core/schemas.ts +10 -0
  24. package/src/database/models/user.ts +54 -0
  25. package/src/database/schemas/user.ts +57 -0
  26. package/src/features/AgentSetting/AgentConfig/index.tsx +2 -2
  27. package/src/features/AvatarWithUpload/index.tsx +6 -2
  28. package/src/features/ChatInput/ActionBar/ModelSwitch.tsx +2 -2
  29. package/src/features/Conversation/Error/ApiKeyForm.tsx +3 -3
  30. package/src/features/Conversation/Error/InvalidAccess.tsx +5 -1
  31. package/src/features/Conversation/Extras/Translate.tsx +3 -4
  32. package/src/layout/GlobalLayout/AppTheme.tsx +4 -3
  33. package/src/layout/GlobalLayout/Locale.tsx +2 -1
  34. package/src/layout/GlobalLayout/StoreHydration.tsx +7 -2
  35. package/src/services/_header.ts +3 -2
  36. package/src/services/user.ts +31 -0
  37. package/src/store/chat/slices/message/selectors.test.ts +7 -2
  38. package/src/store/chat/slices/message/selectors.ts +2 -1
  39. package/src/store/global/hooks/index.ts +0 -1
  40. package/src/store/global/initialState.ts +3 -1
  41. package/src/store/global/selectors.ts +1 -0
  42. package/src/store/global/slices/common/action.test.ts +2 -79
  43. package/src/store/global/slices/common/action.ts +37 -61
  44. package/src/store/global/slices/common/initialState.ts +0 -34
  45. package/src/store/global/slices/common/selectors.ts +2 -7
  46. package/src/store/global/slices/preference/action.test.ts +92 -0
  47. package/src/store/global/slices/preference/action.ts +75 -0
  48. package/src/store/global/slices/preference/initialState.ts +45 -0
  49. package/src/store/global/slices/preference/selectors.ts +10 -0
  50. package/src/store/global/slices/settings/action.test.ts +68 -39
  51. package/src/store/global/slices/settings/action.ts +26 -50
  52. package/src/store/global/slices/settings/initialState.ts +8 -6
  53. package/src/store/global/slices/settings/selectors/__snapshots__/modelProvider.test.ts.snap +100 -0
  54. package/src/store/global/slices/settings/selectors/__snapshots__/selectors.test.ts.snap +132 -0
  55. package/src/store/global/slices/settings/selectors/index.ts +2 -0
  56. package/src/store/global/slices/settings/selectors/modelProvider.test.ts +100 -0
  57. package/src/store/global/slices/settings/selectors/modelProvider.ts +64 -0
  58. package/src/store/global/slices/settings/{selectors.test.ts → selectors/selectors.test.ts} +2 -97
  59. package/src/store/global/slices/settings/selectors/settings.ts +58 -0
  60. package/src/store/global/store.ts +11 -36
  61. package/src/store/session/slices/agent/action.ts +1 -1
  62. package/src/types/settings/base.ts +13 -0
  63. package/src/types/settings/index.ts +34 -0
  64. package/src/types/settings/modelProvider.ts +17 -0
  65. package/src/types/settings/tts.ts +10 -0
  66. package/src/utils/difference.ts +12 -0
  67. package/tests/utils.tsx +8 -0
  68. package/tsconfig.json +9 -8
  69. package/vitest.config.ts +1 -0
  70. package/src/store/global/hooks/useHydrated.ts +0 -18
  71. package/src/store/global/slices/settings/selectors.ts +0 -105
  72. package/src/types/settings.ts +0 -68
@@ -1,4 +1,3 @@
1
1
  export * from './useEffectAfterHydrated';
2
- export * from './useHydrated';
3
2
  export * from './useOnFinishHydrationGlobal';
4
3
  export * from './useSwitchSideBarOnInit';
@@ -1,11 +1,13 @@
1
1
  import { GlobalCommonState, initialCommonState } from './slices/common/initialState';
2
+ import { GlobalPreferenceState, initialPreferenceState } from './slices/preference/initialState';
2
3
  import { GlobalSettingsState, initialSettingsState } from './slices/settings/initialState';
3
4
 
4
5
  export { SettingsTabs, SidebarTabKey } from './slices/common/initialState';
5
6
 
6
- export type GlobalState = GlobalCommonState & GlobalSettingsState;
7
+ export type GlobalState = GlobalCommonState & GlobalSettingsState & GlobalPreferenceState;
7
8
 
8
9
  export const initialState: GlobalState = {
9
10
  ...initialCommonState,
10
11
  ...initialSettingsState,
12
+ ...initialPreferenceState,
11
13
  };
@@ -1,2 +1,3 @@
1
1
  export * from './slices/common/selectors';
2
+ export * from './slices/preference/selectors';
2
3
  export * from './slices/settings/selectors';
@@ -4,7 +4,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4
4
 
5
5
  import { globalService } from '@/services/global';
6
6
  import { useGlobalStore } from '@/store/global';
7
- import { type Guide, SidebarTabKey } from '@/store/global/slices/common/initialState';
7
+
8
+ import { SidebarTabKey } from './initialState';
8
9
 
9
10
  // Mock globalService
10
11
  vi.mock('@/services/global', () => ({
@@ -49,82 +50,4 @@ describe('createCommonSlice', () => {
49
50
  expect(result.current.sidebarKey).toEqual(SidebarTabKey.Market);
50
51
  });
51
52
  });
52
-
53
- describe('toggleChatSideBar', () => {
54
- it('should toggle chat sidebar', () => {
55
- const { result } = renderHook(() => useGlobalStore());
56
-
57
- act(() => {
58
- useGlobalStore.getState().updatePreference({ showChatSideBar: false });
59
- result.current.toggleChatSideBar();
60
- });
61
-
62
- expect(result.current.preference.showChatSideBar).toBe(true);
63
- });
64
-
65
- // Add more tests for other actions in createCommonSlice
66
- });
67
-
68
- describe('toggleExpandSessionGroup', () => {
69
- it('should toggle expand session group', () => {
70
- const { result } = renderHook(() => useGlobalStore());
71
- const groupId = 'group-id';
72
-
73
- act(() => {
74
- result.current.toggleExpandSessionGroup(groupId, true);
75
- });
76
-
77
- expect(result.current.preference.expandSessionGroupKeys).toContain(groupId);
78
- });
79
- });
80
-
81
- describe('toggleMobileTopic', () => {
82
- it('should toggle mobile topic', () => {
83
- const { result } = renderHook(() => useGlobalStore());
84
-
85
- act(() => {
86
- result.current.toggleMobileTopic();
87
- });
88
-
89
- expect(result.current.preference.mobileShowTopic).toBe(true);
90
- });
91
- });
92
-
93
- describe('toggleSystemRole', () => {
94
- it('should toggle system role', () => {
95
- const { result } = renderHook(() => useGlobalStore());
96
-
97
- act(() => {
98
- result.current.toggleSystemRole(true);
99
- });
100
-
101
- expect(result.current.preference.showSystemRole).toBe(true);
102
- });
103
- });
104
-
105
- describe('updateGuideState', () => {
106
- it('should update guide state', () => {
107
- const { result } = renderHook(() => useGlobalStore());
108
- const guide: Guide = { topic: true };
109
-
110
- act(() => {
111
- result.current.updateGuideState(guide);
112
- });
113
-
114
- expect(result.current.preference.guide).toEqual(guide);
115
- });
116
- });
117
-
118
- describe('updatePreference', () => {
119
- it('should update preference', () => {
120
- const { result } = renderHook(() => useGlobalStore());
121
- const preference = { inputHeight: 200 };
122
-
123
- act(() => {
124
- result.current.updatePreference(preference);
125
- });
126
-
127
- expect(result.current.preference.inputHeight).toEqual(200);
128
- });
129
- });
130
53
  });
@@ -1,97 +1,54 @@
1
- import { produce } from 'immer';
2
1
  import { gt } from 'semver';
3
- import useSWR, { SWRResponse } from 'swr';
2
+ import useSWR, { SWRResponse, mutate } from 'swr';
4
3
  import type { StateCreator } from 'zustand/vanilla';
5
4
 
6
5
  import { INBOX_SESSION_ID } from '@/const/session';
7
6
  import { SESSION_CHAT_URL } from '@/const/url';
8
7
  import { CURRENT_VERSION } from '@/const/version';
9
8
  import { globalService } from '@/services/global';
9
+ import { UserConfig, userService } from '@/services/user';
10
10
  import type { GlobalStore } from '@/store/global';
11
11
  import type { GlobalServerConfig } from '@/types/settings';
12
12
  import { merge } from '@/utils/merge';
13
13
  import { setNamespace } from '@/utils/storeDebug';
14
14
 
15
- import type { GlobalCommonState, GlobalPreference, Guide, SidebarTabKey } from './initialState';
15
+ import type { SidebarTabKey } from './initialState';
16
16
 
17
- const n = setNamespace('settings');
17
+ const n = setNamespace('common');
18
18
 
19
19
  /**
20
20
  * 设置操作
21
21
  */
22
22
  export interface CommonAction {
23
+ refreshUserConfig: () => Promise<void>;
23
24
  switchBackToChat: (sessionId?: string) => void;
24
- /**
25
- * 切换侧边栏选项
26
- * @param key - 选中的侧边栏选项
27
- */
28
25
  switchSideBar: (key: SidebarTabKey) => void;
29
- toggleChatSideBar: (visible?: boolean) => void;
30
- toggleExpandSessionGroup: (id: string, expand: boolean) => void;
31
- toggleMobileTopic: (visible?: boolean) => void;
32
- toggleSystemRole: (visible?: boolean) => void;
33
- updateGuideState: (guide: Partial<Guide>) => void;
34
- updatePreference: (preference: Partial<GlobalPreference>, action?: string) => void;
26
+ updateAvatar: (avatar: string) => Promise<void>;
35
27
  useCheckLatestVersion: () => SWRResponse<string>;
36
- useFetchGlobalConfig: () => SWRResponse;
28
+ useFetchServerConfig: () => SWRResponse;
29
+ useFetchUserConfig: (initServer: boolean) => SWRResponse<UserConfig | undefined>;
37
30
  }
38
31
 
32
+ const USER_CONFIG_FETCH_KEY = 'fetchUserConfig';
33
+
39
34
  export const createCommonSlice: StateCreator<
40
35
  GlobalStore,
41
36
  [['zustand/devtools', never]],
42
37
  [],
43
38
  CommonAction
44
39
  > = (set, get) => ({
40
+ refreshUserConfig: async () => {
41
+ await mutate([USER_CONFIG_FETCH_KEY, true]);
42
+ },
45
43
  switchBackToChat: (sessionId) => {
46
44
  get().router?.push(SESSION_CHAT_URL(sessionId || INBOX_SESSION_ID, get().isMobile));
47
45
  },
48
46
  switchSideBar: (key) => {
49
47
  set({ sidebarKey: key }, false, n('switchSideBar', key));
50
48
  },
51
- toggleChatSideBar: (newValue) => {
52
- const showChatSideBar =
53
- typeof newValue === 'boolean' ? newValue : !get().preference.showChatSideBar;
54
-
55
- get().updatePreference({ showChatSideBar }, n('toggleAgentPanel', newValue) as string);
56
- },
57
- toggleExpandSessionGroup: (id, expand) => {
58
- const { preference } = get();
59
- const nextExpandSessionGroup = produce(preference.expandSessionGroupKeys, (draft) => {
60
- if (expand) {
61
- if (draft.includes(id)) return;
62
- draft.push(id);
63
- } else {
64
- const index = draft.indexOf(id);
65
- if (index !== -1) draft.splice(index, 1);
66
- }
67
- });
68
- get().updatePreference({ expandSessionGroupKeys: nextExpandSessionGroup });
69
- },
70
- toggleMobileTopic: (newValue) => {
71
- const mobileShowTopic =
72
- typeof newValue === 'boolean' ? newValue : !get().preference.mobileShowTopic;
73
-
74
- get().updatePreference({ mobileShowTopic }, n('toggleMobileTopic', newValue) as string);
75
- },
76
- toggleSystemRole: (newValue) => {
77
- const showSystemRole =
78
- typeof newValue === 'boolean' ? newValue : !get().preference.mobileShowTopic;
79
-
80
- get().updatePreference({ showSystemRole }, n('toggleMobileTopic', newValue) as string);
81
- },
82
- updateGuideState: (guide) => {
83
- const { updatePreference } = get();
84
- const nextGuide = merge(get().preference.guide, guide);
85
- updatePreference({ guide: nextGuide });
86
- },
87
- updatePreference: (preference, action) => {
88
- set(
89
- produce((draft: GlobalCommonState) => {
90
- draft.preference = merge(draft.preference, preference);
91
- }),
92
- false,
93
- action,
94
- );
49
+ updateAvatar: async (avatar) => {
50
+ await userService.updateAvatar(avatar);
51
+ await get().refreshUserConfig();
95
52
  },
96
53
  useCheckLatestVersion: () =>
97
54
  useSWR('checkLatestVersion', globalService.getLatestVersion, {
@@ -102,11 +59,30 @@ export const createCommonSlice: StateCreator<
102
59
  set({ hasNewVersion: true, latestVersion: data }, false, n('checkLatestVersion'));
103
60
  },
104
61
  }),
105
- useFetchGlobalConfig: () =>
62
+ useFetchServerConfig: () =>
106
63
  useSWR<GlobalServerConfig>('fetchGlobalConfig', globalService.getGlobalConfig, {
107
64
  onSuccess: (data) => {
108
- if (data) set({ serverConfig: data });
65
+ if (data) {
66
+ const defaultSettings = merge(get().defaultSettings, { defaultAgent: data.defaultAgent });
67
+ set({ defaultSettings, serverConfig: data }, false, n('initGlobalConfig'));
68
+ }
109
69
  },
110
70
  revalidateOnFocus: false,
111
71
  }),
72
+ useFetchUserConfig: (initServer) =>
73
+ useSWR<UserConfig | undefined>(
74
+ [USER_CONFIG_FETCH_KEY, initServer],
75
+ async () => {
76
+ if (!initServer) return;
77
+ return userService.getUserConfig();
78
+ },
79
+ {
80
+ onSuccess: (data) => {
81
+ if (!data) return;
82
+
83
+ set({ avatar: data.avatar, settings: data.settings }, false, n('fetchUserConfig', data));
84
+ },
85
+ revalidateOnFocus: false,
86
+ },
87
+ ),
112
88
  });
@@ -1,6 +1,5 @@
1
1
  import { AppRouterInstance } from 'next/dist/shared/lib/app-router-context.shared-runtime';
2
2
 
3
- import { SessionDefaultGroup, SessionGroupId } from '@/types/session';
4
3
 
5
4
  export enum SidebarTabKey {
6
5
  Chat = 'chat',
@@ -20,32 +19,10 @@ export interface Guide {
20
19
  topic?: boolean;
21
20
  }
22
21
 
23
- export interface GlobalPreference {
24
- // which sessionGroup should expand
25
- expandSessionGroupKeys: SessionGroupId[];
26
- guide?: Guide;
27
- inputHeight: number;
28
- mobileShowTopic?: boolean;
29
- sessionsWidth: number;
30
-
31
- showChatSideBar?: boolean;
32
- showSessionPanel?: boolean;
33
- showSystemRole?: boolean;
34
- /**
35
- * whether to use cmd + enter to send message
36
- */
37
- useCmdEnterToSend?: boolean;
38
- }
39
-
40
22
  export interface GlobalCommonState {
41
23
  hasNewVersion?: boolean;
42
24
  isMobile?: boolean;
43
25
  latestVersion?: string;
44
- /**
45
- * 用户偏好的 UI 状态
46
- * @localStorage
47
- */
48
- preference: GlobalPreference;
49
26
  router?: AppRouterInstance;
50
27
  settingsTab: SettingsTabs;
51
28
  sidebarKey: SidebarTabKey;
@@ -53,17 +30,6 @@ export interface GlobalCommonState {
53
30
 
54
31
  export const initialCommonState: GlobalCommonState = {
55
32
  isMobile: false,
56
- preference: {
57
- expandSessionGroupKeys: [SessionDefaultGroup.Pinned, SessionDefaultGroup.Default],
58
- guide: {},
59
- inputHeight: 200,
60
- mobileShowTopic: false,
61
- sessionsWidth: 320,
62
- showChatSideBar: true,
63
- showSessionPanel: true,
64
- showSystemRole: false,
65
- useCmdEnterToSend: false,
66
- },
67
33
  settingsTab: SettingsTabs.Common,
68
34
  sidebarKey: SidebarTabKey.Chat,
69
35
  };
@@ -1,10 +1,5 @@
1
1
  import { GlobalStore } from '@/store/global';
2
2
 
3
- const sessionGroupKeys = (s: GlobalStore): string[] => s.preference.expandSessionGroupKeys || [];
4
-
5
- const useCmdEnterToSend = (s: GlobalStore): boolean => s.preference.useCmdEnterToSend || false;
6
-
7
- export const preferenceSelectors = {
8
- sessionGroupKeys,
9
- useCmdEnterToSend,
3
+ export const commonSelectors = {
4
+ userAvatar: (s: GlobalStore) => s.avatar || '',
10
5
  };
@@ -0,0 +1,92 @@
1
+ import { act, renderHook } from '@testing-library/react';
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
+
4
+ import { useGlobalStore } from '@/store/global';
5
+
6
+ import { type Guide } from './initialState';
7
+
8
+ beforeEach(() => {
9
+ vi.clearAllMocks();
10
+ });
11
+
12
+ afterEach(() => {
13
+ vi.restoreAllMocks();
14
+ });
15
+
16
+ describe('createPreferenceSlice', () => {
17
+ describe('toggleChatSideBar', () => {
18
+ it('should toggle chat sidebar', () => {
19
+ const { result } = renderHook(() => useGlobalStore());
20
+
21
+ act(() => {
22
+ useGlobalStore.getState().updatePreference({ showChatSideBar: false });
23
+ result.current.toggleChatSideBar();
24
+ });
25
+
26
+ expect(result.current.preference.showChatSideBar).toBe(true);
27
+ });
28
+ });
29
+
30
+ describe('toggleExpandSessionGroup', () => {
31
+ it('should toggle expand session group', () => {
32
+ const { result } = renderHook(() => useGlobalStore());
33
+ const groupId = 'group-id';
34
+
35
+ act(() => {
36
+ result.current.toggleExpandSessionGroup(groupId, true);
37
+ });
38
+
39
+ expect(result.current.preference.expandSessionGroupKeys).toContain(groupId);
40
+ });
41
+ });
42
+
43
+ describe('toggleMobileTopic', () => {
44
+ it('should toggle mobile topic', () => {
45
+ const { result } = renderHook(() => useGlobalStore());
46
+
47
+ act(() => {
48
+ result.current.toggleMobileTopic();
49
+ });
50
+
51
+ expect(result.current.preference.mobileShowTopic).toBe(true);
52
+ });
53
+ });
54
+
55
+ describe('toggleSystemRole', () => {
56
+ it('should toggle system role', () => {
57
+ const { result } = renderHook(() => useGlobalStore());
58
+
59
+ act(() => {
60
+ result.current.toggleSystemRole(true);
61
+ });
62
+
63
+ expect(result.current.preference.showSystemRole).toBe(true);
64
+ });
65
+ });
66
+
67
+ describe('updateGuideState', () => {
68
+ it('should update guide state', () => {
69
+ const { result } = renderHook(() => useGlobalStore());
70
+ const guide: Guide = { topic: true };
71
+
72
+ act(() => {
73
+ result.current.updateGuideState(guide);
74
+ });
75
+
76
+ expect(result.current.preference.guide).toEqual(guide);
77
+ });
78
+ });
79
+
80
+ describe('updatePreference', () => {
81
+ it('should update preference', () => {
82
+ const { result } = renderHook(() => useGlobalStore());
83
+ const preference = { inputHeight: 200 };
84
+
85
+ act(() => {
86
+ result.current.updatePreference(preference);
87
+ });
88
+
89
+ expect(result.current.preference.inputHeight).toEqual(200);
90
+ });
91
+ });
92
+ });
@@ -0,0 +1,75 @@
1
+ import { produce } from 'immer';
2
+ import type { StateCreator } from 'zustand/vanilla';
3
+
4
+ import type { GlobalStore } from '@/store/global';
5
+ import { merge } from '@/utils/merge';
6
+ import { setNamespace } from '@/utils/storeDebug';
7
+
8
+ import type { GlobalPreference, GlobalPreferenceState, Guide } from './initialState';
9
+
10
+ const n = setNamespace('preference');
11
+
12
+ /**
13
+ * 设置操作
14
+ */
15
+ export interface PreferenceAction {
16
+ toggleChatSideBar: (visible?: boolean) => void;
17
+ toggleExpandSessionGroup: (id: string, expand: boolean) => void;
18
+ toggleMobileTopic: (visible?: boolean) => void;
19
+ toggleSystemRole: (visible?: boolean) => void;
20
+ updateGuideState: (guide: Partial<Guide>) => void;
21
+ updatePreference: (preference: Partial<GlobalPreference>, action?: string) => void;
22
+ }
23
+
24
+ export const createPreferenceSlice: StateCreator<
25
+ GlobalStore,
26
+ [['zustand/devtools', never]],
27
+ [],
28
+ PreferenceAction
29
+ > = (set, get) => ({
30
+ toggleChatSideBar: (newValue) => {
31
+ const showChatSideBar =
32
+ typeof newValue === 'boolean' ? newValue : !get().preference.showChatSideBar;
33
+
34
+ get().updatePreference({ showChatSideBar }, n('toggleAgentPanel', newValue) as string);
35
+ },
36
+ toggleExpandSessionGroup: (id, expand) => {
37
+ const { preference } = get();
38
+ const nextExpandSessionGroup = produce(preference.expandSessionGroupKeys, (draft) => {
39
+ if (expand) {
40
+ if (draft.includes(id)) return;
41
+ draft.push(id);
42
+ } else {
43
+ const index = draft.indexOf(id);
44
+ if (index !== -1) draft.splice(index, 1);
45
+ }
46
+ });
47
+ get().updatePreference({ expandSessionGroupKeys: nextExpandSessionGroup });
48
+ },
49
+ toggleMobileTopic: (newValue) => {
50
+ const mobileShowTopic =
51
+ typeof newValue === 'boolean' ? newValue : !get().preference.mobileShowTopic;
52
+
53
+ get().updatePreference({ mobileShowTopic }, n('toggleMobileTopic', newValue) as string);
54
+ },
55
+ toggleSystemRole: (newValue) => {
56
+ const showSystemRole =
57
+ typeof newValue === 'boolean' ? newValue : !get().preference.mobileShowTopic;
58
+
59
+ get().updatePreference({ showSystemRole }, n('toggleMobileTopic', newValue) as string);
60
+ },
61
+ updateGuideState: (guide) => {
62
+ const { updatePreference } = get();
63
+ const nextGuide = merge(get().preference.guide, guide);
64
+ updatePreference({ guide: nextGuide });
65
+ },
66
+ updatePreference: (preference, action) => {
67
+ set(
68
+ produce((draft: GlobalPreferenceState) => {
69
+ draft.preference = merge(draft.preference, preference);
70
+ }),
71
+ false,
72
+ action,
73
+ );
74
+ },
75
+ });
@@ -0,0 +1,45 @@
1
+ import { SessionDefaultGroup, SessionGroupId } from '@/types/session';
2
+
3
+ export interface Guide {
4
+ // Topic 引导
5
+ topic?: boolean;
6
+ }
7
+
8
+ export interface GlobalPreference {
9
+ // which sessionGroup should expand
10
+ expandSessionGroupKeys: SessionGroupId[];
11
+ guide?: Guide;
12
+ inputHeight: number;
13
+ mobileShowTopic?: boolean;
14
+ sessionsWidth: number;
15
+
16
+ showChatSideBar?: boolean;
17
+ showSessionPanel?: boolean;
18
+ showSystemRole?: boolean;
19
+ /**
20
+ * whether to use cmd + enter to send message
21
+ */
22
+ useCmdEnterToSend?: boolean;
23
+ }
24
+
25
+ export interface GlobalPreferenceState {
26
+ /**
27
+ * 用户偏好的 UI 状态
28
+ * @localStorage
29
+ */
30
+ preference: GlobalPreference;
31
+ }
32
+
33
+ export const initialPreferenceState: GlobalPreferenceState = {
34
+ preference: {
35
+ expandSessionGroupKeys: [SessionDefaultGroup.Pinned, SessionDefaultGroup.Default],
36
+ guide: {},
37
+ inputHeight: 200,
38
+ mobileShowTopic: false,
39
+ sessionsWidth: 320,
40
+ showChatSideBar: true,
41
+ showSessionPanel: true,
42
+ showSystemRole: false,
43
+ useCmdEnterToSend: false,
44
+ },
45
+ };
@@ -0,0 +1,10 @@
1
+ import { GlobalStore } from '@/store/global';
2
+
3
+ const sessionGroupKeys = (s: GlobalStore): string[] => s.preference.expandSessionGroupKeys || [];
4
+
5
+ const useCmdEnterToSend = (s: GlobalStore): boolean => s.preference.useCmdEnterToSend || false;
6
+
7
+ export const preferenceSelectors = {
8
+ sessionGroupKeys,
9
+ useCmdEnterToSend,
10
+ };