@lobehub/chat 0.161.8 → 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 (74) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/docs/self-hosting/advanced/feature-flags.mdx +45 -0
  3. package/docs/self-hosting/advanced/feature-flags.zh-CN.mdx +42 -0
  4. package/docs/self-hosting/environment-variables/basic.mdx +11 -2
  5. package/docs/self-hosting/environment-variables/basic.zh-CN.mdx +11 -2
  6. package/package.json +1 -1
  7. package/src/app/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/Footer/DragUpload.tsx +92 -42
  8. package/src/app/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/Footer/index.tsx +2 -2
  9. package/src/app/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Files.tsx +4 -3
  10. package/src/app/(main)/settings/llm/components/ProviderConfig/index.tsx +5 -2
  11. package/src/app/(main)/settings/llm/components/ProviderModelList/CustomModelOption.tsx +6 -7
  12. package/src/app/(main)/settings/llm/components/ProviderModelList/{ModelConfigModal.tsx → ModelConfigModal/Form.tsx} +19 -63
  13. package/src/app/(main)/settings/llm/components/ProviderModelList/ModelConfigModal/index.tsx +78 -0
  14. package/src/app/(main)/settings/llm/components/ProviderModelList/Option.tsx +35 -11
  15. package/src/app/(main)/settings/llm/components/ProviderModelList/index.tsx +15 -18
  16. package/src/app/(main)/settings/system-agent/features/Translation.tsx +0 -2
  17. package/src/components/FileList/ImageFileItem.tsx +1 -1
  18. package/src/components/ModelProviderIcon/index.tsx +2 -2
  19. package/src/components/ModelSelect/index.tsx +5 -14
  20. package/src/const/meta.ts +1 -2
  21. package/src/features/User/UserPanel/PanelContent.tsx +1 -1
  22. package/src/hooks/useSyncData.ts +3 -1
  23. package/src/layout/AuthProvider/Clerk/useAppearance.ts +1 -1
  24. package/src/layout/GlobalProvider/StoreInitialization.tsx +17 -9
  25. package/src/layout/GlobalProvider/index.tsx +1 -1
  26. package/src/locales/default/components.ts +1 -0
  27. package/src/services/message/client.test.ts +0 -24
  28. package/src/services/message/client.ts +0 -5
  29. package/src/services/message/type.ts +0 -1
  30. package/src/services/user/client.test.ts +100 -0
  31. package/src/services/user/client.ts +16 -14
  32. package/src/services/user/index.ts +0 -2
  33. package/src/services/user/type.ts +2 -4
  34. package/src/store/user/initialState.ts +10 -1
  35. package/src/store/user/selectors.ts +3 -7
  36. package/src/store/user/slices/auth/action.test.ts +5 -87
  37. package/src/store/user/slices/auth/action.ts +3 -58
  38. package/src/store/user/slices/auth/initialState.ts +2 -1
  39. package/src/store/user/slices/common/action.test.ts +196 -20
  40. package/src/store/user/slices/common/action.ts +55 -26
  41. package/src/store/user/slices/common/initialState.ts +9 -0
  42. package/src/store/user/slices/modelList/action.test.ts +363 -0
  43. package/src/store/user/slices/{settings/actions/llm.ts → modelList/action.ts} +66 -60
  44. package/src/store/user/slices/modelList/initialState.ts +15 -0
  45. package/src/store/user/slices/modelList/selectors/index.ts +2 -0
  46. package/src/store/user/slices/{settings → modelList}/selectors/modelConfig.test.ts +3 -2
  47. package/src/store/user/slices/{settings → modelList}/selectors/modelConfig.ts +1 -1
  48. package/src/store/user/slices/{settings → modelList}/selectors/modelProvider.test.ts +7 -7
  49. package/src/store/user/slices/{settings → modelList}/selectors/modelProvider.ts +2 -4
  50. package/src/store/user/slices/preference/action.test.ts +0 -52
  51. package/src/store/user/slices/preference/action.ts +1 -17
  52. package/src/store/user/slices/preference/initialState.ts +0 -5
  53. package/src/store/user/slices/preference/selectors.test.ts +2 -2
  54. package/src/store/user/slices/preference/selectors.ts +1 -1
  55. package/src/store/user/slices/settings/{actions/general.ts → action.ts} +5 -5
  56. package/src/store/user/slices/settings/initialState.ts +0 -12
  57. package/src/store/user/slices/settings/selectors/index.ts +0 -3
  58. package/src/store/user/slices/sync/action.test.ts +19 -5
  59. package/src/store/user/slices/sync/action.ts +9 -6
  60. package/src/store/user/slices/{settings/selectors/sync.ts → sync/selectors.ts} +2 -2
  61. package/src/store/user/store.ts +5 -2
  62. package/src/styles/antdOverride.ts +6 -0
  63. package/src/types/serverConfig.ts +3 -1
  64. package/src/types/user/index.ts +13 -0
  65. package/src/utils/parseModels.test.ts +121 -1
  66. package/src/utils/parseModels.ts +9 -4
  67. package/src/store/user/slices/settings/actions/index.ts +0 -18
  68. package/src/store/user/slices/settings/actions/llm.test.ts +0 -136
  69. /package/src/app/(main)/settings/llm/components/ProviderModelList/{MaxTokenSlider.tsx → ModelConfigModal/MaxTokenSlider.tsx} +0 -0
  70. /package/src/store/user/slices/{settings → modelList}/reducers/customModelCard.test.ts +0 -0
  71. /package/src/store/user/slices/{settings → modelList}/reducers/customModelCard.ts +0 -0
  72. /package/src/store/user/slices/settings/{actions/general.test.ts → action.test.ts} +0 -0
  73. /package/src/store/user/slices/settings/selectors/__snapshots__/{selectors.test.ts.snap → settings.test.ts.snap} +0 -0
  74. /package/src/store/user/slices/settings/selectors/{selectors.test.ts → settings.test.ts} +0 -0
