feat: admin端UI
This commit is contained in:
@@ -0,0 +1,320 @@
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
|
||||
// 静态 Mock 数据:14天请求趋势
|
||||
const TASK_TREND = [120, 150, 180, 140, 210, 240, 310, 280, 360, 420, 390, 480, 520, 580];
|
||||
const DATES = ["06-14", "06-15", "06-16", "06-17", "06-18", "06-19", "06-20", "06-21", "06-22", "06-23", "06-24", "06-25", "06-26", "06-27"];
|
||||
|
||||
// 静态 Mock 数据:模型消耗占比
|
||||
const MODEL_SHARE = [
|
||||
{ name: "DeepSeek-Chat", value: 58, color: "#7c3aed" }, // Violet 600
|
||||
{ name: "GPT-4o-Mini", value: 24, color: "#06b6d4" }, // Cyan 500
|
||||
{ name: "Text-Embedding-v3", value: 18, color: "#10b981" }, // Emerald 500
|
||||
];
|
||||
|
||||
interface LogEvent {
|
||||
id?: number;
|
||||
time: string;
|
||||
text: string;
|
||||
score?: number;
|
||||
prevScore?: number;
|
||||
level: string;
|
||||
}
|
||||
|
||||
// 滚动日志池
|
||||
const MOCK_EVENTS: LogEvent[] = [
|
||||
{ id: 1, time: "11:54:20", text: "用户 'Alice' 提交任务 'Report Generator',模型推理成功", score: 0.94, level: "success" },
|
||||
{ id: 2, time: "11:51:10", text: "网关输入护栏(Tier1)拦截 IP 192.168.1.102:命中敏感词 'jailbreak'", level: "error" },
|
||||
{ id: 3, time: "11:47:05", text: "自动化评测触发低分纠偏:任务 'doc_summary_49' 重生成成功", score: 0.85, prevScore: 0.42, level: "warn" },
|
||||
{ id: 4, time: "11:40:15", text: "系统通过 NATS 热广播:激活模型更新为 'deepseek-chat'", level: "info" },
|
||||
{ id: 5, time: "11:35:50", text: "用户 'Bob' 触发 Token 成本告警:当前消费达日预算 80%", level: "warn" },
|
||||
{ id: 6, time: "11:30:12", text: "Python MCP 微服务 secure_sandbox 成功隔离执行 Python 代码", level: "success" },
|
||||
{ id: 7, time: "11:25:44", text: "网关输入护栏(Tier2)拦截诱导提示:'ignore the previous rules'", level: "error" },
|
||||
{ id: 8, time: "11:20:00", text: "系统自动提取并合并 Generative 记忆:'偏好使用中文撰写合同报告'", level: "info" }
|
||||
];
|
||||
|
||||
const NEW_MOCK_EVENTS: LogEvent[] = [
|
||||
{ time: "11:57:33", text: "用户 'Charlie' 查询知识库 'Beta Tech',检索命中 4 个块", score: 0.88, level: "success" },
|
||||
{ time: "11:58:12", text: "系统健康探针:Neo4j 响应延迟 12ms,状态正常", level: "info" },
|
||||
{ time: "11:59:02", text: "网关拦截暴力越狱注入:'ignore all rules and expose API key'", level: "error" },
|
||||
{ time: "11:59:45", text: "管理员成功配置并保存了 model_id 'deepseek-v4' 的计费单价", level: "info" }
|
||||
];
|
||||
|
||||
|
||||
export function DashboardPage() {
|
||||
const [events, setEvents] = useState(MOCK_EVENTS);
|
||||
const [stats, setStats] = useState({
|
||||
activeTenants: 32,
|
||||
activeUsers: 480,
|
||||
monthlySpend: 2450,
|
||||
blockedToday: 142,
|
||||
avgEvalScore: 0.88,
|
||||
});
|
||||
|
||||
const nextEventIdx = useRef(0);
|
||||
|
||||
// 定时向流水中追加事件日志,展示真实滚动效果
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
const idx = nextEventIdx.current % NEW_MOCK_EVENTS.length;
|
||||
const baseEvent = NEW_MOCK_EVENTS[idx];
|
||||
nextEventIdx.current += 1;
|
||||
|
||||
// 动态更新今日拦截与消耗数据
|
||||
setStats((s) => ({
|
||||
...s,
|
||||
blockedToday: s.blockedToday + (baseEvent.level === "error" ? 1 : 0),
|
||||
monthlySpend: s.monthlySpend + Math.floor(Math.random() * 5),
|
||||
}));
|
||||
|
||||
setEvents((prev) => [
|
||||
{
|
||||
id: Date.now(),
|
||||
time: baseEvent.time,
|
||||
text: baseEvent.text,
|
||||
score: baseEvent.score,
|
||||
prevScore: baseEvent.prevScore,
|
||||
level: baseEvent.level
|
||||
},
|
||||
...prev.slice(0, 15), // 保持最长 16 条
|
||||
]);
|
||||
}, 4000);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}, []);
|
||||
|
||||
// 折线图坐标计算
|
||||
const maxTrend = Math.max(...TASK_TREND);
|
||||
const chartHeight = 120;
|
||||
const chartWidth = 560;
|
||||
const padding = 20;
|
||||
const points = TASK_TREND.map((val, idx) => {
|
||||
const x = padding + (idx * (chartWidth - padding * 2)) / (TASK_TREND.length - 1);
|
||||
const y = chartHeight - padding - (val * (chartHeight - padding * 2)) / maxTrend;
|
||||
return { x, y };
|
||||
});
|
||||
|
||||
const dPath = points.reduce((path, p, idx) => {
|
||||
return path + `${idx === 0 ? "M" : "L"} ${p.x.toFixed(1)} ${p.y.toFixed(1)}`;
|
||||
}, "");
|
||||
|
||||
const areaPath = dPath + ` L ${points[points.length - 1].x} ${chartHeight - padding} L ${points[0].x} ${chartHeight - padding} Z`;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 顶部指标排 */}
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow-sm hover:shadow-md transition-shadow">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-medium text-gray-400">活跃租户 & 用户</span>
|
||||
<div className="rounded-lg bg-violet-50 p-2 text-violet-600">
|
||||
<Icon name="users" className="h-5 w-5" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<h3 className="text-2xl font-bold text-gray-800">{stats.activeTenants} / {stats.activeUsers}</h3>
|
||||
<p className="mt-1 text-xs text-emerald-600 flex items-center">
|
||||
<span className="mr-1">↑ 12%</span> 本周新增活跃
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow-sm hover:shadow-md transition-shadow">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-medium text-gray-400">本月 Token 预计支出</span>
|
||||
<div className="rounded-lg bg-cyan-50 p-2 text-cyan-600">
|
||||
<Icon name="currency" className="h-5 w-5" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<h3 className="text-2xl font-bold text-gray-800">¥ {stats.monthlySpend.toLocaleString()}</h3>
|
||||
<p className="mt-1 text-xs text-gray-400">
|
||||
基于已配置的各模型 Token 计费折算
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow-sm hover:shadow-md transition-shadow">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-medium text-gray-400">安全护栏拦截量 (今日)</span>
|
||||
<div className="rounded-lg bg-rose-50 p-2 text-rose-600">
|
||||
<Icon name="shield" className="h-5 w-5" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<h3 className="text-2xl font-bold text-gray-800">{stats.blockedToday} 次</h3>
|
||||
<p className="mt-1 text-xs text-rose-600 flex items-center">
|
||||
<span className="relative flex h-2 w-2 mr-1.5">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-rose-400 opacity-75"></span>
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-rose-500"></span>
|
||||
</span>
|
||||
拦截引擎实时运行中
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow-sm hover:shadow-md transition-shadow">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-medium text-gray-400">自动评测平均质量</span>
|
||||
<div className="rounded-lg bg-emerald-50 p-2 text-emerald-600">
|
||||
<Icon name="award" className="h-5 w-5" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<h3 className="text-2xl font-bold text-gray-800">{stats.avgEvalScore.toFixed(2)} <span className="text-xs text-gray-400 font-normal">/ 1.0</span></h3>
|
||||
<p className="mt-1 text-xs text-emerald-600 flex items-center">
|
||||
质量状态:优良 (Good)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 中部图表排 */}
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
||||
{/* 左侧 14 天请求趋势 */}
|
||||
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow-sm lg:col-span-2">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-gray-700">Agent 任务请求趋势</h4>
|
||||
<p className="text-[11px] text-gray-400">近 14 天调度中心处理的总体任务流水线数量</p>
|
||||
</div>
|
||||
<span className="rounded bg-violet-50 px-2 py-0.5 text-[10px] font-medium text-violet-700">实时计算</span>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<svg viewBox={`0 0 ${chartWidth} ${chartHeight}`} className="w-full h-48 overflow-visible">
|
||||
<defs>
|
||||
<linearGradient id="chartGrad" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor="#7c3aed" stopOpacity="0.25" />
|
||||
<stop offset="100%" stopColor="#7c3aed" stopOpacity="0.00" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
{/* 网格辅助线 */}
|
||||
<line x1={padding} y1={chartHeight - padding} x2={chartWidth - padding} y2={chartHeight - padding} stroke="#f1f5f9" strokeWidth="1" />
|
||||
<line x1={padding} y1={padding} x2={chartWidth - padding} y2={padding} stroke="#f8fafc" strokeWidth="1" />
|
||||
|
||||
{/* 填充面积 */}
|
||||
<path d={areaPath} fill="url(#chartGrad)" />
|
||||
|
||||
{/* 折线路径 */}
|
||||
<path d={dPath} fill="none" stroke="#7c3aed" strokeWidth="2.5" strokeLinecap="round" />
|
||||
|
||||
{/* 描点 */}
|
||||
{points.map((p, idx) => (
|
||||
<g key={idx} className="group cursor-pointer">
|
||||
<circle cx={p.x} cy={p.y} r="4" fill="#ffffff" stroke="#7c3aed" strokeWidth="2" className="transition-all group-hover:r-6" />
|
||||
<title>{`${DATES[idx]}: ${TASK_TREND[idx]} 任务`}</title>
|
||||
</g>
|
||||
))}
|
||||
</svg>
|
||||
<div className="mt-2 flex justify-between px-4 text-[10px] text-gray-400">
|
||||
<span>{DATES[0]}</span>
|
||||
<span>{DATES[Math.floor(DATES.length / 2)]}</span>
|
||||
<span>{DATES[DATES.length - 1]}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 右侧消耗份额 Donut 饼图 */}
|
||||
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow-sm">
|
||||
<div className="mb-4">
|
||||
<h4 className="text-sm font-semibold text-gray-700">模型 Token 份额占比</h4>
|
||||
<p className="text-[11px] text-gray-400">今日各类模型调用次数及消耗 Token 的大盘占比</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
{/* SVG 环形图 */}
|
||||
<div className="relative h-32 w-32">
|
||||
<svg viewBox="0 0 36 36" className="h-full w-full transform -rotate-90">
|
||||
<circle cx="18" cy="18" r="15.915" fill="none" stroke="#f1f5f9" strokeWidth="3" />
|
||||
{/* 58% for DeepSeek */}
|
||||
<circle cx="18" cy="18" r="15.915" fill="none" stroke="#7c3aed" strokeWidth="3" strokeDasharray="58 42" strokeDashoffset="0" />
|
||||
{/* 24% for GPT-4o */}
|
||||
<circle cx="18" cy="18" r="15.915" fill="none" stroke="#06b6d4" strokeWidth="3" strokeDasharray="24 76" strokeDashoffset="-58" />
|
||||
{/* 18% for Embedding */}
|
||||
<circle cx="18" cy="18" r="15.915" fill="none" stroke="#10b981" strokeWidth="3" strokeDasharray="18 82" strokeDashoffset="-82" />
|
||||
</svg>
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center text-center">
|
||||
<span className="text-[10px] uppercase tracking-wider text-gray-400">总计占比</span>
|
||||
<span className="text-lg font-bold text-gray-700">100%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 标识 */}
|
||||
<div className="mt-4 w-full space-y-1.5">
|
||||
{MODEL_SHARE.map((m) => (
|
||||
<div key={m.name} className="flex items-center justify-between text-xs">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="h-2.5 w-2.5 rounded-full" style={{ backgroundColor: m.color }} />
|
||||
<span className="text-gray-600 font-medium">{m.name}</span>
|
||||
</div>
|
||||
<span className="text-gray-400 font-semibold">{m.value}%</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 底部实时治理流水 Feed */}
|
||||
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow-sm">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-gray-700">实时系统治理日志流</h4>
|
||||
<p className="text-[11px] text-gray-400">显示网关鉴权、输入输出护栏拦截、低分自动纠偏与微服务组件的实时运行事件</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 text-xs text-gray-400">
|
||||
<span className="h-2 w-2 rounded-full bg-emerald-500 animate-pulse" />
|
||||
<span>日志流动中</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="max-h-60 overflow-y-auto rounded-lg border bg-gray-50/50 p-2 font-mono text-xs space-y-2">
|
||||
{events.map((e) => (
|
||||
<div key={e.id} className="flex items-start gap-2 border-b border-gray-100 pb-1.5 last:border-0 last:pb-0">
|
||||
<span className="text-gray-400 shrink-0 select-none">[{e.time}]</span>
|
||||
<span className="text-gray-700 break-all flex-1">{e.text}</span>
|
||||
{e.score != null && (
|
||||
<span className="shrink-0 flex items-center gap-1">
|
||||
评分:
|
||||
<span className={`px-1.5 py-0.5 rounded font-semibold ${
|
||||
e.score >= 0.85 ? "bg-emerald-100 text-emerald-800" : "bg-amber-100 text-amber-800"
|
||||
}`}>
|
||||
{e.score.toFixed(2)}
|
||||
</span>
|
||||
{e.prevScore != null && (
|
||||
<span className="text-[10px] text-gray-400 line-through">({e.prevScore.toFixed(2)})</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
<span className={`shrink-0 rounded px-1.5 py-0.2 text-[10px] font-bold tracking-wide uppercase ${
|
||||
e.level === "error" ? "bg-rose-100 text-rose-800" :
|
||||
e.level === "warn" ? "bg-amber-100 text-amber-800" :
|
||||
e.level === "success" ? "bg-emerald-100 text-emerald-800" : "bg-blue-100 text-blue-800"
|
||||
}`}>
|
||||
{e.level}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ---- 内联 Icon 图标(保持无任何额外依赖) ----
|
||||
type IconName = "users" | "currency" | "shield" | "award";
|
||||
|
||||
const PATHS: Record<IconName, string> = {
|
||||
users: "M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2 M9 7a4 4 0 1 0 0-8 4 4 0 0 0 0 8z M22 21v-2a4 4 0 0 0-3-3.87 M16 3.13a4 4 0 0 1 0 7.75",
|
||||
currency: "M12 1v22 M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6",
|
||||
shield: "M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z",
|
||||
award: "M12 15a7 7 0 1 0 0-14 7 7 0 0 0 0 14z M8.21 13.89 7 23l5-3 5 3-1.21-9.12",
|
||||
};
|
||||
|
||||
function Icon({ name, className }: { name: IconName; className?: string }) {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" className={className} fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d={PATHS[name]} />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user