@lobehub/chat 0.122.2 → 0.122.3

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 (71) hide show
  1. package/CHANGELOG.md +25 -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/chat/(mobile)/features/SessionHeader.tsx +2 -1
  9. package/src/app/chat/features/ChatHeader/ShareButton/ShareModal.tsx +2 -1
  10. package/src/app/chat/settings/features/SubmitAgentButton/SubmitAgentModal.tsx +2 -1
  11. package/src/app/settings/(mobile)/mobile/index.tsx +2 -1
  12. package/src/app/settings/common/Common.tsx +14 -3
  13. package/src/app/settings/features/ThemeSwatches/ThemeSwatchesNeutral.tsx +2 -1
  14. package/src/app/settings/features/ThemeSwatches/ThemeSwatchesPrimary.tsx +2 -1
  15. package/src/app/settings/llm/LLM/index.tsx +17 -10
  16. package/src/const/settings.ts +0 -1
  17. package/src/database/core/db.ts +41 -2
  18. package/src/database/core/migrations/migrateSettingsToUser/fixtures/input.json +55 -0
  19. package/src/database/core/migrations/migrateSettingsToUser/fixtures/output.json +60 -0
  20. package/src/database/core/migrations/migrateSettingsToUser/index.test.ts +13 -0
  21. package/src/database/core/migrations/migrateSettingsToUser/index.ts +39 -0
  22. package/src/database/core/schemas.ts +10 -0
  23. package/src/database/models/user.ts +54 -0
  24. package/src/database/schemas/user.ts +57 -0
  25. package/src/features/AgentSetting/AgentConfig/index.tsx +2 -2
  26. package/src/features/AvatarWithUpload/index.tsx +6 -2
  27. package/src/features/ChatInput/ActionBar/ModelSwitch.tsx +2 -2
  28. package/src/features/Conversation/Error/ApiKeyForm.tsx +3 -3
  29. package/src/features/Conversation/Error/InvalidAccess.tsx +5 -1
  30. package/src/features/Conversation/Extras/Translate.tsx +3 -4
  31. package/src/layout/GlobalLayout/AppTheme.tsx +4 -3
  32. package/src/layout/GlobalLayout/Locale.tsx +2 -1
  33. package/src/layout/GlobalLayout/StoreHydration.tsx +7 -2
  34. package/src/services/_header.ts +3 -2
  35. package/src/services/user.ts +31 -0
  36. package/src/store/chat/slices/message/selectors.test.ts +7 -2
  37. package/src/store/chat/slices/message/selectors.ts +2 -1
  38. package/src/store/global/hooks/index.ts +0 -1
  39. package/src/store/global/initialState.ts +3 -1
  40. package/src/store/global/selectors.ts +1 -0
  41. package/src/store/global/slices/common/action.test.ts +2 -79
  42. package/src/store/global/slices/common/action.ts +37 -61
  43. package/src/store/global/slices/common/initialState.ts +0 -34
  44. package/src/store/global/slices/common/selectors.ts +2 -7
  45. package/src/store/global/slices/preference/action.test.ts +92 -0
  46. package/src/store/global/slices/preference/action.ts +75 -0
  47. package/src/store/global/slices/preference/initialState.ts +45 -0
  48. package/src/store/global/slices/preference/selectors.ts +10 -0
  49. package/src/store/global/slices/settings/action.test.ts +68 -39
  50. package/src/store/global/slices/settings/action.ts +26 -50
  51. package/src/store/global/slices/settings/initialState.ts +8 -6
  52. package/src/store/global/slices/settings/selectors/__snapshots__/modelProvider.test.ts.snap +100 -0
  53. package/src/store/global/slices/settings/selectors/__snapshots__/selectors.test.ts.snap +132 -0
  54. package/src/store/global/slices/settings/selectors/index.ts +2 -0
  55. package/src/store/global/slices/settings/selectors/modelProvider.test.ts +100 -0
  56. package/src/store/global/slices/settings/selectors/modelProvider.ts +64 -0
  57. package/src/store/global/slices/settings/{selectors.test.ts → selectors/selectors.test.ts} +2 -97
  58. package/src/store/global/slices/settings/selectors/settings.ts +58 -0
  59. package/src/store/global/store.ts +11 -36
  60. package/src/store/session/slices/agent/action.ts +1 -1
  61. package/src/types/settings/base.ts +13 -0
  62. package/src/types/settings/index.ts +34 -0
  63. package/src/types/settings/modelProvider.ts +17 -0
  64. package/src/types/settings/tts.ts +10 -0
  65. package/src/utils/difference.ts +12 -0
  66. package/tests/utils.tsx +8 -0
  67. package/tsconfig.json +9 -8
  68. package/vitest.config.ts +1 -0
  69. package/src/store/global/hooks/useHydrated.ts +0 -18
  70. package/src/store/global/slices/settings/selectors.ts +0 -105
  71. package/src/types/settings.ts +0 -68
