@lobehub/chat 0.122.2 → 0.122.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 (79) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +8 -8
  3. package/README.zh-CN.md +8 -8
  4. package/docs/Deployment/Docker-Deployment.md +118 -0
  5. package/docs/Deployment/Docker-Deployment.zh-CN.md +118 -0
  6. package/package.json +1 -4
  7. package/src/app/api/config/route.ts +3 -1
  8. package/src/app/api/openai/chat/createChatCompletion.ts +1 -1
  9. package/src/app/api/openai/createBizOpenAI/index.ts +1 -1
  10. package/src/app/api/plugin/gateway/route.ts +31 -5
  11. package/src/app/chat/(mobile)/features/SessionHeader.tsx +2 -1
  12. package/src/app/chat/features/ChatHeader/SettingButton.tsx +1 -1
  13. package/src/app/chat/features/ChatHeader/ShareButton/ShareModal.tsx +2 -1
  14. package/src/app/chat/settings/(desktop)/index.tsx +9 -5
  15. package/src/app/chat/settings/features/SubmitAgentButton/SubmitAgentModal.tsx +2 -1
  16. package/src/app/settings/(mobile)/mobile/index.tsx +2 -1
  17. package/src/app/settings/common/Common.tsx +14 -3
  18. package/src/app/settings/features/ThemeSwatches/ThemeSwatchesNeutral.tsx +2 -1
  19. package/src/app/settings/features/ThemeSwatches/ThemeSwatchesPrimary.tsx +2 -1
  20. package/src/app/settings/llm/LLM/index.tsx +17 -10
  21. package/src/const/settings.ts +0 -1
  22. package/src/database/core/db.ts +41 -2
  23. package/src/database/core/migrations/migrateSettingsToUser/fixtures/input.json +55 -0
  24. package/src/database/core/migrations/migrateSettingsToUser/fixtures/output.json +60 -0
  25. package/src/database/core/migrations/migrateSettingsToUser/index.test.ts +13 -0
  26. package/src/database/core/migrations/migrateSettingsToUser/index.ts +39 -0
  27. package/src/database/core/schemas.ts +10 -0
  28. package/src/database/models/user.ts +54 -0
  29. package/src/database/schemas/user.ts +57 -0
  30. package/src/features/AgentSetting/AgentConfig/index.tsx +2 -2
  31. package/src/features/AvatarWithUpload/index.tsx +6 -2
  32. package/src/features/ChatInput/ActionBar/ModelSwitch.tsx +2 -2
  33. package/src/features/Conversation/Error/ApiKeyForm.tsx +3 -3
  34. package/src/features/Conversation/Error/InvalidAccess.tsx +5 -1
  35. package/src/features/Conversation/Extras/Translate.tsx +3 -4
  36. package/src/layout/GlobalLayout/AppTheme.tsx +4 -3
  37. package/src/layout/GlobalLayout/Locale.tsx +2 -1
  38. package/src/layout/GlobalLayout/StoreHydration.tsx +7 -2
  39. package/src/services/_header.ts +3 -2
  40. package/src/services/chat.ts +2 -1
  41. package/src/services/user.ts +31 -0
  42. package/src/store/chat/slices/message/selectors.test.ts +7 -2
  43. package/src/store/chat/slices/message/selectors.ts +2 -1
  44. package/src/store/global/hooks/index.ts +0 -1
  45. package/src/store/global/initialState.ts +3 -1
  46. package/src/store/global/selectors.ts +1 -0
  47. package/src/store/global/slices/common/action.test.ts +2 -79
  48. package/src/store/global/slices/common/action.ts +37 -61
  49. package/src/store/global/slices/common/initialState.ts +0 -34
  50. package/src/store/global/slices/common/selectors.ts +2 -7
  51. package/src/store/global/slices/preference/action.test.ts +92 -0
  52. package/src/store/global/slices/preference/action.ts +75 -0
  53. package/src/store/global/slices/preference/initialState.ts +45 -0
  54. package/src/store/global/slices/preference/selectors.ts +10 -0
  55. package/src/store/global/slices/settings/action.test.ts +68 -39
  56. package/src/store/global/slices/settings/action.ts +26 -50
  57. package/src/store/global/slices/settings/initialState.ts +8 -6
  58. package/src/store/global/slices/settings/selectors/__snapshots__/modelProvider.test.ts.snap +100 -0
  59. package/src/store/global/slices/settings/{__snapshots__ → selectors/__snapshots__}/selectors.test.ts.snap +1 -115
  60. package/src/store/global/slices/settings/selectors/index.ts +2 -0
  61. package/src/store/global/slices/settings/selectors/modelProvider.test.ts +100 -0
  62. package/src/store/global/slices/settings/selectors/modelProvider.ts +64 -0
  63. package/src/store/global/slices/settings/{selectors.test.ts → selectors/selectors.test.ts} +2 -97
  64. package/src/store/global/slices/settings/selectors/settings.ts +58 -0
  65. package/src/store/global/store.ts +11 -36
  66. package/src/store/session/slices/agent/action.ts +1 -1
  67. package/src/types/settings/base.ts +13 -0
  68. package/src/types/settings/index.ts +34 -0
  69. package/src/types/settings/modelProvider.ts +17 -0
  70. package/src/types/settings/tts.ts +10 -0
  71. package/src/utils/difference.ts +12 -0
  72. package/tests/utils.tsx +8 -0
  73. package/tsconfig.json +9 -8
  74. package/vitest.config.ts +1 -0
  75. package/src/store/global/hooks/useHydrated.ts +0 -18
  76. package/src/store/global/slices/settings/selectors.ts +0 -105
  77. package/src/types/settings.ts +0 -68
  78. /package/src/app/api/{openai/errorResponse.test.ts → errorResponse.test.ts} +0 -0
  79. /package/src/app/api/{openai/errorResponse.ts → errorResponse.ts} +0 -0
