@lobehub/chat 0.161.9 → 0.161.10

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 (62) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/package.json +1 -1
  3. package/src/app/(main)/settings/llm/components/ProviderConfig/index.tsx +5 -2
  4. package/src/app/(main)/settings/llm/components/ProviderModelList/CustomModelOption.tsx +6 -7
  5. package/src/app/(main)/settings/llm/components/ProviderModelList/{ModelConfigModal.tsx → ModelConfigModal/Form.tsx} +19 -63
  6. package/src/app/(main)/settings/llm/components/ProviderModelList/ModelConfigModal/index.tsx +78 -0
  7. package/src/app/(main)/settings/llm/components/ProviderModelList/Option.tsx +35 -11
  8. package/src/app/(main)/settings/llm/components/ProviderModelList/index.tsx +15 -18
  9. package/src/components/ModelProviderIcon/index.tsx +2 -2
  10. package/src/components/ModelSelect/index.tsx +5 -14
  11. package/src/features/User/UserPanel/PanelContent.tsx +1 -1
  12. package/src/hooks/useSyncData.ts +3 -1
  13. package/src/layout/GlobalProvider/StoreInitialization.tsx +17 -9
  14. package/src/layout/GlobalProvider/index.tsx +1 -1
  15. package/src/locales/default/components.ts +1 -0
  16. package/src/services/message/client.test.ts +0 -24
  17. package/src/services/message/client.ts +0 -5
  18. package/src/services/message/type.ts +0 -1
  19. package/src/services/user/client.test.ts +100 -0
  20. package/src/services/user/client.ts +16 -14
  21. package/src/services/user/index.ts +0 -2
  22. package/src/services/user/type.ts +2 -4
  23. package/src/store/user/initialState.ts +10 -1
  24. package/src/store/user/selectors.ts +3 -7
  25. package/src/store/user/slices/auth/action.test.ts +5 -87
  26. package/src/store/user/slices/auth/action.ts +3 -58
  27. package/src/store/user/slices/auth/initialState.ts +2 -1
  28. package/src/store/user/slices/common/action.test.ts +196 -20
  29. package/src/store/user/slices/common/action.ts +55 -26
  30. package/src/store/user/slices/common/initialState.ts +9 -0
  31. package/src/store/user/slices/modelList/action.test.ts +363 -0
  32. package/src/store/user/slices/{settings/actions/llm.ts → modelList/action.ts} +66 -60
  33. package/src/store/user/slices/modelList/initialState.ts +15 -0
  34. package/src/store/user/slices/modelList/selectors/index.ts +2 -0
  35. package/src/store/user/slices/{settings → modelList}/selectors/modelConfig.test.ts +3 -2
  36. package/src/store/user/slices/{settings → modelList}/selectors/modelConfig.ts +1 -1
  37. package/src/store/user/slices/{settings → modelList}/selectors/modelProvider.test.ts +7 -7
  38. package/src/store/user/slices/{settings → modelList}/selectors/modelProvider.ts +2 -4
  39. package/src/store/user/slices/preference/action.test.ts +0 -52
  40. package/src/store/user/slices/preference/action.ts +1 -17
  41. package/src/store/user/slices/preference/initialState.ts +0 -5
  42. package/src/store/user/slices/preference/selectors.test.ts +2 -2
  43. package/src/store/user/slices/preference/selectors.ts +1 -1
  44. package/src/store/user/slices/settings/{actions/general.ts → action.ts} +5 -5
  45. package/src/store/user/slices/settings/initialState.ts +0 -12
  46. package/src/store/user/slices/settings/selectors/index.ts +0 -3
  47. package/src/store/user/slices/sync/action.test.ts +19 -5
  48. package/src/store/user/slices/sync/action.ts +9 -6
  49. package/src/store/user/slices/{settings/selectors/sync.ts → sync/selectors.ts} +2 -2
  50. package/src/store/user/store.ts +5 -2
  51. package/src/types/serverConfig.ts +3 -1
  52. package/src/types/user/index.ts +13 -0
  53. package/src/utils/parseModels.test.ts +121 -1
  54. package/src/utils/parseModels.ts +9 -4
  55. package/src/store/user/slices/settings/actions/index.ts +0 -18
  56. package/src/store/user/slices/settings/actions/llm.test.ts +0 -136
  57. /package/src/app/(main)/settings/llm/components/ProviderModelList/{MaxTokenSlider.tsx → ModelConfigModal/MaxTokenSlider.tsx} +0 -0
  58. /package/src/store/user/slices/{settings → modelList}/reducers/customModelCard.test.ts +0 -0
  59. /package/src/store/user/slices/{settings → modelList}/reducers/customModelCard.ts +0 -0
  60. /package/src/store/user/slices/settings/{actions/general.test.ts → action.test.ts} +0 -0
  61. /package/src/store/user/slices/settings/selectors/__snapshots__/{selectors.test.ts.snap → settings.test.ts.snap} +0 -0
  62. /package/src/store/user/slices/settings/selectors/{selectors.test.ts → settings.test.ts} +0 -0
