@lobehub/chat 1.86.1 → 1.87.1

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 +59 -0
  2. package/Dockerfile +1 -1
  3. package/Dockerfile.database +1 -1
  4. package/Dockerfile.pglite +1 -1
  5. package/changelog/v1.json +21 -0
  6. package/locales/ar/setting.json +43 -31
  7. package/locales/bg-BG/setting.json +43 -31
  8. package/locales/de-DE/setting.json +43 -31
  9. package/locales/en-US/setting.json +43 -31
  10. package/locales/es-ES/setting.json +43 -31
  11. package/locales/fa-IR/setting.json +43 -31
  12. package/locales/fr-FR/setting.json +43 -31
  13. package/locales/it-IT/setting.json +43 -31
  14. package/locales/ja-JP/setting.json +43 -31
  15. package/locales/ko-KR/setting.json +43 -31
  16. package/locales/nl-NL/setting.json +43 -31
  17. package/locales/pl-PL/setting.json +43 -31
  18. package/locales/pt-BR/setting.json +43 -31
  19. package/locales/ru-RU/setting.json +43 -31
  20. package/locales/tr-TR/setting.json +43 -31
  21. package/locales/vi-VN/setting.json +43 -31
  22. package/locales/zh-CN/setting.json +43 -31
  23. package/locales/zh-TW/setting.json +43 -31
  24. package/package.json +3 -3
  25. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatList/WelcomeChatItem/WelcomeMessage.tsx +1 -1
  26. package/src/app/[variants]/(main)/settings/agent/index.tsx +8 -2
  27. package/src/app/[variants]/(main)/settings/common/features/Appearance/Preview.tsx +298 -0
  28. package/src/app/[variants]/(main)/settings/common/features/{Theme → Appearance}/ThemeSwatches/ThemeSwatchesNeutral.tsx +6 -11
  29. package/src/app/[variants]/(main)/settings/common/features/{Theme → Appearance}/ThemeSwatches/ThemeSwatchesPrimary.tsx +6 -10
  30. package/src/app/[variants]/(main)/settings/common/features/Appearance/index.tsx +67 -0
  31. package/src/app/[variants]/(main)/settings/common/features/ChatAppearance/ChatPreview.tsx +35 -0
  32. package/src/app/[variants]/(main)/settings/common/features/ChatAppearance/HighlighterPreview.tsx +55 -0
  33. package/src/app/[variants]/(main)/settings/common/features/ChatAppearance/MermaidPreview.tsx +51 -0
  34. package/src/app/[variants]/(main)/settings/common/features/ChatAppearance/index.tsx +128 -0
  35. package/src/app/[variants]/(main)/settings/common/features/Common.tsx +74 -42
  36. package/src/app/[variants]/(main)/settings/common/index.tsx +4 -2
  37. package/src/app/[variants]/(main)/settings/hotkey/features/{HotkeySetting.tsx → Conversation.tsx} +19 -18
  38. package/src/app/[variants]/(main)/settings/hotkey/features/Essential.tsx +88 -0
  39. package/src/app/[variants]/(main)/settings/hotkey/page.tsx +8 -2
  40. package/src/app/[variants]/(main)/settings/llm/components/ProviderModelList/ModelConfigModal/index.tsx +1 -1
  41. package/src/app/[variants]/(main)/settings/storage/Advanced.tsx +26 -0
  42. package/src/app/[variants]/(main)/settings/system-agent/features/createForm.tsx +37 -22
  43. package/src/app/[variants]/(main)/settings/tts/features/OpenAI.tsx +20 -10
  44. package/src/app/[variants]/(main)/settings/tts/features/STT.tsx +20 -11
  45. package/src/config/aiModels/internlm.ts +21 -5
  46. package/src/config/aiModels/spark.ts +16 -14
  47. package/src/config/modelProviders/spark.ts +4 -0
  48. package/src/const/settings/common.ts +2 -0
  49. package/src/features/AgentSetting/AgentTTS/index.tsx +1 -1
  50. package/src/features/ChatItem/index.tsx +35 -0
  51. package/src/features/Conversation/components/ChatItem/index.tsx +2 -6
  52. package/src/features/User/UserPanel/LangButton.tsx +1 -1
  53. package/src/features/User/UserPanel/ThemeButton.tsx +3 -3
  54. package/src/libs/model-runtime/{AgentRuntime.test.ts → ModelRuntime.test.ts} +1 -1
  55. package/src/libs/model-runtime/{AgentRuntime.ts → ModelRuntime.ts} +3 -3
  56. package/src/libs/model-runtime/index.ts +1 -1
  57. package/src/libs/model-runtime/internlm/index.ts +15 -3
  58. package/src/libs/model-runtime/spark/index.test.ts +3 -0
  59. package/src/libs/model-runtime/spark/index.ts +23 -1
  60. package/src/libs/model-runtime/utils/streams/spark.test.ts +66 -0
  61. package/src/libs/model-runtime/utils/streams/spark.ts +31 -2
  62. package/src/libs/oidc-provider/config.ts +1 -1
  63. package/src/locales/default/setting.ts +45 -31
  64. package/src/store/electron/initialState.ts +1 -1
  65. package/src/store/electron/selectors/__tests__/desktopState.test.ts +55 -0
  66. package/src/store/global/selectors/systemStatus.ts +2 -0
  67. package/src/store/user/slices/settings/selectors/general.test.ts +29 -1
  68. package/src/store/user/slices/settings/selectors/general.ts +4 -0
  69. package/src/types/user/settings/general.ts +3 -1
  70. package/src/app/[variants]/(main)/settings/common/features/Theme/index.tsx +0 -146
  71. /package/src/app/[variants]/(main)/settings/common/features/{Theme → Appearance}/ThemeSwatches/index.ts +0 -0
