ghostdrive1's picture
Upload folder using huggingface_hub
0b9dc2e verified
Raw
History Blame Contribute Delete
6.28 kB
import { PlusCircle, Loader2, Check } from 'lucide-react';
import { CircleAlert } from 'lucide-react';
import { useState, useCallback } from 'react';
import type { ReactNode } from 'react';
import type { MCPClient, StdioMCPConfig, HttpMCPConfig } from '@/api/types';
import { Alert, AlertDescription } from '@/components/ui/alert.tsx';
import { Button } from '@/components/ui/button.tsx';
import { Checkbox } from '@/components/ui/checkbox.tsx';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog.tsx';
import {
Field,
FieldContent,
FieldDescription,
FieldGroup,
FieldLabel,
FieldSet,
} from '@/components/ui/field.tsx';
import { InputGroup, InputGroupTextarea } from '@/components/ui/input-group.tsx';
import { useTranslation } from '@/i18n/useI18n.ts';
type Status = 'idle' | 'loading' | 'success' | 'error';
interface Props {
children: ReactNode;
onAdd: (mcps: MCPClient[]) => Promise<void>;
}
function parseMcpConfig(
raw: string,
keepAlive: boolean,
t: (key: string, opts?: Record<string, string>) => string,
): MCPClient[] {
let parsed: unknown;
try {
parsed = JSON.parse(raw);
} catch (e) {
throw new Error(t('dialog-mcp-create.parseError', { message: (e as Error).message }));
}
const obj = parsed as Record<string, unknown>;
const servers = obj.mcpServers as Record<string, Record<string, unknown>> | undefined;
if (!servers || typeof servers !== 'object') {
throw new Error(t('dialog-mcp-create.missingMcpServers'));
}
const entries = Object.entries(servers);
if (entries.length === 0) {
throw new Error(t('dialog-mcp-create.emptyMcpServers'));
}
return entries.map(([name, config]) => {
let mcp_config: StdioMCPConfig | HttpMCPConfig;
if ('url' in config) {
mcp_config = {
type: 'http_mcp',
url: config.url as string,
headers: (config.headers as Record<string, string> | undefined) ?? null,
timeout: (config.timeout as number | undefined) ?? null,
};
} else {
mcp_config = {
type: 'stdio_mcp',
command: config.command as string,
args: (config.args as string[] | undefined) ?? null,
env: (config.env as Record<string, string> | undefined) ?? null,
cwd: (config.cwd as string | undefined) ?? null,
};
}
return { name, is_stateful: keepAlive, mcp_config };
});
}
export const CreateMCPDialog = ({ children, onAdd }: Props) => {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const [configValue, setConfigValue] = useState('');
const [keepAlive, setKeepAlive] = useState(true);
const [status, setStatus] = useState<Status>('idle');
const [errorMsg, setErrorMsg] = useState('');
const reset = useCallback(() => {
setConfigValue('');
setKeepAlive(true);
setStatus('idle');
setErrorMsg('');
}, []);
const handleOpenChange = useCallback(
(next: boolean) => {
if (!next) reset();
setOpen(next);
},
[reset],
);
const handleAdd = useCallback(async () => {
setErrorMsg('');
let mcpClients: MCPClient[];
try {
mcpClients = parseMcpConfig(configValue, keepAlive, t);
} catch (e) {
setErrorMsg((e as Error).message);
setStatus('error');
return;
}
setStatus('loading');
try {
await onAdd(mcpClients);
setStatus('success');
setTimeout(() => {
handleOpenChange(false);
}, 1500);
} catch (e) {
// ApiErrors are already shown via the global toast in client.ts.
// Show only local validation errors (e.g. duplicate name) inline.
const isApiError = e instanceof Error && e.name === 'ApiError';
if (!isApiError) {
setErrorMsg(e instanceof Error ? e.message : String(e));
}
setStatus('idle');
}
}, [configValue, keepAlive, t, onAdd, handleOpenChange]);
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent className="!w-[500px] !max-w-[500px]">
<DialogHeader>
<DialogTitle>{t('dialog-mcp-create.title')}</DialogTitle>
<DialogDescription>{t('dialog-mcp-create.description')}</DialogDescription>
</DialogHeader>
<FieldSet>
<FieldGroup>
<Field>
<FieldContent>
<FieldLabel>{t('dialog-mcp-create.configLabel')}</FieldLabel>
</FieldContent>
<InputGroup>
<InputGroupTextarea
className="max-h-100"
value={configValue}
onChange={(e) => setConfigValue(e.target.value)}
placeholder={
'{\n "mcpServers": {\n "playwright": {\n "command": "npx",\n "args": ["@playwright/mcp@latest"]\n }\n }\n}'
}
/>
</InputGroup>
</Field>
<Field orientation="horizontal">
<Checkbox
id="mcp-keep-alive"
checked={keepAlive}
onCheckedChange={(v) => setKeepAlive(v === true)}
/>
<FieldContent>
<FieldLabel htmlFor="mcp-keep-alive">
{t('dialog-mcp-create.keepAlive')}
</FieldLabel>
<FieldDescription>
{t('dialog-mcp-create.keepAliveDesc')}
</FieldDescription>
</FieldContent>
</Field>
</FieldGroup>
</FieldSet>
{errorMsg && (
<Alert variant="destructive">
<CircleAlert />
<AlertDescription>{errorMsg}</AlertDescription>
</Alert>
)}
<DialogFooter>
<Button variant="ghost" onClick={() => handleOpenChange(false)}>
<CircleAlert className="size-3.5" />
{t('common.cancel')}
</Button>
<Button
onClick={handleAdd}
disabled={status === 'loading' || status === 'success'}
>
{status === 'loading' && <Loader2 className="size-3.5 animate-spin" />}
{status === 'success' && <Check className="size-3.5" />}
{status !== 'loading' && status !== 'success' && (
<PlusCircle className="size-3.5" />
)}
{status === 'loading'
? t('dialog-mcp-create.adding')
: status === 'success'
? t('dialog-mcp-create.added')
: t('common.add')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};