first-update

This commit is contained in:
2026-03-17 14:36:31 +08:00
parent 72f08aee7c
commit 4eddf05e79
516 changed files with 115270 additions and 1 deletions

View File

@@ -0,0 +1,188 @@
import React from 'react';
import { Card, CardContent, Typography, Box, useTheme } from '@mui/material';
import { useTranslation } from 'react-i18next';
import {
AreaChart,
Area,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
PieChart,
Pie,
Cell,
Legend
} from 'recharts';
export default function Charts({ trendData, modelDistribution }) {
const theme = useTheme();
const { t } = useTranslation();
const COLORS = [
theme.palette.primary.main,
theme.palette.secondary.main,
theme.palette.success.main,
theme.palette.warning.main,
theme.palette.error.main,
theme.palette.info.main
];
return (
<Box sx={{ display: 'flex', gap: 3, flexWrap: 'wrap' }}>
{/* 趋势图 */}
<Box sx={{ flex: '1 1 calc(66.67% - 16px)', minWidth: 500 }}>
<Card
elevation={0}
sx={{
height: 400,
border: `1px solid ${theme.palette.divider}`,
borderRadius: 2
}}
>
<CardContent sx={{ height: '100%' }}>
<Box display="flex" justifyContent="space-between" alignItems="center" mb={2}>
<Typography variant="h6" fontWeight="bold">
{t('monitoring.charts.tokenTrend')}
</Typography>
<Box display="flex" gap={2}>
<Box display="flex" alignItems="center" gap={1}>
<Box sx={{ width: 8, height: 8, borderRadius: '50%', bgcolor: theme.palette.primary.main }} />
<Typography variant="caption" color="text.secondary">
{t('monitoring.charts.inputLegend')}
</Typography>
</Box>
<Box display="flex" alignItems="center" gap={1}>
<Box sx={{ width: 8, height: 8, borderRadius: '50%', bgcolor: theme.palette.success.main }} />
<Typography variant="caption" color="text.secondary">
{t('monitoring.charts.outputLegend')}
</Typography>
</Box>
</Box>
</Box>
<ResponsiveContainer width="100%" height="85%">
<AreaChart data={trendData} margin={{ top: 10, right: 30, left: 0, bottom: 0 }}>
<defs>
<linearGradient id="colorInput" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={theme.palette.primary.main} stopOpacity={0.1} />
<stop offset="95%" stopColor={theme.palette.primary.main} stopOpacity={0} />
</linearGradient>
<linearGradient id="colorOutput" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={theme.palette.success.main} stopOpacity={0.1} />
<stop offset="95%" stopColor={theme.palette.success.main} stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke={theme.palette.divider} />
<XAxis
dataKey="name"
axisLine={false}
tickLine={false}
tick={{ fill: theme.palette.text.secondary, fontSize: 12 }}
dy={10}
/>
<YAxis axisLine={false} tickLine={false} tick={{ fill: theme.palette.text.secondary, fontSize: 12 }} />
<Tooltip
contentStyle={{
backgroundColor: theme.palette.background.paper,
border: `1px solid ${theme.palette.divider}`,
borderRadius: 8
}}
/>
<Area
type="monotone"
dataKey="input"
stroke={theme.palette.primary.main}
fillOpacity={1}
fill="url(#colorInput)"
strokeWidth={2}
/>
<Area
type="monotone"
dataKey="output"
stroke={theme.palette.success.main}
fillOpacity={1}
fill="url(#colorOutput)"
strokeWidth={2}
/>
</AreaChart>
</ResponsiveContainer>
</CardContent>
</Card>
</Box>
{/* 模型分布图 */}
<Box sx={{ flex: '1 1 calc(33.33% - 16px)', minWidth: 350 }}>
<Card
elevation={0}
sx={{
height: 400,
border: `1px solid ${theme.palette.divider}`,
borderRadius: 2
}}
>
<CardContent sx={{ height: '100%' }}>
<Typography variant="h6" fontWeight="bold" mb={2}>
{t('monitoring.charts.distributionTitle')}
</Typography>
<Typography variant="body2" color="text.secondary" mb={4}>
{t('monitoring.charts.distributionSubtitle')}
</Typography>
<Box sx={{ height: '70%', position: 'relative' }}>
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={modelDistribution}
cx="50%"
cy="50%"
innerRadius={60}
outerRadius={80}
paddingAngle={5}
dataKey="value"
>
{modelDistribution.map((entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
<Tooltip
formatter={value => t('monitoring.charts.tokensTooltip', { value: (value / 1000).toFixed(1) })}
contentStyle={{
backgroundColor: theme.palette.background.paper,
border: `1px solid ${theme.palette.divider}`,
borderRadius: 8
}}
/>
<Legend
verticalAlign="bottom"
height={36}
formatter={(value, entry, index) => (
<span style={{ color: theme.palette.text.primary, fontSize: 12, marginLeft: 5 }}>{value}</span>
)}
/>
</PieChart>
</ResponsiveContainer>
{/* 中间文字 */}
{/* <Box
sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
textAlign: 'center'
}}
>
<Typography variant="h5" fontWeight="bold">
{modelDistribution.length}
</Typography>
<Typography variant="caption" color="text.secondary">
Models
</Typography>
</Box> */}
</Box>
</CardContent>
</Card>
</Box>
</Box>
);
}

View File

@@ -0,0 +1,145 @@
import React from 'react';
import { Box, Card, CardContent, Grid, Typography, Stack, useTheme, alpha } from '@mui/material';
import {
Storage as StorageIcon,
Balance as BalanceIcon,
Bolt as BoltIcon,
AccessTime as AccessTimeIcon
} from '@mui/icons-material';
import { useTranslation } from 'react-i18next';
function StatCard({ title, value, subValue, icon: Icon, color }) {
const theme = useTheme();
return (
<Card
elevation={0}
sx={{
height: '100%',
border: `1px solid ${theme.palette.divider}`,
borderRadius: 2,
transition: 'all 0.3s ease',
'&:hover': {
boxShadow: theme.shadows[4],
transform: 'translateY(-2px)'
}
}}
>
<CardContent sx={{ p: 3 }}>
<Stack direction="row" alignItems="center" spacing={2} mb={2}>
<Box
sx={{
p: 1.5,
borderRadius: 2,
bgcolor: alpha(color, 0.1),
color: color,
display: 'flex'
}}
>
<Icon fontSize="medium" />
</Box>
<Typography variant="body2" color="text.secondary" fontWeight={500}>
{title}
</Typography>
</Stack>
<Typography variant="h3" fontWeight="bold" sx={{ mb: 1.5, color: 'text.primary' }}>
{value}
</Typography>
{subValue && (
<Typography variant="body2" color="text.secondary" sx={{ lineHeight: 1.6 }}>
{subValue}
</Typography>
)}
</CardContent>
</Card>
);
}
export default function StatsCards({ data }) {
const theme = useTheme();
const { t } = useTranslation();
// 格式化数字
const formatNumber = num => {
if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`;
if (num >= 1000) return `${(num / 1000).toFixed(1)}K`;
return num;
};
return (
<Box sx={{ display: 'flex', gap: 3, flexWrap: 'wrap' }}>
{/* 总 Token 消耗 */}
<Box sx={{ flex: '1 1 calc(25% - 18px)', minWidth: 250 }}>
<StatCard
title={t('monitoring.stats.totalTokens')}
value={formatNumber(data.totalTokens)}
subValue={t('monitoring.stats.inputOutput', {
input: formatNumber(data.inputTokens),
output: formatNumber(data.outputTokens)
})}
icon={StorageIcon}
color={theme.palette.primary.main}
/>
</Box>
{/* 平均 Token 消耗/次 */}
<Box sx={{ flex: '1 1 calc(25% - 18px)', minWidth: 250 }}>
<StatCard
title={t('monitoring.stats.avgTokensPerCall')}
value={formatNumber(data.avgTokensPerCall)}
subValue={t('monitoring.stats.inputOutput', {
input: formatNumber(Math.round(data.inputTokens / (data.totalCalls || 1))),
output: formatNumber(Math.round(data.outputTokens / (data.totalCalls || 1)))
})}
icon={BalanceIcon}
color={theme.palette.info.main}
/>
</Box>
{/* 总调用次数 */}
<Box sx={{ flex: '1 1 calc(25% - 18px)', minWidth: 250 }}>
<StatCard
title={t('monitoring.stats.totalCalls')}
value={formatNumber(data.totalCalls)}
subValue={
<Box component="span">
<Box component="span" sx={{ color: theme.palette.success.main, fontWeight: 600 }}>
{t('monitoring.stats.successCalls', { count: formatNumber(data.successCalls) })}
</Box>
<Box component="span" sx={{ mx: 1 }}>
·
</Box>
<Box component="span" sx={{ color: theme.palette.error.main, fontWeight: 600 }}>
{t('monitoring.stats.failedCalls', { count: formatNumber(data.failedCalls) })}
</Box>
{data.totalCalls > 0 && (
<Box component="span" sx={{ ml: 1, color: 'text.disabled' }}>
({t('monitoring.stats.failureRate', { rate: ((data.failureRate || 0) * 100).toFixed(1) })})
</Box>
)}
</Box>
}
icon={BoltIcon}
color={theme.palette.success.main}
/>
</Box>
{/* 平均响应耗时 */}
<Box sx={{ flex: '1 1 calc(25% - 18px)', minWidth: 250 }}>
<StatCard
title={t('monitoring.stats.avgLatency')}
value={`${(data.avgLatency / 1000).toFixed(2)}s`}
subValue={
data.successCalls > 0
? t('monitoring.stats.basedOnSuccessCalls', { count: formatNumber(data.successCalls) })
: t('monitoring.stats.noSuccessCalls')
}
icon={AccessTimeIcon}
color={theme.palette.warning.main}
/>
</Box>
</Box>
);
}

View File

@@ -0,0 +1,215 @@
import React, { useState } from 'react';
import {
Box,
Card,
CardContent,
Typography,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Chip,
TextField,
InputAdornment,
TablePagination,
useTheme,
alpha,
Tooltip
} from '@mui/material';
import SearchIcon from '@mui/icons-material/Search';
import { useTranslation } from 'react-i18next';
const statusColors = {
SUCCESS: 'success',
FAILED: 'error'
};
export default function UsageTable({
data,
total,
page,
pageSize,
onPageChange,
onPageSizeChange,
searchTerm,
onSearchChange
}) {
const theme = useTheme();
const { t } = useTranslation();
const handleChangePage = (event, newPage) => {
onPageChange(newPage + 1); // MUI uses 0-indexed, our API uses 1-indexed
};
const handleChangeRowsPerPage = event => {
onPageSizeChange(parseInt(event.target.value, 10));
};
const handleSearchChange = event => {
onSearchChange(event.target.value);
};
// 直接使用传入的数据,分页和搜索已在后端完成
return (
<Card
elevation={0}
sx={{
border: `1px solid ${theme.palette.divider}`,
borderRadius: 2,
mt: 3
}}
>
<CardContent>
<Box display="flex" justifyContent="space-between" alignItems="center" mb={3}>
<Typography variant="h6" fontWeight="bold">
{t('monitoring.table.title')}
</Typography>
<TextField
size="small"
placeholder={t('monitoring.table.searchPlaceholder')}
value={searchTerm}
onChange={handleSearchChange}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon fontSize="small" color="action" />
</InputAdornment>
)
}}
sx={{ width: 300 }}
/>
</Box>
<TableContainer>
<Table sx={{ minWidth: 650 }} aria-label="usage table">
<TableHead>
<TableRow>
<TableCell sx={{ fontWeight: 'bold', color: 'text.secondary' }}>
{t('monitoring.table.columns.projectName')}
</TableCell>
<TableCell sx={{ fontWeight: 'bold', color: 'text.secondary' }}>
{t('monitoring.table.columns.provider')}
</TableCell>
<TableCell sx={{ fontWeight: 'bold', color: 'text.secondary' }}>
{t('monitoring.table.columns.model')}
</TableCell>
<TableCell sx={{ fontWeight: 'bold', color: 'text.secondary' }}>
{t('monitoring.table.columns.status')}
</TableCell>
<TableCell sx={{ fontWeight: 'bold', color: 'text.secondary' }}>
{t('monitoring.table.columns.failureReason')}
</TableCell>
<TableCell align="right" sx={{ fontWeight: 'bold', color: 'text.secondary' }}>
{t('monitoring.table.columns.inputTokens')}
</TableCell>
<TableCell align="right" sx={{ fontWeight: 'bold', color: 'text.secondary' }}>
{t('monitoring.table.columns.outputTokens')}
</TableCell>
<TableCell align="right" sx={{ fontWeight: 'bold', color: 'text.secondary' }}>
{t('monitoring.table.columns.totalTokens')}
</TableCell>
<TableCell align="right" sx={{ fontWeight: 'bold', color: 'text.secondary' }}>
{t('monitoring.table.columns.calls')}
</TableCell>
<TableCell align="right" sx={{ fontWeight: 'bold', color: 'text.secondary' }}>
{t('monitoring.table.columns.avgLatency')}
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{data.map((row, index) => (
<TableRow key={index} sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
<TableCell component="th" scope="row">
<Typography variant="body2" fontWeight="500">
{row.projectName}
</Typography>
</TableCell>
<TableCell>
<Chip
label={row.provider}
size="small"
sx={{
borderRadius: 1,
bgcolor: alpha(theme.palette.primary.main, 0.1),
color: theme.palette.primary.main,
fontWeight: 500
}}
/>
</TableCell>
<TableCell>
<Typography variant="body2" sx={{ fontFamily: 'monospace' }}>
{row.model}
</Typography>
</TableCell>
<TableCell>
<Chip
label={row.status === 'SUCCESS' ? t('monitoring.status.success') : t('monitoring.status.failed')}
color={statusColors[row.status] || 'default'}
size="small"
variant="outlined"
sx={{
borderRadius: 1,
borderColor: theme.palette[statusColors[row.status]]?.main,
color: theme.palette[statusColors[row.status]]?.main,
bgcolor: alpha(theme.palette[statusColors[row.status]]?.main || '#000', 0.05)
}}
/>
</TableCell>
<TableCell>
{row.failureReason ? (
<Tooltip title={row.failureReason} arrow placement="top">
<Chip
label={
row.failureReason.length > 20 ? row.failureReason.slice(0, 20) + '...' : row.failureReason
}
size="small"
color="error"
variant="soft"
sx={{
maxWidth: 200,
bgcolor: alpha(theme.palette.error.main, 0.1),
color: theme.palette.error.dark,
cursor: 'pointer'
}}
/>
</Tooltip>
) : (
'-'
)}
</TableCell>
<TableCell align="right">{row.inputTokens.toLocaleString()}</TableCell>
<TableCell align="right">{row.outputTokens.toLocaleString()}</TableCell>
<TableCell align="right" sx={{ fontWeight: 'bold' }}>
{row.totalTokens.toLocaleString()}
</TableCell>
<TableCell align="right">{row.calls}</TableCell>
<TableCell align="right">{row.avgLatency}</TableCell>
</TableRow>
))}
{data.length === 0 && (
<TableRow>
<TableCell colSpan={10} align="center" sx={{ py: 8 }}>
<Typography color="text.secondary">{t('monitoring.table.empty')}</Typography>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
<TablePagination
rowsPerPageOptions={[5, 10, 25, 50]}
component="div"
count={total}
rowsPerPage={pageSize}
page={page - 1}
onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage}
labelRowsPerPage={t('monitoring.table.rowsPerPage')}
/>
</CardContent>
</Card>
);
}