@@ -1,14 +1,18 @@
1
1
  'use client';
2
2
 
3
- import { Button, Form, type FormGroupItemType, InputPassword } from '@lobehub/ui';
4
- import { App } from 'antd';
3
+ import { Form, type FormGroupItemType, Icon, ImageSelect, InputPassword } from '@lobehub/ui';
4
+ import { Select } from '@lobehub/ui';
5
+ import { Skeleton } from 'antd';
5
6
  import isEqual from 'fast-deep-equal';
6
- import { memo, useCallback } from 'react';
7
+ import { Loader2Icon, Monitor, Moon, Sun } from 'lucide-react';
8
+ import { memo, useState } from 'react';
7
9
  import { useTranslation } from 'react-i18next';
8
10
 
9
- import { useSyncSettings } from '@/app/[variants]/(main)/settings/hooks/useSyncSettings';
10
11
  import { FORM_STYLE } from '@/const/layoutTokens';
11
- import { DEFAULT_SETTINGS } from '@/const/settings';
12
+ import { imageUrl } from '@/const/url';
13
+ import { localeOptions } from '@/locales/resources';
14
+ import { useGlobalStore } from '@/store/global';
15
+ import { systemStatusSelectors } from '@/store/global/selectors';
12
16
  import { useServerConfigStore } from '@/store/serverConfig';
13
17
  import { serverConfigSelectors } from '@/store/serverConfig/selectors';
14
18
  import { useUserStore } from '@/store/user';
@@ -16,30 +20,67 @@ import { settingsSelectors } from '@/store/user/selectors';
16
20
 