@@ -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
  };
@@ -0,0 +1,31 @@
1
+ import { DeepPartial } from 'utility-types';
2
+
3
+ import { UserModel } from '@/database/models/user';
4
+ import { GlobalSettings } from '@/types/settings';
5
+
6
+ export interface UserConfig {
7
+ avatar?: string;
8
+ // settings: JSONPatch
9
+ settings: DeepPartial<GlobalSettings>;
10
+ }
11
+
12
+ class UserService {
13
+ getUserConfig = async () => {
14
+ const user = await UserModel.getUser();
15
+ return user as unknown as UserConfig;
16
+ };
17
+
18
+ updateUserSettings = async (patch: DeepPartial<GlobalSettings>) => {
19
+ return UserModel.updateSettings(patch);
20
+ };
21
+
22
+ resetUserSettings = async () => {
23
+ return UserModel.resetSettings();
24
+ };
25
+
26
+ updateAvatar(avatar: string) {
27
+ return UserModel.updateAvatar(avatar);
28
+ }
29
+ }
30
+
31
+ export const userService = new UserService();
@@ -1,9 +1,11 @@
1
+ import { act } from '@testing-library/react';
1
2
  import { describe, expect, it } from 'vitest';
2
3
 
3
4
  import { DEFAULT_INBOX_AVATAR } from '@/const/meta';
4
5
  import { INBOX_SESSION_ID } from '@/const/session';
5
6
  import { ChatStore } from '@/store/chat';
6
7
  import { initialState } from '@/store/chat/initialState';
8
+ import { useGlobalStore } from '@/store/global';
7
9
  import { useSessionStore } from '@/store/session';
8
10
  import { ChatMessage } from '@/types/message';
9
11
  import { MetaData } from '@/types/meta';
@@ -155,8 +157,11 @@ describe('chatSelectors', () => {
155
157
  });
156
158
  it('should slice the messages according to config, assuming historyCount is mocked to 2', async () => {
157
159
  const state = merge(initialStore, { messages: mockMessages });
158
-
159
- useSessionStore.getState().updateAgentConfig({ historyCount: 2, enableHistoryCount: true });
160
+ act(() => {
161
+ useGlobalStore.setState({
162
+ settings: { defaultAgent: { config: { historyCount: 2, enableHistoryCount: true } } },
163
+ });
164
+ });
160
165
 
161
166
  const chats = chatSelectors.currentChatsWithHistoryConfig(state);
162
167
 
@@ -4,6 +4,7 @@ import { t } from 'i18next';
4
4
  import { DEFAULT_INBOX_AVATAR, DEFAULT_USER_AVATAR } from '@/const/meta';
5
5
  import { INBOX_SESSION_ID } from '@/const/session';
6
6
  import { useGlobalStore } from '@/store/global';
7
+ import { commonSelectors } from '@/store/global/selectors';
7
8
  import { useSessionStore } from '@/store/session';
8
9
  import { agentSelectors } from '@/store/session/selectors';
9
10
  import { ChatMessage } from '@/types/message';
@@ -17,7 +18,7 @@ const getMeta = (message: ChatMessage) => {
17
18
  switch (message.role) {
18
19
  case 'user': {
19
20
  return {
20
- avatar: useGlobalStore.getState().settings.avatar || DEFAULT_USER_AVATAR,
21
+ avatar: commonSelectors.userAvatar(useGlobalStore.getState()) || DEFAULT_USER_AVATAR,
21
22
  };
22
23
  }
23
24