mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-18 03:34:20 +00:00
f6e7983890
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.
181 lines
6.1 KiB
TypeScript
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>
|
|
);
|
|
}
|