@lobehub/lobehub 2.0.0-next.274 → 2.0.0-next.276

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,48 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## [Version 2.0.0-next.276](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.275...v2.0.0-next.276)
6
+
7
+ <sup>Released on **2026-01-13**</sup>
8
+
9
+ <br/>
10
+
11
+ <details>
12
+ <summary><kbd>Improvements and Fixes</kbd></summary>
13
+
14
+ </details>
15
+
16
+ <div align="right">
17
+
18
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
19
+
20
+ </div>
21
+
22
+ ## [Version 2.0.0-next.275](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.274...v2.0.0-next.275)
23
+
24
+ <sup>Released on **2026-01-13**</sup>
25
+
26
+ #### ✨ Features
27
+
28
+ - **misc**: Improve PageEditor header UX with DropdownMenu and i18n support.
29
+
30
+ <br/>
31
+
32
+ <details>
33
+ <summary><kbd>Improvements and Fixes</kbd></summary>
34
+
35
+ #### What's improved
36
+
37
+ - **misc**: Improve PageEditor header UX with DropdownMenu and i18n support, closes [#11462](https://github.com/lobehub/lobe-chat/issues/11462) ([ae499c9](https://github.com/lobehub/lobe-chat/commit/ae499c9))
38
+
39
+ </details>
40
+
41
+ <div align="right">
42
+
43
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
44
+
45
+ </div>
46
+
5
47
  ## [Version 2.0.0-next.274](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.273...v2.0.0-next.274)
6
48
 
7
49
  <sup>Released on **2026-01-13**</sup>
package/changelog/v1.json CHANGED
@@ -1,4 +1,18 @@
1
1
  [
2
+ {
3
+ "children": {},
4
+ "date": "2026-01-13",
5
+ "version": "2.0.0-next.276"
6
+ },
7
+ {
8
+ "children": {
9
+ "features": [
10
+ "Improve PageEditor header UX with DropdownMenu and i18n support."
11
+ ]
12
+ },
13
+ "date": "2026-01-13",
14
+ "version": "2.0.0-next.275"
15
+ },
2
16
  {
3
17
  "children": {
4
18
  "features": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/lobehub",
3
- "version": "2.0.0-next.274",
3
+ "version": "2.0.0-next.276",
4
4
  "description": "LobeHub - an open-source,comprehensive AI Agent framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
5
5
  "keywords": [
6
6
  "framework",
@@ -206,7 +206,7 @@
206
206
  "@lobehub/icons": "^4.0.2",
207
207
  "@lobehub/market-sdk": "0.28.1",
208
208
  "@lobehub/tts": "^4.0.2",
209
- "@lobehub/ui": "^4.11.6",
209
+ "@lobehub/ui": "^4.18.0",
210
210
  "@modelcontextprotocol/sdk": "^1.25.1",
211
211
  "@neondatabase/serverless": "^1.0.2",
212
212
  "@next/third-parties": "^16.1.1",
@@ -1,5 +1,5 @@
1
1
  import { Flexbox, TooltipGroup } from '@lobehub/ui';
2
- import { memo } from 'react';
2
+ import React, { memo } from 'react';
3
3
 
4
4
  import DragUploadZone, { useUploadFiles } from '@/components/DragUploadZone';
5
5
  import { useAgentStore } from '@/store/agent';
@@ -10,6 +10,12 @@ import { systemStatusSelectors } from '@/store/global/selectors';
10
10
  import ConversationArea from './ConversationArea';
11
11
  import ChatHeader from './Header';
12
12
 
13
+ const wrapperStyle: React.CSSProperties = {
14
+ height: '100%',
15
+ minWidth: 300,
16
+ width: '100%',
17
+ };
18
+
13
19
  const ChatConversation = memo(() => {
14
20
  const showHeader = useGlobalStore(systemStatusSelectors.showChatHeader);
15
21
 
@@ -19,7 +25,7 @@ const ChatConversation = memo(() => {
19
25
  const { handleUploadFiles } = useUploadFiles({ model, provider });
20
26
 
21
27
  return (
22
- <DragUploadZone onUploadFiles={handleUploadFiles} style={{ height: '100%', width: '100%' }}>
28
+ <DragUploadZone onUploadFiles={handleUploadFiles} style={wrapperStyle}>
23
29
  <Flexbox height={'100%'} style={{ overflow: 'hidden', position: 'relative' }} width={'100%'}>
24
30
  {showHeader && <ChatHeader />}
25
31
  <TooltipGroup>
@@ -39,6 +39,7 @@ const Layout = () => {
39
39
  <Modal
40
40
  allowFullscreen
41
41
  className={cx(isPortalThread && styles.container)}
42
+ destroyOnHidden
42
43
  footer={null}
43
44
  height={'95%'}
44
45
  onCancel={() => togglePortal(false)}
@@ -3,7 +3,7 @@
3
3
  import { DraggablePanel, type DraggablePanelProps } from '@lobehub/ui';
4
4
  import { createStaticStyles } from 'antd-style';
5
5
  import isEqual from 'fast-deep-equal';
6
- import { type PropsWithChildren, memo, useState } from 'react';
6
+ import { Activity, type PropsWithChildren, memo, useState } from 'react';
7
7
 
8
8
  import {
9
9
  CHAT_PORTAL_MAX_WIDTH,
@@ -81,7 +81,9 @@ const PortalPanel = memo(({ children }: PropsWithChildren) => {
81
81
  showHandleWideArea={false}
82
82
  size={{ height: '100%', width: portalWidth }}
83
83
  >
84
- {children}
84
+ <Activity mode={showPortal ? 'visible' : 'hidden'} name="AgentPortal">
85
+ {children}
86
+ </Activity>
85
87
  </DraggablePanel>
86
88
  );
87
89
  });
@@ -3,7 +3,7 @@
3
3
  import { Center, Empty, Markdown } from '@lobehub/ui';
4
4
  import { FileText } from 'lucide-react';
5
5
  import Link from 'next/link';
6
- import { type ReactNode, memo } from 'react';
6
+ import { memo } from 'react';
7
7
  import { useTranslation } from 'react-i18next';
8
8
 
9
9
  import { H1, H2, H3, H4, H5 } from './Toc/Heading';
@@ -26,7 +26,7 @@ const MarkdownRender = memo<{ children?: string }>(({ children }) => {
26
26
  <Markdown
27
27
  allowHtml
28
28
  components={{
29
- a: ({ href, ...rest }: { children: ReactNode; href: string }) => {
29
+ a: ({ href, ...rest }) => {
30
30
  if (href && href.startsWith('http'))
31
31
  return <Link {...rest} href={href} target={'_blank'} />;
32
32
  return rest?.children;
@@ -36,12 +36,14 @@ const MarkdownRender = memo<{ children?: string }>(({ children }) => {
36
36
  h3: H3,
37
37
  h4: H4,
38
38
  h5: H5,
39
- img: ({ src, ...rest }: { src: string }) => {
40
- if (src.includes('glama.ai')) return;
39
+ img: ({ src, ...rest }) => {
40
+ // FIXME ignore experimental blob image prop passing
41
+ if (typeof src !== 'string') return null;
42
+ if (src.includes('glama.ai')) return null;
41
43
 
42
44
  // eslint-disable-next-line @next/next/no-img-element
43
- if (src && src.startsWith('http')) return <img src={src} {...rest} />;
44
- return;
45
+ if (src.startsWith('http')) return <img src={src} {...rest} />;
46
+ return null;
45
47
  },
46
48
  }}
47
49
  enableImageGallery={false}
@@ -39,6 +39,7 @@ const Layout = () => {
39
39
  <Modal
40
40
  allowFullscreen
41
41
  className={cx(isPortalThread && styles.container)}
42
+ destroyOnHidden
42
43
  footer={null}
43
44
  height={'95%'}
44
45
  onCancel={() => togglePortal(false)}
@@ -4,7 +4,7 @@ import { DraggablePanel, DraggablePanelContainer, type DraggablePanelProps } fro
4
4
  import { Flexbox } from '@lobehub/ui';
5
5
  import { createStaticStyles, useResponsive } from 'antd-style';
6
6
  import isEqual from 'fast-deep-equal';
7
- import { type PropsWithChildren, memo, useState } from 'react';
7
+ import { Activity, type PropsWithChildren, memo, useState } from 'react';
8
8
 
9
9
  import {
10
10
  CHAT_PORTAL_MAX_WIDTH,
@@ -94,7 +94,9 @@ const PortalPanel = memo(({ children }: PropsWithChildren) => {
94
94
  minWidth: CHAT_PORTAL_WIDTH,
95
95
  }}
96
96
  >
97
- <Flexbox className={styles.panel}>{children}</Flexbox>
97
+ <Activity mode={showPortal ? 'visible' : 'hidden'} name="GroupPortal">
98
+ <Flexbox className={styles.panel}>{children}</Flexbox>
99
+ </Activity>
98
100
  </DraggablePanelContainer>
99
101
  </DraggablePanel>
100
102
  );
@@ -1,12 +1,13 @@
1
1
  'use client';
2
2
 
3
3
  import { type NetworkProxySettings } from '@lobechat/electron-client-ipc';
4
- import { Alert, Block, Flexbox, Skeleton, Text , Button } from '@lobehub/ui';
5
- import { App, Divider, Form, Input, Radio, Space, Switch } from 'antd';
6
- import isEqual from 'fast-deep-equal';
4
+ import { Alert, Flexbox, Form, type FormGroupItemType, Icon, Skeleton } from '@lobehub/ui';
5
+ import { Form as AntdForm, Button, Input, Radio, Space, Switch } from 'antd';
6
+ import { Loader2Icon } from 'lucide-react';
7
7
  import { useCallback, useEffect, useState } from 'react';
8
8
  import { useTranslation } from 'react-i18next';
9
9
 
10
+ import { FORM_STYLE } from '@/const/layoutTokens';
10
11
  import { desktopSettingsService } from '@/services/electron/settings';
11
12
  import { useElectronStore } from '@/store/electron';
12
13
 
@@ -19,15 +20,15 @@ interface ProxyTestResult {
19
20
  const ProxyForm = () => {
20
21
  const { t } = useTranslation('electron');
21
22
  const [form] = Form.useForm();
22
- const { message } = App.useApp();
23
23
  const [testUrl, setTestUrl] = useState('https://www.google.com');
24
24
  const [isTesting, setIsTesting] = useState(false);
25
25
  const [isSaving, setIsSaving] = useState(false);
26
26
  const [testResult, setTestResult] = useState<ProxyTestResult | null>(null);
27
27
  const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
28
+ const [loading, setLoading] = useState(false);
28
29
 
29
- const isEnableProxy = Form.useWatch('enableProxy', form);
30
- const proxyRequireAuth = Form.useWatch('proxyRequireAuth', form);
30
+ const isEnableProxy = AntdForm.useWatch('enableProxy', form);
31
+ const proxyRequireAuth = AntdForm.useWatch('proxyRequireAuth', form);
31
32
 
32
33
  const [setProxySettings, useGetProxySettings] = useElectronStore((s) => [
33
34
  s.setProxySettings,
@@ -44,19 +45,12 @@ const ProxyForm = () => {
44
45
 
45
46
  // 监听表单变化
46
47
  const handleValuesChange = useCallback(() => {
48
+ setLoading(true);
47
49
  setHasUnsavedChanges(true);
48
50
  setTestResult(null); // 清除之前的测试结果
51
+ setLoading(false);
49
52
  }, []);
50
53
 
51
- const updateFormValue = (value: any) => {
52
- const preValues = form.getFieldsValue();
53
- form.setFieldsValue(value);
54
- const newValues = form.getFieldsValue();
55
- if (isEqual(newValues, preValues)) return;
56
-
57
- handleValuesChange();
58
- };
59
-
60
54
  // 保存配置
61
55
  const handleSave = useCallback(async () => {
62
56
  try {
@@ -64,15 +58,12 @@ const ProxyForm = () => {
64
58
  const values = await form.validateFields();
65
59
  await setProxySettings(values);
66
60
  setHasUnsavedChanges(false);
67
- message.success(t('proxy.saveSuccess'));
68
- } catch (error) {
69
- if (error instanceof Error) {
70
- message.error(t('proxy.saveFailed', { error: error.message }));
71
- }
61
+ } catch {
62
+ // validation error
72
63
  } finally {
73
64
  setIsSaving(false);
74
65
  }
75
- }, [form, t, message]);
66
+ }, [form, setProxySettings]);
76
67
 
77
68
  // 重置配置
78
69
  const handleReset = useCallback(() => {
@@ -107,240 +98,159 @@ const ProxyForm = () => {
107
98
  success: false,
108
99
  };
109
100
  setTestResult(result);
110
- message.error(t('proxy.testFailed'));
111
101
  } finally {
112
102
  setIsTesting(false);
113
103
  }
114
- }, [proxySettings, testUrl]);
104
+ }, [proxySettings, testUrl, form]);
115
105
 
116
- if (isLoading) return <Skeleton />;
106
+ if (isLoading) return <Skeleton active paragraph={{ rows: 5 }} title={false} />;
117
107
 
118
- return (
119
- <Form
120
- disabled={isSaving}
121
- form={form}
122
- layout="vertical"
123
- onValuesChange={handleValuesChange}
124
- requiredMark={false}
125
- >
126
- <Flexbox gap={24}>
127
- {/* 基本代理设置 */}
128
- <Block
129
- paddingBlock={16}
130
- paddingInline={24}
131
- style={{ borderRadius: 12 }}
132
- variant={'outlined'}
133
- >
134
- <Form.Item name="enableProxy" noStyle valuePropName="checked">
135
- <Flexbox align={'center'} horizontal justify={'space-between'}>
136
- <Flexbox>
137
- <Text as={'h4'}>{t('proxy.enable')}</Text>
138
- <Text type={'secondary'}>{t('proxy.enableDesc')}</Text>
139
- </Flexbox>
140
- <Switch
141
- checked={isEnableProxy}
142
- onChange={(checked) => {
143
- updateFormValue({ enableProxy: checked });
144
- }}
145
- />
146
- </Flexbox>
147
- </Form.Item>
148
- </Block>
108
+ const enableProxyGroup: FormGroupItemType = {
109
+ children: [
110
+ {
111
+ children: <Switch />,
112
+ desc: t('proxy.enableDesc'),
113
+ label: t('proxy.enable'),
114
+ layout: 'horizontal',
115
+ minWidth: undefined,
116
+ name: 'enableProxy',
117
+ valuePropName: 'checked',
118
+ },
119
+ ],
120
+ extra: loading && <Icon icon={Loader2Icon} size={16} spin style={{ opacity: 0.5 }} />,
121
+ title: t('proxy.enable'),
122
+ };
149
123
 
150
- {/* 认证设置 */}
151
- <Block
152
- paddingBlock={16}
153
- paddingInline={24}
154
- style={{ borderRadius: 12 }}
155
- variant={'outlined'}
156
- >
157
- <Flexbox gap={24}>
158
- <Flexbox>
159
- <Text as={'h4'}>{t('proxy.basicSettings')}</Text>
160
- <Text type={'secondary'}>{t('proxy.basicSettingsDesc')}</Text>
161
- </Flexbox>
162
- <Flexbox>
163
- <Form.Item
164
- dependencies={['enableProxy']}
165
- label={t('proxy.type')}
166
- name="proxyType"
167
- rules={[
168
- ({ getFieldValue }) => ({
169
- message: t('proxy.validation.typeRequired'),
170
- required: getFieldValue('enableProxy'),
171
- }),
172
- ]}
173
- >
174
- <Radio.Group disabled={!form.getFieldValue('enableProxy')}>
175
- <Radio value="http">HTTP</Radio>
176
- <Radio value="https">HTTPS</Radio>
177
- <Radio value="socks5">SOCKS5</Radio>
178
- </Radio.Group>
179
- </Form.Item>
124
+ const basicSettingsGroup: FormGroupItemType = {
125
+ children: [
126
+ {
127
+ children: (
128
+ <Radio.Group disabled={!isEnableProxy}>
129
+ <Radio value="http">HTTP</Radio>
130
+ <Radio value="https">HTTPS</Radio>
131
+ <Radio value="socks5">SOCKS5</Radio>
132
+ </Radio.Group>
133
+ ),
134
+ label: t('proxy.type'),
135
+ minWidth: undefined,
136
+ name: 'proxyType',
137
+ },
138
+ {
139
+ children: <Input disabled={!isEnableProxy} placeholder="127.0.0.1" />,
140
+ desc: t('proxy.validation.serverRequired'),
141
+ label: t('proxy.server'),
142
+ name: 'proxyServer',
143
+ },
144
+ {
145
+ children: <Input disabled={!isEnableProxy} placeholder="7890" style={{ width: 120 }} />,
146
+ desc: t('proxy.validation.portRequired'),
147
+ label: t('proxy.port'),
148
+ name: 'proxyPort',
149
+ },
150
+ ],
151
+ extra: loading && <Icon icon={Loader2Icon} size={16} spin style={{ opacity: 0.5 }} />,
152
+ title: t('proxy.basicSettings'),
153
+ };
180
154
 
181
- <Space.Compact style={{ width: '100%' }}>
182
- <Form.Item
183
- dependencies={['enableProxy']}
184
- label={t('proxy.server')}
185
- name="proxyServer"
186
- rules={[
187
- ({ getFieldValue }) => ({
188
- message: t('proxy.validation.serverRequired'),
189
- required: getFieldValue('enableProxy'),
190
- }),
191
- {
192
- message: t('proxy.validation.serverInvalid'),
193
- pattern:
194
- /^((25[0-5]|2[0-4]\d|[01]?\d{1,2})\.){3}(25[0-5]|2[0-4]\d|[01]?\d{1,2})$|^[\dA-Za-z]([\dA-Za-z-]*[\dA-Za-z])?(\.[\dA-Za-z]([\dA-Za-z-]*[\dA-Za-z])?)*$/,
195
- },
196
- ]}
197
- style={{ flex: 1, marginBottom: 0 }}
198
- >
199
- <Input disabled={!form.getFieldValue('enableProxy')} placeholder="127.0.0.1" />
200
- </Form.Item>
155
+ const authGroup: FormGroupItemType = {
156
+ children: [
157
+ {
158
+ children: <Switch disabled={!isEnableProxy} />,
159
+ desc: t('proxy.authDesc'),
160
+ label: t('proxy.auth'),
161
+ layout: 'horizontal',
162
+ minWidth: undefined,
163
+ name: 'proxyRequireAuth',
164
+ valuePropName: 'checked',
165
+ },
166
+ ...(proxyRequireAuth && isEnableProxy
167
+ ? [
168
+ {
169
+ children: <Input placeholder={t('proxy.username_placeholder')} />,
170
+ label: t('proxy.username'),
171
+ name: 'proxyUsername',
172
+ },
173
+ {
174
+ children: <Input.Password placeholder={t('proxy.password_placeholder')} />,
175
+ label: t('proxy.password'),
176
+ name: 'proxyPassword',
177
+ },
178
+ ]
179
+ : []),
180
+ ],
181
+ extra: loading && <Icon icon={Loader2Icon} size={16} spin style={{ opacity: 0.5 }} />,
182
+ title: t('proxy.authSettings'),
183
+ };
201
184
 
202
- <Form.Item
203
- dependencies={['enableProxy']}
204
- label={t('proxy.port')}
205
- name="proxyPort"
206
- rules={[
207
- ({ getFieldValue }) => ({
208
- message: t('proxy.validation.portRequired'),
209
- required: getFieldValue('enableProxy'),
210
- }),
211
- {
212
- message: t('proxy.validation.portInvalid'),
213
- pattern:
214
- /^([1-9]\d{0,3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$/,
215
- },
216
- ]}
217
- style={{ marginBottom: 0, width: 120 }}
218
- >
219
- <Input disabled={!form.getFieldValue('enableProxy')} placeholder="7890" />
220
- </Form.Item>
221
- </Space.Compact>
222
- </Flexbox>
223
- <Divider size={'small'} />
224
- <Flexbox gap={12}>
225
- <Form.Item
226
- dependencies={['enableProxy']}
227
- name="proxyRequireAuth"
228
- noStyle
229
- valuePropName="checked"
230
- >
231
- <Flexbox align={'center'} horizontal justify={'space-between'}>
232
- <Flexbox>
233
- <Text as={'h5'}>{t('proxy.auth')}</Text>
234
- <Text type={'secondary'}>{t('proxy.authDesc')}</Text>
185
+ const testGroup: FormGroupItemType = {
186
+ children: [
187
+ {
188
+ children: (
189
+ <Flexbox gap={8}>
190
+ <Space.Compact style={{ width: '100%' }}>
191
+ <Input
192
+ onChange={(e) => setTestUrl(e.target.value)}
193
+ placeholder={t('proxy.testUrlPlaceholder')}
194
+ style={{ flex: 1 }}
195
+ value={testUrl}
196
+ />
197
+ <Button loading={isTesting} onClick={handleTest} type="default">
198
+ {t('proxy.testButton')}
199
+ </Button>
200
+ </Space.Compact>
201
+ {/* 测试结果显示 */}
202
+ {!testResult ? null : testResult.success ? (
203
+ <Alert
204
+ closable
205
+ title={
206
+ <Flexbox align="center" gap={8} horizontal>
207
+ {t('proxy.testSuccessWithTime', { time: testResult.responseTime })}
235
208
  </Flexbox>
236
- <Switch
237
- checked={proxyRequireAuth}
238
- disabled={!isEnableProxy}
239
- onChange={(checked) => {
240
- updateFormValue({ proxyRequireAuth: checked });
241
- }}
242
- />
243
- </Flexbox>
244
- </Form.Item>
245
-
246
- <Form.Item
247
- dependencies={['proxyRequireAuth', 'enableProxy']}
248
- label={t('proxy.username')}
249
- name="proxyUsername"
250
- rules={[
251
- ({ getFieldValue }) => ({
252
- message: t('proxy.validation.usernameRequired'),
253
- required: getFieldValue('proxyRequireAuth') && getFieldValue('enableProxy'),
254
- }),
255
- ]}
256
- style={{
257
- display:
258
- form.getFieldValue('proxyRequireAuth') && form.getFieldValue('enableProxy')
259
- ? 'block'
260
- : 'none',
261
- }}
262
- >
263
- <Input placeholder={t('proxy.username_placeholder')} />
264
- </Form.Item>
265
-
266
- <Form.Item
267
- dependencies={['proxyRequireAuth', 'enableProxy']}
268
- label={t('proxy.password')}
269
- name="proxyPassword"
270
- rules={[
271
- ({ getFieldValue }) => ({
272
- message: t('proxy.validation.passwordRequired'),
273
- required: getFieldValue('proxyRequireAuth') && getFieldValue('enableProxy'),
274
- }),
275
- ]}
276
- style={{
277
- display:
278
- form.getFieldValue('proxyRequireAuth') && form.getFieldValue('enableProxy')
279
- ? 'block'
280
- : 'none',
281
- }}
282
- >
283
- <Input.Password placeholder={t('proxy.password_placeholder')} />
284
- </Form.Item>
285
- </Flexbox>
209
+ }
210
+ type={'success'}
211
+ />
212
+ ) : (
213
+ <Alert
214
+ closable
215
+ title={
216
+ <Flexbox align="center" gap={8} horizontal>
217
+ {t('proxy.testFailed')}: {testResult.message}
218
+ </Flexbox>
219
+ }
220
+ type={'error'}
221
+ variant={'outlined'}
222
+ />
223
+ )}
286
224
  </Flexbox>
287
- </Block>
288
-
289
- {/* 连接测试 */}
225
+ ),
226
+ desc: t('proxy.testDescription'),
227
+ label: t('proxy.testUrl'),
228
+ minWidth: undefined,
229
+ },
230
+ ],
231
+ extra: loading && <Icon icon={Loader2Icon} size={16} spin style={{ opacity: 0.5 }} />,
232
+ title: t('proxy.connectionTest'),
233
+ };
290
234
 
291
- <Block
292
- paddingBlock={16}
293
- paddingInline={24}
294
- style={{ borderRadius: 12 }}
295
- variant={'outlined'}
296
- >
297
- <Flexbox gap={24}>
298
- <Flexbox>
299
- <Text as={'h4'}>{t('proxy.connectionTest')}</Text>
300
- <Text type={'secondary'}>{t('proxy.testDescription')}</Text>
301
- </Flexbox>
302
- <Form.Item label={t('proxy.testUrl')}>
303
- <Flexbox gap={8}>
304
- <Space.Compact style={{ width: '100%' }}>
305
- <Input
306
- onChange={(e) => setTestUrl(e.target.value)}
307
- placeholder={t('proxy.testUrlPlaceholder')}
308
- style={{ flex: 1 }}
309
- value={testUrl}
310
- />
311
- <Button loading={isTesting} onClick={handleTest} type="default">
312
- {t('proxy.testButton')}
313
- </Button>
314
- </Space.Compact>
315
- {/* 测试结果显示 */}
316
- {!testResult ? null : testResult.success ? (
317
- <Alert
318
- closable
319
- title={
320
- <Flexbox align="center" gap={8} horizontal>
321
- {t('proxy.testSuccessWithTime', { time: testResult.responseTime })}
322
- </Flexbox>
323
- }
324
- type={'success'}
325
- />
326
- ) : (
327
- <Alert
328
- closable
329
- title={
330
- <Flexbox align="center" gap={8} horizontal>
331
- {t('proxy.testFailed')}: {testResult.message}
332
- </Flexbox>
333
- }
334
- type={'error'}
335
- variant={'outlined'}
336
- />
337
- )}
338
- </Flexbox>
339
- </Form.Item>
340
- </Flexbox>
341
- </Block>
342
- {/* 操作按钮 */}
343
- <Space>
235
+ return (
236
+ <Flexbox gap={24}>
237
+ <Form
238
+ collapsible={false}
239
+ form={form}
240
+ initialValues={proxySettings}
241
+ items={[enableProxyGroup, basicSettingsGroup, authGroup, testGroup]}
242
+ itemsType={'group'}
243
+ onValuesChange={handleValuesChange}
244
+ variant={'filled'}
245
+ {...FORM_STYLE}
246
+ />
247
+ <Flexbox align="end" justify="flex-end">
248
+ {hasUnsavedChanges && (
249
+ <span style={{ color: 'var(--ant-color-warning)', marginBottom: 8 }}>
250
+ {t('proxy.unsavedChanges')}
251
+ </span>
252
+ )}
253
+ <Flexbox gap={8} horizontal>
344
254
  <Button
345
255
  disabled={!hasUnsavedChanges}
346
256
  loading={isSaving}
@@ -349,19 +259,12 @@ const ProxyForm = () => {
349
259
  >
350
260
  {t('proxy.saveButton')}
351
261
  </Button>
352
-
353
262
  <Button disabled={!hasUnsavedChanges || isSaving} onClick={handleReset}>
354
263
  {t('proxy.resetButton')}
355
264
  </Button>
356
-
357
- {hasUnsavedChanges && (
358
- <Text style={{ marginLeft: 8 }} type="warning">
359
- {t('proxy.unsavedChanges')}
360
- </Text>
361
- )}
362
- </Space>
265
+ </Flexbox>
363
266
  </Flexbox>
364
- </Form>
267
+ </Flexbox>
365
268
  );
366
269
  };
367
270
 
@@ -9,9 +9,7 @@ const Page = () => {
9
9
  return (
10
10
  <>
11
11
  <SettingHeader title={t('tab.proxy')} />
12
- <div style={{ maxWidth: '1024px', width: '100%' }}>
13
- <ProxyForm />
14
- </div>
12
+ <ProxyForm />
15
13
  </>
16
14
  );
17
15
  };
@@ -30,6 +30,7 @@ export const useMarkdown = (id: string): Partial<MarkdownProps> => {
30
30
  () =>
31
31
  ({
32
32
  components: Object.fromEntries(
33
+ // @ts-expect-error
33
34
  markdownElements.map((element) => {
34
35
  const Component = element.Component;
35
36
  return [element.tag, (props: any) => <Component {...props} id={id} />];
@@ -1,12 +1,13 @@
1
1
  'use client';
2
2
 
3
+ import { DEFAULT_INBOX_AVATAR } from '@lobechat/const';
3
4
  import { nanoid } from '@lobechat/utils';
4
5
  import { HIDE_TOOLBAR_COMMAND, type IEditor } from '@lobehub/editor';
5
6
  import { type ChatInputActionsProps } from '@lobehub/editor/react';
6
- import { Block } from '@lobehub/ui';
7
+ import { Avatar, Block } from '@lobehub/ui';
7
8
  import { createStaticStyles, cssVar } from 'antd-style';
8
- import { BotIcon } from 'lucide-react';
9
9
  import { useMemo } from 'react';
10
+ import { useTranslation } from 'react-i18next';
10
11
 
11
12
  import { useFileStore } from '@/store/file';
12
13
  import { useGlobalStore } from '@/store/global';
@@ -23,11 +24,14 @@ const styles = createStaticStyles(({ css }) => ({
23
24
  }));
24
25
 
25
26
  export const useAskCopilotItem = (editor: IEditor | undefined): ChatInputActionsProps['items'] => {
27
+ const { t } = useTranslation('common');
26
28
  const addSelectionContext = useFileStore((s) => s.addChatContextSelection);
27
29
 
28
30
  return useMemo(() => {
29
31
  if (!editor) return [];
30
32
 
33
+ const label = t('cmdk.askLobeAI');
34
+
31
35
  return [
32
36
  {
33
37
  children: (
@@ -82,14 +86,14 @@ export const useAskCopilotItem = (editor: IEditor | undefined): ChatInputActions
82
86
  paddingInline={12}
83
87
  variant="borderless"
84
88
  >
85
- <BotIcon />
86
- <span>Ask Copilot</span>
89
+ <Avatar avatar={DEFAULT_INBOX_AVATAR} shape="square" size={16} />
90
+ <span>{label}</span>
87
91
  </Block>
88
92
  ),
89
93
  key: 'ask-copilot',
90
- label: 'Ask Copilot',
94
+ label,
91
95
  onClick: () => {},
92
96
  },
93
97
  ];
94
- }, [addSelectionContext, editor]);
98
+ }, [addSelectionContext, editor, t]);
95
99
  };
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
- import { ActionIcon, Avatar, Dropdown, Text } from '@lobehub/ui';
4
- import { ArrowLeftIcon, BotMessageSquareIcon, MoreHorizontal } from 'lucide-react';
3
+ import { ActionIcon, Avatar, DropdownMenu, Text } from '@lobehub/ui';
4
+ import { ArrowLeftIcon, MoreHorizontal } from 'lucide-react';
5
5
  import { memo } from 'react';
6
6
  import { useTranslation } from 'react-i18next';
7
7
 
@@ -49,18 +49,20 @@ const Header = memo(() => {
49
49
  }
50
50
  right={
51
51
  <>
52
- <ToggleRightPanelButton icon={BotMessageSquareIcon} showActive={true} />
53
52
  {/* Three-dot menu */}
54
- <Dropdown
55
- menu={{
56
- items: menuItems,
57
- style: { minWidth: 200 },
58
- }}
53
+ <DropdownMenu
54
+ items={menuItems}
55
+ nativeButton={false}
59
56
  placement="bottomRight"
60
- trigger={['click']}
57
+ popupProps={{
58
+ style: {
59
+ minWidth: 200,
60
+ },
61
+ }}
61
62
  >
62
63
  <ActionIcon icon={MoreHorizontal} size={DESKTOP_HEADER_ICON_SIZE} />
63
- </Dropdown>
64
+ </DropdownMenu>
65
+ <ToggleRightPanelButton hideWhenExpanded showActive={false} />
64
66
  </>
65
67
  }
66
68
  />
@@ -1,5 +1,5 @@
1
- import { Flexbox, Icon } from '@lobehub/ui';
2
- import { App, Switch } from 'antd';
1
+ import { type DropdownItem, Icon } from '@lobehub/ui';
2
+ import { App } from 'antd';
3
3
  import { cssVar, useResponsive } from 'antd-style';
4
4
  import dayjs from 'dayjs';
5
5
  import { CopyPlus, Download, Link2, Trash2 } from 'lucide-react';
@@ -75,25 +75,17 @@ export const useMenu = (): { menuItems: any[] } => {
75
75
  }
76
76
  };
77
77
 
78
- const menuItems = useMemo(
79
- () => [
78
+ const menuItems = useMemo<DropdownItem[]>(() => {
79
+ const items: DropdownItem[] = [
80
80
  ...(showViewModeSwitch
81
81
  ? [
82
82
  {
83
+ checked: wideScreen,
83
84
  key: 'full-width',
84
- label: (
85
- <Flexbox align="center" horizontal justify="space-between">
86
- <span>{t('viewMode.fullWidth', { ns: 'chat' })}</span>
87
- <Switch
88
- checked={wideScreen}
89
- onChange={toggleWideScreen}
90
- onClick={(checked, event) => {
91
- event.stopPropagation();
92
- }}
93
- size="small"
94
- />
95
- </Flexbox>
96
- ),
85
+ label: t('viewMode.fullWidth', { ns: 'chat' }),
86
+ onCheckedChange: toggleWideScreen,
87
+
88
+ type: 'checkbox' as const,
97
89
  },
98
90
  {
99
91
  type: 'divider' as const,
@@ -140,38 +132,43 @@ export const useMenu = (): { menuItems: any[] } => {
140
132
  key: 'export',
141
133
  label: t('pageEditor.menu.export'),
142
134
  },
143
- {
144
- type: 'divider' as const,
145
- },
146
- {
147
- disabled: true,
148
- key: 'page-info',
149
- label: (
150
- <div style={{ color: cssVar.colorTextTertiary, fontSize: 12, lineHeight: 1.6 }}>
151
- <div>
152
- {lastUpdatedTime
153
- ? t('pageEditor.editedAt', {
154
- time: dayjs(lastUpdatedTime).format('MMMM D, YYYY [at] h:mm A'),
155
- })
156
- : ''}
135
+ ];
136
+
137
+ if (lastUpdatedTime) {
138
+ items.push(
139
+ {
140
+ type: 'divider' as const,
141
+ },
142
+ {
143
+ disabled: true,
144
+ key: 'page-info',
145
+ label: (
146
+ <div style={{ color: cssVar.colorTextTertiary, fontSize: 12, lineHeight: 1.6 }}>
147
+ <div>
148
+ {lastUpdatedTime
149
+ ? t('pageEditor.editedAt', {
150
+ time: dayjs(lastUpdatedTime).format('MMMM D, YYYY [at] h:mm A'),
151
+ })
152
+ : ''}
153
+ </div>
157
154
  </div>
158
- </div>
159
- ),
160
- },
161
- ],
162
- [
163
- lastUpdatedTime,
164
- storeApi,
165
- t,
166
- message,
167
- modal,
168
- wideScreen,
169
- toggleWideScreen,
170
- showViewModeSwitch,
171
- handleDuplicate,
172
- handleExportMarkdown,
173
- ],
174
- );
155
+ ),
156
+ },
157
+ );
158
+ }
159
+ return items;
160
+ }, [
161
+ lastUpdatedTime,
162
+ storeApi,
163
+ t,
164
+ message,
165
+ modal,
166
+ wideScreen,
167
+ toggleWideScreen,
168
+ showViewModeSwitch,
169
+ handleDuplicate,
170
+ handleExportMarkdown,
171
+ ]);
175
172
 
176
173
  return { menuItems };
177
174
  };
@@ -15,6 +15,7 @@ import { HotkeyEnum } from '@/types/hotkey';
15
15
  export const TOGGLE_BUTTON_ID = 'toggle_right_panel_button';
16
16
 
17
17
  interface ToggleRightPanelButtonProps {
18
+ hideWhenExpanded?: boolean;
18
19
  icon?: ActionIconProps['icon'];
19
20
  showActive?: boolean;
20
21
  size?: ActionIconProps['size'];
@@ -22,7 +23,7 @@ interface ToggleRightPanelButtonProps {
22
23
  }
23
24
 
24
25
  const ToggleRightPanelButton = memo<ToggleRightPanelButtonProps>(
25
- ({ title, showActive, icon, size }) => {
26
+ ({ title, showActive, icon, hideWhenExpanded, size }) => {
26
27
  const [expand, togglePanel] = useGlobalStore((s) => [
27
28
  systemStatusSelectors.showRightPanel(s),
28
29
  s.toggleRightPanel,
@@ -31,6 +32,7 @@ const ToggleRightPanelButton = memo<ToggleRightPanelButtonProps>(
31
32
 
32
33
  const { t } = useTranslation(['chat', 'hotkey']);
33
34
 
35
+ if (hideWhenExpanded && expand) return null;
34
36
  return (
35
37
  <ActionIcon
36
38
  active={showActive ? expand : undefined}
@@ -1,16 +1,19 @@
1
1
  import { parse } from 'partial-json';
2
+ import { useMemo } from 'react';
2
3
  import { stringify } from 'yaml';
3
4
 
4
5
  export const useYamlArguments = (args?: string) => {
5
- if (!args) return '';
6
+ return useMemo(() => {
7
+ if (!args) return '';
6
8
 
7
- try {
8
- const obj = parse(args);
9
+ try {
10
+ const obj = parse(args);
9
11
 
10
- if (Object.keys(obj).length === 0) return '';
12
+ if (Object.keys(obj).length === 0) return '';
11
13
 
12
- return stringify(obj);
13
- } catch {
14
- return args;
15
- }
14
+ return stringify(obj);
15
+ } catch {
16
+ return args;
17
+ }
18
+ }, [args]);
16
19
  };
@@ -0,0 +1,40 @@
1
+ class LRUMap<K, V> {
2
+ private map = new Map<K, V>();
3
+ private limit: number;
4
+
5
+ constructor(limit = 0) {
6
+ this.limit = limit;
7
+ }
8
+
9
+ get size() {
10
+ return this.map.size;
11
+ }
12
+
13
+ get(key: K) {
14
+ return this.map.get(key);
15
+ }
16
+
17
+ set(key: K, value: V) {
18
+ if (!this.map.has(key) && this.limit > 0 && this.map.size >= this.limit) {
19
+ const oldest = this.map.keys().next().value as K | undefined;
20
+
21
+ if (oldest !== undefined) this.map.delete(oldest);
22
+ }
23
+
24
+ this.map.set(key, value);
25
+ return this;
26
+ }
27
+
28
+ delete(key: K) {
29
+ const value = this.map.get(key);
30
+ this.map.delete(key);
31
+ return value;
32
+ }
33
+
34
+ clear() {
35
+ this.map.clear();
36
+ }
37
+ }
38
+
39
+ export { LRUMap };
40
+ export default { LRUMap };
package/vitest.config.mts CHANGED
@@ -56,6 +56,7 @@ export default defineConfig({
56
56
  '@/const': resolve(__dirname, './packages/const/src'),
57
57
  '@': resolve(__dirname, './src'),
58
58
  '~test-utils': resolve(__dirname, './tests/utils.tsx'),
59
+ 'lru_map': resolve(__dirname, './tests/mocks/lru_map'),
59
60
  /* eslint-enable */
60
61
  },
61
62
  coverage: {
@@ -94,7 +95,14 @@ export default defineConfig({
94
95
  globals: true,
95
96
  server: {
96
97
  deps: {
97
- inline: ['vitest-canvas-mock', '@lobehub/ui', '@lobehub/fluent-emoji'],
98
+ inline: [
99
+ 'vitest-canvas-mock',
100
+ '@lobehub/ui',
101
+ '@lobehub/fluent-emoji',
102
+ '@pierre/diffs',
103
+ '@pierre/diffs/react',
104
+ 'lru_map',
105
+ ],
98
106
  },
99
107
  },
100
108
  setupFiles: join(__dirname, './tests/setup.ts'),