@@ -0,0 +1,100 @@
1
+ import { DeepPartial } from 'utility-types';
2
+ import { Mock, beforeEach, describe, expect, it, vi } from 'vitest';
3
+
4
+ import { UserModel } from '@/database/client/models/user';
5
+ import { GlobalSettings } from '@/types/settings';
6
+ import { UserPreference } from '@/types/user';
7
+ import { AsyncLocalStorage } from '@/utils/localStorage';
8
+
9
+ import { ClientService } from './client';
10
+
11
+ vi.mock('@/database/client/models/user', () => ({
12
+ UserModel: {
13
+ getUser: vi.fn(),
14
+ updateSettings: vi.fn(),
15
+ resetSettings: vi.fn(),
16
+ updateAvatar: vi.fn(),
17
+ },
18
+ }));
19
+
20
+ const mockUser = {
21
+ avatar: 'avatar.png',
22
+ settings: { themeMode: 'light' } as unknown as GlobalSettings,
23
+ uuid: 'user-id',
24
+ };
25
+
26
+ const mockPreference = {
27
+ useCmdEnterToSend: true,
28
+ } as UserPreference;
29
+
30
+ describe('ClientService', () => {
31
+ let clientService: ClientService;
32
+
33
+ beforeEach(() => {
34
+ vi.clearAllMocks();
35
+ clientService = new ClientService();
36
+ });
37
+
38
+ it('should get user state correctly', async () => {
39
+ (UserModel.getUser as Mock).mockResolvedValue(mockUser);
40
+ const spyOn = vi
41
+ .spyOn(clientService['preferenceStorage'], 'getFromLocalStorage')
42
+ .mockResolvedValue(mockPreference);
43
+
44
+ const userState = await clientService.getUserState();
45
+
46
+ expect(userState).toEqual({
47
+ avatar: mockUser.avatar,
48
+ isOnboard: true,
49
+ canEnableTrace: false,
50
+ preference: mockPreference,
51
+ settings: mockUser.settings,
52
+ userId: mockUser.uuid,
53
+ });
54
+ expect(UserModel.getUser).toHaveBeenCalledTimes(1);
55
+ expect(spyOn).toHaveBeenCalledTimes(1);
56
+ });
57
+
58
+ it('should update user settings correctly', async () => {
59
+ const settingsPatch: DeepPartial<GlobalSettings> = { themeMode: 'dark' };
60
+ (UserModel.updateSettings as Mock).mockResolvedValue(undefined);
61
+
62
+ await clientService.updateUserSettings(settingsPatch);
63
+
64
+ expect(UserModel.updateSettings).toHaveBeenCalledWith(settingsPatch);
65
+ expect(UserModel.updateSettings).toHaveBeenCalledTimes(1);
66
+ });
67
+
68
+ it('should reset user settings correctly', async () => {
69
+ (UserModel.resetSettings as Mock).mockResolvedValue(undefined);
70
+
71
+ await clientService.resetUserSettings();
72
+
73
+ expect(UserModel.resetSettings).toHaveBeenCalledTimes(1);
74
+ });
75
+
76
+ it('should update user avatar correctly', async () => {
77
+ const newAvatar = 'new-avatar.png';
78
+ (UserModel.updateAvatar as Mock).mockResolvedValue(undefined);
79
+
80
+ await clientService.updateAvatar(newAvatar);
81
+
82
+ expect(UserModel.updateAvatar).toHaveBeenCalledWith(newAvatar);
83
+ expect(UserModel.updateAvatar).toHaveBeenCalledTimes(1);
84
+ });
85
+
86
+ it('should update user preference correctly', async () => {
87
+ const newPreference = {
88
+ useCmdEnterToSend: false,
89
+ } as UserPreference;
90
+
91
+ const spyOn = vi
92
+ .spyOn(clientService['preferenceStorage'], 'saveToLocalStorage')
93
+ .mockResolvedValue(undefined);
94
+
95
+ await clientService.updatePreference(newPreference);
96
+
97
+ expect(spyOn).toHaveBeenCalledWith(newPreference);
98
+ expect(spyOn).toHaveBeenCalledTimes(1);
99
+ });
100
+ });
@@ -1,27 +1,33 @@
1
1
  import { DeepPartial } from 'utility-types';
