@lobehub/chat 1.84.3 → 1.84.5
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 +50 -0
- package/changelog/v1.json +18 -0
- package/locales/ar/plugin.json +18 -0
- package/locales/bg-BG/plugin.json +18 -0
- package/locales/de-DE/plugin.json +18 -0
- package/locales/en-US/plugin.json +18 -0
- package/locales/es-ES/plugin.json +18 -0
- package/locales/fa-IR/plugin.json +18 -0
- package/locales/fr-FR/plugin.json +18 -0
- package/locales/it-IT/plugin.json +18 -0
- package/locales/ja-JP/plugin.json +18 -0
- package/locales/ko-KR/plugin.json +18 -0
- package/locales/nl-NL/plugin.json +18 -0
- package/locales/pl-PL/plugin.json +18 -0
- package/locales/pt-BR/plugin.json +18 -0
- package/locales/ru-RU/plugin.json +18 -0
- package/locales/tr-TR/plugin.json +18 -0
- package/locales/vi-VN/plugin.json +18 -0
- package/locales/zh-CN/plugin.json +18 -0
- package/locales/zh-TW/plugin.json +18 -0
- package/package.json +2 -2
- package/src/config/aiModels/openrouter.ts +177 -1
- package/src/config/aiModels/qwen.ts +202 -4
- package/src/features/PluginDevModal/MCPManifestForm/index.tsx +209 -142
- package/src/features/PluginDevModal/PluginPreview/ApiVisualizer.tsx +180 -0
- package/src/features/PluginDevModal/PluginPreview/EmptyState.tsx +78 -0
- package/src/features/PluginDevModal/PluginPreview/index.tsx +72 -0
- package/src/features/PluginDevModal/index.tsx +75 -62
- package/src/libs/agent-runtime/qwen/index.ts +7 -3
- package/src/locales/default/plugin.ts +18 -0
- 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,
|
11
|
-
import { Form, FormInstance } from 'antd';
|
12
|
-
import {
|
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
|
-
|
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
|
68
|
-
|
69
|
-
|
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
|
-
|
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
|
-
|
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
|
-
//
|
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
|
-
|
169
|
-
|
170
|
-
<
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
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
|
-
|
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.
|
215
|
-
label={t('dev.mcp.
|
216
|
-
name={
|
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.
|
219
|
-
{
|
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={'
|
280
|
+
tag={'identifier'}
|
222
281
|
>
|
223
|
-
<Input placeholder=
|
282
|
+
<Input placeholder={t('dev.mcp.identifier.placeholder')} />
|
224
283
|
</FormItem>
|
225
|
-
)}
|
226
284
|
|
227
|
-
|
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
|
-
|
260
|
-
label={t('dev.mcp.
|
261
|
-
name={
|
262
|
-
|
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
|
-
<
|
296
|
+
<Input placeholder="https://mcp.higress.ai/mcp-github/xxxxx" />
|
265
297
|
</FormItem>
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
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
|
+
}
|