@@ -40,56 +40,4 @@ describe('createPreferenceSlice', () => {
40
40
  expect(result.current.preference.hideSyncAlert).toEqual(true);
41
41
  });
42
42
  });
43
-
44
- describe('useInitPreference', () => {
45
- it('should return false when userId is empty', async () => {
46
- const { result } = renderHook(() => useUserStore());
47
-
48
- vi.spyOn(userService, 'getPreference').mockResolvedValueOnce({} as any);
49
-
50
- const { result: prefernce } = renderHook(() => result.current.useInitPreference(), {
51
- wrapper: withSWR,
52
- });
53
-
54
- await waitFor(() => {
55
- expect(prefernce.current.data).toEqual({});
56
- expect(result.current.isPreferenceInit).toBeTruthy();
57
- });
58
- });
59
- it('should return default preference when local storage is empty', async () => {
60
- const { result } = renderHook(() => useUserStore());
61
-
62
- vi.spyOn(userService, 'getPreference').mockResolvedValueOnce({} as any);
63
-
64
- renderHook(() => result.current.useInitPreference(), {
65
- wrapper: withSWR,
66
- });
67
-
68
- await waitFor(() => {
69
- expect(result.current.preference).toEqual(DEFAULT_PREFERENCE);
70
- expect(result.current.isPreferenceInit).toBeTruthy();
71
- });
72
- });
73
-
74
- it('should return saved preference when local storage has data', async () => {
75
- const { result } = renderHook(() => useUserStore());
76
- const savedPreference: UserPreference = {
77
- ...DEFAULT_PREFERENCE,
78
- hideSyncAlert: true,
79
- guide: { topic: false, moveSettingsToAvatar: true },
80
- };
81
-
82
- vi.spyOn(userService, 'getPreference').mockResolvedValueOnce(savedPreference);
83
-
84
- const { result: prefernce } = renderHook(() => result.current.useInitPreference(), {
85
- wrapper: withSWR,
86
- });
87
-
88
- await waitFor(() => {
89
- expect(prefernce.current.data).toEqual(savedPreference);
90
- expect(result.current.isPreferenceInit).toBeTruthy();
91
- expect(result.current.preference).toEqual(savedPreference);
92
- });
93
- });
94
- });
95
43
  });
