@lobehub/chat 0.148.3 → 0.148.4

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,31 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ### [Version 0.148.4](https://github.com/lobehub/lobe-chat/compare/v0.148.3...v0.148.4)
6
+
7
+ <sup>Released on **2024-04-21**</sup>
8
+
9
+ #### 🐛 Bug Fixes
10
+
11
+ - **misc**: Fix model list menu not display correctly.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### What's fixed
19
+
20
+ - **misc**: Fix model list menu not display correctly, closes [#2133](https://github.com/lobehub/lobe-chat/issues/2133) ([98c844b](https://github.com/lobehub/lobe-chat/commit/98c844b))
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
+
5
30
  ### [Version 0.148.3](https://github.com/lobehub/lobe-chat/compare/v0.148.2...v0.148.3)
6
31
 
7
32
  <sup>Released on **2024-04-21**</sup>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "0.148.3",
3
+ "version": "0.148.4",
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",
@@ -11,28 +11,32 @@ import { GlobalLLMProviderKey } from '@/types/settings';
11
11
 
12
12
  import CustomModelOption from './CustomModelOption';
13
13
 
14
- const OptionRender = memo<{ displayName: string; id: string; provider: GlobalLLMProviderKey }>(
15
- ({ displayName, id, provider }) => {
16
- const model = useGlobalStore((s) => modelProviderSelectors.getModelCardById(id)(s), isEqual);
14
+ interface OptionRenderProps {
15
+ displayName: string;
16
+ id: string;
17
+ isAzure?: boolean;
18
+ provider: GlobalLLMProviderKey;
19
+ }
20
+ const OptionRender = memo<OptionRenderProps>(({ displayName, id, provider, isAzure }) => {
21
+ const model = useGlobalStore((s) => modelProviderSelectors.getModelCardById(id)(s), isEqual);
17
22
 
18
- // if there is isCustom, it means it is a user defined custom model
19
- if (model?.isCustom) return <CustomModelOption id={id} provider={provider} />;
23
+ // if there is isCustom, it means it is a user defined custom model
24
+ if (model?.isCustom || isAzure) return <CustomModelOption id={id} provider={provider} />;
20
25
 
21
- return (
22
- <Flexbox align={'center'} gap={8} horizontal>
23
- <ModelIcon model={id} size={32} />
24
- <Flexbox>
25
- <Flexbox align={'center'} gap={8} horizontal>
26
- {displayName}
27
- <ModelInfoTags directionReverse placement={'top'} {...model!} />
28
- </Flexbox>
29
- <Typography.Text style={{ fontSize: 12 }} type={'secondary'}>
30
- {id}
31
- </Typography.Text>
26
+ return (
27
+ <Flexbox align={'center'} gap={8} horizontal>
28
+ <ModelIcon model={id} size={32} />
29
+ <Flexbox>
30
+ <Flexbox align={'center'} gap={8} horizontal>
31
+ {displayName}
32
+ <ModelInfoTags directionReverse placement={'top'} {...model!} />
32
33
  </Flexbox>
34
+ <Typography.Text style={{ fontSize: 12 }} type={'secondary'}>
35
+ {id}
36
+ </Typography.Text>
33
37
  </Flexbox>
34
- );
35
- },
36
- );
38
+ </Flexbox>
39
+ );
40
+ });
37
41
 
38
42
  export default OptionRender;
@@ -111,6 +111,7 @@ const ProviderModelListSelect = memo<CustomModelSelectProps>(
111
111
  <OptionRender
112
112
  displayName={label as string}
113
113
  id={value as string}
114
+ isAzure={showAzureDeployName}
114
115
  provider={provider}
115
116
  />
116
117
  );
@@ -0,0 +1,45 @@
1
+ 'use client';
2
+
3
+ import { useRouter } from 'next/navigation';
4
+ import { memo, useEffect } from 'react';
5
+ import { createStoreUpdater } from 'zustand-utils';
6
+
7
+ import { useIsMobile } from '@/hooks/useIsMobile';
8
+ import { useEnabledDataSync } from '@/hooks/useSyncData';
9
+ import { useGlobalStore } from '@/store/global';
10
+
11
+ const StoreInitialization = memo(() => {
12
+ const [useFetchServerConfig, useFetchUserConfig, useInitPreference] = useGlobalStore((s) => [
13
+ s.useFetchServerConfig,
14
+ s.useFetchUserConfig,
15
+ s.useInitPreference,
16
+ ]);
17
+ // init the system preference
18
+ useInitPreference();
19
+
20
+ const { isLoading } = useFetchServerConfig();
21
+ useFetchUserConfig(!isLoading);
22
+
23
+ useEnabledDataSync();
24
+
25
+ const useStoreUpdater = createStoreUpdater(useGlobalStore);
26
+
27
+ const mobile = useIsMobile();
28
+ const router = useRouter();
29
+
30
+ useStoreUpdater('isMobile', mobile);
31
+ useStoreUpdater('router', router);
32
+
33
+ useEffect(() => {
34
+ router.prefetch('/chat');
35
+ router.prefetch('/chat/settings');
36
+ router.prefetch('/market');
37
+ router.prefetch('/settings/common');
38
+ router.prefetch('/settings/agent');
39
+ router.prefetch('/settings/sync');
40
+ }, [router]);
41
+
42
+ return null;
43
+ });
44
+
45
+ export default StoreInitialization;
@@ -13,7 +13,7 @@ import { getAntdLocale } from '@/utils/locale';
13
13
 
14
14
  import AppTheme from './AppTheme';
15
15
  import Locale from './Locale';
16
- import StoreHydration from './StoreHydration';
16
+ import StoreInitialization from './StoreInitialization';
17
17
  import StyleRegistry from './StyleRegistry';
18
18
 
19
19
  let DebugUI: FC = () => null;
@@ -50,7 +50,7 @@ const GlobalLayout = async ({ children }: GlobalLayoutProps) => {
50
50
  defaultNeutralColor={neutralColor?.value as any}
51
51
  defaultPrimaryColor={primaryColor?.value as any}
52
52
  >
53
- <StoreHydration />
53
+ <StoreInitialization />
54
54
  {children}
55
55
  <DebugUI />
56
56
  </AppTheme>
@@ -147,7 +147,7 @@ export const createCommonSlice: StateCreator<
147
147
  },
148
148
  {
149
149
  onSuccess: (syncEnabled) => {
150
- set({ syncEnabled });
150
+ set({ syncEnabled }, false, n('useEnabledSync'));
151
151
  },
152
152
  revalidateOnFocus: false,
153
153
  },
@@ -165,7 +165,7 @@ export const createCommonSlice: StateCreator<
165
165
 
166
166
  set({ defaultSettings, serverConfig: data }, false, n('initGlobalConfig'));
167
167
 
168
- get().refreshDefaultModelProviderList();
168
+ get().refreshDefaultModelProviderList({ trigger: 'fetchServerConfig' });
169
169
  }
