@lobehub/chat 1.84.2 → 1.84.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/apps/desktop/electron-builder.js +8 -4
  3. package/apps/desktop/package.json +1 -0
  4. package/apps/desktop/src/main/index.ts +3 -0
  5. package/changelog/v1.json +18 -0
  6. package/locales/ar/plugin.json +18 -0
  7. package/locales/bg-BG/plugin.json +18 -0
  8. package/locales/de-DE/plugin.json +18 -0
  9. package/locales/en-US/plugin.json +18 -0
  10. package/locales/es-ES/plugin.json +18 -0
  11. package/locales/fa-IR/plugin.json +18 -0
  12. package/locales/fr-FR/plugin.json +18 -0
  13. package/locales/it-IT/plugin.json +18 -0
  14. package/locales/ja-JP/plugin.json +18 -0
  15. package/locales/ko-KR/plugin.json +18 -0
  16. package/locales/nl-NL/plugin.json +18 -0
  17. package/locales/pl-PL/plugin.json +18 -0
  18. package/locales/pt-BR/plugin.json +18 -0
  19. package/locales/ru-RU/plugin.json +18 -0
  20. package/locales/tr-TR/plugin.json +18 -0
  21. package/locales/vi-VN/plugin.json +18 -0
  22. package/locales/zh-CN/plugin.json +18 -0
  23. package/locales/zh-TW/plugin.json +18 -0
  24. package/package.json +2 -2
  25. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/InputArea/index.tsx +3 -2
  26. package/src/app/[variants]/(main)/chat/_layout/Desktop/RegisterHotkeys.tsx +6 -3
  27. package/src/app/[variants]/(main)/chat/settings/page.tsx +4 -33
  28. package/src/app/[variants]/(main)/settings/_layout/Desktop/Header.tsx +7 -2
  29. package/src/app/[variants]/(main)/settings/_layout/Mobile/Header.tsx +9 -1
  30. package/src/app/[variants]/(main)/settings/agent/_layout/Desktop.tsx +1 -1
  31. package/src/app/[variants]/(main)/settings/agent/_layout/Mobile.tsx +1 -8
  32. package/src/app/[variants]/(main)/settings/agent/index.tsx +34 -14
  33. package/src/app/[variants]/(main)/settings/common/features/Common.tsx +1 -0
  34. package/src/app/[variants]/(main)/settings/provider/features/ModelList/index.tsx +11 -1
  35. package/src/app/[variants]/(main)/settings/storage/Advanced.tsx +3 -0
  36. package/src/features/ChatInput/ActionBar/Params/ParamsControls.tsx +17 -7
  37. package/src/features/PluginDevModal/MCPManifestForm/index.tsx +209 -142
  38. package/src/features/PluginDevModal/PluginPreview/ApiVisualizer.tsx +180 -0
  39. package/src/features/PluginDevModal/PluginPreview/EmptyState.tsx +78 -0
  40. package/src/features/PluginDevModal/PluginPreview/index.tsx +72 -0
  41. package/src/features/PluginDevModal/index.tsx +75 -62
  42. package/src/hooks/useHotkeys/chatScope.ts +0 -2
  43. package/src/hooks/useHotkeys/filesScope.ts +0 -2
  44. package/src/libs/agent-runtime/openai/index.ts +11 -0
  45. package/src/libs/mcp/client.ts +9 -1
  46. package/src/libs/mcp/types.ts +1 -0
  47. package/src/locales/default/plugin.ts +18 -0
  48. package/src/features/PluginDevModal/PluginPreview.tsx +0 -34
@@ -7,14 +7,12 @@ import {
7
7
  SiPython,
8
8
  } from '@icons-pack/react-simple-icons';