17
21
  const Common = memo(() => {
18
22
  const { t } = useTranslation('setting');
19
- const [form] = Form.useForm();
20
23
 
21
24
  const showAccessCodeConfig = useServerConfigStore(serverConfigSelectors.enabledAccessCode);
22
-
23
25
  const settings = useUserStore(settingsSelectors.currentSettings, isEqual);
24
- const [setSettings, resetSettings] = useUserStore((s) => [s.setSettings, s.resetSettings]);
25
-
26
- const { message, modal } = App.useApp();
26
+ const themeMode = useGlobalStore(systemStatusSelectors.themeMode);
27
+ const language = useGlobalStore(systemStatusSelectors.language);
28
+ const [setSettings, isUserStateInit] = useUserStore((s) => [s.setSettings, s.isUserStateInit]);
29
+ const [setThemeMode, switchLocale, isStatusInit] = useGlobalStore((s) => [
30
+ s.switchThemeMode,
31
+ s.switchLocale,
32
+ s.isStatusInit,
33
+ ]);
34
+ const [loading, setLoading] = useState(false);
27
35
 
28
- const handleReset = useCallback(() => {
29
- modal.confirm({
30
- centered: true,
31
- okButtonProps: { danger: true },
32
- onOk: () => {
33
- resetSettings();
34
- form.setFieldsValue(DEFAULT_SETTINGS);
35
- message.success(t('danger.reset.success'));
36
- },
37
- title: t('danger.reset.confirm'),
38
- });
39
- }, []);
36
+ if (!(isStatusInit && isUserStateInit))
37
+ return <Skeleton active paragraph={{ rows: 5 }} title={false} />;
40
38
 
41
- const system: FormGroupItemType = {
39
+ const theme: FormGroupItemType = {
42
40
  children: [
41
+ {
42
+ children: (
43
+ <ImageSelect
44
+ height={60}
45
+ onChange={setThemeMode}
46
+ options={[
47
+ {
48
+ icon: Sun,
49
+ img: imageUrl('theme_light.webp'),
50
+ label: t('settingCommon.themeMode.light'),
51
+ value: 'light',
52
+ },
53
+ {
54
+ icon: Moon,
55
+ img: imageUrl('theme_dark.webp'),
56
+ label: t('settingCommon.themeMode.dark'),
57
+ value: 'dark',
58
+ },
59
+ {
60
+ icon: Monitor,
61
+ img: imageUrl('theme_auto.webp'),
62
+ label: t('settingCommon.themeMode.auto'),
63
+ value: 'auto',
64
+ },
65
+ ]}
66
+ unoptimized={false}
67
+ value={themeMode}
68
+ width={100}
69
+ />
70
+ ),
71
+ label: t('settingCommon.themeMode.title'),
72
+ minWidth: undefined,
73
+ },
74
+ {
75
+ children: (
76
+ <Select
77
+ defaultValue={language}
78
+ onChange={switchLocale}
79
+ options={[{ label: t('settingCommon.lang.autoMode'), value: 'auto' }, ...localeOptions]}
80
+ />
81
+ ),
82
+ label: t('settingCommon.lang.title'),
83
+ },
43
84
  {
44
85
  children: (
45
86
  <InputPassword
@@ -50,32 +91,23 @@ const Common = memo(() => {
50
91
  desc: t('settingSystem.accessCode.desc'),
51
92
  hidden: !showAccessCodeConfig,
52
93
  label: t('settingSystem.accessCode.title'),
53
- name: ['keyVaults', 'password'],
54
- },
55
- {
56
- children: (
57
- <Button danger onClick={handleReset} type={'primary'}>
58
- {t('danger.reset.action')}
59
- </Button>
60
- ),
61
- desc: t('danger.reset.desc'),
62
- label: t('danger.reset.title'),
63
- layout: 'horizontal',
64
- minWidth: undefined,
94
+ name: 'password',
65
95
  },
66
96
  ],
67
- title: t('settingSystem.title'),
97
+ extra: loading && <Icon icon={Loader2Icon} size={16} spin style={{ opacity: 0.5 }} />,
98
+ title: t('settingCommon.title'),
68
99
  };
69
100
 
70
- useSyncSettings(form);
71
-
72
101
  return (
73
102
  <Form
74
- form={form}
75
- initialValues={settings}
76
- items={[system]}
103
+ initialValues={settings.keyVaults}
104
+ items={[theme]}
77
105
  itemsType={'group'}
78
- onValuesChange={setSettings}
106
+ onValuesChange={async (v) => {
107
+ setLoading(true);
108
+ await setSettings({ keyVaults: v });
109
+ setLoading(false);
110
+ }}
79
111
  variant={'borderless'}
80
112
  {...FORM_STYLE}
81
113
  />
@@ -1,11 +1,13 @@
1
+ import Appearance from './features/Appearance';
2
+ import ChatAppearance from './features/ChatAppearance';
1
3
  import Common from './features/Common';
2
- import Theme from './features/Theme';
3
4
 
4
5
  const Page = () => {
5
6
  return (
6
7
  <>
7
- <Theme />
8
8
  <Common />
9
+ <Appearance />
10
+ <ChatAppearance />
9
11
  </>
10
12
  );
11
13
  };
@@ -1,8 +1,10 @@
1
1
  'use client';
2
2
 
3
- import { Form, type FormGroupItemType, HotkeyInput } from '@lobehub/ui';
3
+ import { Form, type FormGroupItemType, HotkeyInput, Icon } from '@lobehub/ui';
4
+ import { Skeleton } from 'antd';
4
5
  import isEqual from 'fast-deep-equal';
5
- import { memo } from 'react';
6
+ import { Loader2Icon } from 'lucide-react';
7
+ import { memo, useState } from 'react';
6
8
  import { useTranslation } from 'react-i18next';
7
9
 
8
10
  import { HOTKEYS_REGISTRATION } from '@/const/hotkeys';
@@ -20,17 +22,18 @@ const filterByDesktop = (item: HotkeyItem) => {
20
22
  if (!isDesktop) return !item.isDesktop;
21
23
  };
22
24
 
23
- const HOTKEY_SETTING_KEY = 'hotkey';
24
-
25
25
  const HotkeySetting = memo(() => {
26
26
  const { t } = useTranslation(['setting', 'hotkey']);
27
27
  const [form] = Form.useForm();
28
28
 
29
- const settings = useUserStore(settingsSelectors.currentSettings, isEqual);
30
- const [setSettings] = useUserStore((s) => [s.setSettings]);
29
+ const { hotkey } = useUserStore(settingsSelectors.currentSettings, isEqual);
30
+ const [setSettings, isUserStateInit] = useUserStore((s) => [s.setSettings, s.isUserStateInit]);
31
+ const [loading, setLoading] = useState(false);
32
+
33
+ if (!isUserStateInit) return <Skeleton active paragraph={{ rows: 5 }} title={false} />;
31
34
 
32
35
  const mapHotkeyItem = (item: HotkeyItem) => {
33
- const hotkeyConflicts = Object.entries(settings.hotkey)
36
+ const hotkeyConflicts = Object.entries(hotkey)
34
37
  .map(([key, value]) => {
35
38
  if (key === item.id) return false;
36
39
  return value;
@@ -53,31 +56,29 @@ const HotkeySetting = memo(() => {
53
56
  ),
54
57
  desc: hotkeyMeta[item.id].desc ? t(`${item.id}.desc`, { ns: 'hotkey' }) : undefined,
55
58
  label: t(`${item.id}.title`, { ns: 'hotkey' }),
56
- name: [HOTKEY_SETTING_KEY, item.id],
59
+ name: item.id,
57
60
  };
58
61
  };
59
62
 
60
- const essential: FormGroupItemType = {
61
- children: HOTKEYS_REGISTRATION.filter((item) => item.group === HotkeyGroupEnum.Essential)
62
- .filter((item) => filterByDesktop(item))
63
- .map((item) => mapHotkeyItem(item)),
64
- title: t('hotkey.group.essential'),
65
- };
66
-
67
63
  const conversation: FormGroupItemType = {
68
64
  children: HOTKEYS_REGISTRATION.filter((item) => item.group === HotkeyGroupEnum.Conversation)
69
65
  .filter((item) => filterByDesktop(item))
70
66
  .map((item) => mapHotkeyItem(item)),
67
+ extra: loading && <Icon icon={Loader2Icon} size={16} spin style={{ opacity: 0.5 }} />,
71
68
  title: t('hotkey.group.conversation'),
72
69
  };
73
70
 
74
71
  return (
75
72
  <Form
76
73
  form={form}
77
- initialValues={settings}
78
- items={[essential, conversation]}
74
+ initialValues={hotkey}
75
+ items={[conversation]}
79
76
  itemsType={'group'}
80
- onValuesChange={setSettings}
77
+ onValuesChange={async (values) => {
78
+ setLoading(true);
79
+ await setSettings({ hotkey: values });
80
+ setLoading(false);
81
+ }}
81
82
  variant={'borderless'}
82
83
  {...FORM_STYLE}
83
84
  />
@@ -0,0 +1,88 @@
1
+ 'use client';
2
+
3
+ import { Form, type FormGroupItemType, HotkeyInput, Icon } from '@lobehub/ui';
4
+ import { Skeleton } from 'antd';
5
+ import isEqual from 'fast-deep-equal';
6
+ import { Loader2Icon } from 'lucide-react';
7
+ import { memo, useState } from 'react';
8
+ import { useTranslation } from 'react-i18next';
9
+
10
+ import { HOTKEYS_REGISTRATION } from '@/const/hotkeys';
11
+ import { FORM_STYLE } from '@/const/layoutTokens';
12
+ import { isDesktop } from '@/const/version';
13
+ import hotkeyMeta from '@/locales/default/hotkey';
14
+ import { useUserStore } from '@/store/user';
15
+ import { settingsSelectors } from '@/store/user/selectors';
16
+ import { HotkeyGroupEnum, HotkeyItem } from '@/types/hotkey';
17
+
18
+ const filterByDesktop = (item: HotkeyItem) => {
19
+ if (isDesktop) return true;
20
+
21
+ // is not desktop, filter out desktop only items
22
+ if (!isDesktop) return !item.isDesktop;
23
+ };
24
+
25
+ const HotkeySetting = memo(() => {
26
+ const { t } = useTranslation(['setting', 'hotkey']);
27
+ const [form] = Form.useForm();
28
+
29
+ const { hotkey } = useUserStore(settingsSelectors.currentSettings, isEqual);
30
+ const [setSettings, isUserStateInit] = useUserStore((s) => [s.setSettings, s.isUserStateInit]);
31
+ const [loading, setLoading] = useState(false);
32
+
33
+ if (!isUserStateInit) return <Skeleton active paragraph={{ rows: 5 }} title={false} />;
34
+
35
+ const mapHotkeyItem = (item: HotkeyItem) => {
36
+ const hotkeyConflicts = Object.entries(hotkey)
37
+ .map(([key, value]) => {
38
+ if (key === item.id) return false;
39
+ return value;
40
+ })
41
+ .filter(Boolean) as string[];
42
+
43
+ return {
44
+ children: (
45
+ <HotkeyInput
46
+ disabled={item.nonEditable}
47
+ hotkeyConflicts={hotkeyConflicts}
48
+ placeholder={t('hotkey.record')}
49
+ resetValue={item.keys}
50
+ texts={{
51
+ conflicts: t('hotkey.conflicts'),
52
+ invalidCombination: t('hotkey.invalidCombination'),
53
+ reset: t('hotkey.reset'),
54
+ }}
55
+ />
56
+ ),
57
+ desc: hotkeyMeta[item.id].desc ? t(`${item.id}.desc`, { ns: 'hotkey' }) : undefined,
58
+ label: t(`${item.id}.title`, { ns: 'hotkey' }),
59
+ name: item.id,
60
+ };
61
+ };
62
+
63
+ const essential: FormGroupItemType = {
64
+ children: HOTKEYS_REGISTRATION.filter((item) => item.group === HotkeyGroupEnum.Essential)
65
+ .filter((item) => filterByDesktop(item))
66
+ .map((item) => mapHotkeyItem(item)),
67
+ extra: loading && <Icon icon={Loader2Icon} size={16} spin style={{ opacity: 0.5 }} />,
68
+ title: t('hotkey.group.essential'),
69
+ };
70
+
71
+ return (
72
+ <Form
73
+ form={form}
74
+ initialValues={hotkey}
75
+ items={[essential]}
76
+ itemsType={'group'}
77
+ onValuesChange={async (values) => {
78
+ setLoading(true);
79
+ await setSettings({ hotkey: values });
80
+ setLoading(false);
81
+ }}
82
+ variant={'borderless'}
83
+ {...FORM_STYLE}
84
+ />
85
+ );
86
+ });
87
+
88
+ export default HotkeySetting;
@@ -3,7 +3,8 @@ import { translation } from '@/server/translation';
3
3
  import { DynamicLayoutProps } from '@/types/next';
4
4
  import { RouteVariants } from '@/utils/server/routeVariants';
5
5
 
6
- import HotkeySetting from './features/HotkeySetting';
6
+ import Conversation from './features/Conversation';
7
+ import Essential from './features/Essential';
7
8
 
8
9
  export const generateMetadata = async (props: DynamicLayoutProps) => {
9
10
  const locale = await RouteVariants.getLocale(props);
@@ -16,7 +17,12 @@ export const generateMetadata = async (props: DynamicLayoutProps) => {
16
17
  };
17
18
 
18
19
  const Page = () => {
19
- return <HotkeySetting />;
20
+ return (
21
+ <>
22
+ <Essential />
23
+ <Conversation />
24
+ </>
25
+ );
20
26
  };
21
27
 
22
28
  Page.displayName = 'HotkeySetting';
@@ -5,7 +5,7 @@ import { memo, useState } from 'react';
5
5
  import { useTranslation } from 'react-i18next';
6
6
 
7
7
  import { useUserStore } from '@/store/user';
8
- import { modelConfigSelectors } from '@/store/user/slices/modelList/selectors';
8
+ import { modelConfigSelectors } from '@/store/user/selectors';
9
9
 
10
10
  import ModelConfigForm from './Form';
11
11
 
@@ -8,6 +8,7 @@ import { useCallback } from 'react';
8
8
  import { useTranslation } from 'react-i18next';
9
9
 
10
10
  import { FORM_STYLE } from '@/const/layoutTokens';
11
+ import { DEFAULT_SETTINGS } from '@/const/settings';
11
12
  import DataImporter from '@/features/DataImporter';
12
13
  import { configService } from '@/services/config';
13
14
  import { useChatStore } from '@/store/chat';
@@ -32,6 +33,7 @@ const AdvancedActions = () => {
32
33
  const [removeAllFiles] = useFileStore((s) => [s.removeAllFiles]);
33
34
  const removeAllPlugins = useToolStore((s) => s.removeAllPlugins);
34
35
  const settings = useUserStore(settingsSelectors.currentSettings, isEqual);
36
+ const [resetSettings] = useUserStore((s) => [s.resetSettings]);
35
37
 
36
38
  const handleClear = useCallback(() => {
37
39
  modal.confirm({
@@ -53,6 +55,19 @@ const AdvancedActions = () => {
53
55
  });
54
56
  }, []);
55
57
 
58
+ const handleReset = useCallback(() => {
59
+ modal.confirm({
60
+ centered: true,
61
+ okButtonProps: { danger: true },
62
+ onOk: () => {
63
+ resetSettings();
64
+ form.setFieldsValue(DEFAULT_SETTINGS);
65
+ message.success(t('danger.reset.success'));
66
+ },
67
+ title: t('danger.reset.confirm'),
68
+ });
69
+ }, []);
70
+
56
71
  const system: FormGroupItemType = {
57
72
  children: [
58
73
  {
@@ -93,6 +108,17 @@ const AdvancedActions = () => {
93
108
  layout: 'horizontal',
94
109
  minWidth: undefined,
95
110
  },
111
+ {
112
+ children: (
113
+ <Button danger onClick={handleReset} type={'primary'}>
114
+ {t('danger.reset.action')}
115
+ </Button>
116
+ ),
117
+ desc: t('danger.reset.desc'),
118
+ label: t('danger.reset.title'),
119
+ layout: 'horizontal',
120
+ minWidth: undefined,
121
+ },
96
122
  ],
97
123
  title: t('storage.actions.title'),
98
124
  };
@@ -1,11 +1,12 @@
1
1
  'use client';
2
2
 
3
- import { Button, Form, type FormGroupItemType, type FormItemProps } from '@lobehub/ui';
4
- import { Form as AntForm, Switch } from 'antd';
3
+ import { Button, Form, type FormGroupItemType, type FormItemProps, Icon } from '@lobehub/ui';
4
+ import { Form as AntForm, Skeleton, Switch } from 'antd';
5
5
  import isEqual from 'fast-deep-equal';
6
- import { PencilIcon } from 'lucide-react';
7
- import { memo } from 'react';
6
+ import { Loader2Icon, PencilIcon } from 'lucide-react';
7
+ import { memo, useState } from 'react';
8
8
  import { useTranslation } from 'react-i18next';
9
+ import { Flexbox } from 'react-layout-kit';
9
10
 
10
11
  import TextArea from '@/components/TextArea';
11
12
  import { FORM_STYLE } from '@/const/layoutTokens';
@@ -14,8 +15,6 @@ import { useUserStore } from '@/store/user';
14
15
  import { settingsSelectors } from '@/store/user/selectors';
15
16
  import { type UserSystemAgentConfigKey } from '@/types/user/settings';
16
17
 
17
- import { useSyncSystemAgent } from './useSync';
18
-
19
18
  interface SystemAgentFormProps {
20
19
  allowCustomPrompt?: boolean;
21
20
  allowDisable?: boolean;
@@ -26,11 +25,16 @@ interface SystemAgentFormProps {
26
25
  const SystemAgentForm = memo(
27
26
  ({ systemAgentKey, allowDisable, allowCustomPrompt, defaultPrompt }: SystemAgentFormProps) => {
28
27
  const { t } = useTranslation('setting');
29
-
28
+ const [form] = AntForm.useForm();
30
29
  const settings = useUserStore(settingsSelectors.currentSystemAgent, isEqual);
31
- const [updateSystemAgent] = useUserStore((s) => [s.updateSystemAgent]);
30
+ const [updateSystemAgent, isUserStateInit] = useUserStore((s) => [
31
+ s.updateSystemAgent,
32
+ s.isUserStateInit,
33
+ ]);
34
+ const [loading, setLoading] = useState(false);
35
+
36
+ if (!isUserStateInit) return <Skeleton active paragraph={{ rows: 5 }} title={false} />;
32
37
 
33
- const [form] = AntForm.useForm();
34
38
  const value = settings[systemAgentKey];
35
39
 
36
40
  const systemAgentSettings: FormGroupItemType = {
@@ -38,8 +42,10 @@ const SystemAgentForm = memo(
38
42
  {
39
43
  children: (
40
44
  <ModelSelect
41
- onChange={(props) => {
42
- updateSystemAgent(systemAgentKey, props);
45
+ onChange={async (props) => {
46
+ setLoading(true);
47
+ await updateSystemAgent(systemAgentKey, props);
48
+ setLoading(false);
43
49
  }}
44
50
  showAbility={false}
45
51
  // value={value}
@@ -52,8 +58,10 @@ const SystemAgentForm = memo(
52
58
  (!!allowCustomPrompt && {
53
59
  children: !!value.customPrompt ? (
54
60
  <TextArea
55
- onChange={(e) => {
56
- updateSystemAgent(systemAgentKey, { customPrompt: e });
61
+ onBlur={async (e) => {
62
+ setLoading(true);
63
+ await updateSystemAgent(systemAgentKey, { customPrompt: e.target.value });
64
+ setLoading(false);
57
65
  }}
58
66
  placeholder={t('systemAgent.customPrompt.placeholder')}
59
67
  style={{ minHeight: 160 }}
@@ -64,7 +72,9 @@ const SystemAgentForm = memo(
64
72
  block
65
73
  icon={PencilIcon}
66
74
  onClick={async () => {
75
+ setLoading(true);
67
76
  await updateSystemAgent(systemAgentKey, { customPrompt: defaultPrompt });
77
+ setLoading(false);
68
78
  }}
69
79
  >
70
80
  {t('systemAgent.customPrompt.addPrompt')}
@@ -75,13 +85,20 @@ const SystemAgentForm = memo(
75
85
  name: [systemAgentKey, 'customPrompt'],
76
86
  }) as FormItemProps,
77
87
  ].filter(Boolean),
78
- extra: allowDisable && (
79
- <Switch
80
- onChange={(enabled) => {
81
- updateSystemAgent(systemAgentKey, { enabled });
82
- }}
83
- value={value.enabled}
84
- />
88
+ extra: (
89
+ <Flexbox>
90
+ {loading && <Icon icon={Loader2Icon} size={16} spin style={{ opacity: 0.5 }} />}
91
+ {allowDisable && (
92
+ <Switch
93
+ onChange={async (enabled) => {
94
+ setLoading(true);
95
+ await updateSystemAgent(systemAgentKey, { enabled });
96
+ setLoading(false);
97
+ }}
98
+ value={value.enabled}
99
+ />
100
+ )}
101
+ </Flexbox>
85
102
  ),
86
103
  title: (
87
104
  <span
@@ -94,8 +111,6 @@ const SystemAgentForm = memo(
94
111
  ),
95
112
  };
96
113
 
97
- useSyncSystemAgent(form, settings);
98
-
99
114
  return (
100
115
  <Form form={form} initialValues={settings} items={[systemAgentSettings]} {...FORM_STYLE} />
101
116
  );
@@ -1,9 +1,11 @@
1
1
  'use client';
2
2
 
3
- import { Form, type FormGroupItemType } from '@lobehub/ui';
3
+ import { Form, type FormGroupItemType, Icon } from '@lobehub/ui';
4
4
  import { Select } from '@lobehub/ui';
5
+ import { Skeleton } from 'antd';
5
6
  import isEqual from 'fast-deep-equal';
6
- import { memo } from 'react';
7
+ import { Loader2Icon } from 'lucide-react';
8
+ import { memo, useState } from 'react';
7
9
  import { useTranslation } from 'react-i18next';
8
10
 
9
11
  import { FORM_STYLE } from '@/const/layoutTokens';
@@ -12,37 +14,45 @@ import { settingsSelectors } from '@/store/user/selectors';
12
14
 
13
15
  import { opeanaiSTTOptions, opeanaiTTSOptions } from './const';
14
16
 
15
- const TTS_SETTING_KEY = 'tts';
16
-
17
17
  const OpenAI = memo(() => {
18
18
  const { t } = useTranslation('setting');
19
19
  const [form] = Form.useForm();
20
- const settings = useUserStore(settingsSelectors.currentSettings, isEqual);
21
- const [setSettings] = useUserStore((s) => [s.setSettings]);
20
+ const { tts } = useUserStore(settingsSelectors.currentSettings, isEqual);
21
+ const [setSettings, isUserStateInit] = useUserStore((s) => [s.setSettings, s.isUserStateInit]);
22
+ const [loading, setLoading] = useState(false);
23
+
24
+ if (!isUserStateInit) return <Skeleton active paragraph={{ rows: 5 }} title={false} />;
22
25
 
23
26
  const openai: FormGroupItemType = {
24
27
  children: [
25
28
  {
26
29
  children: <Select options={opeanaiTTSOptions} />,
27
30
  label: t('settingTTS.openai.ttsModel'),
28
- name: [TTS_SETTING_KEY, 'openAI', 'ttsModel'],
31
+ name: ['openAI', 'ttsModel'],
29
32
  },
30
33
  {
31
34
  children: <Select options={opeanaiSTTOptions} />,
32
35
  label: t('settingTTS.openai.sttModel'),
33
- name: [TTS_SETTING_KEY, 'openAI', 'sttModel'],
36
+ name: ['openAI', 'sttModel'],
34
37
  },
35
38
  ],
39
+ extra: loading && <Icon icon={Loader2Icon} size={16} spin style={{ opacity: 0.5 }} />,
36
40
  title: t('settingTTS.openai.title'),
37
41
  };
38
42
 
39
43
  return (
40
44
  <Form
41
45
  form={form}
42
- initialValues={settings}
46
+ initialValues={tts}
43
47
  items={[openai]}
44
48
  itemsType={'group'}
45
- onValuesChange={setSettings}
49
+ onValuesChange={async (values) => {
50
+ setLoading(true);
51
+ await setSettings({
52
+ tts: values,
53
+ });
54
+ setLoading(false);
55
+ }}
46
56
  variant={'borderless'}
47
57
  {...FORM_STYLE}
48
58
  />
@@ -1,9 +1,10 @@
1
1
  'use client';
2
2
 
3
- import { Form, type FormGroupItemType, Select } from '@lobehub/ui';
4
- import { Switch } from 'antd';
3
+ import { Form, type FormGroupItemType, Icon, Select } from '@lobehub/ui';
4
+ import { Skeleton, Switch } from 'antd';
5
5
  import isEqual from 'fast-deep-equal';
6
- import { memo } from 'react';
6
+ import { Loader2Icon } from 'lucide-react';
7
+ import { memo, useState } from 'react';
7
8
  import { useTranslation } from 'react-i18next';
8
9
 
9
10
  import { FORM_STYLE } from '@/const/layoutTokens';
@@ -12,13 +13,14 @@ import { settingsSelectors } from '@/store/user/selectors';
12
13
 
13
14
  import { sttOptions } from './const';
14
15
 
15
- const TTS_SETTING_KEY = 'tts';
16
-
17
16
  const STT = memo(() => {
18
17
  const { t } = useTranslation('setting');
19
18
  const [form] = Form.useForm();
20
- const settings = useUserStore(settingsSelectors.currentSettings, isEqual);
21
- const [setSettings] = useUserStore((s) => [s.setSettings]);
19
+ const { tts } = useUserStore(settingsSelectors.currentSettings, isEqual);
20
+ const [setSettings, isUserStateInit] = useUserStore((s) => [s.setSettings, s.isUserStateInit]);
21
+ const [loading, setLoading] = useState(false);
22
+
23
+ if (!isUserStateInit) return <Skeleton active paragraph={{ rows: 5 }} title={false} />;
22
24
 
23
25
  const stt: FormGroupItemType = {
24
26
  children: [
@@ -26,7 +28,7 @@ const STT = memo(() => {
26
28
  children: <Select options={sttOptions} />,
27
29
  desc: t('settingTTS.sttService.desc'),
28
30
  label: t('settingTTS.sttService.title'),
29
- name: [TTS_SETTING_KEY, 'sttServer'],
31
+ name: 'sttServer',
30
32
  },
31
33
  {
32
34
  children: <Switch />,
@@ -34,20 +36,27 @@ const STT = memo(() => {
34
36
  label: t('settingTTS.sttAutoStop.title'),
35
37
  layout: 'horizontal',
36
38
  minWidth: undefined,
37
- name: [TTS_SETTING_KEY, 'sttAutoStop'],
39
+ name: 'sttAutoStop',
38
40
  valuePropName: 'checked',
39
41
  },
40
42
  ],
43
+ extra: loading && <Icon icon={Loader2Icon} size={16} spin style={{ opacity: 0.5 }} />,
41
44
  title: t('settingTTS.stt'),
42
45
  };
43
46
 
44
47
  return (
45
48
  <Form
46
49
  form={form}
47
- initialValues={settings}
50
+ initialValues={tts}
48
51
  items={[stt]}
49
52
  itemsType={'group'}
50
- onValuesChange={setSettings}
53
+ onValuesChange={async (values) => {
54
+ setLoading(true);
55
+ await setSettings({
56
+ tts: values,
57
+ });
58
+ setLoading(false);
59
+ }}
51
60
  variant={'borderless'}
52
61
  {...FORM_STYLE}
53
62
  />