170
170
  },
171
171
  revalidateOnFocus: false,
@@ -188,7 +188,7 @@ export const createCommonSlice: StateCreator<
188
188
  );
189
189
 
190
190
  // when get the user config ,refresh the model provider list to the latest
191
- get().refreshModelProviderList();
191
+ get().refreshDefaultModelProviderList({ trigger: 'fetchUserConfig' });
192
192
 
193
193
  const { language } = settingsSelectors.currentSettings(get());
194
194
  if (language === 'auto') {
@@ -1,11 +1,13 @@
1
1
  import { produce } from 'immer';
2
+ import { SWRResponse } from 'swr';
2
3
  import type { StateCreator } from 'zustand/vanilla';
3
4
 
5
+ import { useClientDataSWR } from '@/libs/swr';
4
6
  import type { GlobalStore } from '@/store/global';
5
7
  import { merge } from '@/utils/merge';
6
8
  import { setNamespace } from '@/utils/storeDebug';
7
9
 
8
- import type { GlobalPreference, GlobalPreferenceState, Guide } from './initialState';
10
+ import type { GlobalPreference, Guide } from './initialState';
9
11
 
10
12
  const n = setNamespace('preference');
11
13
 
@@ -18,7 +20,8 @@ export interface PreferenceAction {
18
20
  toggleMobileTopic: (visible?: boolean) => void;
19
21
  toggleSystemRole: (visible?: boolean) => void;
20
22
  updateGuideState: (guide: Partial<Guide>) => void;
21
- updatePreference: (preference: Partial<GlobalPreference>, action?: string) => void;
23
+ updatePreference: (preference: Partial<GlobalPreference>, action?: any) => void;
24
+ useInitPreference: () => SWRResponse;
22
25
  }
23
26
 
24
27
  export const createPreferenceSlice: StateCreator<
@@ -31,7 +34,7 @@ export const createPreferenceSlice: StateCreator<
31
34
  const showChatSideBar =
32
35
  typeof newValue === 'boolean' ? newValue : !get().preference.showChatSideBar;
33
36
 
34
- get().updatePreference({ showChatSideBar }, n('toggleAgentPanel', newValue) as string);
37
+ get().updatePreference({ showChatSideBar }, n('toggleAgentPanel', newValue));
35
38
  },