9
9
  import { LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
10
- import { Alert, AutoComplete, Button, FormItem, Icon, Input } from '@lobehub/ui';
11
- import { Form, FormInstance } from 'antd';
12
- import { FileCode } from 'lucide-react';
13
- import { ChangeEvent, FC, useState } from 'react';
10
+ import { Alert, AutoComplete, FormItem, Input, TextArea } from '@lobehub/ui';
11
+ import { Button, Form, FormInstance } from 'antd';
12
+ import { FC, useState } from 'react';
14
13
  import { useTranslation } from 'react-i18next';
15
14
  import { Flexbox } from 'react-layout-kit';
16
15
 
17
- import ManifestPreviewer from '@/components/ManifestPreviewer';
18
16
  import { isDesktop } from '@/const/version';
19
17
  import { mcpService } from '@/services/mcp';
20
18
  import { useToolStore } from '@/store/tool';
@@ -58,57 +56,81 @@ const MCP_TYPE = ['customParams', 'mcp', 'type'];
58
56
  const MCPManifestForm = ({ form, isEditMode }: MCPManifestFormProps) => {
59
57
  const { t } = useTranslation('plugin');
60
58
  const mcpType = Form.useWatch(MCP_TYPE, form);
61
- const [manifest, setManifest] = useState<LobeChatPluginManifest>();
59
+
62
60
  const pluginIds = useToolStore(pluginSelectors.storeAndInstallPluginsIdList);
63
- const [pasteError, setPasteError] = useState<string | null>(null);
64
61
  const [isTesting, setIsTesting] = useState(false);
65
62
  const [connectionError, setConnectionError] = useState<string | null>(null);
63
+ const [isImportModalVisible, setIsImportModalVisible] = useState(false);
64
+ const [jsonInput, setJsonInput] = useState('');
65
+ const [importError, setImportError] = useState<string | null>(null);
66
66
 
67
- const handleIdentifierChange = (e: ChangeEvent<HTMLInputElement>) => {
68
- const value = e.target.value.trim();
69
- setPasteError(null); // Clear previous errors on new input
70
- setConnectionError(null); // Clear connection error on identifier change
67
+ const handleImportConfirm = () => {
68
+ setImportError(null); // Clear previous import error
69
+ setConnectionError(null); // Clear connection error
71
70
 
71
+ const value = jsonInput.trim(); // Use the text area input
72
+ if (!value) {
73
+ setImportError(t('dev.mcp.quickImportError.empty'));
74
+ return;
75
+ }
76
+
77
+ // Use the existing parseMcpInput function
72
78
  const parseResult = parseMcpInput(value);
73
79
 
74
- if (parseResult.status !== 'success') return;
80
+ // Handle parsing errors from parseMcpInput
81
+ if (parseResult.status === 'error') {
82
+ // Assuming parseMcpInput returns an error message or code in parseResult
83
+ // We might need a more specific error message based on parseResult.error
84
+ setImportError(parseResult.errorCode);
85
+ return;
86
+ }
87
+
88
+ if (parseResult.status === 'noop') {
89
+ setImportError(t('dev.mcp.quickImportError.invalidJson'));
90
+ return;
91
+ }
75
92
 
93
+ // Extract identifier and mcpConfig from the successful parse result
76
94
  const { identifier, mcpConfig } = parseResult;
77
95
 
96
+ // Check for desktop requirement for stdio
78
97
  if (!isDesktop && mcpConfig.type === 'stdio') {
98
+ setImportError(t('dev.mcp.stdioNotSupported'));
79
99
  return;
80
100
  }
81
101
 
82
102
  // Check for duplicate identifier (only in create mode)
83
103
  if (!isEditMode && pluginIds.includes(identifier)) {
84
- setPasteError(t('dev.meta.identifier.errorDuplicate'));
85
104
  // Update form fields even if duplicate, so user sees the pasted values
86
105
  form.setFieldsValue({
87
- // Update identifier field
88
- customParams: {
89
- mcp: mcpConfig, // Spread the parsed config (includes type)
90
- },
106
+ customParams: { mcp: mcpConfig },
91
107
  identifier: identifier,
92
108
  });
93
109
  // Trigger validation to show Form.Item error
94
110
  form.validateFields(['identifier']);
111
+ setIsImportModalVisible(false); // Close modal even on duplicate error
112
+ setJsonInput(''); // Clear modal input
95
113
  return;
96
114
  }
97
115
 
98
- // No duplicate or in edit mode, fill the form
116
+ // All checks passed, fill the form
99
117
  form.setFieldsValue({
100
118
  customParams: { mcp: mcpConfig },
101
119
  identifier: identifier,
102
120
  });
103
121
 
104
- // Clear potential old validation error on identifier
122
+ // Clear potential old validation error on identifier field
105
123
  form.setFields([{ errors: [], name: 'identifier' }]);
124
+
125
+ // Clear modal state and close (or rather, hide the import UI)
126
+ setIsImportModalVisible(false);
127
+ // setJsonInput(''); // Keep input for potential edits?
128
+ setImportError(null);
106
129
  };
107
130
 
108
131
  const handleTestConnection = async () => {
109
132
  setIsTesting(true);
110
133
  setConnectionError(null);
111
- setManifest(undefined); // Reset manifest before testing
112
134
 
113
135
  // Manually trigger validation for fields needed for the test
114
136
  let isValid = false;
@@ -142,7 +164,6 @@ const MCPManifestForm = ({ form, isEditMode }: MCPManifestFormProps) => {
142
164
  throw new Error('Invalid MCP type'); // Internal error
143
165
  }
144
166
 
145
- setManifest(data);
146
167
  // Optionally update form if manifest ID differs or to store the fetched manifest
147
168
  // Be careful about overwriting user input if not desired
148
169
  form.setFieldsValue({ manifest: data });
@@ -165,138 +186,184 @@ const MCPManifestForm = ({ form, isEditMode }: MCPManifestFormProps) => {
165
186
  };
166
187
 
167
188
  return (
168
- <Form form={form} layout={'vertical'}>
169
- <Flexbox>
170
- <Form.Item
171
- label={t('dev.mcp.type.title')}
172
- name={['customParams', 'mcp', 'type']}
173
- rules={[{ required: true }]}
174
- >
175
- <MCPTypeSelect />
176
- </Form.Item>
177
- {/* 仅在有粘贴相关错误时显示 Alert */}
178
- {pasteError && (
179
- <Alert message={pasteError} showIcon style={{ marginBottom: 16 }} type="error" />
180
- )}
181
- <FormItem
182
- desc={t('dev.mcp.identifier.desc')}
183
- label={t('dev.mcp.identifier.label')}
184
- name={'identifier'}
185
- rules={[
186
- { message: t('dev.mcp.identifier.required'), required: true },
187
- {
188
- message: t('dev.mcp.identifier.invalid'),
189
- pattern: /^[\w-]+$/,
190
- },
191
- isEditMode
192
- ? {}
193
- : {
194
- message: t('dev.meta.identifier.errorDuplicate'),
195
- validator: async () => {
196
- const id = form.getFieldValue('identifier');
197
- if (!id) return true;
198
- if (pluginIds.includes(id)) {
199
- throw new Error('Duplicate');
200
- }
201
- },
202
- },
203
- ]}
204
- tag={'identifier'}
205
- >
206
- <Input
207
- onChange={handleIdentifierChange}
208
- placeholder={t('dev.mcp.identifier.placeholder')}
189
+ <>
190
+ {isImportModalVisible ? (
191
+ <Flexbox gap={8}>
192
+ {importError && (
193
+ <Alert message={importError} showIcon style={{ marginBottom: 8 }} type="error" />
194
+ )}
195
+ <TextArea
196
+ autoSize={{ maxRows: 15, minRows: 10 }}
197
+ onChange={(e) => {
198
+ setJsonInput(e.target.value);
199
+ if (importError) setImportError(null);
200
+ }}
201
+ placeholder={`{
202
+ "mcpServers": {
203
+ "github": {
204
+ "command": "npx",
205
+ "args": [
206
+ "-y",
207
+ "@modelcontextprotocol/server-github"
208
+ ],
209
+ "env": {
210
+ "GITHUB_PERSONAL_ACCESS_TOKEN": "<your-api-key>"
211
+ }
212
+ }
213
+ }
214
+ }`}
215
+ value={jsonInput}
209
216
  />
210
- </FormItem>
217
+ <Flexbox horizontal justify={'space-between'}>
218
+ <Button
219
+ onClick={() => {
220
+ setIsImportModalVisible(false);
221
+ }}
222
+ size={'small'}
223
+ >
224
+ 取消
225
+ </Button>
226
+ <Button onClick={handleImportConfirm} size={'small'} type={'primary'}>
227
+ 导入
228
+ </Button>
229
+ </Flexbox>
230
+ </Flexbox>
231
+ ) : (
232
+ <div>
233
+ <Button
234
+ block // Make button full width
235
+ onClick={() => {
236
+ setImportError(null); // Clear previous errors when opening
237
+ setIsImportModalVisible(true);
238
+ }}
239
+ style={{ marginBottom: 16 }} // Add some spacing
240
+ type="dashed"
241
+ >
242
+ {t('dev.mcp.quickImport')}
243
+ </Button>
244
+ </div>
245
+ )}
246
+
247
+ <Form form={form} layout={'vertical'}>
248
+ <Flexbox>
249
+ <Form.Item
250
+ label={t('dev.mcp.type.title')}
251
+ name={['customParams', 'mcp', 'type']}
252
+ rules={[{ required: true }]}
253
+ >
254
+ <MCPTypeSelect />
255
+ </Form.Item>
211
256
 
212
- {mcpType === 'http' && (
213
257
  <FormItem
214
- desc={t('dev.mcp.url.desc')}
215
- label={t('dev.mcp.url.label')}
216
- name={HTTP_URL_KEY}
258
+ desc={t('dev.mcp.identifier.desc')}
259
+ label={t('dev.mcp.identifier.label')}
260
+ name={'identifier'}
217
261
  rules={[
218
- { message: t('dev.mcp.url.required'), required: true },
219
- { message: t('dev.mcp.url.invalid'), type: 'url' },
262
+ { message: t('dev.mcp.identifier.required'), required: true },
263
+ {
264
+ message: t('dev.mcp.identifier.invalid'),
265
+ pattern: /^[\w-]+$/,
266
+ },
267
+ isEditMode
268
+ ? {}
269
+ : {
270
+ message: t('dev.meta.identifier.errorDuplicate'),
271
+ validator: async () => {
272
+ const id = form.getFieldValue('identifier');
273
+ if (!id) return true;
274
+ if (pluginIds.includes(id)) {
275
+ throw new Error('Duplicate');
276
+ }
277
+ },
278
+ },
220
279
  ]}
221
- tag={'url'}
280
+ tag={'identifier'}
222
281
  >
223
- <Input placeholder="https://mcp.higress.ai/mcp-github/xxxxx" />
282
+ <Input placeholder={t('dev.mcp.identifier.placeholder')} />
224
283
  </FormItem>
225
- )}
226
284
 
227
- {mcpType === 'stdio' && (
228
- <>
229
- <FormItem
230
- desc={t('dev.mcp.command.desc')}
231
- label={t('dev.mcp.command.label')}
232
- name={STDIO_COMMAND}
233
- rules={[{ message: t('dev.mcp.command.required'), required: true }]}
234
- tag={'command'}
235
- >
236
- <AutoComplete
237
- options={STDIO_COMMAND_OPTIONS.map(({ value, icon: Icon, color }) => ({
238
- label: (
239
- <Flexbox align={'center'} gap={8} horizontal>
240
- {Icon && <Icon color={color} size={16} />}
241
- {value}
242
- </Flexbox>
243
- ),
244
- value: value,
245
- }))}
246
- placeholder={t('dev.mcp.command.placeholder')}
247
- />
248
- </FormItem>
249
- <FormItem
250
- desc={t('dev.mcp.args.desc')}
251
- label={t('dev.mcp.args.label')}
252
- name={STDIO_ARGS}
253
- rules={[{ message: t('dev.mcp.args.required'), required: true }]}
254
- tag={'args'}
255
- >
256
- <ArgsInput placeholder={t('dev.mcp.args.placeholder')} />
257
- </FormItem>
285
+ {mcpType === 'http' && (
258
286
  <FormItem
259
- extra={t('dev.mcp.env.desc')}
260
- label={t('dev.mcp.env.label')}
261
- name={STDIO_ENV}
262
- tag={'env'}
287
+ desc={t('dev.mcp.url.desc')}
288
+ label={t('dev.mcp.url.label')}
289
+ name={HTTP_URL_KEY}
290
+ rules={[
291
+ { message: t('dev.mcp.url.required'), required: true },
292
+ { message: t('dev.mcp.url.invalid'), type: 'url' },
293
+ ]}
294
+ tag={'url'}
263
295
  >
264
- <EnvEditor />
296
+ <Input placeholder="https://mcp.higress.ai/mcp-github/xxxxx" />
265
297
  </FormItem>
266
- </>
267
- )}
268
- <FormItem colon={false} label={t('dev.mcp.testConnectionTip')} layout={'horizontal'}>
269
- <Flexbox align={'center'} gap={8} horizontal justify={'flex-end'}>
270
- {manifest && !connectionError && !isTesting && (
271
- <ManifestPreviewer manifest={manifest}>
272
- <Flexbox>
273
- <Button icon={<Icon icon={FileCode} />}>{t('dev.mcp.previewManifest')}</Button>
274
- </Flexbox>
275
- </ManifestPreviewer>
276
- )}
277
- <Button
278
- loading={isTesting}
279
- onClick={handleTestConnection}
280
- type={!!mcpType ? 'primary' : undefined}
281
- >
282
- {t('dev.mcp.testConnection')}
283
- </Button>
284
- </Flexbox>
285
- </FormItem>
286
-
287
- {connectionError && (
288
- <Alert
289
- closable
290
- message={connectionError}
291
- onClose={() => setConnectionError(null)}
292
- showIcon
293
- style={{ marginBottom: 16 }}
294
- type="error"
295
- />
296
- )}
297
- <FormItem name={'manifest'} noStyle />
298
- </Flexbox>
299
- </Form>
298
+ )}
299
+
300
+ {mcpType === 'stdio' && (
301
+ <>
302
+ <FormItem
303
+ desc={t('dev.mcp.command.desc')}
304
+ label={t('dev.mcp.command.label')}
305
+ name={STDIO_COMMAND}
306
+ rules={[{ message: t('dev.mcp.command.required'), required: true }]}
307
+ tag={'command'}
308
+ >
309
+ <AutoComplete
310
+ options={STDIO_COMMAND_OPTIONS.map(({ value, icon: Icon, color }) => ({
311
+ label: (
312
+ <Flexbox align={'center'} gap={8} horizontal>
313
+ {Icon && <Icon color={color} size={16} />}
314
+ {value}
315
+ </Flexbox>
316
+ ),
317
+ value: value,
318
+ }))}
319
+ placeholder={t('dev.mcp.command.placeholder')}
320
+ />
321
+ </FormItem>
322
+ <FormItem
323
+ desc={t('dev.mcp.args.desc')}
324
+ label={t('dev.mcp.args.label')}
325
+ name={STDIO_ARGS}
326
+ rules={[{ message: t('dev.mcp.args.required'), required: true }]}
327
+ tag={'args'}
328
+ >
329
+ <ArgsInput placeholder={t('dev.mcp.args.placeholder')} />
330
+ </FormItem>
331
+ <FormItem
332
+ extra={t('dev.mcp.env.desc')}
333
+ label={t('dev.mcp.env.label')}
334
+ name={STDIO_ENV}
335
+ tag={'env'}
336
+ >
337
+ <EnvEditor />
338
+ </FormItem>
339
+ </>
340
+ )}
341
+ <FormItem colon={false} label={t('dev.mcp.testConnectionTip')} layout={'horizontal'}>
342
+ <Flexbox align={'center'} gap={8} horizontal justify={'flex-end'}>
343
+ <Button
344
+ loading={isTesting}
345
+ onClick={handleTestConnection}
346
+ type={!!mcpType ? 'primary' : undefined}
347
+ >
348
+ {t('dev.mcp.testConnection')}
349
+ </Button>
350
+ </Flexbox>
351
+ </FormItem>
352
+
353
+ {connectionError && (
354
+ <Alert
355
+ closable
356
+ message={connectionError}
357
+ onClose={() => setConnectionError(null)}
358
+ showIcon
359
+ style={{ marginBottom: 16 }}
360
+ type="error"
361
+ />
362
+ )}
363
+ <FormItem name={'manifest'} noStyle />
364
+ </Flexbox>
365
+ </Form>
366
+ </>
300
367
  );
301
368
  };
302
369
 
@@ -0,0 +1,180 @@
1
+ 'use client';
2
+
3
+ import { Block, Icon, Tag } from '@lobehub/ui';
4
+ import { Input, Space, Typography } from 'antd';
5
+ import { createStyles } from 'antd-style';
6
+ import { ChevronDown, ChevronRight } from 'lucide-react';
7
+ import { memo, useState } from 'react';
8
+ import { useTranslation } from 'react-i18next';
9
+ import { Flexbox } from 'react-layout-kit';
10
+
11
+ const useStyles = createStyles(({ css, token }) => ({
12
+ apiHeader: css`
13
+ cursor: pointer;
14
+ display: flex;
15
+ align-items: center;
16
+ justify-content: space-between;
17
+ `,
18
+
19
+ apiTitle: css`
20
+ font-family: ${token.fontFamilyCode};
21
+ `,
22
+
23
+ emptyState: css`
24
+ padding: 32px;
25
+ color: ${token.colorTextDisabled};
26
+ text-align: center;
27
+ `,
28
+ header: css`
29
+ display: flex;
30
+ gap: 8px;
31
+ align-items: center;
32
+ margin-block-end: 24px;
33
+ `,
34
+ paramDesc: css`
35
+ font-size: 12px;
36
+ line-height: 18px;
37
+ color: ${token.colorTextSecondary};
38
+ `,
39
+ paramGrid: css`
40
+ display: grid;
41
+ grid-template-columns: 1fr 2fr;
42
+ gap: 12px;
43
+ align-items: center;
44
+
45
+ margin-block-end: 12px;
46
+ `,
47
+ paramName: css`
48
+ display: flex;
49
+ gap: 6px;
50
+ align-items: center;
51
+ font-family: monospace;
52
+ `,
53
+ params: css`
54
+ color: ${token.colorTextQuaternary};
55
+ `,
56
+ required: css`
57
+ margin-inline-start: 2px;
58
+ color: ${token.colorError};
59
+ `,
60
+ searchIcon: css`
61
+ position: absolute;
62
+ z-index: 1;
63
+ inset-block-start: 50%;
64
+ inset-inline-start: 12px;
65
+ transform: translateY(-50%);
66
+
67
+ color: ${token.colorTextSecondary};
68
+ `,
69
+ searchWrapper: css`
70
+ position: relative;
71
+ `,
72
+ typeTag: css`
73
+ height: 20px;
74
+ padding-block: 0;
75
+ padding-inline: 6px;
76
+
77
+ font-size: 12px;
78
+ line-height: 20px;
79
+ `,
80
+ }));
81
+
82
+ interface ApiItemProps {
83
+ api: {
84
+ description: string;
85
+ name: string;
86
+ parameters: {
87
+ properties: Record<string, { description: string; type: string }>;
88
+ required: string[];
89
+ };
90
+ };
91
+ }
92
+
93
+ const ApiItem = memo<ApiItemProps>(({ api }) => {
94
+ const { styles, theme } = useStyles();
95
+ const [expanded, setExpanded] = useState(false);
96
+ const { t } = useTranslation('plugin');
97
+
98
+ const params = Object.entries(api.parameters.properties || {});
99
+ return (
100
+ <Block gap={8} padding={16}>
101
+ <div className={styles.apiHeader} onClick={() => setExpanded(!expanded)}>
102
+ <Flexbox gap={4}>
103
+ <div className={styles.apiTitle}>{api.name}</div>
104
+ <Typography.Text type="secondary">{api.description}</Typography.Text>
105
+ </Flexbox>
106
+
107
+ <Icon icon={expanded ? ChevronDown : ChevronRight} />
108
+ </div>
109
+
110
+ {expanded && (
111
+ <Flexbox
112
+ gap={12}
113
+ padding={16}
114
+ style={{ background: theme.colorFillQuaternary, borderRadius: 6 }}
115
+ >
116
+ {params.length === 0 ? (
117
+ <div className={styles.params}>{t('dev.preview.api.noParams')}</div>
118
+ ) : (
119
+ <>
120
+ <div className={styles.params}>{t('dev.preview.api.params')}</div>
121
+ <Space direction="vertical" style={{ width: '100%' }}>
122
+ {params.map(([name, param]) => {
123
+ const isRequired = api.parameters.required?.includes(name);
124
+ return (
125
+ <div className={styles.paramGrid} key={name}>
126
+ <div className={styles.paramName}>
127
+ <span>{name}</span>
128
+ {isRequired && <span className={styles.required}>*</span>}
129
+ <Tag className={styles.typeTag}>{param.type}</Tag>
130
+ </div>
131
+ <div className={styles.paramDesc}>{param.description}</div>
132
+ </div>
133
+ );
134
+ })}
135
+ </Space>
136
+ </>
137
+ )}
138
+ </Flexbox>
139
+ )}
140
+ </Block>
141
+ );
142
+ });
143
+
144
+ interface ApiVisualizerProps {
145
+ apis: ApiItemProps['api'][];
146
+ }
147
+
148
+ const ApiVisualizer = memo<ApiVisualizerProps>(({ apis = [] }) => {
149
+ const { styles } = useStyles();
150
+ const [searchQuery, setSearchQuery] = useState('');
151
+ const { t } = useTranslation('plugin');
152
+
153
+ const filteredApis = apis.filter(
154
+ (api) =>
155
+ api.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
156
+ api.description.toLowerCase().includes(searchQuery.toLowerCase()),
157
+ );
158
+
159
+ return (
160
+ <Flexbox gap={8} width={'100%'}>
161
+ <div className={styles.searchWrapper}>
162
+ <Input.Search
163
+ onChange={(e) => setSearchQuery(e.target.value)}
164
+ placeholder={t('dev.preview.api.searchPlaceholder')}
165
+ value={searchQuery}
166
+ />
167
+ </div>
168
+
169
+ <Space direction="vertical" style={{ width: '100%' }}>
170
+ {filteredApis.length > 0 ? (
171
+ filteredApis.map((api, index) => <ApiItem api={api} key={index} />)
172
+ ) : (
173
+ <div className={styles.emptyState}>{t('dev.preview.api.noResults')}</div>
174
+ )}
175
+ </Space>
176
+ </Flexbox>
177
+ );
178
+ });
179
+
180
+ export default ApiVisualizer;
@@ -0,0 +1,78 @@
1
+ import { Icon } from '@lobehub/ui';
2
+ import { Space, Typography } from 'antd';
3
+ import { createStyles } from 'antd-style';
4
+ import { Puzzle } from 'lucide-react';
5
+ import { useTranslation } from 'react-i18next';
6
+
7
+ const { Title, Paragraph } = Typography;
8
+
9
+ // Create styles using antd-style
10
+ const useStyles = createStyles(({ token, css }) => ({
11
+ container: css`
12
+ display: flex;
13
+ flex-direction: column;
14
+ align-items: center;
15
+ justify-content: center;
16
+
17
+ width: 100%;
18
+ height: 100%;
19
+ padding: ${token.paddingLG}px;
20
+ `,
21
+ description: css`
22
+ max-width: 320px;
23
+ color: ${token.colorTextSecondary};
24
+ text-align: center;
25
+ `,
26
+ iconWrapper: css`
27
+ display: flex;
28
+ align-items: center;
29
+ justify-content: center;
30
+
31
+ width: 64px;
32
+ height: 64px;
33
+ margin-block-end: ${token.marginMD}px;
34
+ border-radius: 50%;
35
+
36
+ background-color: ${token.colorPrimaryBg};
37
+ `,
38
+ line: css`
39
+ height: 6px;
40
+ border-radius: 3px;
41
+ background: ${token.colorBorderSecondary};
42
+ `,
43
+ placeholderLine: css`
44
+ height: 6px;
45
+ margin-block: ${token.marginXS}px;
46
+ margin-inline: 0;
47
+ border-radius: ${token.borderRadiusLG}px;
48
+
49
+ background-color: ${token.colorBorderSecondary};
50
+ `,
51
+ title: css`
52
+ margin-block-end: ${token.marginXS}px;
53
+ font-size: ${token.fontSizeLG}px;
54
+ font-weight: 500;
55
+ `,
56
+ }));
57
+
58
+ export default function PluginEmptyState() {
59
+ const { styles } = useStyles();
60
+ const { t } = useTranslation('plugin');
61
+
62
+ return (
63
+ <div className={styles.container}>
64
+ <div className={styles.iconWrapper}>
65
+ <Icon icon={Puzzle} size={32} />
66
+ </div>
67
+ <Title className={styles.title} level={4}>
68
+ {t('dev.preview.empty.title')}
69
+ </Title>
70
+ <Paragraph className={styles.description}>{t('dev.preview.empty.desc')}</Paragraph>
71
+ <Space align="center" direction="vertical" style={{ marginTop: 24 }}>
72
+ <div className={styles.line} style={{ width: 128 }} />
73
+ <div className={styles.line} style={{ width: 96 }} />
74
+ <div className={styles.line} style={{ width: 48 }} />
75
+ </Space>
76
+ </div>
77
+ );
78
+ }