2
2
 
3
+ import { MessageModel } from '@/database/client/models/message';
3
4
  import { UserModel } from '@/database/client/models/user';
4
- import { IUserService } from '@/services/user/type';
5
5
  import { GlobalSettings } from '@/types/settings';
6
- import { UserPreference } from '@/types/user';
6
+ import { UserInitializationState, UserPreference } from '@/types/user';
7
7
  import { AsyncLocalStorage } from '@/utils/localStorage';
8
8
 
9
- export interface UserConfig {
10
- avatar?: string;
11
- settings: DeepPartial<GlobalSettings>;
12
- uuid: string;
13
- }
9
+ import { IUserService } from './type';
14
10
 
15
11
  export class ClientService implements IUserService {
16
12
  private preferenceStorage: AsyncLocalStorage<UserPreference>;
13
+
17
14
  constructor() {
18
15
  this.preferenceStorage = new AsyncLocalStorage('LOBE_PREFERENCE');
19
16
  }
20
17
 
21
- getUserConfig = async () => {
18
+ async getUserState(): Promise<UserInitializationState> {
22
19
  const user = await UserModel.getUser();
23
- return user as unknown as UserConfig;
24
- };
20
+ const messageCount = await MessageModel.count();
21
+
22
+ return {
23
+ avatar: user.avatar,
24
+ canEnableTrace: messageCount >= 4,
25
+ isOnboard: true,
26
+ preference: await this.preferenceStorage.getFromLocalStorage(),
27
+ settings: user.settings as GlobalSettings,
28
+ userId: user.uuid,
29
+ };
30
+ }
25
31
 
26
32
  updateUserSettings = async (patch: DeepPartial<GlobalSettings>) => {
27
33
  return UserModel.updateSettings(patch);
@@ -35,10 +41,6 @@ export class ClientService implements IUserService {
35
41
  return UserModel.updateAvatar(avatar);
36
42
  }
37
43
 
38
- async getPreference() {
39
- return this.preferenceStorage.getFromLocalStorage();
40
- }
41
-
42
44
  async updatePreference(preference: UserPreference) {
43
45
  await this.preferenceStorage.saveToLocalStorage(preference);
44
46
  }
@@ -8,6 +8,4 @@
8
8
  // export const userService = ENABLED_SERVER_SERVICE ? new ServerService() : new ClientService();
9
9
  import { ClientService } from './client';
10
10
 
11
- export type { UserConfig } from './client';
12
-
13
11
  export const userService = new ClientService();
@@ -1,12 +1,10 @@
1
1
  import { DeepPartial } from 'utility-types';
2
2
 
3
- import { UserConfig } from '@/services/user/client';
4
3
  import { GlobalSettings } from '@/types/settings';
5
- import { UserPreference } from '@/types/user';
4
+ import { UserInitializationState, UserPreference } from '@/types/user';
6
5
 
7
6
  export interface IUserService {
8
- getPreference: () => Promise<UserPreference>;
9
- getUserConfig: () => Promise<UserConfig>;
7
+ getUserState: () => Promise<UserInitializationState>;
10
8
  resetUserSettings: () => Promise<any>;
11
9
  updateAvatar: (avatar: string) => Promise<any>;
12
10
  updatePreference: (preference: UserPreference) => Promise<any>;
@@ -1,13 +1,22 @@
1
1
  import { UserAuthState, initialAuthState } from './slices/auth/initialState';
2
+ import { CommonState, initialCommonState } from './slices/common/initialState';
3
+ import { ModelListState, initialModelListState } from './slices/modelList/initialState';
2
4
  import { UserPreferenceState, initialPreferenceState } from './slices/preference/initialState';
3
5
  import { UserSettingsState, initialSettingsState } from './slices/settings/initialState';
4
6
  import { UserSyncState, initialSyncState } from './slices/sync/initialState';
5
7
 
6
- export type UserState = UserSyncState & UserSettingsState & UserPreferenceState & UserAuthState;
8
+ export type UserState = UserSyncState &
9
+ UserSettingsState &
10
+ UserPreferenceState &
11
+ UserAuthState &
12
+ ModelListState &
13
+ CommonState;
7
14
 
8
15
  export const initialState: UserState = {
9
16
  ...initialSyncState,
10
17
  ...initialSettingsState,
11
18
  ...initialPreferenceState,
12
19
  ...initialAuthState,
20
+ ...initialCommonState,
21
+ ...initialModelListState,
13
22
  };
@@ -1,9 +1,5 @@
1
1
  export { authSelectors, userProfileSelectors } from './slices/auth/selectors';
2
+ export { modelConfigSelectors, modelProviderSelectors } from './slices/modelList/selectors';
2
3
  export { preferenceSelectors } from './slices/preference/selectors';
3
- export {
4
- modelConfigSelectors,
5
- modelProviderSelectors,
6
- settingsSelectors,
7
- syncSettingsSelectors,
8
- systemAgentSelectors,
9
- } from './slices/settings/selectors';
4
+ export { settingsSelectors, systemAgentSelectors } from './slices/settings/selectors';
5
+ export { syncSettingsSelectors } from './slices/sync/selectors';
@@ -9,10 +9,6 @@ import { switchLang } from '@/utils/client/switchLang';
9
9
 
10
10
  vi.mock('zustand/traditional');
11
11
 
12
- vi.mock('@/utils/client/switchLang', () => ({
13
- switchLang: vi.fn(),
14
- }));
15
-
16
12
  vi.mock('swr', async (importOriginal) => {
17
13
  const modules = await importOriginal();
18
14
  return {
@@ -54,93 +50,15 @@ vi.mock('next-auth/react', async () => {
54
50
  });
55
51
 
56
52
  describe('createAuthSlice', () => {
57
- describe('refreshUserConfig', () => {
53
+ describe('refreshUserState', () => {
58
54
  it('should refresh user config', async () => {
59
55
  const { result } = renderHook(() => useUserStore());
60
56
 
61
57
  await act(async () => {
62
- await result.current.refreshUserConfig();
58
+ await result.current.refreshUserState();
63
59
  });
64
60
 
65
- expect(mutate).toHaveBeenCalledWith(['fetchUserConfig', true]);
66
- });
67
- });
68
-
69
- describe('useFetchUserConfig', () => {
70
- it('should not fetch user config if initServer is false', async () => {
71
- const mockUserConfig: any = undefined; // 模拟未初始化服务器的情况
72
- vi.spyOn(userService, 'getUserConfig').mockResolvedValueOnce(mockUserConfig);
73
-
74
- const { result } = renderHook(() => useUserStore().useFetchUserConfig(false), {
75
- wrapper: withSWR,
76
- });
77
-
78
- // 因为 initServer 为 false,所以不会触发 getUserConfig 的调用
79
- expect(userService.getUserConfig).not.toHaveBeenCalled();
80
- // 确保状态未改变
81
- expect(result.current.data).toBeUndefined();
82
- });
83
-
84
- it('should fetch user config correctly when initServer is true', async () => {
85
- const mockUserConfig: any = {
86
- avatar: 'new-avatar-url',
87
- settings: {
88
- language: 'en',
89
- },
90
- };
91
- vi.spyOn(userService, 'getUserConfig').mockResolvedValueOnce(mockUserConfig);
92
-
93
- const { result } = renderHook(() => useUserStore().useFetchUserConfig(true), {
94
- wrapper: withSWR,
95
- });
96
-
97
- // 等待 SWR 完成数据获取
98
- await waitFor(() => expect(result.current.data).toEqual(mockUserConfig));
99
-
100
- // 验证状态是否正确更新
101
- expect(useUserStore.getState().avatar).toBe(mockUserConfig.avatar);
102
- expect(useUserStore.getState().settings).toEqual(mockUserConfig.settings);
103
-
104
- // 验证是否正确处理了语言设置
105
- expect(switchLang).not.toHaveBeenCalledWith('auto');
106
- });
107
- it('should call switch language when language is auto', async () => {
108
- const mockUserConfig: any = {
109
- avatar: 'new-avatar-url',
110
- settings: {
111
- language: 'auto',
112
- },
113
- };
114
- vi.spyOn(userService, 'getUserConfig').mockResolvedValueOnce(mockUserConfig);
115
-
116
- const { result } = renderHook(() => useUserStore().useFetchUserConfig(true), {
117
- wrapper: withSWR,
118
- });
119
-
120
- // 等待 SWR 完成数据获取
121
- await waitFor(() => expect(result.current.data).toEqual(mockUserConfig));
122
-
123
- // 验证状态是否正确更新
124
- expect(useUserStore.getState().avatar).toBe(mockUserConfig.avatar);
125
- expect(useUserStore.getState().settings).toEqual(mockUserConfig.settings);
126
-
127
- // 验证是否正确处理了语言设置
128
- expect(switchLang).toHaveBeenCalledWith('auto');
129
- });
130
-
131
- it('should handle the case when user config is null', async () => {
132
- vi.spyOn(userService, 'getUserConfig').mockResolvedValueOnce(null as any);
133
-
134
- const { result } = renderHook(() => useUserStore().useFetchUserConfig(true), {
135
- wrapper: withSWR,
136
- });
137
-
138
- // 等待 SWR 完成数据获取
139
- await waitFor(() => expect(result.current.data).toBeNull());
140
-
141
- // 验证状态未被错误更新
142
- expect(useUserStore.getState().avatar).toBeUndefined();
143
- expect(useUserStore.getState().settings).toEqual({});
61
+ expect(mutate).toHaveBeenCalledWith('initUserState');
144
62
  });
145
63
  });
146
64
 
@@ -174,7 +92,7 @@ describe('createAuthSlice', () => {
174
92
  });
175
93
 
176
94
  it('should call next-auth signOut when NextAuth is enabled', async () => {
177
- useUserStore.setState({ enabledNextAuth: () => true });
95
+ useUserStore.setState({ enabledNextAuth: true });
178
96
 
179
97
  const { result } = renderHook(() => useUserStore());
180
98
 
@@ -228,7 +146,7 @@ describe('createAuthSlice', () => {
228
146
  });
229
147
 
230
148
  it('should call next-auth signIn when NextAuth is enabled', async () => {
231
- useUserStore.setState({ enabledNextAuth: () => true });
149
+ useUserStore.setState({ enabledNextAuth: true });
232
150
 
233
151
  const { result } = renderHook(() => useUserStore());
234
152
 
@@ -1,21 +1,11 @@
1
- import useSWR, { SWRResponse, mutate } from 'swr';
2
1
  import { StateCreator } from 'zustand/vanilla';
3
2
 
4
3
  import { enableClerk } from '@/const/auth';
5
- import { UserConfig, userService } from '@/services/user';
6
- import { switchLang } from '@/utils/client/switchLang';
7
- import { setNamespace } from '@/utils/storeDebug';
8
4
 
9
5
  import { UserStore } from '../../store';
10
- import { settingsSelectors } from '../settings/selectors';
11
-
12
- const n = setNamespace('auth');
13
- const USER_CONFIG_FETCH_KEY = 'fetchUserConfig';
14
6
 
15
7
  export interface UserAuthAction {
16
8
  enableAuth: () => boolean;
17
- enabledNextAuth: () => boolean;
18
- getUserConfig: () => void;
19
9
  /**
20
10
  * universal logout method
21
11
  */
@@ -25,9 +15,6 @@ export interface UserAuthAction {
25
15
  */
26
16
  openLogin: () => Promise<void>;
27
17
  openUserProfile: () => Promise<void>;
28
-
29
- refreshUserConfig: () => Promise<void>;
30
- useFetchUserConfig: (initServer: boolean) => SWRResponse<UserConfig | undefined>;
31
18
  }
32
19
 
33
20
  export const createAuthSlice: StateCreator<
@@ -37,13 +24,7 @@ export const createAuthSlice: StateCreator<
37
24
  UserAuthAction
38
25
  > = (set, get) => ({
39
26
  enableAuth: () => {
40
- return enableClerk || get()?.enabledNextAuth();
41
- },
42
- enabledNextAuth: () => {
43
- return !!get()?.serverConfig.enabledOAuthSSO;
44
- },
45
- getUserConfig: () => {
46
- console.log(n('userconfig'));
27
+ return enableClerk || get()?.enabledNextAuth || false;
47
28
  },
48
29
  logout: async () => {
49
30
  if (enableClerk) {
@@ -52,7 +33,7 @@ export const createAuthSlice: StateCreator<
52
33
  return;
53
34
  }
54
35
 
55
- const enableNextAuth = get().enabledNextAuth();
36
+ const enableNextAuth = get().enabledNextAuth;
56
37
  if (enableNextAuth) {
57
38
  const { signOut } = await import('next-auth/react');
58
39
  signOut();
@@ -60,14 +41,12 @@ export const createAuthSlice: StateCreator<
60
41
  },
61
42
  openLogin: async () => {
62
43
  if (enableClerk) {
63
- console.log('fallbackRedirectUrl:', location.toString());
64
-
65
44
  get().clerkSignIn?.({ fallbackRedirectUrl: location.toString() });
66
45
 
67
46
  return;
68
47
  }
69
48
 
70
- const enableNextAuth = get().enabledNextAuth();
49
+ const enableNextAuth = get().enabledNextAuth;
71
50
  if (enableNextAuth) {
72
51
  const { signIn } = await import('next-auth/react');
73
52
  signIn();
@@ -81,38 +60,4 @@ export const createAuthSlice: StateCreator<
81
60
  return;
82
61
  }
83
62
  },
84
- refreshUserConfig: async () => {
85
- await mutate([USER_CONFIG_FETCH_KEY, true]);
86
-
87
- // when get the user config ,refresh the model provider list to the latest
88
- get().refreshModelProviderList();
89
- },
90
- useFetchUserConfig: (initServer) =>
91
- useSWR<UserConfig | undefined>(
92
- [USER_CONFIG_FETCH_KEY, initServer],
93
- async () => {
94
- if (!initServer) return;
95
- return userService.getUserConfig();
96
- },
97
- {
98
- onSuccess: (data) => {
99
- if (!data) return;
100
-
101
- set(
102
- { avatar: data.avatar, settings: data.settings, userId: data.uuid },
103
- false,
104
- n('fetchUserConfig', data),
105
- );
106
-
107
- // when get the user config ,refresh the model provider list to the latest
108
- get().refreshDefaultModelProviderList({ trigger: 'fetchUserConfig' });
109
-
110
- const { language } = settingsSelectors.currentSettings(get());
111
- if (language === 'auto') {
112
- switchLang('auto');
113
- }
114
- },
115
- revalidateOnFocus: false,
116
- },
117
- ),
118
63
  });
@@ -15,12 +15,13 @@ export interface UserAuthState {
15
15
  * @deprecated
16
16
  */
17
17
  avatar?: string;
18
-
19
18
  clerkOpenUserProfile?: (props?: UserProfileProps) => void;
19
+
20
20
  clerkSession?: ActiveSessionResource;
21
21
  clerkSignIn?: (props?: SignInProps) => void;
22
22
  clerkSignOut?: SignOut;
23
23
  clerkUser?: UserResource;
24
+ enabledNextAuth?: boolean;
24
25
 
25
26
  isLoaded?: boolean;
26
27
  isSignedIn?: boolean;