diff --git a/web/src/app/home/models/component/llm-form/LLMForm.tsx b/web/src/app/home/models/component/llm-form/LLMForm.tsx index 6d46da6d..2b663fa5 100644 --- a/web/src/app/home/models/component/llm-form/LLMForm.tsx +++ b/web/src/app/home/models/component/llm-form/LLMForm.tsx @@ -304,7 +304,7 @@ export default function LLMForm({ onLLMDeleted(); toast.success(t('models.deleteSuccess')); }) - .catch((err) => { + .catch ((err) => { toast.error(t('models.deleteError') + err.message); }); } diff --git a/web/src/app/home/plugins/page.tsx b/web/src/app/home/plugins/page.tsx index 37fd57c4..4f071593 100644 --- a/web/src/app/home/plugins/page.tsx +++ b/web/src/app/home/plugins/page.tsx @@ -43,6 +43,20 @@ import { systemInfo } from '@/app/infra/http/HttpClient'; import { ApiRespPluginSystemStatus } from '@/app/infra/entities/api'; import { set } from 'lodash'; import { passiveEventSupported } from '@tanstack/react-table'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@radix-ui/react-select'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { number, z } from 'zod'; +import { DialogDescription } from '@radix-ui/react-dialog'; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; enum PluginInstallStatus { @@ -52,7 +66,23 @@ enum PluginInstallStatus { ERROR = 'error', } -export default function PluginConfigPage() { +export default function PluginConfigPage( + { + editMode, + initMCPId, + onFormSubmit, + onFormCancel, + onMcpDeleted, + }: + { + editMode: boolean; + initMCPId?: string; + onFormSubmit: () => void; + onFormCancel: () => void; + onMcpDeleted: () => void; + } +) { + const { t } = useTranslation(); const [activeTab, setActiveTab] = useState('installed'); const [modalOpen, setModalOpen] = useState(false); @@ -63,16 +93,9 @@ export default function PluginConfigPage() { // const [mcpModalOpen, setMcpModalOpen] = useState(false); const [mcpMarketInstallModalOpen, setMcpMarketInstallModalOpen] = useState(false); - const [mcpSSEInstallModalOpen, setMcpSSEInstallModalOpen] = useState(false); - const [mcpDescription,setMcpDescription] = useState(''); + const [mcpSSEModalOpen, setMcpSSEModalOpen] = useState(false); const [pluginInstallStatus, setPluginInstallStatus] = useState(PluginInstallStatus.WAIT_INPUT); - const [mcpInstallStatus, setMcpInstallStatus] = useState( - PluginInstallStatus.WAIT_INPUT, - ); - const [mcpSSEHeaders,setMcpSSEHeaders] = useState('') - const [mcpName,setMcpName] = useState('') - const [mcpTimeout,setMcpTimeout] = useState(60) const [installError, setInstallError] = useState(null); const [mcpInstallError, setMcpInstallError] = useState(null); const [githubURL, setGithubURL] = useState(''); @@ -81,7 +104,77 @@ export default function PluginConfigPage() { useState(null); const [statusLoading, setStatusLoading] = useState(true); const fileInputRef = useRef(null); - + const addExtraArg = () => { + setExtraArgs([...extraArgs, { key: '', type: 'string', value: '' }]); + }; + const getExtraArgSchema = (t: (key: string) => string) => + z + .object({ + key: z.string().min(1, { message: t('models.keyNameRequired') }), + type: z.enum(['string', 'number', 'boolean']), + value: z.string(), + }) + .superRefine((data, ctx) => { + if (data.type === 'number' && isNaN(Number(data.value))) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: t('models.mustBeValidNumber'), + path: ['value'], + }); + } + if ( + data.type === 'boolean' && + data.value !== 'true' && + data.value !== 'false' + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: t('models.mustBeTrueOrFalse'), + path: ['value'], + }); + } + }); + const removeExtraArg = (index: number) => { + const newArgs = extraArgs.filter((_, i) => i !== index); + setExtraArgs(newArgs); + form.setValue('extra_args', newArgs); + }; + const getFormSchema = (t: (key: string) => string) => + z.object({ + name: z.string().min(1, { message: t('mcp.nameRequired') }), + timeout: z.number().min(30, { message: t('mcp.timeoutMin30') }), + ssereadtimeout: z.number().min(300, { message: t('mcp.sseTimeoutMin300') }), + url: z.string().min(1, { message: t('mcp.requestURLRequired') }), + extra_args: z.array(getExtraArgSchema(t)).optional(), + }); + const formSchema = getFormSchema(t); + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + name: '', + url: '', + timeout: 30, + ssereadtimeout: 300, + extra_args: [], + }, + }); + const [extraArgs, setExtraArgs] = useState< + { key: string; type: 'string' | 'number' | 'boolean'; value: string }[] + >([]); + const updateExtraArg = ( + index: number, + field: 'key' | 'type' | 'value', + value: string, + ) => { + const newArgs = [...extraArgs]; + newArgs[index] = { + ...newArgs[index], + [field]: value, + }; + setExtraArgs(newArgs); + form.setValue('extra_args', newArgs); + }; + const [showDeleteConfirmModal, setShowDeleteConfirmModal] = useState(false); useEffect(() => { const fetchPluginSystemStatus = async () => { try { @@ -132,14 +225,11 @@ export default function PluginConfigPage() { const [mcpInstallConfig, setMcpInstallConfig] = useState | null>(null); const pluginInstalledRef = useRef(null); const mcpComponentRef = useRef(null); + const [mcpTesting, setMcpTesting] = useState(false); function handleModalConfirm() { installPlugin(installSource, installInfo as Record); // eslint-disable-line @typescript-eslint/no-explicit-any } - - function handleMcpModalConfirm() { - installMcpServerFromSSE(mcpSSEConfig ?? {}); - } function installPlugin( installSource: string, installInfo: Record, // eslint-disable-line @typescript-eslint/no-explicit-any @@ -183,6 +273,53 @@ export default function PluginConfigPage() { } } + function deleteMCPServer() { + + } + + function handleFormSubmit(value: z.infer) { + const extraArgsObj: Record = {}; + value.extra_args?.forEach( + (arg: { key: string; type: string; value: string }) => { + if (arg.type === 'number') { + extraArgsObj[arg.key] = Number(arg.value); + } else if (arg.type === 'boolean') { + extraArgsObj[arg.key] = arg.value === 'true'; + } else { + extraArgsObj[arg.key] = arg.value; + } + }, + ); + } + + function testMcp() { + setMcpTesting(true); + const extraArgsObj: Record = {}; + form + .getValues('extra_args') + ?.forEach((arg: { key: string; type: string; value: string }) => { + if (arg.type === 'number') { + extraArgsObj[arg.key] = Number(arg.value); + } else if (arg.type === 'boolean') { + extraArgsObj[arg.key] = arg.value === 'true'; + } else { + extraArgsObj[arg.key] = arg.value; + } + }); + httpClient.testMCPServer( + form.getValues('name'), + ).then((res) => { + console.log(res); + toast.success(t('models.testSuccess')); + }) + .catch(() => { + toast.error(t('models.testError')); + }) + .finally(() => { + setMcpTesting(false); + }); + } + const validateFileType = (file: File): boolean => { const allowedExtensions = ['.lbpkg', '.zip']; const fileName = file.name.toLowerCase(); @@ -320,74 +457,6 @@ export default function PluginConfigPage() { return renderPluginConnectionErrorState(); } - function installMcpServerFromSSE(config?: Record) { - setMcpInstallStatus(PluginInstallStatus.INSTALLING); - console.log('installing mcp server from sse with config:', config); - httpClient.installMCPServerFromSSE(config ?? {}) - .then((resp:any) => { - if (resp && resp.status === 'success') { - console.log('MCP server installed successfully'); - toast.success(t('mcp.installSuccess')); - setMcpSSEURL(''); - setMcpName(''); - setMcpDescription(''); - setMcpSSEHeaders(''); - setMcpTimeout(60); - setMcpSSEInstallModalOpen(false); - mcpComponentRef.current?.refreshServerList(); - } else { - setMcpInstallError(t('mcp.installFailed')); - setMcpInstallStatus(PluginInstallStatus.ERROR); - } - }) - .catch((err) => { - console.log('error when install mcp server:', err); - setMcpInstallError(err.message); - setMcpInstallStatus(PluginInstallStatus.ERROR); - }); - } - - // function installMcpServer(url: string, config?: Record) { - // setMcpInstallStatus(PluginInstallStatus.INSTALLING); - // // NOTE: backend currently only accepts url. If backend accepts config in future, - // // replace this call with: httpClient.installMCPServerFromGithub(url, config) - // console.log('installing mcp server with config:', config); - // httpClient.installMCPServerFromGithub(url) - // .then((resp) => { - // const taskId = resp.task_id; - - // let alreadySuccess = false; - // console.log('taskId:', taskId); - - // // 每秒拉取一次任务状态 - // const interval = setInterval(() => { - // httpClient.getAsyncTask(taskId).then((resp) => { - // console.log('task status:', resp); - // if (resp.runtime.done) { - // clearInterval(interval); - // if (resp.runtime.exception) { - // setMcpInstallError(resp.runtime.exception); - // setMcpInstallStatus(PluginInstallStatus.ERROR); - // } else { - // // success - // if (!alreadySuccess) { - // toast.success(t('mcp.installSuccess')); - // alreadySuccess = true; - // } - // setMcpGithubURL(''); - // setMcpMarketInstallModalOpen(false); - // mcpComponentRef.current?.refreshServerList(); - // } - // } - // }); - // }, 1000); - // }) - // .catch((err) => { - // console.log('error when install mcp server:', err); - // setMcpInstallError(err.message); - // setMcpInstallStatus(PluginInstallStatus.ERROR); - // }); - // } return (
{ setActiveTab('mcp-market'); - setMcpSSEInstallModalOpen(true); - setMcpInstallStatus(PluginInstallStatus.WAIT_INPUT); - setMcpInstallError(null); + setMcpSSEModalOpen(true); }} > @@ -511,7 +578,7 @@ export default function PluginConfigPage() { askInstallServer={(githubURL) => { setMcpGithubURL(githubURL); setMcpMarketInstallModalOpen(true); - setMcpInstallStatus(PluginInstallStatus.WAIT_INPUT); + // setMcpInstallStatus(PluginInstallStatus.WAIT_INPUT); setMcpInstallError(null); }} /> @@ -593,316 +660,229 @@ export default function PluginConfigPage() {
)} - {/* { - pluginInstalledRef.current?.refreshPluginList(); - }} - /> */} - - {/* 通过sse安装MCP服务器 */} - - +
+ + - - - {t('mcp.installFromSSE')} - + {t('plugins.confirmDeleteTitle')} - -
-
- - setMcpName(e.target.value)} - className='mb-1' - /> -
-
- -
-
- - setMcpDescription(e.target.value)} - className='mb-1' - /> -
-
- - {/* form fields */} -
-
- - setMcpSSEURL(e.target.value)} - className="mb-1" - /> -
-
- - - -
-
- - setMcpTimeout(Number(e.target.value))} - className="mb-1" - /> -
-
- - {mcpInstallStatus === PluginInstallStatus.INSTALLING && ( -
-

{t('mcp.installing')}

-
- )} - {mcpInstallStatus === PluginInstallStatus.ERROR && ( -
-

{t('mcp.installFailed')}

-

{mcpInstallError}

-
- )} - + + {t('plugins.deleteConfirmation')} + - {(mcpInstallStatus === PluginInstallStatus.WAIT_INPUT || - mcpInstallStatus === PluginInstallStatus.ERROR) && ( - <> - - - - )} +
-
- - {/* MCP Server 从github安装对话框(表单) */} - - + + + + - - - {t('mcp.installFromGithub')} + + {t('mcp.createServer')} - - {/* form fields */} -
-
- - setMcpGithubURL(e.target.value)} - className="mb-1" +
+ +
+ ( + + {t('mcp.name')} + + + + + + )} /> -
- -
- - - setMcpInstallConfig((c) => ({ ...(c || {}), displayName: e.target.value })) - } - className="mb-1" - /> -
- -
-
- - - setMcpInstallConfig((c) => ({ ...(c || {}), port: e.target.value })) - } + + ( + + + {t('mcp.url')} + + + + + + + )} /> -
-
- - - setMcpInstallConfig((c) => ({ ...(c || {}), env: e.target.value })) - } + + ( + + + {t('mcp.timeout')} + + + + + + + ) + } /> -
-
-
- - - setMcpInstallConfig((c) => ({ ...(c || {}), adminToken: e.target.value })) - } - /> -
+ + ( + + + {t('mcp.ssereadtimeout')} + + + + + + + ) + } + /> -
- - ) => - setMcpInstallConfig((c) => ({ ...(c || {}), extraConfig: e.target.value })) - } - /> -

