540 lines
18 KiB
TypeScript
540 lines
18 KiB
TypeScript
import React, { useState } from 'react';
|
|
import {
|
|
Box,
|
|
Typography,
|
|
Card,
|
|
CardContent,
|
|
Grid,
|
|
Button,
|
|
Chip,
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableContainer,
|
|
TableHead,
|
|
TableRow,
|
|
Paper,
|
|
LinearProgress,
|
|
IconButton,
|
|
Menu,
|
|
MenuItem,
|
|
Tabs,
|
|
Tab,
|
|
TextField,
|
|
InputAdornment,
|
|
} from '@mui/material';
|
|
import {
|
|
Memory as MemoryIcon,
|
|
Storage as StorageIcon,
|
|
Speed as SpeedIcon,
|
|
CloudDownload as CloudDownloadIcon,
|
|
Settings as SettingsIcon,
|
|
MoreVert as MoreVertIcon,
|
|
Search as SearchIcon,
|
|
Add as AddIcon,
|
|
} from '@mui/icons-material';
|
|
import { styled } from '@mui/material/styles';
|
|
|
|
const PageContainer = styled(Box)(({ theme }) => ({
|
|
padding: '1.5rem',
|
|
backgroundColor: '#F8F9FA',
|
|
minHeight: 'calc(100vh - 60px)',
|
|
}));
|
|
|
|
const PageHeader = styled(Box)(({ theme }) => ({
|
|
display: 'flex',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
marginBottom: '1.5rem',
|
|
}));
|
|
|
|
const ResourceCard = styled(Card)(({ theme }) => ({
|
|
height: '100%',
|
|
border: '1px solid #E5E5E5',
|
|
transition: 'all 0.2s ease-in-out',
|
|
'&:hover': {
|
|
boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
|
|
},
|
|
}));
|
|
|
|
const StatusChip = styled(Chip)<{ status: string }>(({ status, theme }) => ({
|
|
fontSize: '0.75rem',
|
|
height: '24px',
|
|
backgroundColor:
|
|
status === 'active' ? '#E8F5E8' :
|
|
status === 'loading' ? '#FFF3CD' :
|
|
status === 'error' ? '#F8D7DA' : '#F8F9FA',
|
|
color:
|
|
status === 'active' ? '#155724' :
|
|
status === 'loading' ? '#856404' :
|
|
status === 'error' ? '#721C24' : '#666',
|
|
}));
|
|
|
|
const UsageBar = styled(Box)(({ theme }) => ({
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '0.5rem',
|
|
marginTop: '0.5rem',
|
|
}));
|
|
|
|
const mockModels = [
|
|
{
|
|
id: 1,
|
|
name: 'GPT-4',
|
|
type: 'LLM',
|
|
provider: 'OpenAI',
|
|
status: 'active',
|
|
usage: 75,
|
|
cost: '$234.56',
|
|
requests: '12.3K',
|
|
latency: '1.2s',
|
|
},
|
|
{
|
|
id: 2,
|
|
name: 'text-embedding-ada-002',
|
|
type: 'Embedding',
|
|
provider: 'OpenAI',
|
|
status: 'active',
|
|
usage: 45,
|
|
cost: '$89.12',
|
|
requests: '45.6K',
|
|
latency: '0.3s',
|
|
},
|
|
{
|
|
id: 3,
|
|
name: 'Claude-3-Sonnet',
|
|
type: 'LLM',
|
|
provider: 'Anthropic',
|
|
status: 'loading',
|
|
usage: 0,
|
|
cost: '$0.00',
|
|
requests: '0',
|
|
latency: 'N/A',
|
|
},
|
|
{
|
|
id: 4,
|
|
name: 'BGE-Large-ZH',
|
|
type: 'Embedding',
|
|
provider: 'BAAI',
|
|
status: 'error',
|
|
usage: 0,
|
|
cost: '$0.00',
|
|
requests: '0',
|
|
latency: 'N/A',
|
|
},
|
|
];
|
|
|
|
const mockResources = [
|
|
{
|
|
id: 1,
|
|
name: 'GPU-Server-01',
|
|
type: 'GPU',
|
|
specs: 'NVIDIA A100 80GB',
|
|
usage: 85,
|
|
status: 'active',
|
|
location: '北京机房',
|
|
},
|
|
{
|
|
id: 2,
|
|
name: 'CPU-Cluster-01',
|
|
type: 'CPU',
|
|
specs: '64 Core Intel Xeon',
|
|
usage: 45,
|
|
status: 'active',
|
|
location: '上海机房',
|
|
},
|
|
{
|
|
id: 3,
|
|
name: 'Storage-Pool-01',
|
|
type: 'Storage',
|
|
specs: '10TB NVMe SSD',
|
|
usage: 67,
|
|
status: 'active',
|
|
location: '深圳机房',
|
|
},
|
|
];
|
|
|
|
const ModelsResources: React.FC = () => {
|
|
const [tabValue, setTabValue] = useState(0);
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
|
const [selectedItem, setSelectedItem] = useState<number | null>(null);
|
|
|
|
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
|
|
setTabValue(newValue);
|
|
};
|
|
|
|
const handleMenuClick = (event: React.MouseEvent<HTMLElement>, itemId: number) => {
|
|
setAnchorEl(event.currentTarget);
|
|
setSelectedItem(itemId);
|
|
};
|
|
|
|
const handleMenuClose = () => {
|
|
setAnchorEl(null);
|
|
setSelectedItem(null);
|
|
};
|
|
|
|
const filteredModels = mockModels.filter(model =>
|
|
model.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
model.provider.toLowerCase().includes(searchTerm.toLowerCase())
|
|
);
|
|
|
|
const filteredResources = mockResources.filter(resource =>
|
|
resource.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
resource.type.toLowerCase().includes(searchTerm.toLowerCase())
|
|
);
|
|
|
|
return (
|
|
<PageContainer>
|
|
<PageHeader>
|
|
<Typography variant="h4" fontWeight={600} color="#333">
|
|
模型与资源
|
|
</Typography>
|
|
<Button
|
|
variant="contained"
|
|
startIcon={<AddIcon />}
|
|
sx={{ borderRadius: '6px' }}
|
|
>
|
|
添加模型
|
|
</Button>
|
|
</PageHeader>
|
|
|
|
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 3 }}>
|
|
<Tabs value={tabValue} onChange={handleTabChange}>
|
|
<Tab label="AI 模型" />
|
|
<Tab label="计算资源" />
|
|
</Tabs>
|
|
</Box>
|
|
|
|
<Box display="flex" gap={2} mb={3}>
|
|
<TextField
|
|
placeholder={tabValue === 0 ? "搜索模型..." : "搜索资源..."}
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
InputProps={{
|
|
startAdornment: (
|
|
<InputAdornment position="start">
|
|
<SearchIcon color="action" />
|
|
</InputAdornment>
|
|
),
|
|
}}
|
|
sx={{ width: '400px' }}
|
|
/>
|
|
</Box>
|
|
|
|
{tabValue === 0 && (
|
|
<>
|
|
{/* 模型概览卡片 */}
|
|
<Grid container spacing={3} sx={{ mb: 3 }}>
|
|
<Grid size={{xs:12,sm:6,md:3}}>
|
|
<ResourceCard>
|
|
<CardContent>
|
|
<Box display="flex" alignItems="center" justifyContent="space-between">
|
|
<Box>
|
|
<Typography color="text.secondary" variant="body2">
|
|
活跃模型
|
|
</Typography>
|
|
<Typography variant="h4" fontWeight={600} color="primary">
|
|
{mockModels.filter(m => m.status === 'active').length}
|
|
</Typography>
|
|
</Box>
|
|
<MemoryIcon color="primary" sx={{ fontSize: '3rem', opacity: 0.3 }} />
|
|
</Box>
|
|
</CardContent>
|
|
</ResourceCard>
|
|
</Grid>
|
|
|
|
<Grid size={{xs:12,sm:6,md:3}}>
|
|
<ResourceCard>
|
|
<CardContent>
|
|
<Box display="flex" alignItems="center" justifyContent="space-between">
|
|
<Box>
|
|
<Typography color="text.secondary" variant="body2">
|
|
总请求数
|
|
</Typography>
|
|
<Typography variant="h4" fontWeight={600} color="success.main">
|
|
57.9K
|
|
</Typography>
|
|
</Box>
|
|
<SpeedIcon color="success" sx={{ fontSize: '3rem', opacity: 0.3 }} />
|
|
</Box>
|
|
</CardContent>
|
|
</ResourceCard>
|
|
</Grid>
|
|
|
|
<Grid size={{xs:12,sm:6,md:3}}>
|
|
<ResourceCard>
|
|
<CardContent>
|
|
<Box display="flex" alignItems="center" justifyContent="space-between">
|
|
<Box>
|
|
<Typography color="text.secondary" variant="body2">
|
|
总成本
|
|
</Typography>
|
|
<Typography variant="h4" fontWeight={600} color="warning.main">
|
|
$323.68
|
|
</Typography>
|
|
</Box>
|
|
<CloudDownloadIcon color="warning" sx={{ fontSize: '3rem', opacity: 0.3 }} />
|
|
</Box>
|
|
</CardContent>
|
|
</ResourceCard>
|
|
</Grid>
|
|
|
|
<Grid size={{xs:12,sm:6,md:3}}>
|
|
<ResourceCard>
|
|
<CardContent>
|
|
<Box display="flex" alignItems="center" justifyContent="space-between">
|
|
<Box>
|
|
<Typography color="text.secondary" variant="body2">
|
|
平均延迟
|
|
</Typography>
|
|
<Typography variant="h4" fontWeight={600} color="info.main">
|
|
0.8s
|
|
</Typography>
|
|
</Box>
|
|
<SpeedIcon color="info" sx={{ fontSize: '3rem', opacity: 0.3 }} />
|
|
</Box>
|
|
</CardContent>
|
|
</ResourceCard>
|
|
</Grid>
|
|
</Grid>
|
|
|
|
{/* 模型列表 */}
|
|
<Card sx={{ border: '1px solid #E5E5E5' }}>
|
|
<CardContent>
|
|
<Typography variant="h6" fontWeight={600} mb={2}>
|
|
模型列表
|
|
</Typography>
|
|
<TableContainer>
|
|
<Table>
|
|
<TableHead>
|
|
<TableRow>
|
|
<TableCell>模型名称</TableCell>
|
|
<TableCell>类型</TableCell>
|
|
<TableCell>提供商</TableCell>
|
|
<TableCell>状态</TableCell>
|
|
<TableCell>使用率</TableCell>
|
|
<TableCell>请求数</TableCell>
|
|
<TableCell>成本</TableCell>
|
|
<TableCell>延迟</TableCell>
|
|
<TableCell>操作</TableCell>
|
|
</TableRow>
|
|
</TableHead>
|
|
<TableBody>
|
|
{filteredModels.map((model) => (
|
|
<TableRow key={model.id} hover>
|
|
<TableCell>
|
|
<Typography variant="body2" fontWeight={600}>
|
|
{model.name}
|
|
</Typography>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Chip
|
|
label={model.type}
|
|
size="small"
|
|
variant="outlined"
|
|
color={model.type === 'LLM' ? 'primary' : 'secondary'}
|
|
/>
|
|
</TableCell>
|
|
<TableCell>{model.provider}</TableCell>
|
|
<TableCell>
|
|
<StatusChip
|
|
status={model.status}
|
|
label={
|
|
model.status === 'active' ? '活跃' :
|
|
model.status === 'loading' ? '加载中' : '错误'
|
|
}
|
|
size="small"
|
|
/>
|
|
</TableCell>
|
|
<TableCell>
|
|
<UsageBar>
|
|
<LinearProgress
|
|
variant="determinate"
|
|
value={model.usage}
|
|
sx={{ width: '60px', height: '6px', borderRadius: '3px' }}
|
|
color={model.usage > 80 ? 'error' : model.usage > 60 ? 'warning' : 'primary'}
|
|
/>
|
|
<Typography variant="body2" color="text.secondary">
|
|
{model.usage}%
|
|
</Typography>
|
|
</UsageBar>
|
|
</TableCell>
|
|
<TableCell>{model.requests}</TableCell>
|
|
<TableCell>{model.cost}</TableCell>
|
|
<TableCell>{model.latency}</TableCell>
|
|
<TableCell>
|
|
<IconButton
|
|
size="small"
|
|
onClick={(e) => handleMenuClick(e, model.id)}
|
|
>
|
|
<MoreVertIcon />
|
|
</IconButton>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</TableContainer>
|
|
</CardContent>
|
|
</Card>
|
|
</>
|
|
)}
|
|
|
|
{tabValue === 1 && (
|
|
<>
|
|
{/* 资源概览卡片 */}
|
|
<Grid container spacing={3} sx={{ mb: 3 }}>
|
|
<Grid size={{xs:12,sm:6,md:4}}>
|
|
<ResourceCard>
|
|
<CardContent>
|
|
<Box display="flex" alignItems="center" justifyContent="space-between">
|
|
<Box>
|
|
<Typography color="text.secondary" variant="body2">
|
|
GPU 使用率
|
|
</Typography>
|
|
<Typography variant="h4" fontWeight={600} color="error.main">
|
|
85%
|
|
</Typography>
|
|
</Box>
|
|
<MemoryIcon color="error" sx={{ fontSize: '3rem', opacity: 0.3 }} />
|
|
</Box>
|
|
</CardContent>
|
|
</ResourceCard>
|
|
</Grid>
|
|
|
|
<Grid size={{xs:12,sm:6,md:4}}>
|
|
<ResourceCard>
|
|
<CardContent>
|
|
<Box display="flex" alignItems="center" justifyContent="space-between">
|
|
<Box>
|
|
<Typography color="text.secondary" variant="body2">
|
|
CPU 使用率
|
|
</Typography>
|
|
<Typography variant="h4" fontWeight={600} color="warning.main">
|
|
45%
|
|
</Typography>
|
|
</Box>
|
|
<SpeedIcon color="warning" sx={{ fontSize: '3rem', opacity: 0.3 }} />
|
|
</Box>
|
|
</CardContent>
|
|
</ResourceCard>
|
|
</Grid>
|
|
|
|
<Grid size={{xs:12,sm:6,md:4}}>
|
|
<ResourceCard>
|
|
<CardContent>
|
|
<Box display="flex" alignItems="center" justifyContent="space-between">
|
|
<Box>
|
|
<Typography color="text.secondary" variant="body2">
|
|
存储使用率
|
|
</Typography>
|
|
<Typography variant="h4" fontWeight={600} color="success.main">
|
|
67%
|
|
</Typography>
|
|
</Box>
|
|
<StorageIcon color="success" sx={{ fontSize: '3rem', opacity: 0.3 }} />
|
|
</Box>
|
|
</CardContent>
|
|
</ResourceCard>
|
|
</Grid>
|
|
</Grid>
|
|
|
|
{/* 资源列表 */}
|
|
<Card sx={{ border: '1px solid #E5E5E5' }}>
|
|
<CardContent>
|
|
<Typography variant="h6" fontWeight={600} mb={2}>
|
|
计算资源
|
|
</Typography>
|
|
<TableContainer>
|
|
<Table>
|
|
<TableHead>
|
|
<TableRow>
|
|
<TableCell>资源名称</TableCell>
|
|
<TableCell>类型</TableCell>
|
|
<TableCell>规格</TableCell>
|
|
<TableCell>使用率</TableCell>
|
|
<TableCell>状态</TableCell>
|
|
<TableCell>位置</TableCell>
|
|
<TableCell>操作</TableCell>
|
|
</TableRow>
|
|
</TableHead>
|
|
<TableBody>
|
|
{filteredResources.map((resource) => (
|
|
<TableRow key={resource.id} hover>
|
|
<TableCell>
|
|
<Typography variant="body2" fontWeight={600}>
|
|
{resource.name}
|
|
</Typography>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Chip
|
|
label={resource.type}
|
|
size="small"
|
|
variant="outlined"
|
|
color={
|
|
resource.type === 'GPU' ? 'error' :
|
|
resource.type === 'CPU' ? 'warning' : 'success'
|
|
}
|
|
/>
|
|
</TableCell>
|
|
<TableCell>{resource.specs}</TableCell>
|
|
<TableCell>
|
|
<UsageBar>
|
|
<LinearProgress
|
|
variant="determinate"
|
|
value={resource.usage}
|
|
sx={{ width: '80px', height: '6px', borderRadius: '3px' }}
|
|
color={resource.usage > 80 ? 'error' : resource.usage > 60 ? 'warning' : 'primary'}
|
|
/>
|
|
<Typography variant="body2" color="text.secondary">
|
|
{resource.usage}%
|
|
</Typography>
|
|
</UsageBar>
|
|
</TableCell>
|
|
<TableCell>
|
|
<StatusChip
|
|
status={resource.status}
|
|
label={resource.status === 'active' ? '活跃' : '离线'}
|
|
size="small"
|
|
/>
|
|
</TableCell>
|
|
<TableCell>{resource.location}</TableCell>
|
|
<TableCell>
|
|
<IconButton
|
|
size="small"
|
|
onClick={(e) => handleMenuClick(e, resource.id)}
|
|
>
|
|
<MoreVertIcon />
|
|
</IconButton>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</TableContainer>
|
|
</CardContent>
|
|
</Card>
|
|
</>
|
|
)}
|
|
|
|
<Menu
|
|
anchorEl={anchorEl}
|
|
open={Boolean(anchorEl)}
|
|
onClose={handleMenuClose}
|
|
>
|
|
<MenuItem onClick={handleMenuClose}>
|
|
<SettingsIcon sx={{ mr: 1 }} fontSize="small" />
|
|
配置
|
|
</MenuItem>
|
|
<MenuItem onClick={handleMenuClose}>查看详情</MenuItem>
|
|
<MenuItem onClick={handleMenuClose}>监控</MenuItem>
|
|
<MenuItem onClick={handleMenuClose} sx={{ color: 'error.main' }}>
|
|
停用
|
|
</MenuItem>
|
|
</Menu>
|
|
</PageContainer>
|
|
);
|
|
};
|
|
|
|
export default ModelsResources; |