forked from xiaohei/taiji-pda-v0
459 lines
18 KiB
TypeScript
459 lines
18 KiB
TypeScript
"use client"
|
|
|
|
import { useState, useEffect } from "react"
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
|
import { Activity, Zap, GitBranch, Boxes } from "lucide-react"
|
|
import { useLanguage } from "@/contexts/language-context"
|
|
import { TaijiAPIClient } from "@/lib/api-client"
|
|
import {
|
|
ResponsiveContainer,
|
|
LineChart,
|
|
Line,
|
|
PieChart,
|
|
Pie,
|
|
Cell,
|
|
XAxis,
|
|
YAxis,
|
|
Tooltip,
|
|
AreaChart,
|
|
Area,
|
|
} from "recharts"
|
|
|
|
// 默认数据(加载时显示)
|
|
const defaultEuConsumptionData = [
|
|
{ time: "00:00", value: 0 },
|
|
{ time: "04:00", value: 0 },
|
|
{ time: "08:00", value: 0 },
|
|
{ time: "12:00", value: 0 },
|
|
{ time: "16:00", value: 0 },
|
|
{ time: "20:00", value: 0 },
|
|
{ time: "24:00", value: 0 },
|
|
]
|
|
|
|
// 模型颜色映射(用于动态分配颜色)
|
|
const MODEL_COLORS = [
|
|
"oklch(0.60 0.20 264)", // 蓝紫色
|
|
"oklch(0.65 0.16 220)", // 蓝色
|
|
"oklch(0.55 0.18 300)", // 紫色
|
|
"oklch(0.70 0.18 150)", // 绿色
|
|
"oklch(0.65 0.20 30)", // 橙色
|
|
"oklch(0.60 0.22 0)", // 红色
|
|
"oklch(0.70 0.15 60)", // 黄色
|
|
"oklch(0.55 0.20 180)", // 青色
|
|
]
|
|
|
|
// 获取模型颜色(根据索引循环使用)
|
|
const getModelColor = (index: number) => MODEL_COLORS[index % MODEL_COLORS.length]
|
|
|
|
// 默认模型分布数据(加载时显示)
|
|
const defaultModelDistribution: Array<{ name: string; value: number; color: string }> = []
|
|
|
|
const defaultApiCallsData = [
|
|
{ time: "Mon", calls: 0 },
|
|
{ time: "Tue", calls: 0 },
|
|
{ time: "Wed", calls: 0 },
|
|
{ time: "Thu", calls: 0 },
|
|
{ time: "Fri", calls: 0 },
|
|
{ time: "Sat", calls: 0 },
|
|
{ time: "Sun", calls: 0 },
|
|
]
|
|
|
|
export function DashboardOverview() {
|
|
const { t } = useLanguage()
|
|
const [loading, setLoading] = useState(true)
|
|
const [stats, setStats] = useState({
|
|
activeAgents: 0,
|
|
totalRequests: 0,
|
|
euBalance: 0,
|
|
systemHealth: 0,
|
|
})
|
|
const [euConsumptionData, setEuConsumptionData] = useState(defaultEuConsumptionData)
|
|
const [modelDistribution, setModelDistribution] = useState(defaultModelDistribution)
|
|
const [apiCallsData, setApiCallsData] = useState(defaultApiCallsData)
|
|
const [systemComponents, setSystemComponents] = useState<Array<{ name: string; status: string; latency: string; statusType: string }>>([])
|
|
|
|
useEffect(() => {
|
|
loadDashboardData()
|
|
}, [])
|
|
|
|
const loadDashboardData = async () => {
|
|
try {
|
|
setLoading(true)
|
|
|
|
// 并行加载数据
|
|
const [dashboardStats, monitoringDashboard, billingBalance] = await Promise.allSettled([
|
|
TaijiAPIClient.getUserDashboardStats(),
|
|
TaijiAPIClient.getMonitoringDashboard(),
|
|
TaijiAPIClient.getBillingBalance(),
|
|
])
|
|
|
|
// 处理仪表板统计
|
|
if (dashboardStats.status === "fulfilled" && dashboardStats.value?.success) {
|
|
const data = dashboardStats.value.data
|
|
setStats({
|
|
activeAgents: data.activeAgents || 0,
|
|
totalRequests: data.totalRequests || 0,
|
|
euBalance: data.euBalance || 0,
|
|
systemHealth: data.systemHealth || 0,
|
|
})
|
|
}
|
|
|
|
// 处理监控数据
|
|
if (monitoringDashboard.status === "fulfilled" && monitoringDashboard.value) {
|
|
const monitoring = monitoringDashboard.value
|
|
|
|
// 处理系统组件状态
|
|
if (monitoring.health?.services) {
|
|
const components = Object.entries(monitoring.health.services).map(([name, serviceData]: [string, any]) => ({
|
|
name,
|
|
status: serviceData?.status === "healthy" ? t("正常运行", "Operational") :
|
|
serviceData?.status === "maintenance" ? t("维护中", "Maintenance") : t("异常", "Error"),
|
|
statusType: serviceData?.status || "error", // 保存原始状态类型用于显示不同颜色
|
|
latency: serviceData?.latency !== undefined ? `${serviceData.latency}ms` : "-",
|
|
}))
|
|
setSystemComponents(components)
|
|
}
|
|
|
|
// 处理模型分布数据(从后端 modelUsage 获取)
|
|
if (monitoring.modelUsage?.models && monitoring.modelUsage.models.length > 0) {
|
|
const totalCalls = monitoring.modelUsage.totalCalls ||
|
|
monitoring.modelUsage.models.reduce((sum: number, m: any) => sum + (m.calls || 0), 0)
|
|
|
|
const modelData = monitoring.modelUsage.models.map((model: any, index: number) => ({
|
|
name: model.name,
|
|
// 如果没有调用数据,平均分配百分比以显示饼图
|
|
value: totalCalls > 0
|
|
? Math.round((model.calls / totalCalls) * 100)
|
|
: Math.round(100 / monitoring.modelUsage.models.length),
|
|
calls: model.calls || 0,
|
|
color: getModelColor(index),
|
|
}))
|
|
setModelDistribution(modelData)
|
|
}
|
|
|
|
// 处理EU消耗数据(从后端 euConsumption24h 获取)
|
|
if (monitoring.euConsumption24h?.hourlyData && monitoring.euConsumption24h.hourlyData.length > 0) {
|
|
const euData = monitoring.euConsumption24h.hourlyData.map((item: any) => {
|
|
// 从 timestamp 提取小时
|
|
const date = new Date(item.timestamp)
|
|
const hour = date.getHours()
|
|
return {
|
|
time: `${hour.toString().padStart(2, '0')}:00`,
|
|
value: item.value || 0,
|
|
}
|
|
})
|
|
setEuConsumptionData(euData)
|
|
}
|
|
|
|
// 处理每周API调用数据(从后端 weeklyApiCalls 获取)
|
|
if (monitoring.weeklyApiCalls?.dailyData && monitoring.weeklyApiCalls.dailyData.length > 0) {
|
|
const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
|
|
const apiData = monitoring.weeklyApiCalls.dailyData.map((item: any) => {
|
|
const date = new Date(item.date)
|
|
const dayOfWeek = dayNames[date.getDay()]
|
|
return {
|
|
time: dayOfWeek,
|
|
calls: item.total || 0,
|
|
}
|
|
})
|
|
setApiCallsData(apiData)
|
|
}
|
|
}
|
|
|
|
// 处理余额数据
|
|
if (billingBalance.status === "fulfilled" && billingBalance.value?.success) {
|
|
const balance = billingBalance.value.data
|
|
setStats((prev) => ({
|
|
...prev,
|
|
euBalance: balance.euBalance || balance.balance || 0,
|
|
}))
|
|
}
|
|
|
|
// 注意:EU消耗数据、模型分布数据、每周API调用数据已从 monitoringDashboard 中获取
|
|
// 如果需要单独获取趋势数据,可以使用 TaijiAPIClient.getMonitoringTrends()
|
|
|
|
} catch (error) {
|
|
console.error("Failed to load dashboard data:", error)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Hero Stats */}
|
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
|
<Card className="bg-card">
|
|
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
|
<CardTitle className="text-sm font-medium text-muted-foreground">
|
|
{t("活跃代理", "Active Agents")}
|
|
</CardTitle>
|
|
<Boxes className="h-4 w-4 text-primary" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-3xl font-bold">
|
|
{loading ? (
|
|
<span className="inline-block h-8 w-16 animate-pulse bg-muted rounded" />
|
|
) : (
|
|
stats.activeAgents.toLocaleString()
|
|
)}
|
|
</div>
|
|
<p className="text-xs text-muted-foreground mt-1">
|
|
{loading ? "" : t("活跃代理", "Active Agents")}
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="bg-card">
|
|
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
|
<CardTitle className="text-sm font-medium text-muted-foreground">
|
|
{t("全局 API 调用", "Global API Calls")}
|
|
</CardTitle>
|
|
<Activity className="h-4 w-4 text-secondary" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-3xl font-bold">
|
|
{loading ? (
|
|
<span className="inline-block h-8 w-20 animate-pulse bg-muted rounded" />
|
|
) : (
|
|
stats.totalRequests > 1000
|
|
? `${(stats.totalRequests / 1000).toFixed(1)}K`
|
|
: stats.totalRequests.toLocaleString()
|
|
)}
|
|
</div>
|
|
<p className="text-xs text-muted-foreground mt-1">
|
|
{loading ? "" : t("总请求数", "Total Requests")}
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="bg-card">
|
|
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
|
<CardTitle className="text-sm font-medium text-muted-foreground">
|
|
{t("EU 余额", "EU Balance")}
|
|
</CardTitle>
|
|
<Zap className="h-4 w-4 text-chart-4" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-3xl font-bold">
|
|
{loading ? (
|
|
<span className="inline-block h-8 w-20 animate-pulse bg-muted rounded" />
|
|
) : (
|
|
stats.euBalance.toLocaleString()
|
|
)}
|
|
</div>
|
|
<p className="text-xs text-muted-foreground mt-1">
|
|
{loading ? "" : t("可用余额", "Available Balance")}
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="bg-card">
|
|
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
|
<CardTitle className="text-sm font-medium text-muted-foreground">
|
|
{t("系统健康度", "System Health")}
|
|
</CardTitle>
|
|
<GitBranch className="h-4 w-4 text-chart-2" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-3xl font-bold">
|
|
{loading ? (
|
|
<span className="inline-block h-8 w-12 animate-pulse bg-muted rounded" />
|
|
) : (
|
|
`${stats.systemHealth}%`
|
|
)}
|
|
</div>
|
|
<p className="text-xs text-muted-foreground mt-1">
|
|
{loading ? "" : t("系统状态", "System Status")}
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Charts Row */}
|
|
<div className="grid gap-4 lg:grid-cols-2">
|
|
{/* EU Consumption Chart */}
|
|
<Card className="bg-card">
|
|
<CardHeader>
|
|
<CardTitle className="text-base">{t("执行单元 (EU) 消耗", "Execution Unit (EU) Consumption")}</CardTitle>
|
|
<p className="text-xs text-muted-foreground">{t("过去 24 小时", "Last 24 hours")}</p>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<ResponsiveContainer width="100%" height={250}>
|
|
<AreaChart data={euConsumptionData}>
|
|
<defs>
|
|
<linearGradient id="colorEU" x1="0" y1="0" x2="0" y2="1">
|
|
<stop offset="5%" stopColor="oklch(0.60 0.20 264)" stopOpacity={0.3} />
|
|
<stop offset="95%" stopColor="oklch(0.60 0.20 264)" stopOpacity={0} />
|
|
</linearGradient>
|
|
</defs>
|
|
<XAxis dataKey="time" stroke="oklch(0.45 0 0)" fontSize={12} tickLine={false} />
|
|
<YAxis stroke="oklch(0.45 0 0)" fontSize={12} tickLine={false} />
|
|
<Tooltip
|
|
contentStyle={{
|
|
backgroundColor: "oklch(0.15 0 0)",
|
|
border: "1px solid oklch(0.22 0 0)",
|
|
borderRadius: "8px",
|
|
}}
|
|
/>
|
|
<Area
|
|
type="monotone"
|
|
dataKey="value"
|
|
stroke="oklch(0.60 0.20 264)"
|
|
strokeWidth={2}
|
|
fill="url(#colorEU)"
|
|
/>
|
|
</AreaChart>
|
|
</ResponsiveContainer>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Model Distribution Chart */}
|
|
<Card className="bg-card">
|
|
<CardHeader>
|
|
<CardTitle className="text-base">{t("模型分布", "Model Distribution")}</CardTitle>
|
|
<p className="text-xs text-muted-foreground">{t("当前推理负载", "Current inference load")}</p>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{modelDistribution.length > 0 ? (
|
|
<>
|
|
<ResponsiveContainer width="100%" height={250}>
|
|
<PieChart>
|
|
<Pie
|
|
data={modelDistribution}
|
|
cx="50%"
|
|
cy="50%"
|
|
innerRadius={60}
|
|
outerRadius={90}
|
|
paddingAngle={2}
|
|
dataKey="value"
|
|
>
|
|
{modelDistribution.map((entry, index) => (
|
|
<Cell key={`cell-${index}`} fill={entry.color} />
|
|
))}
|
|
</Pie>
|
|
<Tooltip
|
|
contentStyle={{
|
|
backgroundColor: "oklch(0.15 0 0)",
|
|
border: "1px solid oklch(0.22 0 0)",
|
|
borderRadius: "8px",
|
|
}}
|
|
/>
|
|
</PieChart>
|
|
</ResponsiveContainer>
|
|
<div className="mt-4 grid grid-cols-3 gap-4">
|
|
{modelDistribution.map((model) => (
|
|
<div key={model.name} className="flex flex-col items-center">
|
|
<div className="flex items-center gap-2">
|
|
<div className="h-3 w-3 rounded-full" style={{ backgroundColor: model.color }} />
|
|
<span className="text-xs font-medium">{model.value}%</span>
|
|
</div>
|
|
<span className="text-xs text-muted-foreground mt-1">{model.name}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</>
|
|
) : (
|
|
<div className="flex items-center justify-center h-[250px]">
|
|
<div className="text-center">
|
|
<div className="text-muted-foreground text-sm">
|
|
{loading ? t("加载中...", "Loading...") : t("暂无模型使用数据", "No model usage data")}
|
|
</div>
|
|
{!loading && (
|
|
<p className="text-xs text-muted-foreground mt-2">
|
|
{t("模型调用后将显示分布图", "Distribution chart will appear after model calls")}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* API Calls Chart */}
|
|
<Card className="bg-card">
|
|
<CardHeader>
|
|
<CardTitle className="text-base">{t("每周 API 调用", "Weekly API Calls")}</CardTitle>
|
|
<p className="text-xs text-muted-foreground">{t("每日总调用次数", "Total calls per day")}</p>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<ResponsiveContainer width="100%" height={200}>
|
|
<LineChart data={apiCallsData}>
|
|
<XAxis dataKey="time" stroke="oklch(0.45 0 0)" fontSize={12} tickLine={false} />
|
|
<YAxis stroke="oklch(0.45 0 0)" fontSize={12} tickLine={false} />
|
|
<Tooltip
|
|
contentStyle={{
|
|
backgroundColor: "oklch(0.15 0 0)",
|
|
border: "1px solid oklch(0.22 0 0)",
|
|
borderRadius: "8px",
|
|
}}
|
|
/>
|
|
<Line
|
|
type="monotone"
|
|
dataKey="calls"
|
|
stroke="oklch(0.65 0.16 220)"
|
|
strokeWidth={2}
|
|
dot={{ fill: "oklch(0.65 0.16 220)", r: 4 }}
|
|
/>
|
|
</LineChart>
|
|
</ResponsiveContainer>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* System Status */}
|
|
<Card className="bg-card">
|
|
<CardHeader>
|
|
<CardTitle className="text-base">{t("系统组件", "System Components")}</CardTitle>
|
|
<p className="text-xs text-muted-foreground">{t("实时状态监控", "Real-time status monitoring")}</p>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="grid gap-3 md:grid-cols-2 lg:grid-cols-3">
|
|
{(systemComponents.length > 0
|
|
? systemComponents
|
|
: [
|
|
{ name: "agent_manager", status: t("加载中", "Loading"), statusType: "loading", latency: "-" },
|
|
{ name: "mcp_server", status: t("加载中", "Loading"), statusType: "loading", latency: "-" },
|
|
{ name: "data_ingestion", status: t("加载中", "Loading"), statusType: "loading", latency: "-" },
|
|
{ name: "model_gateway", status: t("加载中", "Loading"), statusType: "loading", latency: "-" },
|
|
]
|
|
).map((component) => {
|
|
// 根据状态类型确定颜色
|
|
const getStatusColor = (statusType: string) => {
|
|
switch (statusType) {
|
|
case "healthy":
|
|
case "running":
|
|
return { dot: "bg-green-500", text: "text-green-500" }
|
|
case "maintenance":
|
|
return { dot: "bg-yellow-500", text: "text-yellow-500" }
|
|
case "loading":
|
|
return { dot: "bg-gray-500", text: "text-gray-500" }
|
|
default:
|
|
return { dot: "bg-red-500", text: "text-red-500" }
|
|
}
|
|
}
|
|
const colors = getStatusColor(component.statusType)
|
|
|
|
return (
|
|
<div
|
|
key={component.name}
|
|
className="flex items-center justify-between rounded-lg border border-border bg-muted/30 p-3"
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<div className={`h-2 w-2 rounded-full ${colors.dot}`} />
|
|
<div>
|
|
<p className="text-sm font-medium">{component.name}</p>
|
|
<p className="text-xs text-muted-foreground">{component.latency}</p>
|
|
</div>
|
|
</div>
|
|
<span className={`text-xs ${colors.text}`}>{component.status}</span>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
)
|
|
}
|