@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
@@ -0,0 +1,298 @@
1
+ import { Block } from '@lobehub/ui';
2
+ import { createStyles } from 'antd-style';
3
+ import { rgba } from 'polished';
4
+ import { memo } from 'react';
5
+ import { Flexbox } from 'react-layout-kit';
6
+
7
+ const useStyles = createStyles(({ css, token }) => {
8
+ return {
9
+ agent: css`
10
+ padding: 4px;
11
+ border-radius: 2px;
12
+ `,
13
+ agentActive: css`
14
+ background: ${token.colorFillSecondary};
15
+ `,
16
+ bubble: css`
17
+ padding: 6px;
18
+ border: 1px solid ${rgba(token.colorBorderSecondary, 0.66)};
19
+ border-radius: 3px;
20
+ background-color: ${token.colorBgContainer};
21
+ `,
22
+ container: css`
23
+ overflow: hidden;
24
+ justify-self: flex-end;
25
+
26
+ width: 332px;
27
+ height: 200px;
28
+ border: 1px solid ${token.colorBorder};
29
+ border-radius: ${token.borderRadiusLG}px;
30
+
31
+ background: ${token.colorBgLayout};
32
+ `,
33
+ conversation: css`
34
+ background: ${token.colorBgContainerSecondary};
35
+ `,
36
+ header: css`
37
+ border-block-end: 1px solid ${token.colorBorderSecondary};
38
+ `,
39
+ icon: css`
40
+ flex: none;
41
+ border-radius: 2px;
42
+ background: ${token.colorFillSecondary};
43
+ `,
44
+ input: css`
45
+ border-block-start: 1px solid ${token.colorBorderSecondary};
46
+ `,
47
+ nav: css`
48
+ padding: 4px;
49
+ border-inline-end: 1px solid ${token.colorBorderSecondary};
50
+ background: ${token.colorBgLayout};
51
+ `,
52
+ sidebar: css`
53
+ padding: 4px;
54
+ border-inline-end: 1px solid ${token.colorBorderSecondary};
55
+ background: ${token.colorBgLayout};
56
+ `,
57
+ };
58
+ });
59
+
60
+ const AgentItem = memo<{
61
+ active?: boolean;
62
+ color?: string;
63
+ }>(({ active, color }) => {
64
+ const { cx, styles, theme } = useStyles();
65
+ return (
66
+ <Flexbox
67
+ align={'center'}
68
+ className={cx(styles.agent, active && styles.agentActive)}
69
+ gap={4}
70
+ horizontal
71
+ width={'100%'}
72
+ >
73
+ <Flexbox
74
+ className={styles.icon}
75
+ height={12}
76
+ style={{ background: color, borderRadius: '50%' }}
77
+ width={12}
78
+ />
79
+ <Flexbox flex={1} gap={4}>
80
+ <Flexbox
81
+ className={styles.icon}
82
+ height={2}
83
+ style={{
84
+ background: theme.colorTextTertiary,
85
+ }}
86
+ width={'66%'}
87
+ />
88
+ <Flexbox
89
+ className={styles.icon}
90
+ height={2}
91
+ style={{
92
+ background: theme.colorTextQuaternary,
93
+ }}
94
+ width={'100%'}
95
+ />
96
+ </Flexbox>
97
+ </Flexbox>
98
+ );
99
+ });
100
+
101
+ const Preview = memo(() => {
102
+ const { styles, theme } = useStyles();
103
+
104
+ const nav = (
105
+ <Flexbox align={'center'} className={styles.nav} gap={8} width={24}>
106
+ <Flexbox
107
+ className={styles.icon}
108
+ height={14}
109
+ style={{ border: `2px solid ${theme.colorPrimary}`, borderRadius: '50%' }}
110
+ width={14}
111
+ />
112
+ <Flexbox className={styles.icon} height={12} width={12} />
113
+ <Flexbox className={styles.icon} height={12} width={12} />
114
+ <Flexbox className={styles.icon} height={12} width={12} />
115
+ </Flexbox>
116
+ );
117
+
118
+ const sidebar = (
119
+ <Flexbox className={styles.sidebar} gap={4} width={72}>
120
+ <Flexbox
121
+ gap={4}
122
+ paddingInline={2}
123
+ style={{
124
+ paddingTop: 4,
125
+ }}
126
+ >
127
+ <Flexbox className={styles.icon} height={8} width={'50%'} />
128
+ <Flexbox
129
+ className={styles.icon}
130
+ height={8}
131
+ style={{
132
+ background: theme.colorFillTertiary,
133
+ }}
134
+ width={'100%'}
135
+ />
136
+ </Flexbox>
137
+ <AgentItem />
138
+ <AgentItem active />
139
+ <AgentItem />
140
+ <AgentItem />
141
+ </Flexbox>
142
+ );
143
+
144
+ const header = (
145
+ <Flexbox
146
+ align={'center'}
147
+ className={styles.header}
148
+ horizontal
149
+ justify={'space-between'}
150
+ padding={4}
151
+ >
152
+ <Flexbox align={'center'} gap={4} horizontal>
153
+ <Flexbox className={styles.icon} height={12} style={{ borderRadius: '50%' }} width={12} />
154
+ <Flexbox className={styles.icon} height={8} width={32} />
155
+ </Flexbox>
156
+ <Flexbox gap={2} horizontal>
157
+ <Flexbox className={styles.icon} height={10} width={10} />
158
+ <Flexbox className={styles.icon} height={10} width={10} />
159
+ </Flexbox>
160
+ </Flexbox>
161
+ );
162
+
163
+ const input = (
164
+ <Flexbox
165
+ align={'flex-end'}
166
+ className={styles.input}
167
+ height={48}
168
+ justify={'flex-end'}
169
+ padding={8}
170
+ >
171
+ <Flexbox
172
+ className={styles.icon}
173
+ height={12}
174
+ style={{
175
+ background: theme.colorPrimary,
176
+ }}
177
+ width={32}
178
+ />
179
+ </Flexbox>
180
+ );
181
+
182
+ return (
183
+ <Block className={styles.container} horizontal shadow variant={'outlined'}>
184
+ {nav}
185
+ {sidebar}
186
+ <Flexbox className={styles.conversation} flex={1}>
187
+ {header}
188
+ <Flexbox align={'flex-start'} flex={1} gap={8} padding={6}>
189
+ <Flexbox align={'center'} gap={4} horizontal justify={'flex-end'} width={'100%'}>
190
+ <Flexbox className={styles.bubble} gap={4} width={64}>
191
+ <Flexbox
192
+ className={styles.icon}
193
+ height={2}
194
+ style={{
195
+ background: theme.colorTextQuaternary,
196
+ }}
197
+ width={'100%'}
198
+ />
199
+ <Flexbox
200
+ className={styles.icon}
201
+ height={2}
202
+ style={{
203
+ background: theme.colorTextQuaternary,
204
+ }}
205
+ width={'66%'}
206
+ />
207
+ </Flexbox>
208
+ <Flexbox
209
+ className={styles.icon}
210
+ height={14}
211
+ style={{ borderRadius: '50%' }}
212
+ width={14}
213
+ />
214
+ </Flexbox>
215
+ <Flexbox gap={4} horizontal>
216
+ <Flexbox
217
+ className={styles.icon}
218
+ height={14}
219
+ style={{ borderRadius: '50%' }}
220
+ width={14}
221
+ />
222
+ <Flexbox className={styles.bubble} gap={4} width={160}>
223
+ <Flexbox
224
+ className={styles.icon}
225
+ height={2}
226
+ style={{
227
+ background: theme.colorTextQuaternary,
228
+ }}
229
+ width={'100%'}
230
+ />
231
+ <Flexbox
232
+ className={styles.icon}
233
+ height={2}
234
+ style={{
235
+ background: theme.colorTextQuaternary,
236
+ }}
237
+ width={'66%'}
238
+ />
239
+ <Flexbox
240
+ className={styles.icon}
241
+ height={2}
242
+ style={{
243
+ background: theme.colorTextQuaternary,
244
+ }}
245
+ width={'100%'}
246
+ />
247
+ <Flexbox
248
+ className={styles.icon}
249
+ height={2}
250
+ style={{
251
+ background: theme.colorTextQuaternary,
252
+ }}
253
+ width={'100%'}
254
+ />
255
+ <Flexbox
256
+ className={styles.icon}
257
+ height={2}
258
+ style={{
259
+ background: theme.colorTextQuaternary,
260
+ }}
261
+ width={'33%'}
262
+ />
263
+ </Flexbox>
264
+ </Flexbox>
265
+ <Flexbox align={'center'} gap={4} horizontal justify={'flex-end'} width={'100%'}>
266
+ <Flexbox className={styles.bubble} gap={4} width={100}>
267
+ <Flexbox
268
+ className={styles.icon}
269
+ height={2}
270
+ style={{
271
+ background: theme.colorTextQuaternary,
272
+ }}
273
+ width={'100%'}
274
+ />
275
+ <Flexbox
276
+ className={styles.icon}
277
+ height={2}
278
+ style={{
279
+ background: theme.colorTextQuaternary,
280
+ }}
281
+ width={'66%'}
282
+ />
283
+ </Flexbox>
284
+ <Flexbox
285
+ className={styles.icon}
286
+ height={14}
287
+ style={{ borderRadius: '50%' }}
288
+ width={14}
289
+ />
290
+ </Flexbox>
291
+ </Flexbox>
292
+ {input}
293
+ </Flexbox>
294
+ </Block>
295
+ );
296
+ });
297
+
298
+ export default Preview;
@@ -2,20 +2,15 @@ import { ColorSwatches, NeutralColors, findCustomThemeName, neutralColors } from
2
2
  import { memo } from 'react';