- {t('mcp.extraConfigHint', 'Optional JSON string for advanced config')} -

-
-
- - {mcpInstallStatus === PluginInstallStatus.INSTALLING && ( -
-

{t('mcp.installing')}

-
- )} - {mcpInstallStatus === PluginInstallStatus.ERROR && ( -
-

{t('mcp.installFailed')}

-

{mcpInstallError}

-
- )} - - - {(mcpInstallStatus === PluginInstallStatus.WAIT_INPUT || - mcpInstallStatus === PluginInstallStatus.ERROR) && ( - <> - - +
+ ))} + - +
+ + {t('llm.extraParametersDescription')} + + + + + + {editMode && ( + )} + + + + + + + + +
-
- + + + ); } diff --git a/web/src/i18n/locales/zh-Hans.ts b/web/src/i18n/locales/zh-Hans.ts index d81b7386..cac04c99 100644 --- a/web/src/i18n/locales/zh-Hans.ts +++ b/web/src/i18n/locales/zh-Hans.ts @@ -348,6 +348,7 @@ const zhHans = { headersExample:'示例: Authorization: Bearer token123', enterTimeout:'输入超时时间,单位为毫秒', installFromSSE:'从SSE安装', + sseTimeout:'SSE超时时间' }, pipelines: { title: '流水线',