@@ -1,8 +1,5 @@
1
- import { SWRResponse } from 'swr';
2
1
  import type { StateCreator } from 'zustand/vanilla';
3
2
 
4
- import { DEFAULT_PREFERENCE } from '@/const/user';
5
- import { useClientDataSWR } from '@/libs/swr';
6
3
  import { userService } from '@/services/user';
7
4
  import type { UserStore } from '@/store/user';
8
5
  import { UserGuide, UserPreference } from '@/types/user';
@@ -14,7 +11,6 @@ const n = setNamespace('preference');
14
11
  export interface PreferenceAction {
15
12
  updateGuideState: (guide: Partial<UserGuide>) => Promise<void>;
16
13
  updatePreference: (preference: Partial<UserPreference>, action?: any) => Promise<void>;
17
- useInitPreference: () => SWRResponse;
18
14
  }
19
15
 
20
16
  export const createPreferenceSlice: StateCreator<
@@ -28,6 +24,7 @@ export const createPreferenceSlice: StateCreator<
28
24
  const nextGuide = merge(get().preference.guide, guide);
29
25
  await updatePreference({ guide: nextGuide });
30
26
  },
27
+
31
28
  updatePreference: async (preference, action) => {
32
29
  const nextPreference = merge(get().preference, preference);
33
30
 
@@ -35,17 +32,4 @@ export const createPreferenceSlice: StateCreator<
35
32
 
36
33
  await userService.updatePreference(nextPreference);
37
34
  },
38
-
39
- useInitPreference: () =>
40
- useClientDataSWR<UserPreference>('initUserPreference', userService.getPreference, {
41
- onSuccess: (preference) => {
42
- const isEmpty = Object.keys(preference).length === 0;
43
-
44
- set(
45
- { isPreferenceInit: true, preference: isEmpty ? DEFAULT_PREFERENCE : preference },
46
- false,
47
- n('initPreference'),
48
- );
49
- },
50
- }),
51
35
  });
@@ -1,18 +1,13 @@
1
1
  import { DEFAULT_PREFERENCE } from '@/const/user';
2
2
  import { UserPreference } from '@/types/user';
3
- import { AsyncLocalStorage } from '@/utils/localStorage';
4
3
 
5
4
  export interface UserPreferenceState {
6
- isPreferenceInit: boolean;
7
5
  /**
8
6
  * the user preference, which only store in local storage
9
7
  */
10
8
  preference: UserPreference;
11
- preferenceStorage: AsyncLocalStorage<UserPreference>;
12
9
  }
13
10
 
14
11
  export const initialPreferenceState: UserPreferenceState = {
15
- isPreferenceInit: false,
16
12
  preference: DEFAULT_PREFERENCE,
17
- preferenceStorage: new AsyncLocalStorage('LOBE_PREFERENCE'),
18
13
  };
