@lobehub/chat 0.148.2 → 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.
Files changed (36) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/locales/ar/error.json +8 -7
  3. package/locales/bg-BG/error.json +8 -7
  4. package/locales/de-DE/error.json +8 -7
  5. package/locales/en-US/error.json +8 -7
  6. package/locales/es-ES/error.json +8 -7
  7. package/locales/fr-FR/error.json +8 -7
  8. package/locales/it-IT/error.json +8 -7
  9. package/locales/ja-JP/error.json +8 -7
  10. package/locales/ko-KR/error.json +8 -7
  11. package/locales/nl-NL/error.json +8 -7
  12. package/locales/pl-PL/error.json +8 -7
  13. package/locales/pt-BR/error.json +8 -7
  14. package/locales/ru-RU/error.json +8 -7
  15. package/locales/tr-TR/error.json +8 -7
  16. package/locales/vi-VN/error.json +8 -7
  17. package/locales/zh-CN/error.json +8 -7
  18. package/locales/zh-TW/error.json +8 -7
  19. package/package.json +1 -1
  20. package/src/app/settings/llm/components/ProviderModelList/Option.tsx +23 -19
  21. package/src/app/settings/llm/components/ProviderModelList/index.tsx +1 -0
  22. package/src/features/Conversation/Error/InvalidOllamaModel/index.tsx +29 -11
  23. package/src/features/Conversation/Error/InvalidOllamaModel/useDownloadMonitor.ts +25 -19
  24. package/src/layout/GlobalProvider/StoreInitialization.tsx +45 -0
  25. package/src/layout/GlobalProvider/index.tsx +2 -2
  26. package/src/locales/default/error.ts +8 -7
  27. package/src/services/__tests__/ollama.test.ts +5 -3
  28. package/src/services/ollama.ts +24 -2
  29. package/src/store/global/slices/common/action.ts +3 -3
  30. package/src/store/global/slices/preference/action.ts +26 -12
  31. package/src/store/global/slices/preference/initialState.ts +5 -2
  32. package/src/store/global/slices/settings/actions/llm.ts +10 -7
  33. package/src/store/global/store.ts +5 -25
  34. package/src/utils/localStorage.ts +36 -0
  35. package/src/layout/GlobalProvider/StoreHydration.tsx +0 -61
  36. package/src/store/global/hooks/useEffectAfterHydrated.ts +0 -22
@@ -10,7 +10,7 @@ import { ollamaService } from '@/services/ollama';
10
10
  import { useChatStore } from '@/store/chat';
11
11
 
12
12
  import { ErrorActionContainer, FormAction } from '../style';
13
- import { useDownloadMonitor } from './useDownloadMonitor';
13
+ import { formatSize, useDownloadMonitor } from './useDownloadMonitor';
14
14
 