3
3
  import { useTranslation } from 'react-i18next';
4
4
 
5
- import { useUserStore } from '@/store/user';
6
- import { userGeneralSettingsSelectors } from '@/store/user/selectors';
7
-
8
- const ThemeSwatchesNeutral = memo(() => {
5
+ const ThemeSwatchesNeutral = memo<{
6
+ onChange?: (v: NeutralColors) => void;
7
+ value?: NeutralColors;
8
+ }>(({ value, onChange }) => {
9
9
  const { t } = useTranslation('color');
10
10
 
11
- const [neutralColor, updateGeneralConfig] = useUserStore((s) => [
12
- userGeneralSettingsSelectors.neutralColor(s),
13
- s.updateGeneralConfig,
14
- ]);
15
-
16
11
  const handleSelect = (v: any) => {
17
12
  const name = findCustomThemeName('neutral', v) as NeutralColors;
18
- updateGeneralConfig({ neutralColor: name || '' });
13
+ onChange?.(name || '');
19
14
  };
20
15
 
21
16
  return (
@@ -47,7 +42,7 @@ const ThemeSwatchesNeutral = memo(() => {
47
42
  },
48
43
  ]}
49
44
  onChange={handleSelect}
50
- value={neutralColor ? neutralColors[neutralColor] : undefined}
45
+ value={value ? neutralColors[value] : undefined}
51
46
  />
52
47
  );
53
48
  });
@@ -2,19 +2,15 @@ import { ColorSwatches, PrimaryColors, findCustomThemeName, primaryColors } from
2
2
  import { memo } from 'react';
3
3
  import { useTranslation } from 'react-i18next';
4
4
 
5
- import { useUserStore } from '@/store/user';
6
- import { userGeneralSettingsSelectors } from '@/store/user/selectors';
7
-
8
- const ThemeSwatchesPrimary = memo(() => {
5
+ const ThemeSwatchesPrimary = memo<{
6
+ onChange?: (v: PrimaryColors) => void;
7
+ value?: PrimaryColors;
8
+ }>(({ onChange, value }) => {
9
9
  const { t } = useTranslation('color');
10
- const [primaryColor, updateGeneralConfig] = useUserStore((s) => [
11
- userGeneralSettingsSelectors.primaryColor(s),
12
- s.updateGeneralConfig,
13
- ]);
14
10
 
15
11
  const handleSelect = (v: any) => {
16
12
  const name = findCustomThemeName('primary', v) as PrimaryColors;
17
- updateGeneralConfig({ primaryColor: name || '' });
13
+ onChange?.(name || '');
18
14
  };
19
15
 
20
16
  return (
@@ -74,7 +70,7 @@ const ThemeSwatchesPrimary = memo(() => {
74
70
  },
75
71
  ]}
76
72
  onChange={handleSelect}
77
- value={primaryColor ? primaryColors[primaryColor] : undefined}
73
+ value={value ? primaryColors[value] : undefined}
78
74
  />
79
75
  );
80
76
  });
@@ -0,0 +1,67 @@
1
+ 'use client';
2
+
3
+ import { Form, type FormGroupItemType, 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 { FORM_STYLE } from '@/const/layoutTokens';
11
+ import { useUserStore } from '@/store/user';
12
+ import { settingsSelectors } from '@/store/user/slices/settings/selectors';
13
+
14
+ import Preview from './Preview';
15
+ import { ThemeSwatchesNeutral, ThemeSwatchesPrimary } from './ThemeSwatches';
16
+
17
+ const Appearance = memo(() => {
18
+ const { t } = useTranslation('setting');
19
+ const { general } = 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} />;
24
+
25
+ const theme: FormGroupItemType = {
26
+ children: [
27
+ {
28
+ children: <Preview />,
29
+ label: t('settingAppearance.preview.title'),
30
+ minWidth: undefined,
31
+ },
32
+ {
33
+ children: <ThemeSwatchesPrimary />,
34
+ desc: t('settingAppearance.primaryColor.desc'),
35
+ label: t('settingAppearance.primaryColor.title'),
36
+ minWidth: undefined,
37
+ name: 'primaryColor',
38
+ },
39
+ {
40
+ children: <ThemeSwatchesNeutral />,
41
+ desc: t('settingAppearance.neutralColor.desc'),
42
+ label: t('settingAppearance.neutralColor.title'),
43
+ minWidth: undefined,
44
+ name: 'neutralColor',
45
+ },
46
+ ],
47
+ extra: loading && <Icon icon={Loader2Icon} size={16} spin style={{ opacity: 0.5 }} />,
48
+ title: t('settingAppearance.title'),
49
+ };
50
+
51
+ return (
52
+ <Form
53
+ initialValues={general}
54
+ items={[theme]}
55
+ itemsType={'group'}
56
+ onValuesChange={async (value) => {
57
+ setLoading(true);
58
+ await setSettings({ general: value });
59
+ setLoading(false);
60
+ }}
61
+ variant={'borderless'}
62
+ {...FORM_STYLE}
63
+ />
64
+ );
65
+ });
66
+
67
+ export default Appearance;
@@ -0,0 +1,35 @@
1
+ import { Block, MarkdownProps } from '@lobehub/ui';
2
+ import { ChatItem } from '@lobehub/ui/chat';
3
+ import { useTheme } from 'antd-style';
4
+ import { memo } from 'react';
5
+ import { useTranslation } from 'react-i18next';
6
+
7
+ import { BRANDING_NAME } from '@/const/branding';
8
+ import { DEFAULT_INBOX_AVATAR } from '@/const/meta';
9
+
10
+ const ChatPreview = memo<Pick<MarkdownProps, 'fontSize'>>(({ fontSize }) => {
11
+ const theme = useTheme();
12
+ const { t } = useTranslation('welcome');
13
+ return (
14
+ <Block
15
+ style={{
16
+ background: theme.colorBgContainerSecondary,
17
+ marginBlock: 16,
18
+ minHeight: 110,
19
+ }}
20
+ variant={'outlined'}
21
+ >
22
+ <ChatItem
23
+ avatar={{
24
+ avatar: DEFAULT_INBOX_AVATAR,
25
+ }}
26
+ fontSize={fontSize}
27
+ message={t('guide.defaultMessageWithoutCreate', {
28
+ appName: BRANDING_NAME,
29
+ })}
30
+ />
31
+ </Block>
32
+ );
33
+ });
34
+
35
+ export default ChatPreview;
@@ -0,0 +1,55 @@
1
+ import { Block, HighlighterProps } from '@lobehub/ui';
2
+ import { ChatItem } from '@lobehub/ui/chat';
3
+ import { useTheme } from 'antd-style';
4
+ import { memo } from 'react';
5
+
6
+ import { DEFAULT_INBOX_AVATAR } from '@/const/meta';
7
+
8
+ const code = `
9
+ \`\`\`ts
10
+ const person = { name: "Alice", age: 30 };
11
+ type PersonType = typeof person; // { name: string; age: number }
12
+
13
+ // 'satisfies' to ensure a type matches but allows more specific types
14
+ type Animal = { name: string };
15
+ const dog = { name: "Buddy", breed: "Golden Retriever" } satisfies Animal;
16
+ \`\`\`
17
+ `;
18
+
19
+ const HighlighterPreview = memo<{ theme?: HighlighterProps['theme'] }>(
20
+ ({ theme }) => {
21
+ const token = useTheme();
22
+
23
+ return (
24
+ <Block
25
+ style={{
26
+ background: token.colorBgContainerSecondary,
27
+ marginBlock: 16,
28
+ minHeight: 260,
29
+ paddingBottom: 16,
30
+ }}
31
+ variant={'outlined'}
32
+ >
33
+ <ChatItem
34
+ avatar={{
35
+ avatar: DEFAULT_INBOX_AVATAR,
36
+ }}
37
+ markdownProps={{
38
+ componentProps: {
39
+ highlight: {
40
+ fullFeatured: false,
41
+ theme,
42
+ },
43
+ },
44
+ }}
45
+ message={code}
46
+ />
47
+ </Block>
48
+ );
49
+ },
50
+ (prevProps, nextProps) => {
51
+ return prevProps.theme === nextProps.theme;
52
+ },
53
+ );
54
+
55
+ export default HighlighterPreview;
@@ -0,0 +1,51 @@
1
+ import { Block, MermaidProps } from '@lobehub/ui';
2
+ import { ChatItem } from '@lobehub/ui/chat';
3
+ import { useTheme } from 'antd-style';
4
+ import { memo } from 'react';
5
+
6
+ import { DEFAULT_INBOX_AVATAR } from '@/const/meta';
7
+
8
+ const code = `\`\`\`mermaid
9
+ sequenceDiagram
10
+ Alice->>John: Hello John, how are you?
11
+ John-->>Alice: Great!
12
+ Alice-)John: See you later!
13
+ \`\`\`
14
+ `;
15
+
16
+ const MermaidPreview = memo<{ theme?: MermaidProps['theme'] }>(
17
+ ({ theme }) => {
18
+ const token = useTheme();
19
+ return (
20
+ <Block
21
+ style={{
22
+ background: token.colorBgContainerSecondary,
23
+ marginBlock: 16,
24
+ minHeight: 320,
25
+ paddingBottom: 16,
26
+ }}
27
+ variant={'outlined'}
28
+ >
29
+ <ChatItem
30
+ avatar={{
31
+ avatar: DEFAULT_INBOX_AVATAR,
32
+ }}
33
+ markdownProps={{
34
+ componentProps: {
35
+ mermaid: {
36
+ fullFeatured: false,
37
+ theme,
38
+ },
39
+ },
40
+ }}
41
+ message={code}
42
+ />
43
+ </Block>
44
+ );
45
+ },
46
+ (prevProps, nextProps) => {
47
+ return prevProps.theme === nextProps.theme;
48
+ },
49
+ );
50
+
51
+ export default MermaidPreview;
@@ -0,0 +1,128 @@
1
+ 'use client';
2
+
3
+ import {
4
+ Form,
5
+ type FormGroupItemType,
6
+ Icon,
7
+ Select,
8
+ SliderWithInput,
9
+ highlighterThemes,
10
+ mermaidThemes,
11
+ } from '@lobehub/ui';
12
+ import { Skeleton } from 'antd';
13
+ import isEqual from 'fast-deep-equal';
14
+ import { Loader2Icon } from 'lucide-react';
15
+ import { memo, useState } from 'react';
16
+ import { useTranslation } from 'react-i18next';
17
+
18
+ import { FORM_STYLE } from '@/const/layoutTokens';
19
+ import { useUserStore } from '@/store/user';
20
+ import { settingsSelectors } from '@/store/user/selectors';
21
+
22
+ import ChatPreview from './ChatPreview';
23
+ import HighlighterPreview from './HighlighterPreview';
24
+ import MermaidPreview from './MermaidPreview';
25
+
26
+ const ChatAppearance = memo(() => {
27
+ const { t } = useTranslation('setting');
28
+ const { general } = useUserStore(settingsSelectors.currentSettings, isEqual);
29
+ const [setSettings, isUserStateInit] = useUserStore((s) => [s.setSettings, s.isUserStateInit]);
30
+ const [loading, setLoading] = useState(false);
31
+
32
+ if (!isUserStateInit) return <Skeleton active paragraph={{ rows: 5 }} title={false} />;
33
+
34
+ const theme: FormGroupItemType = {
35
+ children: [
36
+ {
37
+ children: <ChatPreview fontSize={general.fontSize} />,
38
+ noStyle: true,
39
+ },
40
+ {
41
+ children: (
42
+ <SliderWithInput
43
+ marks={{
44
+ 12: {
45
+ label: 'A',
46
+ style: {
47
+ fontSize: 12,
48
+ marginTop: 4,
49
+ },
50
+ },
51
+ 14: {
52
+ label: t('settingChatAppearance.fontSize.marks.normal'),
53
+ style: {
54
+ fontSize: 14,
55
+ marginTop: 4,
56
+ },
57
+ },
58
+ 18: {
59
+ label: 'A',
60
+ style: {
61
+ fontSize: 18,
62
+ marginTop: 4,
63
+ },
64
+ },
65
+ }}
66
+ max={18}
67
+ min={12}
68
+ step={1}
69
+ />
70
+ ),
71
+ desc: t('settingChatAppearance.fontSize.desc'),
72
+ label: t('settingChatAppearance.fontSize.title'),
73
+ name: 'fontSize',
74
+ },
75
+ {
76
+ children: <HighlighterPreview theme={general.highlighterTheme} />,
77
+ noStyle: true,
78
+ },
79
+ {
80
+ children: (
81
+ <Select
82
+ options={highlighterThemes.map((item) => ({
83
+ label: item.displayName,
84
+ value: item.id,
85
+ }))}
86
+ />
87
+ ),
88
+ label: t('settingChatAppearance.highlighterTheme.title'),
89
+ name: 'highlighterTheme',
90
+ },
91
+ {
92
+ children: <MermaidPreview theme={general.mermaidTheme} />,
93
+ noStyle: true,
94
+ },
95
+ {
96
+ children: (
97
+ <Select
98
+ options={mermaidThemes.map((item) => ({
99
+ label: item.displayName,
100
+ value: item.id,
101
+ }))}
102
+ />
103
+ ),
104
+ label: t('settingChatAppearance.mermaidTheme.title'),
105
+ name: 'mermaidTheme',
106
+ },
107
+ ],
108
+ extra: loading && <Icon icon={Loader2Icon} size={16} spin style={{ opacity: 0.5 }} />,
109
+ title: t('settingChatAppearance.title'),
110
+ };
111
+
112
+ return (
113
+ <Form
114
+ initialValues={general}
115
+ items={[theme]}
116
+ itemsType={'group'}
117
+ onValuesChange={async (value) => {
118
+ setLoading(true);
119
+ await setSettings({ general: value });
120
+ setLoading(false);
121
+ }}
122
+ variant={'borderless'}
123
+ {...FORM_STYLE}
124
+ />
125
+ );
126
+ });
127
+
128
+ export default ChatAppearance;