@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.
- package/CHANGELOG.md +50 -0
- package/locales/ar/error.json +8 -7
- package/locales/bg-BG/error.json +8 -7
- package/locales/de-DE/error.json +8 -7
- package/locales/en-US/error.json +8 -7
- package/locales/es-ES/error.json +8 -7
- package/locales/fr-FR/error.json +8 -7
- package/locales/it-IT/error.json +8 -7
- package/locales/ja-JP/error.json +8 -7
- package/locales/ko-KR/error.json +8 -7
- package/locales/nl-NL/error.json +8 -7
- package/locales/pl-PL/error.json +8 -7
- package/locales/pt-BR/error.json +8 -7
- package/locales/ru-RU/error.json +8 -7
- package/locales/tr-TR/error.json +8 -7
- package/locales/vi-VN/error.json +8 -7
- package/locales/zh-CN/error.json +8 -7
- package/locales/zh-TW/error.json +8 -7
- package/package.json +1 -1
- package/src/app/settings/llm/components/ProviderModelList/Option.tsx +23 -19
- package/src/app/settings/llm/components/ProviderModelList/index.tsx +1 -0
- package/src/features/Conversation/Error/InvalidOllamaModel/index.tsx +29 -11
- package/src/features/Conversation/Error/InvalidOllamaModel/useDownloadMonitor.ts +25 -19
- package/src/layout/GlobalProvider/StoreInitialization.tsx +45 -0
- package/src/layout/GlobalProvider/index.tsx +2 -2
- package/src/locales/default/error.ts +8 -7
- package/src/services/__tests__/ollama.test.ts +5 -3
- package/src/services/ollama.ts +24 -2
- package/src/store/global/slices/common/action.ts +3 -3
- package/src/store/global/slices/preference/action.ts +26 -12
- package/src/store/global/slices/preference/initialState.ts +5 -2
- package/src/store/global/slices/settings/actions/llm.ts +10 -7
- package/src/store/global/store.ts +5 -25
- package/src/utils/localStorage.ts +36 -0
- package/src/layout/GlobalProvider/StoreHydration.tsx +0 -61
- 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.
|
|
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.
|
|
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
|
-
{
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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,
|
|
2
|
-
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
if (
|
|
6
|
-
return `${
|
|
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
|
|
9
|
-
return `${
|
|
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
|
|
34
|
+
const lastCompletedRef = useRef(completedSize);
|
|
35
|
+
const lastTimedRef = useRef(Date.now());
|
|
29
36
|
|
|
30
37
|
useEffect(() => {
|
|
31
38
|
const currentTime = Date.now();
|
|
32
|
-
|
|
33
|
-
if (
|
|
34
|
-
const
|
|
35
|
-
|
|
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
|
-
|
|
43
|
-
|
|
47
|
+
|
|
48
|
+
lastCompletedRef.current = completedSize;
|
|
49
|
+
lastTimedRef.current = currentTime;
|
|
44
50
|
}
|
|
45
|
-
}, [
|
|
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
|
|
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
|
-
<
|
|
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 {
|
|
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(
|
|
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(
|
|
25
|
+
expect(fetch).toHaveBeenCalled();
|
|
24
26
|
});
|
|
25
27
|
});
|
|
26
28
|
});
|
package/src/services/ollama.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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().
|
|
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,
|
|
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?:
|
|
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)
|
|
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)
|
|
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)
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
*
|
|
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,
|
|
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,
|
|
142
|
+
set({ modelProviderList }, false, n(`refreshModelList - ${params?.trigger}`));
|
|
140
143
|
},
|
|
141
144
|
|
|
142
145
|
removeEnabledModels: async (provider, model) => {
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
};
|