Files
taiji-pda-v0/components/dashboard-overview.tsx
T

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>
)
}