feat: change sse frontend

This commit is contained in:
WangCham
2025-10-22 19:09:39 +08:00
parent 760db38c11
commit 345eccf04c
3 changed files with 361 additions and 380 deletions

View File

@@ -304,7 +304,7 @@ export default function LLMForm({
onLLMDeleted(); onLLMDeleted();
toast.success(t('models.deleteSuccess')); toast.success(t('models.deleteSuccess'));
}) })
.catch((err) => { .catch ((err) => {
toast.error(t('models.deleteError') + err.message); toast.error(t('models.deleteError') + err.message);
}); });
} }

View File

@@ -43,6 +43,20 @@ import { systemInfo } from '@/app/infra/http/HttpClient';
import { ApiRespPluginSystemStatus } from '@/app/infra/entities/api'; import { ApiRespPluginSystemStatus } from '@/app/infra/entities/api';
import { set } from 'lodash'; import { set } from 'lodash';
import { passiveEventSupported } from '@tanstack/react-table'; 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 { enum PluginInstallStatus {
@@ -52,7 +66,23 @@ enum PluginInstallStatus {
ERROR = 'error', 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 { t } = useTranslation();
const [activeTab, setActiveTab] = useState('installed'); const [activeTab, setActiveTab] = useState('installed');
const [modalOpen, setModalOpen] = useState(false); const [modalOpen, setModalOpen] = useState(false);
@@ -63,16 +93,9 @@ export default function PluginConfigPage() {
// const [mcpModalOpen, setMcpModalOpen] = useState(false); // const [mcpModalOpen, setMcpModalOpen] = useState(false);
const [mcpMarketInstallModalOpen, setMcpMarketInstallModalOpen] = const [mcpMarketInstallModalOpen, setMcpMarketInstallModalOpen] =
useState(false); useState(false);
const [mcpSSEInstallModalOpen, setMcpSSEInstallModalOpen] = useState(false); const [mcpSSEModalOpen, setMcpSSEModalOpen] = useState(false);
const [mcpDescription,setMcpDescription] = useState('');
const [pluginInstallStatus, setPluginInstallStatus] = const [pluginInstallStatus, setPluginInstallStatus] =
useState<PluginInstallStatus>(PluginInstallStatus.WAIT_INPUT); useState<PluginInstallStatus>(PluginInstallStatus.WAIT_INPUT);
const [mcpInstallStatus, setMcpInstallStatus] = useState<PluginInstallStatus>(
PluginInstallStatus.WAIT_INPUT,
);
const [mcpSSEHeaders,setMcpSSEHeaders] = useState('')
const [mcpName,setMcpName] = useState('')
const [mcpTimeout,setMcpTimeout] = useState(60)
const [installError, setInstallError] = useState<string | null>(null); const [installError, setInstallError] = useState<string | null>(null);
const [mcpInstallError, setMcpInstallError] = useState<string | null>(null); const [mcpInstallError, setMcpInstallError] = useState<string | null>(null);
const [githubURL, setGithubURL] = useState(''); const [githubURL, setGithubURL] = useState('');
@@ -81,7 +104,77 @@ export default function PluginConfigPage() {
useState<ApiRespPluginSystemStatus | null>(null); useState<ApiRespPluginSystemStatus | null>(null);
const [statusLoading, setStatusLoading] = useState(true); const [statusLoading, setStatusLoading] = useState(true);
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(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<z.infer<typeof formSchema>>({
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(() => { useEffect(() => {
const fetchPluginSystemStatus = async () => { const fetchPluginSystemStatus = async () => {
try { try {
@@ -132,14 +225,11 @@ export default function PluginConfigPage() {
const [mcpInstallConfig, setMcpInstallConfig] = useState<Record<string, any> | null>(null); const [mcpInstallConfig, setMcpInstallConfig] = useState<Record<string, any> | null>(null);
const pluginInstalledRef = useRef<PluginInstalledComponentRef>(null); const pluginInstalledRef = useRef<PluginInstalledComponentRef>(null);
const mcpComponentRef = useRef<MCPComponentRef>(null); const mcpComponentRef = useRef<MCPComponentRef>(null);
const [mcpTesting, setMcpTesting] = useState(false);
function handleModalConfirm() { function handleModalConfirm() {
installPlugin(installSource, installInfo as Record<string, any>); // eslint-disable-line @typescript-eslint/no-explicit-any installPlugin(installSource, installInfo as Record<string, any>); // eslint-disable-line @typescript-eslint/no-explicit-any
} }
function handleMcpModalConfirm() {
installMcpServerFromSSE(mcpSSEConfig ?? {});
}
function installPlugin( function installPlugin(
installSource: string, installSource: string,
installInfo: Record<string, any>, // eslint-disable-line @typescript-eslint/no-explicit-any installInfo: Record<string, any>, // eslint-disable-line @typescript-eslint/no-explicit-any
@@ -183,6 +273,53 @@ export default function PluginConfigPage() {
} }
} }
function deleteMCPServer() {
}
function handleFormSubmit(value: z.infer<typeof formSchema>) {
const extraArgsObj: Record<string, string | number | boolean> = {};
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<string, string | number | boolean> = {};
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 validateFileType = (file: File): boolean => {
const allowedExtensions = ['.lbpkg', '.zip']; const allowedExtensions = ['.lbpkg', '.zip'];
const fileName = file.name.toLowerCase(); const fileName = file.name.toLowerCase();
@@ -320,74 +457,6 @@ export default function PluginConfigPage() {
return renderPluginConnectionErrorState(); return renderPluginConnectionErrorState();
} }
function installMcpServerFromSSE(config?: Record<string, any>) {
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<string, any>) {
// 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 ( return (
<div <div
@@ -455,9 +524,7 @@ export default function PluginConfigPage() {
<DropdownMenuItem <DropdownMenuItem
onClick={() => { onClick={() => {
setActiveTab('mcp-market'); setActiveTab('mcp-market');
setMcpSSEInstallModalOpen(true); setMcpSSEModalOpen(true);
setMcpInstallStatus(PluginInstallStatus.WAIT_INPUT);
setMcpInstallError(null);
}} }}
> >
<PlusIcon className="w-4 h-4" /> <PlusIcon className="w-4 h-4" />
@@ -511,7 +578,7 @@ export default function PluginConfigPage() {
askInstallServer={(githubURL) => { askInstallServer={(githubURL) => {
setMcpGithubURL(githubURL); setMcpGithubURL(githubURL);
setMcpMarketInstallModalOpen(true); setMcpMarketInstallModalOpen(true);
setMcpInstallStatus(PluginInstallStatus.WAIT_INPUT); // setMcpInstallStatus(PluginInstallStatus.WAIT_INPUT);
setMcpInstallError(null); setMcpInstallError(null);
}} }}
/> />
@@ -593,316 +660,229 @@ export default function PluginConfigPage() {
</div> </div>
)} )}
{/* <PluginSortDialog <div>
open={sortModalOpen} <Dialog
onOpenChange={setSortModalOpen} open={showDeleteConfirmModal}
onSortComplete={() => { onOpenChange={setShowDeleteConfirmModal}
pluginInstalledRef.current?.refreshPluginList(); >
}} <DialogContent>
/> */}
{/* 通过sse安装MCP服务器 */}
<Dialog
open={mcpSSEInstallModalOpen}
onOpenChange={setMcpSSEInstallModalOpen}
>
<DialogContent className="w-[520px] p-6 bg-white dark:bg-[#1a1a1e]">
<DialogHeader> <DialogHeader>
<DialogTitle className="flex items-center gap-4"> <DialogTitle>{t('plugins.confirmDeleteTitle')}</DialogTitle>
<Download className="size-6" />
<span>{t('mcp.installFromSSE')}</span>
</DialogTitle>
</DialogHeader> </DialogHeader>
<DialogDescription>
<div> {t('plugins.deleteConfirmation')}
<div> </DialogDescription>
<label className='text-sm text-muted-foreground block mb-1'>
{t('mcp.name')}
</label>
<Input
placeholder={t('mcp.nameExplained')}
value={mcpName}
onChange={(e) => setMcpName(e.target.value)}
className='mb-1'
/>
</div>
</div>
<div>
<div>
<label className="text-sm text-muted-foreground block mb-1">
{t('mcp.mcpDescription')}
</label>
<Input
placeholder={t('mcp.descriptionExplained')}
value={mcpDescription}
onChange={(e) => setMcpDescription(e.target.value)}
className='mb-1'
/>
</div>
</div>
{/* form fields */}
<div className="mt-4 space-y-3">
<div>
<label className="text-sm text-muted-foreground block mb-1">
{t('mcp.sseURL')}
</label>
<Input
placeholder={t('mcp.enterSSELink')}
value={mcpSSEURL}
onChange={(e) => setMcpSSEURL(e.target.value)}
className="mb-1"
/>
</div>
</div>
<div className='mt-4'>
<div>
<label className='text-sm text-muted-foreground block mb-1'>
{t('mcp.timeout')}
</label>
<Input
placeholder={t('mcp.enterTimeout')}
value={mcpTimeout || 60}
onChange={(e) => setMcpTimeout(Number(e.target.value))}
className="mb-1"
/>
</div>
</div>
{mcpInstallStatus === PluginInstallStatus.INSTALLING && (
<div className="mt-4">
<p className="mb-2">{t('mcp.installing')}</p>
</div>
)}
{mcpInstallStatus === PluginInstallStatus.ERROR && (
<div className="mt-4">
<p className="mb-2">{t('mcp.installFailed')}</p>
<p className="mb-2 text-red-500">{mcpInstallError}</p>
</div>
)}
<DialogFooter> <DialogFooter>
{(mcpInstallStatus === PluginInstallStatus.WAIT_INPUT || <Button
mcpInstallStatus === PluginInstallStatus.ERROR) && ( variant='destructive'
<> onClick={() => {
<Button deleteMCPServer();
variant="outline" setShowDeleteConfirmModal(false);
onClick={() => { }}
setMcpSSEInstallModalOpen(false) >
setMcpInstallStatus(PluginInstallStatus.WAIT_INPUT); {t('common.confirm')}
setMcpInstallError(null); </Button>
setMcpInstallConfig(null);
setMcpSSEURL('')
setMcpName('')
setMcpTimeout(60)
setMcpDescription('')
setMcpSSEHeaders('')
}}
>
{t('common.cancel')}
</Button>
<Button
onClick={() => {
// basic validation
if (!mcpSSEURL) {
toast.error(t('mcp.urlRequired'));
return;
}
if (!mcpName) {
toast.error(t('mcp.nameRequired'));
}
if (!mcpTimeout) {
toast.error(t('mcp.timeoutRequired'));
}
const configToSend = {
name: mcpName,
description: mcpDescription,
sse_url: mcpSSEURL,
sse_headers: mcpSSEHeaders,
timeout: Number(mcpTimeout) || 60,
};
// handleMcpModalConfirm();
// call installer (for now installMcpServer will log config and call backend with url only)
installMcpServerFromSSE(configToSend);
}}
>
{t('common.confirm')}
</Button>
</>
)}
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
{/* MCP Server 从github安装对话框表单 */} <Dialog
<Dialog open={mcpSSEModalOpen}
open={mcpMarketInstallModalOpen} onOpenChange={setMcpSSEModalOpen}
onOpenChange={setMcpMarketInstallModalOpen} >
> <DialogContent>
<DialogContent className="w-[520px] p-6 bg-white dark:bg-[#1a1a1e]">
<DialogHeader> <DialogHeader>
<DialogTitle className="flex items-center gap-4"> <DialogTitle>
<Download className="size-6" /> {t('mcp.createServer')}
<span>{t('mcp.installFromGithub')}</span>
</DialogTitle> </DialogTitle>
</DialogHeader> </DialogHeader>
<Form {...form}>
{/* form fields */} <form
<div className="mt-4 space-y-3"> onSubmit={form.handleSubmit(handleFormSubmit)}
<div> className='space-y-4'
<label className="text-sm text-muted-foreground block mb-1"> >
{t('mcp.githubUrl')} <div className='space-y-4'>
</label> <FormField
<Input control={form.control}
placeholder={t('mcp.enterGithubLink')} name='name'
value={mcpGithubURL} render={({ field }) => (
onChange={(e) => setMcpGithubURL(e.target.value)} <FormItem>
className="mb-1" <FormLabel>{t('mcp.name')}</FormLabel>
<FormControl>
<Input
{...field}
/>
</FormControl>
<FormMessage/>
</FormItem>
)}
/> />
</div>
<div> <FormField
<label className="text-sm text-muted-foreground block mb-1"> control = {form.control}
{t('mcp.displayName', 'Display Name')} name = 'url'
</label> render={
<Input ({field}) => (
placeholder={t('mcp.displayNamePlaceholder', 'My MCP Server')} <FormItem>
value={(mcpInstallConfig as any)?.displayName || ''} <FormLabel>
onChange={(e) => {t('mcp.url')}
setMcpInstallConfig((c) => ({ ...(c || {}), displayName: e.target.value })) </FormLabel>
} <FormControl>
className="mb-1" <Input
/> {...field}
</div> />
</FormControl>
<div className="flex gap-2"> <FormMessage/>
<div className="flex-1"> </FormItem>
<label className="text-sm text-muted-foreground block mb-1"> )}
{t('mcp.port', 'Port')}
</label>
<Input
placeholder="8080"
value={(mcpInstallConfig as any)?.port || ''}
onChange={(e) =>
setMcpInstallConfig((c) => ({ ...(c || {}), port: e.target.value }))
}
/> />
</div>
<div className="flex-1"> <FormField
<label className="text-sm text-muted-foreground block mb-1"> control={form.control}
{t('mcp.env', 'Environment')} name='timeout'
</label> render = {
<Input ({field}) => (
placeholder="production" <FormItem>
value={(mcpInstallConfig as any)?.env || ''} <FormLabel>
onChange={(e) => {t('mcp.timeout')}
setMcpInstallConfig((c) => ({ ...(c || {}), env: e.target.value })) </FormLabel>
} <FormControl>
<Input
{...field}
/>
</FormControl>
<FormMessage/>
</FormItem>
)
}
/> />
</div>
</div>
<div> <FormField
<label className="text-sm text-muted-foreground block mb-1"> control={form.control}
{t('mcp.adminToken', 'Admin Token')} name='ssereadtimeout'
</label> render = {
<Input (field) =>
placeholder={t('mcp.adminTokenPlaceholder', 'secret-token')} (
value={(mcpInstallConfig as any)?.adminToken || ''} <FormItem>
onChange={(e) => <FormLabel>
setMcpInstallConfig((c) => ({ ...(c || {}), adminToken: e.target.value })) {t('mcp.ssereadtimeout')}
} </FormLabel>
/> <FormControl>
</div> <Input
placeholder={t('mcp.sseTimeout')}
{...field}
/>
</FormControl>
<FormMessage/>
</FormItem>
)
}
/>
<div> <FormItem>
<label className="text-sm text-muted-foreground block mb-1"> <FormLabel>{t('models.extraParameters')}</FormLabel>
{t('mcp.extraConfig', 'Extra JSON Config')} <div className="space-y-2">
</label> {extraArgs.map((arg, index) => (
<Input <div key={index} className="flex gap-2">
placeholder='{"key":"value"}' <Input
value={(mcpInstallConfig as any)?.extraConfig || ''} placeholder={t('models.keyName')}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => value={arg.key}
setMcpInstallConfig((c) => ({ ...(c || {}), extraConfig: e.target.value })) onChange={(e) =>
} updateExtraArg(index, 'key', e.target.value)
/>
<p className="text-xs text-muted-foreground mt-1">
{t('mcp.extraConfigHint', 'Optional JSON string for advanced config')}
</p>
</div>
</div>
{mcpInstallStatus === PluginInstallStatus.INSTALLING && (
<div className="mt-4">
<p className="mb-2">{t('mcp.installing')}</p>
</div>
)}
{mcpInstallStatus === PluginInstallStatus.ERROR && (
<div className="mt-4">
<p className="mb-2">{t('mcp.installFailed')}</p>
<p className="mb-2 text-red-500">{mcpInstallError}</p>
</div>
)}
<DialogFooter>
{(mcpInstallStatus === PluginInstallStatus.WAIT_INPUT ||
mcpInstallStatus === PluginInstallStatus.ERROR) && (
<>
<Button
variant="outline"
onClick={() => {
setMcpMarketInstallModalOpen(false);
setMcpInstallStatus(PluginInstallStatus.WAIT_INPUT);
setMcpInstallError(null);
setMcpInstallConfig(null);
setMcpGithubURL('');
}}
>
{t('common.cancel')}
</Button>
<Button
onClick={() => {
// basic validation
if (!mcpGithubURL) {
toast.error(t('mcp.urlRequired'));
return;
}
// try parse extraConfig JSON
let parsedExtra: any = undefined;
try {
if (mcpInstallConfig?.extraConfig) {
parsedExtra = JSON.parse(mcpInstallConfig.extraConfig);
} }
} catch (err) { />
toast.error(t('mcp.extraConfigInvalid')); <Select
return; value={arg.type}
} onValueChange={(value) =>
updateExtraArg(index, 'type', value)
const configToSend = { }
displayName: mcpInstallConfig?.displayName, >
port: mcpInstallConfig?.port, <SelectTrigger className="w-[120px] bg-[#ffffff] dark:bg-[#2a2a2e]">
env: mcpInstallConfig?.env, <SelectValue placeholder={t('models.type')} />
adminToken: mcpInstallConfig?.adminToken, </SelectTrigger>
extraConfig: parsedExtra, <SelectContent>
}; <SelectItem value="string">
{t('models.string')}
handleMcpModalConfirm(); </SelectItem>
// call installer (for now installMcpServer will log config and call backend with url only) <SelectItem value="number">
// installMcpServer(mcpGithubURL, configToSend); {t('models.number')}
}} </SelectItem>
> <SelectItem value="boolean">
{t('common.confirm')} {t('models.boolean')}
</SelectItem>
</SelectContent>
</Select>
<Input
placeholder={t('models.value')}
value={arg.value}
onChange={(e) =>
updateExtraArg(index, 'value', e.target.value)
}
/>
<button
type="button"
className="p-2 hover:bg-gray-100 rounded"
onClick={() => removeExtraArg(index)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
className="w-5 h-5 text-red-500"
>
<path d="M7 4V2H17V4H22V6H20V21C20 21.5523 19.5523 22 19 22H5C4.44772 22 4 21.5523 4 21V6H2V4H7ZM6 6V20H18V6H6ZM9 9H11V17H9V9ZM13 9H15V17H13V9Z"></path>
</svg>
</button>
</div>
))}
<Button type="button" variant="outline" onClick={addExtraArg}>
{t('models.addParameter')}
</Button> </Button>
</> </div>
<FormDescription>
{t('llm.extraParametersDescription')}
</FormDescription>
<FormMessage />
</FormItem>
<DialogFooter>
{editMode && (
<Button
type="button"
variant="destructive"
onClick={() => setShowDeleteConfirmModal(true)}
>
{t('common.delete')}
</Button>
)} )}
<Button type="submit">
{editMode ? t('common.save') : t('common.submit')}
</Button>
<Button
type="button"
variant="outline"
onClick={() => testMcp()}
disabled={mcpTesting}
>
{t('common.test')}
</Button>
<Button
type="button"
variant="outline"
onClick={() => onFormCancel()}
>
{t('common.cancel')}
</Button>
</DialogFooter> </DialogFooter>
</div>
</form>
</Form>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
</div> </div>
</div>
); );
} }

View File

@@ -348,6 +348,7 @@ const zhHans = {
headersExample:'示例: Authorization: Bearer token123', headersExample:'示例: Authorization: Bearer token123',
enterTimeout:'输入超时时间,单位为毫秒', enterTimeout:'输入超时时间,单位为毫秒',
installFromSSE:'从SSE安装', installFromSSE:'从SSE安装',
sseTimeout:'SSE超时时间'
}, },
pipelines: { pipelines: {
title: '流水线', title: '流水线',