36
39
  toggleExpandSessionGroup: (id, expand) => {
37
40
  const { preference } = get();
@@ -50,13 +53,13 @@ export const createPreferenceSlice: StateCreator<
50
53
  const mobileShowTopic =
51
54
  typeof newValue === 'boolean' ? newValue : !get().preference.mobileShowTopic;
52
55
 
53
- get().updatePreference({ mobileShowTopic }, n('toggleMobileTopic', newValue) as string);
56
+ get().updatePreference({ mobileShowTopic }, n('toggleMobileTopic', newValue));
54
57
  },
55
58
  toggleSystemRole: (newValue) => {
56
59
  const showSystemRole =
57
60
  typeof newValue === 'boolean' ? newValue : !get().preference.mobileShowTopic;
58
61
 
59
- get().updatePreference({ showSystemRole }, n('toggleMobileTopic', newValue) as string);
62
+ get().updatePreference({ showSystemRole }, n('toggleMobileTopic', newValue));
60
63
  },
61
64
  updateGuideState: (guide) => {
62
65
  const { updatePreference } = get();
@@ -64,12 +67,23 @@ export const createPreferenceSlice: StateCreator<
64
67
  updatePreference({ guide: nextGuide });
65
68
  },
66
69
  updatePreference: (preference, action) => {
67
- set(
68
- produce((draft: GlobalPreferenceState) => {
69
- draft.preference = merge(draft.preference, preference);
70
- }),
71
- false,
72
- action,
73
- );
70
+ const nextPreference = merge(get().preference, preference);
71
+
72
+ set({ preference: nextPreference }, false, action || n('updatePreference'));
73
+
74
+ get().preferenceStorage.saveToLocalStorage(nextPreference);
74
75
  },
76
+
77
+ useInitPreference: () =>
78
+ useClientDataSWR<GlobalPreference>(
79
+ 'preference',
80
+ () => get().preferenceStorage.getFromLocalStorage(),
81
+ {
82
+ onSuccess: (preference) => {
83
+ if (preference) {
84
+ set({ preference }, false, n('initPreference'));
85
+ }
86
+ },
87
+ },
88
+ ),
75
89
  });
@@ -1,4 +1,5 @@
1
1
  import { SessionDefaultGroup, SessionGroupId } from '@/types/session';
2
+ import { AsyncLocalStorage } from '@/utils/localStorage';
2
3
 