@@ -72,10 +72,10 @@ describe('preferenceSelectors', () => {
72
72
 
73
73
  describe('isPreferenceInit', () => {
74
74
  it('should return the value of isPreferenceInit state', () => {
75
- store.isPreferenceInit = true;
75
+ store.isUserStateInit = true;
76
76
  expect(preferenceSelectors.isPreferenceInit(store)).toBe(true);
77
77
 
78
- store.isPreferenceInit = false;
78
+ store.isUserStateInit = false;
79
79
  expect(preferenceSelectors.isPreferenceInit(store)).toBe(false);
80
80
  });
81
81
  });
@@ -8,7 +8,7 @@ const hideSyncAlert = (s: UserStore) => s.preference.hideSyncAlert;
8
8
 
9
9
  const hideSettingsMoveGuide = (s: UserStore) => s.preference.guide?.moveSettingsToAvatar;
10
10
 
11
- const isPreferenceInit = (s: UserStore) => s.isPreferenceInit;
11
+ const isPreferenceInit = (s: UserStore) => s.isUserStateInit;
12
12
 
13
13
  export const preferenceSelectors = {
14
14
  hideSettingsMoveGuide,
@@ -12,7 +12,7 @@ import { switchLang } from '@/utils/client/switchLang';
12
12
  import { difference } from '@/utils/difference';
13
13
  import { merge } from '@/utils/merge';
14
14
 
15
- export interface GeneralSettingsAction {
15
+ export interface UserSettingsAction {
16
16
  importAppSettings: (settings: GlobalSettings) => Promise<void>;
17
17
  resetSettings: () => Promise<void>;
18
18
  setSettings: (settings: DeepPartial<GlobalSettings>) => Promise<void>;
@@ -22,11 +22,11 @@ export interface GeneralSettingsAction {
22
22
  updateDefaultAgent: (agent: DeepPartial<LobeAgentSettings>) => Promise<void>;
23
23
  }
24
24
 
25
- export const generalSettingsSlice: StateCreator<
25
+ export const createSettingsSlice: StateCreator<
26
26
  UserStore,
27
27
  [['zustand/devtools', never]],
28
28
  [],
29
- GeneralSettingsAction
29
+ UserSettingsAction
30
30
  > = (set, get) => ({
31
31
  importAppSettings: async (importAppSettings) => {
32
32
  const { setSettings } = get();
@@ -37,7 +37,7 @@ export const generalSettingsSlice: StateCreator<
37
37
  },
38
38
  resetSettings: async () => {
39
39
  await userService.resetUserSettings();
40
- await get().refreshUserConfig();
40
+ await get().refreshUserState();
41
41
  },
42
42
  setSettings: async (settings) => {
43
43
  const { settings: prevSetting, defaultSettings } = get();
@@ -49,7 +49,7 @@ export const generalSettingsSlice: StateCreator<
49
49
  const diffs = difference(nextSettings, defaultSettings);
50
50
 
51
51
  await userService.updateUserSettings(diffs);
52
- await get().refreshUserConfig();
52
+ await get().refreshUserState();
53
53
  },
54
54
  setTranslationSystemAgent: async (provider, model) => {
55
55
  await get().setSettings({
@@ -1,26 +1,14 @@
1
1
  import { DeepPartial } from 'utility-types';
2
2
 
3
- import { DEFAULT_MODEL_PROVIDER_LIST } from '@/config/modelProviders';
4
3
  import { DEFAULT_SETTINGS } from '@/const/settings';
5
- import { ModelProviderCard } from '@/types/llm';
6
- import { GlobalServerConfig } from '@/types/serverConfig';
7
4
  import { GlobalSettings } from '@/types/settings';
8
5
 
9
6
  export interface UserSettingsState {
10
- defaultModelProviderList: ModelProviderCard[];
11
7
  defaultSettings: GlobalSettings;
12
- editingCustomCardModel?: { id: string; provider: string } | undefined;
13
- modelProviderList: ModelProviderCard[];
14
- serverConfig: GlobalServerConfig;
15
8
  settings: DeepPartial<GlobalSettings>;
16
9
  }
17
10
 
18
11
  export const initialSettingsState: UserSettingsState = {
19
- defaultModelProviderList: DEFAULT_MODEL_PROVIDER_LIST,
20
12
  defaultSettings: DEFAULT_SETTINGS,
21
- modelProviderList: DEFAULT_MODEL_PROVIDER_LIST,
22
- serverConfig: {
23
- telemetry: {},
24
- },
25
13
  settings: {},
26
14
  };
@@ -1,5 +1,2 @@
1
- export { modelConfigSelectors } from './modelConfig';
2
- export { modelProviderSelectors } from './modelProvider';
3
1
  export { settingsSelectors } from './settings';
4
- export { syncSettingsSelectors } from './sync';
5
2
  export { systemAgentSelectors } from './systemAgent';
@@ -107,9 +107,17 @@ describe('createSyncSlice', () => {
107
107
 
108
108
  describe('useEnabledSync', () => {
109
109
  it('should return false when userId is empty', async () => {
110
- const { result } = renderHook(() => useUserStore().useEnabledSync(true, undefined, vi.fn()), {
111
- wrapper: withSWR,
112
- });
110
+ const { result } = renderHook(
111
+ () =>
112
+ useUserStore().useEnabledSync(true, {
113
+ userEnableSync: true,
114
+ userId: undefined,
115
+ onEvent: vi.fn(),
116
+ }),
117
+ {
118
+ wrapper: withSWR,
119
+ },
120
+ );
113
121
 
114
122
  await waitFor(() => expect(result.current.data).toBe(false));
115
123
  });
@@ -118,7 +126,13 @@ describe('createSyncSlice', () => {
118
126
  const disableSyncSpy = vi.spyOn(syncService, 'disableSync').mockResolvedValueOnce(false);
119
127
 
120
128
  const { result } = renderHook(
121
- () => useUserStore().useEnabledSync(false, 'user-id', vi.fn()),
129
+ () =>
130
+ useUserStore().useEnabledSync(true, {
131
+ userEnableSync: false,
132
+ userId: 'user-id',
133
+ onEvent: vi.fn(),
134
+ }),
135
+
122
136
  { wrapper: withSWR },
123
137
  );
124
138
 
@@ -137,7 +151,7 @@ describe('createSyncSlice', () => {
137
151
  result.current.triggerEnableSync = triggerEnableSyncSpy;
138
152
 
139
153
  const { result: swrResult } = renderHook(
140
- () => result.current.useEnabledSync(true, userId, onEvent),
154
+ () => result.current.useEnabledSync(true, { userEnableSync: true, userId, onEvent }),
141
155
  {
142
156
  wrapper: withSWR,
143
157
  },
@@ -8,7 +8,7 @@ import { browserInfo } from '@/utils/platform';
8
8
  import { setNamespace } from '@/utils/storeDebug';
9
9
 
10
10
  import { userProfileSelectors } from '../auth/selectors';
11
- import { syncSettingsSelectors } from '../settings/selectors';
11
+ import { syncSettingsSelectors } from './selectors';
12
12
 
13
13
  const n = setNamespace('sync');
14
14
 
@@ -19,9 +19,12 @@ export interface SyncAction {
19
19
  refreshConnection: (onEvent: OnSyncEvent) => Promise<void>;
20
20
  triggerEnableSync: (userId: string, onEvent: OnSyncEvent) => Promise<boolean>;
21
21
  useEnabledSync: (
22
- userEnableSync: boolean,
23
- userId: string | undefined,
24
- onEvent: OnSyncEvent,
22
+ systemEnable: boolean | undefined,
23
+ params: {
24
+ onEvent: OnSyncEvent;
25
+ userEnableSync: boolean;
26
+ userId: string | undefined;
27
+ },
25
28
  ) => SWRResponse;
26
29
  }
27
30
 
@@ -72,9 +75,9 @@ export const createSyncSlice: StateCreator<
72
75
  });
73
76
  },
74
77
 
75
- useEnabledSync: (userEnableSync, userId, onEvent) =>
78
+ useEnabledSync: (systemEnable, { userEnableSync, userId, onEvent }) =>
76
79
  useSWR<boolean>(
77
- ['enableSync', userEnableSync, userId],
80
+ systemEnable ? ['enableSync', userEnableSync, userId] : null,
78
81
  async () => {
79
82
  // if user don't enable sync or no userId ,don't start sync
80
83
  if (!userId) return false;
@@ -1,5 +1,5 @@
1
- import { UserStore } from '../../../store';
2
- import { currentSettings } from './settings';
1
+ import { UserStore } from '../../store';
2
+ import { currentSettings } from '../settings/selectors/settings';
3
3
 
4
4
  const webrtcConfig = (s: UserStore) => currentSettings(s).sync.webrtc;
5
5
  const webrtcChannelName = (s: UserStore) => webrtcConfig(s).channelName;
@@ -8,16 +8,18 @@ import { isDev } from '@/utils/env';
8
8
  import { type UserState, initialState } from './initialState';
9
9
  import { type UserAuthAction, createAuthSlice } from './slices/auth/action';
10
10
  import { type CommonAction, createCommonSlice } from './slices/common/action';
11
+ import { type ModelListAction, createModelListSlice } from './slices/modelList/action';
11
12
  import { type PreferenceAction, createPreferenceSlice } from './slices/preference/action';
12
- import { type SettingsAction, createSettingsSlice } from './slices/settings/actions';
13
+ import { type UserSettingsAction, createSettingsSlice } from './slices/settings/action';
13
14
  import { type SyncAction, createSyncSlice } from './slices/sync/action';
14
15
 
15
16
  // =============== 聚合 createStoreFn ============ //
16
17
 
17
18
  export type UserStore = SyncAction &
18
19
  UserState &
19
- SettingsAction &
20
+ UserSettingsAction &
20
21
  PreferenceAction &
22
+ ModelListAction &
21
23
  UserAuthAction &
22
24
  CommonAction;
23
25
 
@@ -28,6 +30,7 @@ const createStore: StateCreator<UserStore, [['zustand/devtools', never]]> = (...
28
30
  ...createPreferenceSlice(...parameters),
29
31
  ...createAuthSlice(...parameters),
30
32
  ...createCommonSlice(...parameters),
33
+ ...createModelListSlice(...parameters),
31
34
  });
32
35
 
33
36
  // =============== 实装 useStore ============ //
@@ -9,4 +9,10 @@ export default ({ token }: { prefixCls: string; token: Theme }) => css`
9
9
  border: 1px solid ${token.colorBorder};
10
10
  box-shadow: ${token.boxShadow};
11
11
  }
12
+
13
+ .${token.prefixCls}-menu-item-selected {
14
+ .${token.prefixCls}-menu-title-content {
15
+ color: ${token.colorText};
16
+ }
17
+ }
12
18
  `;
@@ -13,12 +13,14 @@ export interface ServerModelProviderConfig {
13
13
  serverModelCards?: ChatModelCard[];
14
14
  }
15
15
 
16
+ export type ServerLanguageModel = Partial<Record<GlobalLLMProviderKey, ServerModelProviderConfig>>;
17
+
16
18
  export interface GlobalServerConfig {
17
19
  defaultAgent?: DeepPartial<GlobalDefaultAgent>;
18
20
  enableUploadFileToServer?: boolean;
19
21
  enabledAccessCode?: boolean;
20
22
  enabledOAuthSSO?: boolean;
21
- languageModel?: Partial<Record<GlobalLLMProviderKey, ServerModelProviderConfig>>;
23
+ languageModel?: ServerLanguageModel;
22
24
  telemetry: {
23
25
  langfuse?: boolean;
24
26
  };
@@ -1,3 +1,7 @@
1
+ import { DeepPartial } from 'utility-types';
2
+
3
+ import { GlobalSettings } from '@/types/settings';
4
+
1
5
  export interface LobeUser {
2
6
  avatar?: string;
3
7
  email?: string | null;
@@ -27,3 +31,12 @@ export interface UserPreference {
27
31
  */
28
32
  useCmdEnterToSend?: boolean;
29
33
  }
34
+
35
+ export interface UserInitializationState {
36
+ avatar?: string;
37
+ canEnableTrace?: boolean;
38
+ isOnboard?: boolean;
39
+ preference: UserPreference;
40
+ settings: DeepPartial<GlobalSettings>;
41
+ userId: string;
42
+ }
@@ -1,6 +1,9 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
 
3
- import { parseModelString } from './parseModels';
3
+ import { LOBE_DEFAULT_MODEL_LIST, OpenAIProviderCard } from '@/config/modelProviders';
4
+ import { ChatModelCard } from '@/types/llm';
5
+
6
+ import { parseModelString, transformToChatModelCards } from './parseModels';
4
7
 
5
8
  describe('parseModelString', () => {
6
9
  it('custom deletion, addition, and renaming of models', () => {
@@ -67,6 +70,29 @@ describe('parseModelString', () => {
67
70
  ]);
68
71
  });
69
72
 
73
+ it('should have file with builtin models like gpt-4-0125-preview', () => {
74
+ const result = parseModelString(
75
+ '-all,+gpt-4-0125-preview=ChatGPT-4<128000:fc:file>,+gpt-4-turbo-2024-04-09=ChatGPT-4 Vision<128000:fc:vision:file>',
76
+ );
77
+ expect(result.add).toEqual([
78
+ {
79
+ displayName: 'ChatGPT-4',
80
+ files: true,
81
+ functionCall: true,
82
+ id: 'gpt-4-0125-preview',
83
+ tokens: 128000,
84
+ },
85
+ {
86
+ displayName: 'ChatGPT-4 Vision',
87
+ files: true,
88
+ functionCall: true,
89
+ id: 'gpt-4-turbo-2024-04-09',
90
+ tokens: 128000,
91
+ vision: true,
92
+ },
93
+ ]);
94
+ });
95
+
70
96
  it('should handle empty extension capability value', () => {
71
97
  const result = parseModelString('model1<1024:>');
72
98
  expect(result.add[0]).toEqual({ id: 'model1', tokens: 1024 });
@@ -168,3 +194,97 @@ describe('parseModelString', () => {
168
194
  });
169
195
  });
170
196
  });
197
+
198
+ describe('transformToChatModelCards', () => {
199
+ const defaultChatModels: ChatModelCard[] = [
200
+ { id: 'model1', displayName: 'Model 1', enabled: true },
201
+ { id: 'model2', displayName: 'Model 2', enabled: false },
202
+ ];
203
+
204
+ it('should return undefined when modelString is empty', () => {
205
+ const result = transformToChatModelCards({
206
+ modelString: '',
207
+ defaultChatModels,
208
+ });
209
+ expect(result).toBeUndefined();
210
+ });
211
+
212
+ it('should remove all models when removeAll is true', () => {
213
+ const result = transformToChatModelCards({
214
+ modelString: '-all',
215
+ defaultChatModels,
216
+ });
217
+ expect(result).toEqual([]);
218
+ });
219
+
220
+ it('should remove specified models', () => {
221
+ const result = transformToChatModelCards({
222
+ modelString: '-model1',
223
+ defaultChatModels,
224
+ });
225
+ expect(result).toEqual([{ id: 'model2', displayName: 'Model 2', enabled: false }]);
226
+ });
227
+
228
+ it('should add a new known model', () => {
229
+ const knownModel = LOBE_DEFAULT_MODEL_LIST[0];
230
+ const result = transformToChatModelCards({
231
+ modelString: `${knownModel.id}`,
232
+ defaultChatModels,
233
+ });
234
+ expect(result).toContainEqual({
235
+ ...knownModel,
236
+ displayName: knownModel.displayName || knownModel.id,
237
+ enabled: true,
238
+ });
239
+ });
240
+
241
+ it('should update an existing known model', () => {
242
+ const knownModel = LOBE_DEFAULT_MODEL_LIST[0];
243
+ const result = transformToChatModelCards({
244
+ modelString: `+${knownModel.id}=Updated Model`,
245
+ defaultChatModels: [knownModel],
246
+ });
247
+ expect(result![0]).toEqual({ ...knownModel, displayName: 'Updated Model', enabled: true });
248
+ });
249
+
250
+ it('should add a new custom model', () => {
251
+ const result = transformToChatModelCards({
252
+ modelString: '+custom_model=Custom Model',
253
+ defaultChatModels,
254
+ });
255
+ expect(result).toContainEqual({
256
+ id: 'custom_model',
257
+ displayName: 'Custom Model',
258
+ enabled: true,
259
+ });
260
+ });
261
+
262
+ it('should have file with builtin models like gpt-4-0125-preview', () => {
263
+ const result = transformToChatModelCards({
264
+ modelString:
265
+ '-all,+gpt-4-0125-preview=ChatGPT-4<128000:fc:file>,+gpt-4-turbo-2024-04-09=ChatGPT-4 Vision<128000:fc:vision:file>',
266
+ defaultChatModels: OpenAIProviderCard.chatModels,
267
+ });
268
+
269
+ expect(result).toEqual([
270
+ {
271
+ displayName: 'ChatGPT-4',
272
+ files: true,
273
+ functionCall: true,
274
+ enabled: true,
275
+ id: 'gpt-4-0125-preview',
276
+ tokens: 128000,
277
+ },
278
+ {
279
+ description: 'GPT-4 Turbo 视觉版 (240409)',
280
+ displayName: 'ChatGPT-4 Vision',
281
+ files: true,
282
+ functionCall: true,
283
+ enabled: true,
284
+ id: 'gpt-4-turbo-2024-04-09',
285
+ tokens: 128000,
286
+ vision: true,
287
+ },
288
+ ]);
289
+ });
290
+ });
@@ -114,17 +114,22 @@ export const transformToChatModelCards = ({
114
114
 
115
115
  // if the model is known, update it based on the known model
116
116
  if (knownModel) {
117
- const modelInList = draft.find((model) => model.id === toAddModel.id);
117
+ const index = draft.findIndex((model) => model.id === toAddModel.id);
118
+ const modelInList = draft[index];
118
119
 
119
120
  // if the model is already in chatModels, update it
120
121
  if (modelInList) {
121
- // if (modelInList.hidden) delete modelInList.hidden;
122
- modelInList.enabled = true;
123
- if (toAddModel.displayName) modelInList.displayName = toAddModel.displayName;
122
+ draft[index] = {
123
+ ...modelInList,
124
+ ...toAddModel,
125
+ displayName: toAddModel.displayName || modelInList.displayName || modelInList.id,
126
+ enabled: true,
127
+ };
124
128
  } else {
125
129
  // if the model is not in chatModels, add it
126
130
  draft.push({
127
131
  ...knownModel,
132
+ ...toAddModel,
128
133
  displayName: toAddModel.displayName || knownModel.displayName || knownModel.id,
129
134
  enabled: true,
130
135
  });
@@ -1,18 +0,0 @@
1
- import type { StateCreator } from 'zustand/vanilla';
2
-
3
- import type { UserStore } from '@/store/user';
4
-
5
- import { GeneralSettingsAction, generalSettingsSlice } from './general';
6
- import { LLMSettingsAction, llmSettingsSlice } from './llm';
7
-
8
- export interface SettingsAction extends LLMSettingsAction, GeneralSettingsAction {}
9
-
10
- export const createSettingsSlice: StateCreator<
11
- UserStore,
12
- [['zustand/devtools', never]],
13
- [],
14
- SettingsAction
15
- > = (...params) => ({
16
- ...llmSettingsSlice(...params),
17
- ...generalSettingsSlice(...params),
18
- });