Files
LangBot/web/src/app/home/plugins/mcp-server/mcp-card/MCPCardComponent.tsx
T
Junyan Qin f6e7983890 refactor(web): replace all hardcoded SVG icons with lucide-react
Unify icon usage across the entire frontend by replacing 67 hardcoded
SVG icons with lucide-react components across ~25 files. This improves
consistency, maintainability, and reduces bundle duplication.

Key replacements:
- Sidebar nav: Zap, LayoutDashboard, Bot, Workflow, BookMarked, etc.
- MCP forms: Loader2, XCircle, Trash2
- Monitoring: Sparkles, MessageSquare, CheckCircle2, RefreshCw, etc.
- Cards: Clock, Star, Workflow, Hexagon, Puzzle, Github, etc.
- Misc: Paperclip, AudioLines, CloudUpload, Layers, Heart, Smile

Zero hardcoded <svg> tags remain in .tsx files.
2026-05-04 21:33:03 +08:00

181 lines
6.1 KiB
TypeScript

import { MCPCardVO } from '@/app/home/plugins/mcp-server/MCPCardVO';
import { useState, useEffect } from 'react';
import { httpClient } from '@/app/infra/http/HttpClient';
import { Switch } from '@/components/ui/switch';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { toast } from 'sonner';
import { useTranslation } from 'react-i18next';
import {
RefreshCcw,
Wrench,
Ban,
AlertCircle,
Loader2,
Link,
} from 'lucide-react';
import { MCPSessionStatus } from '@/app/infra/entities/api';
export default function MCPCardComponent({
cardVO,
onCardClick,
onRefresh,
}: {
cardVO: MCPCardVO;
onCardClick: () => void;
onRefresh: () => void;
}) {
const { t } = useTranslation();
const [enabled, setEnabled] = useState(cardVO.enable);
const [switchEnable, setSwitchEnable] = useState(true);
const [testing, setTesting] = useState(false);
const [toolsCount, setToolsCount] = useState(cardVO.tools);
const [status, setStatus] = useState(cardVO.status);
useEffect(() => {
setStatus(cardVO.status);
setToolsCount(cardVO.tools);
setEnabled(cardVO.enable);
}, [cardVO.status, cardVO.tools, cardVO.enable]);
function handleEnable(checked: boolean) {
setSwitchEnable(false);
httpClient
.toggleMCPServer(cardVO.name, checked)
.then(() => {
setEnabled(checked);
toast.success(t('mcp.saveSuccess'));
onRefresh();
setSwitchEnable(true);
})
.catch((err) => {
toast.error(t('mcp.modifyFailed') + err.msg);
setSwitchEnable(true);
});
}
function handleTest(e: React.MouseEvent) {
e.stopPropagation();
setTesting(true);
httpClient
.testMCPServer(cardVO.name, {})
.then((resp) => {
const taskId = resp.task_id;
const interval = setInterval(() => {
httpClient.getAsyncTask(taskId).then((taskResp) => {
if (taskResp.runtime.done) {
clearInterval(interval);
setTesting(false);
if (taskResp.runtime.exception) {
toast.error(
t('mcp.refreshFailed') + taskResp.runtime.exception,
);
} else {
toast.success(t('mcp.refreshSuccess'));
}
// Refresh to get updated runtime_info
onRefresh();
}
});
}, 1000);
})
.catch((err) => {
toast.error(t('mcp.refreshFailed') + err.msg);
setTesting(false);
});
}
return (
<div
className="w-[100%] h-[10rem] bg-white dark:bg-[#1f1f22] rounded-[10px] border border-[#e4e4e7] dark:border-[#27272a] p-[1.2rem] cursor-pointer transition-all duration-200 hover:border-[#a1a1aa] dark:hover:border-[#3f3f46]"
onClick={onCardClick}
>
<div className="w-full h-full flex flex-row items-start justify-start gap-[1.2rem]">
<Link
className="w-16 h-16 flex-shrink-0"
style={{ color: 'rgba(70,146,221,1)' }}
/>
<div className="w-full h-full flex flex-col items-start justify-between gap-[0.6rem]">
<div className="flex flex-col items-start justify-start gap-[0.3rem]">
<div className="flex flex-row items-center gap-[0.5rem]">
<div className="text-[1.2rem] text-black dark:text-[#f0f0f0] font-medium">
{cardVO.name}
</div>
<Badge variant="secondary" className="text-[0.65rem] px-1.5 py-0">
{cardVO.mode.toUpperCase()}
</Badge>
</div>
</div>
<div className="w-full flex flex-row items-start justify-start gap-[0.6rem]">
{!enabled ? (
// 未启用 - 橙色
<div className="flex flex-row items-center gap-[0.4rem]">
<Ban className="w-4 h-4 text-orange-500 dark:text-orange-400" />
<div className="text-sm text-orange-500 dark:text-orange-400 font-medium">
{t('mcp.statusDisabled')}
</div>
</div>
) : status === MCPSessionStatus.CONNECTED ? (
// 连接成功 - 显示工具数量
<div className="flex h-full flex-row items-center justify-center gap-[0.4rem]">
<Wrench className="w-5 h-5" />
<div className="text-base text-black dark:text-[#f0f0f0] font-medium">
{t('mcp.toolCount', { count: toolsCount })}
</div>
</div>
) : status === MCPSessionStatus.CONNECTING ? (
// 连接中 - 蓝色加载
<div className="flex flex-row items-center gap-[0.4rem]">
<Loader2 className="w-4 h-4 text-blue-500 dark:text-blue-400 animate-spin" />
<div className="text-sm text-blue-500 dark:text-blue-400 font-medium">
{t('mcp.connecting')}
</div>
</div>
) : (
// 连接失败 - 红色
<div className="flex flex-row items-center gap-[0.4rem]">
<AlertCircle className="w-4 h-4 text-red-500 dark:text-red-400" />
<div className="text-sm text-red-500 dark:text-red-400 font-medium">
{t('mcp.connectionFailedStatus')}
</div>
</div>
)}
</div>
</div>
<div className="flex flex-col items-center justify-between h-full">
<div
className="flex items-center justify-center"
onClick={(e) => e.stopPropagation()}
>
<Switch
className="cursor-pointer"
checked={enabled}
onCheckedChange={handleEnable}
disabled={!switchEnable}
/>
</div>
<div className="flex items-center justify-center gap-[0.4rem]">
<Button
variant="ghost"
size="sm"
className="p-1 h-8 w-8"
onClick={(e) => handleTest(e)}
disabled={testing}
>
<RefreshCcw className="w-4 h-4" />
</Button>
</div>
</div>
</div>
</div>
);
}