@@ -8,10 +8,11 @@ import {
8
8
  import { memo } from 'react';
9
9
 
10
10
  import { useGlobalStore } from '@/store/global';
11
+ import { settingsSelectors } from '@/store/global/selectors';
11
12
 
12
13
  const ThemeSwatchesPrimary = memo(() => {
13
14
  const [primaryColor, setSettings] = useGlobalStore((s) => [
14
- s.settings.primaryColor,
15
+ settingsSelectors.currentSettings(s).primaryColor,
15
16
  s.setSettings,
16
17
  ]);
17
18
 
@@ -3,12 +3,12 @@ import { Form as AntForm, AutoComplete, Input, Switch } from 'antd';
3
3
  import { createStyles } from 'antd-style';
4
4
  import { debounce } from 'lodash-es';
5
5
  import { Webhook } from 'lucide-react';
6
- import { memo } from 'react';
6
+ import { memo, useEffect } from 'react';
7
7
  import { useTranslation } from 'react-i18next';
8
8
 
9
9
  import { FORM_STYLE } from '@/const/layoutTokens';
10
- import { useEffectAfterGlobalHydrated, useGlobalStore } from '@/store/global';
11
- import { settingsSelectors } from '@/store/global/selectors';
10
+ import { useGlobalStore } from '@/store/global';
11
+ import { modelProviderSelectors } from '@/store/global/selectors';
12
12
 
13
13
  import Checker from './Checker';
14
14
 
@@ -35,16 +35,23 @@ const LLM = memo(() => {
35
35
  const { t } = useTranslation('setting');
36
36
  const [form] = AntForm.useForm();
37
37
  const { styles } = useStyles();
38
- const [setSettings] = useGlobalStore((s) => [s.setSettings]);
38
+ const [useAzure, setSettings] = useGlobalStore((s) => [
39
+ modelProviderSelectors.enableAzure(s),
40
+ s.setSettings,
41
+ ]);
39
42
 
40
- useEffectAfterGlobalHydrated((store) => {
41
- const settings = settingsSelectors.currentSettings(store.getState());
42
-
43
- form.setFieldsValue(settings);
43
+ useEffect(() => {
44
+ const unsubscribe = useGlobalStore.subscribe(
45
+ (s) => s.settings,
46
+ (settings) => {
47
+ form.setFieldsValue(settings);
48
+ },
49
+ );
50
+ return () => {
51
+ unsubscribe();
52
+ };
44
53
  }, []);
45
54
 
46
- const useAzure = useGlobalStore((s) => s.settings.languageModel.openAI.useAzure);
47
-
48
55
  const openAI: ItemGroup = {
49
56
  children: [
50
57
  {
@@ -11,7 +11,6 @@ import {
11
11
  } from '@/types/settings';
12
12
 
13
13
  export const DEFAULT_BASE_SETTINGS: GlobalBaseSettings = {
14
- avatar: '',
15
14
  fontSize: 14,
16
15
  language: 'auto',
17
16
  password: '',
@@ -1,14 +1,16 @@
1
1
  import Dexie, { Transaction } from 'dexie';
2
2
 
3
- import { DBModel, LOBE_CHAT_LOCAL_DB_NAME } from '@/database/core/types/db';
4
3
  import { DB_File } from '@/database/schemas/files';
5
4
  import { DB_Message } from '@/database/schemas/message';
6
5
  import { DB_Plugin } from '@/database/schemas/plugin';
7
6
  import { DB_Session } from '@/database/schemas/session';
8
7
  import { DB_SessionGroup } from '@/database/schemas/sessionGroup';
9
8
  import { DB_Topic } from '@/database/schemas/topic';
9
+ import { DB_User } from '@/database/schemas/user';
10
10
 
11
- import { dbSchemaV1, dbSchemaV2, dbSchemaV3, dbSchemaV4 } from './schemas';
11
+ import { migrateSettingsToUser } from './migrations/migrateSettingsToUser';
12
+ import { dbSchemaV1, dbSchemaV2, dbSchemaV3, dbSchemaV4, dbSchemaV5 } from './schemas';
13
+ import { DBModel, LOBE_CHAT_LOCAL_DB_NAME } from './types/db';
12
14
 
13
15
  interface LobeDBSchemaMap {
14
16
  files: DB_File;
@@ -17,6 +19,7 @@ interface LobeDBSchemaMap {
17
19
  sessionGroups: DB_SessionGroup;
18
20
  sessions: DB_Session;
19
21
  topics: DB_Topic;
22
+ users: DB_User;
20
23
  }
21
24
 
22
25
  // Define a local DB
@@ -27,6 +30,7 @@ export class LocalDB extends Dexie {
27
30
  public topics: LobeDBTable<'topics'>;
28
31
  public plugins: LobeDBTable<'plugins'>;
29
32
  public sessionGroups: LobeDBTable<'sessionGroups'>;
33
+ public users: LobeDBTable<'users'>;
30
34
 
31
35
  constructor() {
32
36
  super(LOBE_CHAT_LOCAL_DB_NAME);
@@ -37,12 +41,17 @@ export class LocalDB extends Dexie {
37
41
  .stores(dbSchemaV4)
38
42
  .upgrade((trans) => this.upgradeToV4(trans));
39
43
 
44
+ this.version(5)
45
+ .stores(dbSchemaV5)
46
+ .upgrade((trans) => this.upgradeToV5(trans));
47
+
40
48
  this.files = this.table('files');
41
49
  this.sessions = this.table('sessions');
42
50
  this.messages = this.table('messages');
43
51
  this.topics = this.table('topics');
44
52
  this.plugins = this.table('plugins');
45
53
  this.sessionGroups = this.table('sessionGroups');
54
+ this.users = this.table('users');
46
55
  }
47
56
 
48
57
  /**
@@ -59,6 +68,36 @@ export class LocalDB extends Dexie {
59
68
  session.group = 'default';
60
69
  });
61
70
  };
71
+
72
+ /**
73
+ * 2024.01.29
74
+ * settings from localStorage to indexedDB
75
+ */
76
+ upgradeToV5 = async (trans: Transaction) => {
77
+ const users = trans.table('users');
78
+
79
+ // if no user, create one
80
+ if ((await users.count()) === 0) {
81
+ const data = localStorage.getItem('LOBE_SETTINGS');
82
+
83
+ if (data) {
84
+ let json;
85
+
86
+ try {
87
+ json = JSON.parse(data);
88
+ } catch {
89
+ /* empty */
90
+ }
91
+
92
+ if (!json?.state?.settings) return;
93
+
94
+ const settings = json.state.settings;
95
+
96
+ const user = migrateSettingsToUser(settings);
97
+ await users.add(user);
98
+ }
99
+ }
100
+ };
62
101
  }
63
102
 
64
103
  export const LocalDBInstance = new LocalDB();
@@ -0,0 +1,55 @@
1
+ {
2
+ "avatar": "",
3
+ "defaultAgent": {
4
+ "config": {
5
+ "autoCreateTopicThreshold": 2,
6
+ "displayMode": "chat",
7
+ "enableAutoCreateTopic": true,
8
+ "historyCount": 1,
9
+ "model": "gpt-3.5-turbo-1106",
10
+ "params": {
11
+ "frequency_penalty": 0,
12
+ "presence_penalty": 0,
13
+ "temperature": 0.7,
14
+ "top_p": 0.7
15
+ },
16
+ "plugins": ["website-crawler"],
17
+ "systemRole": "",
18
+ "tts": {
19
+ "showAllLocaleVoice": false,
20
+ "sttLocale": "auto",
21
+ "ttsService": "openai",
22
+ "voice": { "openai": "alloy" }
23
+ },
24
+ "enableHistoryCount": true
25
+ },
26
+ "meta": {}
27
+ },
28
+ "fontSize": 14,
29
+ "language": "auto",
30
+ "languageModel": {
31
+ "openAI": {
32
+ "OPENAI_API_KEY": "dfdf",
33
+ "models": [
34
+ "gpt-3.5-turbo",
35
+ "gpt-3.5-turbo-1106",
36
+ "gpt-3.5-turbo-16k",
37
+ "gpt-4",
38
+ "gpt-4-32k",
39
+ "gpt-4-1106-preview",
40
+ "gpt-4-vision-preview"
41
+ ],
42
+ "customModelName": "+glm-4,+glm-4v",
43
+ "endpoint": "sdaddsd"
44
+ },
45
+ "zhipu": { "ZHIPU_API_KEY": "", "enabled": false }
46
+ },
47
+ "password": "",
48
+ "themeMode": "auto",
49
+ "tool": { "dalle": { "autoGenerate": false } },
50
+ "tts": {
51
+ "openAI": { "sttModel": "whisper-1", "ttsModel": "tts-1" },
52
+ "sttAutoStop": true,
53
+ "sttServer": "openai"
54
+ }
55
+ }
@@ -0,0 +1,60 @@
1
+ {
2
+ "avatar": "",
3
+ "settings": {
4
+ "defaultAgent": {
5
+ "config": {
6
+ "autoCreateTopicThreshold": 2,
7
+ "displayMode": "chat",
8
+ "enableAutoCreateTopic": true,
9
+ "historyCount": 1,
10
+ "model": "gpt-3.5-turbo-1106",
11
+ "params": {
12
+ "frequency_penalty": 0,
13
+ "presence_penalty": 0,
14
+ "temperature": 0.7,
15
+ "top_p": 0.7
16
+ },
17
+ "plugins": ["website-crawler"],
18
+ "systemRole": "",
19
+ "tts": {
20
+ "showAllLocaleVoice": false,
21
+ "sttLocale": "auto",
22
+ "ttsService": "openai",
23
+ "voice": {
24
+ "openai": "alloy"
25
+ }
26
+ },
27
+ "enableHistoryCount": true
28
+ },
29
+ "meta": {}
30
+ },
31
+ "fontSize": 14,
32
+ "language": "auto",
33
+ "languageModel": {
34
+ "openai": {
35
+ "OPENAI_API_KEY": "dfdf",
36
+ "models": [
37
+ "gpt-3.5-turbo",
38
+ "gpt-3.5-turbo-1106",
39
+ "gpt-3.5-turbo-16k",
40
+ "gpt-4",
41
+ "gpt-4-32k",
42
+ "gpt-4-1106-preview",
43
+ "gpt-4-vision-preview"
44
+ ],
45
+ "customModelName": "+glm-4,+glm-4v",
46
+ "endpoint": "sdaddsd"
47
+ }
48
+ },
49
+ "password": "",
50
+ "themeMode": "auto",
51
+ "tts": {
52
+ "openAI": {
53
+ "sttModel": "whisper-1",
54
+ "ttsModel": "tts-1"
55
+ },
56
+ "sttAutoStop": true,
57
+ "sttServer": "openai"
58
+ }
59
+ }
60
+ }
@@ -0,0 +1,13 @@
1
+ import { describe, expect } from 'vitest';
2
+
3
+ import input from './fixtures/input.json';
4
+ import outputData from './fixtures/output.json';
5
+ import { V4Settings, migrateSettingsToUser } from './index';
6
+
7
+ describe('migrateSettingsFromLocalStorage', () => {
8
+ it('from localStorage to indexedDB', () => {
9
+ const output = migrateSettingsToUser(input as V4Settings);
10
+
11
+ expect(output).toEqual(outputData);
12
+ });
13
+ });
@@ -0,0 +1,39 @@
1
+ import type { NeutralColors, PrimaryColors } from '@lobehub/ui';
2
+ import type { ThemeMode } from 'antd-style';
3
+
4
+ import { DB_Settings, DB_User } from '@/database/schemas/user';
5
+ import { LocaleMode } from '@/types/locale';
6
+ import { GlobalDefaultAgent, GlobalLLMConfig, GlobalTTSConfig, GlobalTool } from '@/types/settings';
7
+
8
+ export interface V4Settings {
9
+ avatar: string;
10
+ defaultAgent: GlobalDefaultAgent;
11
+ fontSize: number;
12
+ language: LocaleMode;
13
+ languageModel: GlobalLLMConfig;
14
+ neutralColor?: NeutralColors;
15
+ password: string;
16
+ primaryColor?: PrimaryColors;
17
+ themeMode: ThemeMode;
18
+ tool: GlobalTool;
19
+ tts: GlobalTTSConfig;
20
+ }
21
+
22
+ export const migrateSettingsToUser = (settings: V4Settings): DB_User => {
23
+ const dbSettings: DB_Settings = {
24
+ defaultAgent: settings.defaultAgent,
25
+ fontSize: settings.fontSize,
26
+ language: settings.language,
27
+ languageModel: {
28
+ openai: settings.languageModel.openAI,
29
+ },
30
+ password: settings.password,
31
+ themeMode: settings.themeMode,
32
+ tts: settings.tts,
33
+ };
34
+
35
+ return {
36
+ avatar: settings.avatar,
37
+ settings: dbSettings,
38
+ };
39
+ };
@@ -44,3 +44,13 @@ export const dbSchemaV4 = {
44
44
  sessions:
45
45
  '&id, type, group, pinned, meta.title, meta.description, meta.tags, createdAt, updatedAt',
46
46
  };
47
+
48
+ // ************************************** //
49
+ // ******* Version 5 - 2024-01-29 ******* //
50
+ // ************************************** //
51
+ // - Added `users` table
52
+
53
+ export const dbSchemaV5 = {
54
+ ...dbSchemaV4,
55
+ users: '++id',
56
+ };
@@ -0,0 +1,54 @@
1
+ import { DeepPartial } from 'utility-types';
2
+
3
+ import { BaseModel } from '@/database/core';
4
+ import { GlobalSettings } from '@/types/settings';
5
+
6
+ import { DB_User, DB_UserSchema } from '../schemas/user';
7
+
8
+ class _UserModel extends BaseModel {
9
+ constructor() {
10
+ super('users', DB_UserSchema);
11
+ }
12
+
13
+ getUser = async (): Promise<DB_User & { id: number }> => {
14
+ const hasUSer = !!(await this.table.count());
15
+
16
+ if (!hasUSer) await this.table.put({});
17
+
18
+ const list = await this.table.toArray();
19
+
20
+ return list[0];
21
+ };
22
+
23
+ create = async (user: DB_User) => {
24
+ return this.table.put(user);
25
+ };
26
+
27
+ private update = async (id: number, value: DeepPartial<DB_User>) => {
28
+ return this.table.update(id, value);
29
+ };
30
+
31
+ clear() {
32
+ return this.table.clear();
33
+ }
34
+
35
+ async updateSettings(settings: DeepPartial<GlobalSettings>) {
36
+ const user = await this.getUser();
37
+
38
+ return this.update(user.id, { settings: settings as any });
39
+ }
40
+
41
+ async resetSettings() {
42
+ const user = await this.getUser();
43
+
44
+ return this.update(user.id, { avatar: undefined, settings: undefined });
45
+ }
46
+
47
+ async updateAvatar(avatar: string) {
48
+ const user = await this.getUser();
49
+
50
+ return this.update(user.id, { avatar });
51
+ }
52
+ }
53
+
54
+ export const UserModel = new _UserModel();
@@ -0,0 +1,57 @@
1
+ import { z } from 'zod';
2
+
3
+ import { AgentSchema } from '@/database/schemas/session';
4
+ import { LobeMetaDataSchema } from '@/types/meta';
5
+
6
+ const modelProviderSchema = z.object({
7
+ openai: z.object({
8
+ OPENAI_API_KEY: z.string().optional(),
9
+ azureApiVersion: z.string().optional(),
10
+ customModelName: z.string().optional(),
11
+ endpoint: z.string().optional(),
12
+ models: z.array(z.string()).optional(),
13
+ useAzure: z.boolean().optional(),
14
+ }),
15
+ // zhipu: z.object({
16
+ // ZHIPU_API_KEY: z.string().optional(),
17
+ // enabled: z.boolean().default(false),
18
+ // }),
19
+ });
20
+
21
+ const settingsSchema = z.object({
22
+ defaultAgent: z.object({
23
+ config: AgentSchema,
24
+ meta: LobeMetaDataSchema,
25
+ }),
26
+ fontSize: z.number().default(14),
27
+ language: z.string(),
28
+ languageModel: modelProviderSchema.partial(),
29
+ password: z.string(),
30
+ themeMode: z.string(),
31
+ tts: z.object({
32
+ openAI: z.object({
33
+ sttModel: z.string(),
34
+ ttsModel: z.string(),
35
+ }),
36
+ sttAutoStop: z.boolean(),
37
+ sttServer: z.string(),
38
+ }),
39
+ });
40
+
41
+ // const patchSchema = z.array(
42
+ // z.object({
43
+ // op: z.string(),
44
+ // path: z.string(),
45
+ // value: z.any(),
46
+ // }),
47
+ // );
48
+
49
+ export const DB_UserSchema = z.object({
50
+ avatar: z.string().optional(),
51
+ settings: settingsSchema.partial(),
52
+ // settings: patchSchema,
53
+ });
54
+
55
+ export type DB_User = z.infer<typeof DB_UserSchema>;
56
+
57
+ export type DB_Settings = z.infer<typeof settingsSchema>;
@@ -9,7 +9,7 @@ import { useTranslation } from 'react-i18next';
9
9
 
10
10
  import { FORM_STYLE } from '@/const/layoutTokens';
11
11
  import { useGlobalStore } from '@/store/global';
12
- import { settingsSelectors } from '@/store/global/selectors';
12
+ import { modelProviderSelectors } from '@/store/global/selectors';
13
13
 
14
14
  import { useStore } from '../store';
15
15
 
@@ -21,7 +21,7 @@ const AgentConfig = memo(() => {
21
21
 
22
22
  const config = useStore((s) => s.config, isEqual);
23
23
 
24
- const modelList = useGlobalStore(settingsSelectors.modelList);
24
+ const modelList = useGlobalStore(modelProviderSelectors.modelList);
25
25
 
26
26
  useEffect(() => {
27
27
  form.setFieldsValue(config);
@@ -5,6 +5,7 @@ import Avatar from 'next/image';
5
5
  import { CSSProperties, memo } from 'react';
6
6
 
7
7
  import { useGlobalStore } from '@/store/global';
8
+ import { commonSelectors } from '@/store/global/selectors';
8
9
  import { imageToBase64 } from '@/utils/imageToBase64';
9
10
  import { createUploadImageHandler } from '@/utils/uploadFIle';
10
11
 
@@ -36,15 +37,18 @@ interface AvatarWithUploadProps {
36
37
 
37
38
  const AvatarWithUpload = memo<AvatarWithUploadProps>(
38
39
  ({ size = 40, compressSize = 256, style, id }) => {
39
- const [avatar, setSettings] = useGlobalStore((st) => [st.settings.avatar, st.setSettings]);
40
40
  const { styles } = useStyle();
41
+ const [avatar, updateAvatar] = useGlobalStore((s) => [
42
+ commonSelectors.userAvatar(s),
43
+ s.updateAvatar,
44
+ ]);
41
45
 
42
46
  const handleUploadAvatar = createUploadImageHandler((avatar) => {
43
47
  const img = new Image();
44
48
  img.src = avatar;
45
49
  img.addEventListener('load', () => {
46
50
  const webpBase64 = imageToBase64({ img, size: compressSize });
47
- setSettings({ avatar: webpBase64 });
51
+ updateAvatar(webpBase64);
48
52
  });
49
53
  });
50
54
 
@@ -6,7 +6,7 @@ import { memo } from 'react';
6
6
  import { useTranslation } from 'react-i18next';
7
7
 
8
8
  import { useGlobalStore } from '@/store/global';
9
- import { settingsSelectors } from '@/store/global/selectors';
9
+ import { modelProviderSelectors } from '@/store/global/selectors';
10
10
  import { useSessionStore } from '@/store/session';
11
11
  import { agentSelectors } from '@/store/session/selectors';
12
12
  import { LanguageModel } from '@/types/llm';
@@ -18,7 +18,7 @@ const ModelSwitch = memo(() => {
18
18
  return [agentSelectors.currentAgentModel(s), s.updateAgentConfig];
19
19
  });
20
20
 
21
- const modelList = useGlobalStore(settingsSelectors.modelList, isEqual);
21
+ const modelList = useGlobalStore(modelProviderSelectors.modelList, isEqual);
22
22
 
23
23
  return (
24
24
  <Dropdown
@@ -7,7 +7,7 @@ import { Center, Flexbox } from 'react-layout-kit';
7
7
 
8
8
  import { useChatStore } from '@/store/chat';
9
9
  import { useGlobalStore } from '@/store/global';
10
- import { settingsSelectors } from '@/store/global/selectors';
10
+ import { modelProviderSelectors } from '@/store/global/selectors';
11
11
 
12
12
  import { FormAction } from './style';
13
13
 
@@ -16,8 +16,8 @@ const APIKeyForm = memo<{ id: string }>(({ id }) => {
16
16
  const [showProxy, setShow] = useState(false);
17
17
 
18
18
  const [apiKey, proxyUrl, setConfig] = useGlobalStore((s) => [
19
- settingsSelectors.openAIAPI(s),
20
- settingsSelectors.openAIProxyUrl(s),
19
+ modelProviderSelectors.openAIAPI(s),
20
+ modelProviderSelectors.openAIProxyUrl(s),
21
21
  s.setOpenAIConfig,
22
22
  ]);
23
23
 
@@ -7,6 +7,7 @@ import { Flexbox } from 'react-layout-kit';
7
7
 
8
8
  import { useChatStore } from '@/store/chat';
9
9
  import { useGlobalStore } from '@/store/global';
10
+ import { settingsSelectors } from '@/store/global/selectors';
10
11
 
11
12
  import { RenderErrorMessage } from '../types';
12
13
  import APIKeyForm from './ApiKeyForm';
@@ -20,7 +21,10 @@ enum Tab {
20
21
  const InvalidAccess: RenderErrorMessage['Render'] = memo(({ id }) => {
21
22
  const { t } = useTranslation('error');
22
23
  const [mode, setMode] = useState<Tab>(Tab.Password);
23
- const [password, setSettings] = useGlobalStore((s) => [s.settings.password, s.setSettings]);
24
+ const [password, setSettings] = useGlobalStore((s) => [
25
+ settingsSelectors.currentSettings(s).password,
26
+ s.setSettings,
27
+ ]);
24
28
  const [resend, deleteMessage] = useChatStore((s) => [s.resendMessage, s.deleteMessage]);
25
29
 
26
30
  return (
@@ -1,7 +1,6 @@
1
- import { ActionIcon, Icon, Markdown, Tag } from '@lobehub/ui';
1
+ import { ActionIcon, Icon, Markdown, Tag, copyToClipboard } from '@lobehub/ui';
2
2
  import { App } from 'antd';
3
3
  import { createStyles } from 'antd-style';
4
- import copy from 'copy-to-clipboard';
5
4
  import { ChevronDown, ChevronUp, ChevronsRight, CopyIcon, TrashIcon } from 'lucide-react';
6
5
  import { memo, useState } from 'react';
7
6
  import { useTranslation } from 'react-i18next';
@@ -41,8 +40,8 @@ const Translate = memo<TranslateProps>(({ content = '', from, to, id, loading })
41
40
  <Flexbox horizontal>
42
41
  <ActionIcon
43
42
  icon={CopyIcon}
44
- onClick={() => {
45
- copy(content);
43
+ onClick={async () => {
44
+ await copyToClipboard(content);
46
45
  message.success(t('copySuccess'));
47
46
  }}
48
47
  size={'small'}
@@ -9,6 +9,7 @@ import {
9
9
  LOBE_THEME_PRIMARY_COLOR,
10
10
  } from '@/const/theme';
11
11
  import { useGlobalStore } from '@/store/global';
12
+ import { settingsSelectors } from '@/store/global/selectors';
12
13
  import { GlobalStyle } from '@/styles';
13
14
  import { setCookie } from '@/utils/cookie';
14
15
 
@@ -24,11 +25,11 @@ const AppTheme = memo<AppThemeProps>(
24
25
  // console.debug('server:appearance', defaultAppearance);
25
26
  // console.debug('server:primaryColor', defaultPrimaryColor);
26
27
  // console.debug('server:neutralColor', defaultNeutralColor);
27
- const themeMode = useGlobalStore((s) => s.settings.themeMode);
28
+ const themeMode = useGlobalStore((s) => settingsSelectors.currentSettings(s).themeMode);
28
29
 
29
30
  const [primaryColor, neutralColor] = useGlobalStore((s) => [
30
- s.settings.primaryColor,
31
- s.settings.neutralColor,
31
+ settingsSelectors.currentSettings(s).primaryColor,
32
+ settingsSelectors.currentSettings(s).neutralColor,
32
33
  ]);
33
34
 
34
35
  useEffect(() => {
@@ -5,6 +5,7 @@ import useSWR from 'swr';
5
5
  import { createI18nNext } from '@/locales/create';
6
6
  import { normalizeLocale } from '@/locales/resources';
7
7
  import { useOnFinishHydrationGlobal } from '@/store/global';
8
+ import { settingsSelectors } from '@/store/global/selectors';
8
9
  import { isOnServerSide } from '@/utils/env';
9
10
  import { switchLang } from '@/utils/switchLang';
10
11
 
@@ -48,7 +49,7 @@ const Locale = memo<LocaleLayoutProps>(({ children, defaultLang }) => {
48
49
  }
49
50
 
50
51
  useOnFinishHydrationGlobal((s) => {
51
- if (s.settings.language === 'auto') {
52
+ if (settingsSelectors.currentSettings(s).language === 'auto') {
52
53
  switchLang('auto');
53
54
  }
54
55
  }, []);
@@ -5,8 +5,13 @@ import { memo, useEffect } from 'react';
5
5
  import { useEffectAfterGlobalHydrated, useGlobalStore } from '@/store/global';
6
6
 
7
7
  const StoreHydration = memo(() => {
8
- const useFetchGlobalConfig = useGlobalStore((s) => s.useFetchGlobalConfig);
9
- useFetchGlobalConfig();
8
+ const [useFetchServerConfig, useFetchUserConfig] = useGlobalStore((s) => [
9
+ s.useFetchServerConfig,
10
+ s.useFetchUserConfig,
11
+ ]);
12
+ const { isLoading } = useFetchServerConfig();
13
+
14
+ useFetchUserConfig(!isLoading);
10
15
 
11
16
  useEffect(() => {
12
17
  // refs: https://github.com/pmndrs/zustand/blob/main/docs/integrations/persisting-store-data.md#hashydrated
@@ -6,10 +6,11 @@ import {
6
6
  USE_AZURE_OPENAI,
7
7
  } from '@/const/fetch';
8
8
  import { useGlobalStore } from '@/store/global';
9
+ import { modelProviderSelectors, settingsSelectors } from '@/store/global/selectors';
9
10
 
10
11
  // eslint-disable-next-line no-undef
11
12
  export const createHeaderWithOpenAI = (header?: HeadersInit): HeadersInit => {
12
- const openai = useGlobalStore.getState().settings.languageModel.openAI;
13
+ const openai = modelProviderSelectors.openAIConfig(useGlobalStore.getState());
13
14
 
14
15
  const apiKey = openai.OPENAI_API_KEY || '';
15
16
  const endpoint = openai.endpoint || '';
@@ -17,7 +18,7 @@ export const createHeaderWithOpenAI = (header?: HeadersInit): HeadersInit => {
17
18
  // eslint-disable-next-line no-undef
18
19
  const result: HeadersInit = {
19
20
  ...header,
20
- [LOBE_CHAT_ACCESS_CODE]: useGlobalStore.getState().settings.password || '',
21
+ [LOBE_CHAT_ACCESS_CODE]: settingsSelectors.password(useGlobalStore.getState()),
21
22
  [OPENAI_API_KEY_HEADER_KEY]: apiKey,
22
23
  [OPENAI_END_POINT]: endpoint,
23
24
  };