3
4
  export interface Guide {
4
5
  // Topic 引导
@@ -18,6 +19,7 @@ export interface GlobalPreference {
18
19
  showSessionPanel?: boolean;
19
20
  showSystemRole?: boolean;
20
21
  telemetry: boolean | null;
22
+
21
23
  /**
22
24
  * whether to use cmd + enter to send message
23
25
  */
@@ -26,10 +28,10 @@ export interface GlobalPreference {
26
28
 
27
29
  export interface GlobalPreferenceState {
28
30
  /**
29
- * 用户偏好的 UI 状态
30
- * @localStorage
31
+ * the user preference, which only store in local storage
31
32
  */
32
33
  preference: GlobalPreference;
34
+ preferenceStorage: AsyncLocalStorage<GlobalPreference>;
33
35
  }
34
36
 
35
37
  export const initialPreferenceState: GlobalPreferenceState = {
@@ -45,4 +47,5 @@ export const initialPreferenceState: GlobalPreferenceState = {
45
47
  telemetry: null,
46
48
  useCmdEnterToSend: false,
47
49
  },
50
+ preferenceStorage: new AsyncLocalStorage('LOBE_PREFERENCE'),
48
51
  };
@@ -20,11 +20,14 @@ import {
20
20
  import { GlobalStore } from '@/store/global';
21
21
  import { ChatModelCard } from '@/types/llm';
22
22
  import { GlobalLLMConfig, GlobalLLMProviderKey } from '@/types/settings';
23
+ import { setNamespace } from '@/utils/storeDebug';
23
24
 
24
25
  import { CustomModelCardDispatch, customModelCardsReducer } from '../reducers/customModelCard';
25
26
  import { modelProviderSelectors } from '../selectors/modelProvider';
26
27
  import { settingsSelectors } from '../selectors/settings';
27
28
 
29
+ const n = setNamespace('settings');
30
+
28
31
  /**
29
32
  * 设置操作
30
33
  */
@@ -36,8 +39,8 @@ export interface LLMSettingsAction {
36
39
  /**
37
40
  * make sure the default model provider list is sync to latest state
38
41
  */
39
- refreshDefaultModelProviderList: () => void;
40
- refreshModelProviderList: () => void;
42
+ refreshDefaultModelProviderList: (params?: { trigger?: string }) => void;
43
+ refreshModelProviderList: (params?: { trigger?: string }) => void;
41
44
  removeEnabledModels: (provider: GlobalLLMProviderKey, model: string) => Promise<void>;
42
45
  setModelProviderConfig: <T extends GlobalLLMProviderKey>(
43
46
  provider: T,
@@ -69,7 +72,7 @@ export const llmSettingsSlice: StateCreator<
69
72
  await get().setModelProviderConfig(provider, { customModelCards: nextState });
70
73
  },
71
74
 
72
- refreshDefaultModelProviderList: () => {
75
+ refreshDefaultModelProviderList: (params) => {
73
76
  /**
74
77
  * Because we have several model cards sources, we need to merge the model cards
75
78
  * the priority is below:
@@ -113,12 +116,12 @@ export const llmSettingsSlice: StateCreator<
113
116
  ZhiPuProviderCard,
114
117
  ];
115
118
 
116
- set({ defaultModelProviderList }, false, 'refreshDefaultModelProviderList');
119
+ set({ defaultModelProviderList }, false, n(`refreshDefaultModelList - ${params?.trigger}`));
117
120
 
118
- get().refreshModelProviderList();
121
+ get().refreshModelProviderList({ trigger: 'refreshDefaultModelList' });
119
122
  },
120
123
 
121
- refreshModelProviderList: () => {
124
+ refreshModelProviderList: (params) => {
122
125
  const modelProviderList = get().defaultModelProviderList.map((list) => ({
123
126
  ...list,
124
127
  chatModels: modelProviderSelectors
@@ -136,7 +139,7 @@ export const llmSettingsSlice: StateCreator<
136
139
  enabled: modelProviderSelectors.isProviderEnabled(list.id as any)(get()),
137
140
  }));
138
141
 
139
- set({ modelProviderList }, false, 'refreshModelProviderList');
142
+ set({ modelProviderList }, false, n(`refreshModelList - ${params?.trigger}`));
140
143
  },
141
144
 
142
145
  removeEnabledModels: async (provider, model) => {
@@ -1,11 +1,10 @@
1
- import { PersistOptions, devtools, persist, subscribeWithSelector } from 'zustand/middleware';
1
+ import { devtools, subscribeWithSelector } from 'zustand/middleware';
2
2
  import { shallow } from 'zustand/shallow';
3
3
  import { createWithEqualityFn } from 'zustand/traditional';
4
4
  import { StateCreator } from 'zustand/vanilla';
5
5
 
6
6
  import { isDev } from '@/utils/env';
7
7
 
8
- import { createHyperStorage } from '../middleware/createHyperStorage';
9
8
  import { type GlobalState, initialState } from './initialState';
10
9
  import { type CommonAction, createCommonSlice } from './slices/common/action';
11
10
  import { type PreferenceAction, createPreferenceSlice } from './slices/preference/action';
@@ -22,32 +21,13 @@ const createStore: StateCreator<GlobalStore, [['zustand/devtools', never]]> = (.
22
21
  ...createPreferenceSlice(...parameters),
23
22
  });
24
23
 
25
- // =============== persist 本地缓存中间件配置 ============ //
26
- type GlobalPersist = Pick<GlobalStore, 'preference' | 'settings'>;
27
-
28
- const persistOptions: PersistOptions<GlobalStore, GlobalPersist> = {
29
- name: 'LOBE_GLOBAL',
30
-
31
- skipHydration: true,
32
-
33
- storage: createHyperStorage({
34
- localStorage: {
35
- dbName: 'LobeHub',
36
- selectors: ['preference'],
37
- },
38
- }),
39
- };
40
-
41
24
  // =============== 实装 useStore ============ //
42
25
 
43
26
  export const useGlobalStore = createWithEqualityFn<GlobalStore>()(
44
- persist(
45
- subscribeWithSelector(
46
- devtools(createStore, {
47
- name: 'LobeChat_Global' + (isDev ? '_DEV' : ''),
48
- }),
49
- ),
50
- persistOptions,
27
+ subscribeWithSelector(
28
+ devtools(createStore, {
29
+ name: 'LobeChat_Global' + (isDev ? '_DEV' : ''),
30
+ }),
51
31
  ),
52
32
  shallow,
53
33
  );
@@ -0,0 +1,36 @@
1
+ const PREV_KEY = 'LOBE_GLOBAL';
2
+
3
+ type StorageKey = 'LOBE_PREFERENCE';
4
+
5
+ export class AsyncLocalStorage<State> {
6
+ private storageKey: StorageKey;
7
+
8
+ constructor(storageKey: StorageKey) {
9
+ this.storageKey = storageKey;
10
+
11
+ // skip server side rendering
12
+ if (typeof window === 'undefined') return;
13
+
14
+ // migrate old data
15
+ if (localStorage.getItem(PREV_KEY)) {
16
+ const data = JSON.parse(localStorage.getItem(PREV_KEY) || '{}');
17
+
18
+ const preference = data.state.preference;
19
+
20
+ if (data.state?.preference) {
21
+ localStorage.setItem('LOBE_PREFERENCE', JSON.stringify(preference));
22
+ }
23
+ localStorage.removeItem(PREV_KEY);
24
+ }
25
+ }
26
+
27
+ async saveToLocalStorage(state: object) {
28
+ const data = await this.getFromLocalStorage();
29
+
30
+ localStorage.setItem(this.storageKey, JSON.stringify({ ...data, ...state }));
31
+ }
32
+
33
+ async getFromLocalStorage(key: StorageKey = this.storageKey): Promise<State> {
34
+ return JSON.parse(localStorage.getItem(key) || '{}');
35
+ }
36
+ }
@@ -1,61 +0,0 @@
1
- 'use client';
2
-
3
- import { useResponsive } from 'antd-style';
4
- import { useRouter } from 'next/navigation';
5
- import { memo, useEffect } from 'react';
6
-
7
- import { useEnabledDataSync } from '@/hooks/useSyncData';
8
- import { useGlobalStore } from '@/store/global';
9
- import { useEffectAfterGlobalHydrated } from '@/store/global/hooks/useEffectAfterHydrated';
10
-
11
- const StoreHydration = memo(() => {
12
- const [useFetchServerConfig, useFetchUserConfig] = useGlobalStore((s) => [
13
- s.useFetchServerConfig,
14
- s.useFetchUserConfig,
15
- ]);
16
-
17
- const { isLoading } = useFetchServerConfig();
18
-
19
- useFetchUserConfig(!isLoading);
20
-
21
- useEnabledDataSync();
22
-
23
- useEffect(() => {
24
- // refs: https://github.com/pmndrs/zustand/blob/main/docs/integrations/persisting-store-data.md#hashydrated
25
- useGlobalStore.persist.rehydrate();
26
- }, []);
27
-
28
- const { mobile } = useResponsive();
29
- useEffectAfterGlobalHydrated(
30
- (store) => {
31
- const prevState = store.getState().isMobile;
32
-
33
- if (prevState !== mobile) {
34
- store.setState({ isMobile: mobile });
35
- }
36
- },
37
- [mobile],
38
- );
39
-
40
- const router = useRouter();
41
-
42
- useEffectAfterGlobalHydrated(
43
- (store) => {
44
- store.setState({ router });
45
- },
46
- [router],
47
- );
48
-
49
- useEffect(() => {
50
- router.prefetch('/chat');
51
- router.prefetch('/chat/settings');
52
- router.prefetch('/market');
53
- router.prefetch('/settings/common');
54
- router.prefetch('/settings/agent');
55
- router.prefetch('/settings/sync');
56
- }, [router]);
57
-
58
- return null;
59
- });
60
-
61
- export default StoreHydration;
@@ -1,22 +0,0 @@
1
- import { useEffect } from 'react';
2
-
3
- import { useGlobalStore } from '../store';
4
-
5
- export const useEffectAfterGlobalHydrated = (
6
- fn: (store: typeof useGlobalStore) => void,
7
- deps: any[] = [],
8
- ) => {
9
- useEffect(() => {
10
- const hasRehydrated = useGlobalStore.persist.hasHydrated();
11
-
12
- if (hasRehydrated) {
13
- // 等价 useEffect 多次触发
14
- fn(useGlobalStore);
15
- } else {
16
- // 等价于 useEffect 第一次触发
17
- useGlobalStore.persist.onFinishHydration(() => {
18
- fn(useGlobalStore);
19
- });
20
- }
21
- }, deps);
22
- };