15
15
  interface OllamaModelFormProps {
16
16
  id: string;
@@ -61,12 +61,12 @@ const OllamaModelForm = memo<OllamaModelFormProps>(({ id, model }) => {
61
61
  <FormAction
62
62
  avatar={<Ollama color={theme.colorPrimary} size={64} />}
63
63
  description={
64
- isDownloading ? settingT('ollama.download.desc') : t('unlock.model.Ollama.description')
64
+ isDownloading ? settingT('ollama.download.desc') : t('unlock.ollama.description')
65
65
  }
66
66
  title={
67
67
  isDownloading
68
68
  ? settingT('ollama.download.title', { model: modelToPull })
69
- : t('unlock.model.Ollama.title')
69
+ : t('unlock.ollama.title')
70
70
  }
71
71
  >
72
72
  {!isDownloading && (
@@ -110,15 +110,33 @@ const OllamaModelForm = memo<OllamaModelFormProps>(({ id, model }) => {
110
110
  style={{ marginTop: 8 }}
111
111
  type={'primary'}
112
112
  >
113
- {t('unlock.model.Ollama.confirm')}
114
- </Button>
115
- <Button
116
- onClick={() => {
117
- deleteMessage(id);
118
- }}
119
- >
120
- {t('unlock.closeMessage')}
113
+ {!isDownloading
114
+ ? t('unlock.ollama.confirm')
115
+ : // if total is 0, show starting, else show downloaded
116
+ !total
117
+ ? t('unlock.ollama.starting')
118
+ : t('unlock.ollama.downloaded', {
119
+ completed: formatSize(completed),
120
+ total: formatSize(total),
121
+ })}
121
122
  </Button>
123
+ {isDownloading ? (
124
+ <Button
125
+ onClick={() => {
126
+ ollamaService.abort();
127
+ }}
128
+ >
129
+ {t('unlock.ollama.cancel')}
130
+ </Button>
131
+ ) : (
132
+ <Button
133
+ onClick={() => {
134
+ deleteMessage(id);
135
+ }}
136
+ >
137
+ {t('unlock.closeMessage')}
138
+ </Button>
139
+ )}
122
140
  </Flexbox>
123
141
  </Center>
124
142
  );
@@ -1,15 +1,22 @@
1
- import { useEffect, useMemo, useState } from 'react';
2
-
3
- const formatSpeed = (speed: number): string => {
4
- const kbPerSecond = speed / 1024;
5
- if (kbPerSecond < 1024) {
6
- return `${kbPerSecond.toFixed(1)} KB/s`;
1
+ import { useEffect, useRef, useState } from 'react';
2
+
3
+ export const formatSize = (bytes: number): string => {
4
+ const kbSize = bytes / 1024;
5
+ if (kbSize < 1024) {
6
+ return `${kbSize.toFixed(1)} KB`;
7
+ } else if (kbSize < 1_048_576) {
8
+ const mbSize = kbSize / 1024;
9
+ return `${mbSize.toFixed(1)} MB`;
7
10
  } else {
8
- const mbPerSecond = kbPerSecond / 1024;
9
- return `${mbPerSecond.toFixed(1)} MB/s`;
11
+ const gbSize = kbSize / 1_048_576;
12
+ return `${gbSize.toFixed(1)} GB`;
10
13
  }
11
14
  };
12
15
 
16
+ const formatSpeed = (speed: number): string => {
17
+ return `${formatSize(speed)}/s`;
18
+ };
19
+
13
20
  const formatTime = (timeInSeconds: number): string => {
14
21
  if (timeInSeconds < 60) {
15
22
  return `${timeInSeconds.toFixed(1)} s`;
@@ -21,28 +28,27 @@ const formatTime = (timeInSeconds: number): string => {
21
28
  };
22
29
 
23
30
  export const useDownloadMonitor = (totalSize: number, completedSize: number) => {
24
- const [startTime, setStartTime] = useState<number>(Date.now());
25
31
  const [downloadSpeed, setDownloadSpeed] = useState<string>('0 KB/s');
26
32
  const [remainingTime, setRemainingTime] = useState<string>('-');
27
33
 
28
- const isReady = useMemo(() => completedSize > 0, [completedSize]);
34
+ const lastCompletedRef = useRef(completedSize);
35
+ const lastTimedRef = useRef(Date.now());
29
36
 
30
37
  useEffect(() => {
31
38
  const currentTime = Date.now();
32
- // mark as start download
33
- if (isReady) {
34
- const elapsedTime = (currentTime - startTime) / 1000; // in seconds
35
- const speed = completedSize / elapsedTime; // in bytes per second
39
+ const elapsedTime = (currentTime - lastTimedRef.current) / 1000; // in seconds
40
+ if (completedSize > 0 && elapsedTime > 1) {
41
+ const speed = Math.max(0, (completedSize - lastCompletedRef.current) / elapsedTime); // in bytes per second
42
+ setDownloadSpeed(formatSpeed(speed));
36
43
 
37
44
  const remainingSize = totalSize - completedSize;
38
45
  const time = remainingSize / speed; // in seconds
39
-
40
- setDownloadSpeed(formatSpeed(speed));
41
46
  setRemainingTime(formatTime(time));
42
- } else {
43
- setStartTime(currentTime);
47
+
48
+ lastCompletedRef.current = completedSize;
49
+ lastTimedRef.current = currentTime;
44
50
  }
45
- }, [isReady, completedSize]);
51
+ }, [completedSize]);
46
52
 
47
53
  return { downloadSpeed, remainingTime };
48
54
  };
@@ -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>
@@ -111,19 +111,20 @@ export default {
111
111
  addProxyUrl: '添加 OpenAI 代理地址(可选)',
112
112
  closeMessage: '关闭提示',
113
113
  confirm: '确认并重试',
114
- model: {
115
- Ollama: {
116
- confirm: '下载',
117
- description: '输入你的 Ollama 模型标签,完成即可继续会话',
118
- title: '下载指定的 Ollama 模型',
119
- },
120
- },
121
114
  oauth: {
122
115
  description: '管理员已开启统一登录认证,点击下方按钮登录,即可解锁应用',
123
116
  success: '登录成功',
124
117
  title: '登录账号',
125
118
  welcome: '欢迎你!',
126
119
  },
120
+ ollama: {
121
+ cancel: '取消下载',
122
+ confirm: '下载',
123
+ description: '输入你的 Ollama 模型标签,完成即可继续会话',
124
+ downloaded: '{{completed}} / {{total}}',
125
+ starting: '开始下载...',
126
+ title: '下载指定的 Ollama 模型',
127
+ },
127
128
  password: {
128
129
  description: '管理员已开启应用加密,输入应用密码后即可解锁应用。密码只需填写一次',
129
130
  placeholder: '请输入密码',
@@ -1,9 +1,11 @@
1
1
  import { Mock, describe, expect, it, vi } from 'vitest';
2
2
 
3
- import { ollamaService } from '../ollama';
3
+ import { OllamaService } from '../ollama';
4
4
 
5
5
  vi.stubGlobal('fetch', vi.fn());
6
6
 
7
+ const ollamaService = new OllamaService({ fetch });
8
+
7
9
  describe('OllamaService', () => {
8
10
  describe('list models', async () => {
9
11
  it('should make a GET request with the correct payload', async () => {
@@ -11,7 +13,7 @@ describe('OllamaService', () => {
11
13
 
12
14
  expect(await ollamaService.getModels()).toEqual({ models: [] });
13
15
 
14
- expect(global.fetch).toHaveBeenCalled();
16
+ expect(fetch).toHaveBeenCalled();
15
17
  });
16
18
 
17
19
  it('should make a GET request with the error', async () => {
@@ -20,7 +22,7 @@ describe('OllamaService', () => {
20
22
 
21
23
  await expect(ollamaService.getModels()).rejects.toThrow();
22
24
 
23
- expect(global.fetch).toHaveBeenCalled();
25
+ expect(fetch).toHaveBeenCalled();
24
26
  });
25
27
  });
26
28
  });
@@ -9,7 +9,21 @@ import { getMessageError } from '@/utils/fetch';
9
9
 
10
10
  const DEFAULT_BASE_URL = 'http://127.0.0.1:11434/v1';
11
11
 
12
- class OllamaService {
12
+ interface OllamaServiceParams {
13
+ fetch?: typeof fetch;
14
+ }
15
+
16
+ export class OllamaService {
17
+ private _host: string;
18
+ private _client: OllamaBrowser;
19
+ private _fetch?: typeof fetch;
20
+
21
+ constructor(params: OllamaServiceParams = {}) {
22
+ this._host = this.getHost();
23
+ this._fetch = params.fetch;
24
+ this._client = new OllamaBrowser({ fetch: params?.fetch, host: this._host });
25
+ }
26
+
13
27
  getHost = (): string => {
14
28
  const config = modelConfigSelectors.ollamaConfig(useGlobalStore.getState());
15
29
 
@@ -18,7 +32,15 @@ class OllamaService {
18
32
  };
19
33
 
20
34
  getOllamaClient = () => {
21
- return new OllamaBrowser({ host: this.getHost() });
35
+ if (this.getHost() !== this._host) {
36
+ this._host = this.getHost();
37
+ this._client = new OllamaBrowser({ fetch: this._fetch, host: this.getHost() });
38
+ }
39
+ return this._client;
40
+ };
41
+
42
+ abort = () => {
43
+ this._client.abort();
22
44
  };
23
45
 
24
46
  pullModel = async (model: string): Promise<AsyncGenerator<ProgressResponse>> => {
@@ -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
- };