feat: implement RAG dashboard with MUI and react-router
Add new RAG dashboard feature including: - Main application layout with header and sidebar - Multiple pages (Home, KnowledgeBase, PipelineConfig, Dashboard, MCP) - Theme configuration and styling - Routing setup with protected routes - Login page and authentication flow - Various UI components and mock data for dashboard views
This commit is contained in:
42
src/App.tsx
42
src/App.tsx
@@ -1,35 +1,17 @@
|
||||
import { useState } from 'react'
|
||||
import reactLogo from './assets/react.svg'
|
||||
import viteLogo from '/vite.svg'
|
||||
import './App.css'
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { CssBaseline, ThemeProvider } from '@mui/material';
|
||||
import { theme } from './theme';
|
||||
import AppRoutes from './routes';
|
||||
|
||||
function App() {
|
||||
const [count, setCount] = useState(0)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<a href="https://vite.dev" target="_blank">
|
||||
<img src={viteLogo} className="logo" alt="Vite logo" />
|
||||
</a>
|
||||
<a href="https://react.dev" target="_blank">
|
||||
<img src={reactLogo} className="logo react" alt="React logo" />
|
||||
</a>
|
||||
</div>
|
||||
<h1>Vite + React</h1>
|
||||
<div className="card">
|
||||
<button onClick={() => setCount((count) => count + 1)}>
|
||||
count is {count}
|
||||
</button>
|
||||
<p>
|
||||
Edit <code>src/App.tsx</code> and save to test HMR
|
||||
</p>
|
||||
</div>
|
||||
<p className="read-the-docs">
|
||||
Click on the Vite and React logos to learn more
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<BrowserRouter>
|
||||
<AppRoutes />
|
||||
</BrowserRouter>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App
|
||||
export default App;
|
||||
|
||||
73
src/components/Layout/Header.tsx
Normal file
73
src/components/Layout/Header.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { Box, InputBase, styled } from '@mui/material';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import AccountCircleIcon from '@mui/icons-material/AccountCircle';
|
||||
|
||||
const HeaderContainer = styled(Box)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '0 20px',
|
||||
height: '60px',
|
||||
backgroundColor: '#FFFFFF',
|
||||
color: '#333',
|
||||
boxShadow: '0 1px 3px rgba(0,0,0,0.1)',
|
||||
borderBottom: '1px solid #E5E5E5',
|
||||
}));
|
||||
|
||||
const BrandTitle = styled(Box)(({ theme }) => ({
|
||||
fontSize: '1.2rem',
|
||||
fontWeight: 'bold',
|
||||
color: '#333',
|
||||
}));
|
||||
|
||||
const SearchBox = styled(Box)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#F8F9FA',
|
||||
borderRadius: '6px',
|
||||
padding: '6px 12px',
|
||||
width: '320px',
|
||||
border: '1px solid #E5E5E5',
|
||||
marginLeft: 'auto',
|
||||
marginRight: '20px',
|
||||
'&:focus-within': {
|
||||
borderColor: theme.palette.primary.main,
|
||||
boxShadow: `0 0 0 2px rgba(226,0,116,0.1)`,
|
||||
},
|
||||
'& .MuiInputBase-input': {
|
||||
border: 'none',
|
||||
outline: 'none',
|
||||
backgroundColor: 'transparent',
|
||||
fontSize: '14px',
|
||||
color: '#333',
|
||||
'&::placeholder': {
|
||||
color: '#999',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const SearchInput = styled(InputBase)(({ theme }) => ({
|
||||
marginLeft: '8px',
|
||||
flex: 1,
|
||||
fontSize: '0.875rem',
|
||||
}));
|
||||
|
||||
const UserAvatar = styled(AccountCircleIcon)(({ theme }) => ({
|
||||
color: '#666',
|
||||
cursor: 'pointer',
|
||||
fontSize: '2rem',
|
||||
}));
|
||||
|
||||
const Header = () => {
|
||||
return (
|
||||
<HeaderContainer>
|
||||
<BrandTitle>RAG Dashboard</BrandTitle>
|
||||
<SearchBox>
|
||||
<SearchIcon sx={{ color: '#999', fontSize: '1.2rem' }} />
|
||||
<SearchInput placeholder="Search queries, KB names..." />
|
||||
</SearchBox>
|
||||
<UserAvatar titleAccess="User Profile" />
|
||||
</HeaderContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
39
src/components/Layout/MainLayout.tsx
Normal file
39
src/components/Layout/MainLayout.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { Box, styled } from '@mui/material';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import Header from './Header';
|
||||
import Sidebar from './Sidebar';
|
||||
|
||||
const LayoutContainer = styled(Box)({
|
||||
display: 'flex',
|
||||
height: '100vh',
|
||||
});
|
||||
|
||||
const MainContent = styled(Box)({
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'hidden',
|
||||
});
|
||||
|
||||
const ContentArea = styled(Box)({
|
||||
flex: 1,
|
||||
padding: '20px',
|
||||
overflow: 'auto',
|
||||
backgroundColor: '#f5f7fa',
|
||||
});
|
||||
|
||||
const MainLayout = () => {
|
||||
return (
|
||||
<LayoutContainer>
|
||||
<Sidebar />
|
||||
<MainContent>
|
||||
<Header />
|
||||
<ContentArea>
|
||||
<Outlet />
|
||||
</ContentArea>
|
||||
</MainContent>
|
||||
</LayoutContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default MainLayout;
|
||||
73
src/components/Layout/Sidebar.tsx
Normal file
73
src/components/Layout/Sidebar.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { Box, List, ListItem, ListItemButton, ListItemText, Typography, styled } from '@mui/material';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
|
||||
const SidebarContainer = styled(Box)(({ theme }) => ({
|
||||
width: '240px',
|
||||
backgroundColor: '#1E1E24',
|
||||
color: '#DDD',
|
||||
height: '100vh',
|
||||
padding: '1rem 0',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}));
|
||||
|
||||
const Logo = styled(Typography)(({ theme }) => ({
|
||||
fontSize: '1.05rem',
|
||||
fontWeight: 600,
|
||||
padding: '0 1.25rem 1rem',
|
||||
margin: '0 0 0.5rem',
|
||||
color: '#FFF',
|
||||
letterSpacing: '0.5px',
|
||||
}));
|
||||
|
||||
const NavItem = styled(ListItemButton)<{ active?: boolean }>(({ active, theme }) => ({
|
||||
color: active ? '#FFF' : '#B9B9C2',
|
||||
backgroundColor: active ? 'rgba(226,0,116,0.12)' : 'transparent',
|
||||
borderLeft: active ? `4px solid ${theme.palette.primary.main}` : '4px solid transparent',
|
||||
fontWeight: active ? 600 : 'normal',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(255,255,255,0.05)',
|
||||
color: '#FFF',
|
||||
},
|
||||
'& .MuiListItemText-primary': {
|
||||
fontSize: '0.9rem',
|
||||
},
|
||||
}));
|
||||
|
||||
const Footer = styled(Box)(({ theme }) => ({
|
||||
marginTop: 'auto',
|
||||
padding: '20px',
|
||||
fontSize: '0.75rem',
|
||||
opacity: 0.7,
|
||||
}));
|
||||
|
||||
const navItems = [
|
||||
{ text: 'Overview', path: '/' },
|
||||
{ text: 'Knowledge Bases', path: '/kb-list' },
|
||||
{ text: 'RAG Pipeline', path: '/pipeline-config' },
|
||||
{ text: 'Operations', path: '/dashboard' },
|
||||
{ text: 'Models & Resources', path: '/models-resources' },
|
||||
{ text: 'MCP', path: '/mcp' },
|
||||
];
|
||||
|
||||
const Sidebar = () => {
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<SidebarContainer>
|
||||
<Logo>RAGflow Prototype</Logo>
|
||||
<List>
|
||||
{navItems.map((item) => (
|
||||
<Link to={item.path} style={{ textDecoration: 'none', color: 'inherit' }}>
|
||||
<NavItem active={location.pathname === item.path}>
|
||||
<ListItemText primary={item.text} />
|
||||
</NavItem>
|
||||
</Link>
|
||||
))}
|
||||
</List>
|
||||
<Footer>© 2025 RAG Demo</Footer>
|
||||
</SidebarContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
369
src/pages/Dashboard.tsx
Normal file
369
src/pages/Dashboard.tsx
Normal file
@@ -0,0 +1,369 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Card,
|
||||
CardContent,
|
||||
Grid,
|
||||
Button,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Paper,
|
||||
Chip,
|
||||
LinearProgress,
|
||||
Select,
|
||||
MenuItem,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
TrendingUp as TrendingUpIcon,
|
||||
TrendingDown as TrendingDownIcon,
|
||||
Assessment as AssessmentIcon,
|
||||
Speed as SpeedIcon,
|
||||
Error as ErrorIcon,
|
||||
CheckCircle as CheckCircleIcon,
|
||||
} 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 MetricCard = 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 MetricValue = styled(Typography)(({ theme }) => ({
|
||||
fontSize: '2rem',
|
||||
fontWeight: 700,
|
||||
lineHeight: 1.2,
|
||||
}));
|
||||
|
||||
const TrendIndicator = styled(Box)<{ trend: 'up' | 'down' }>(({ trend, theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.25rem',
|
||||
color: trend === 'up' ? '#28A745' : '#DC3545',
|
||||
fontSize: '0.875rem',
|
||||
fontWeight: 500,
|
||||
}));
|
||||
|
||||
const StatusChip = styled(Chip)<{ status: string }>(({ status, theme }) => ({
|
||||
fontSize: '0.75rem',
|
||||
height: '24px',
|
||||
backgroundColor:
|
||||
status === 'success' ? '#E8F5E8' :
|
||||
status === 'warning' ? '#FFF3CD' :
|
||||
status === 'error' ? '#F8D7DA' : '#F8F9FA',
|
||||
color:
|
||||
status === 'success' ? '#155724' :
|
||||
status === 'warning' ? '#856404' :
|
||||
status === 'error' ? '#721C24' : '#666',
|
||||
}));
|
||||
|
||||
const mockMetrics = {
|
||||
totalQueries: { value: 12847, trend: 'up', change: '+12.5%' },
|
||||
avgResponseTime: { value: '2.3s', trend: 'down', change: '-8.2%' },
|
||||
successRate: { value: '98.7%', trend: 'up', change: '+0.3%' },
|
||||
activeUsers: { value: 1256, trend: 'up', change: '+5.7%' },
|
||||
};
|
||||
|
||||
const mockRecentQueries = [
|
||||
{
|
||||
id: 1,
|
||||
query: '如何配置RAG Pipeline的参数?',
|
||||
user: 'user@example.com',
|
||||
status: 'success',
|
||||
responseTime: '1.8s',
|
||||
timestamp: '2024-01-15 14:30:25',
|
||||
knowledgeBase: '产品文档库',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
query: '客服系统的常见问题有哪些?',
|
||||
user: 'admin@company.com',
|
||||
status: 'success',
|
||||
responseTime: '2.1s',
|
||||
timestamp: '2024-01-15 14:28:15',
|
||||
knowledgeBase: '客服FAQ',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
query: '法律合规要求的详细说明',
|
||||
user: 'legal@company.com',
|
||||
status: 'warning',
|
||||
responseTime: '4.2s',
|
||||
timestamp: '2024-01-15 14:25:10',
|
||||
knowledgeBase: '法律合规',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
query: '员工培训流程和要求',
|
||||
user: 'hr@company.com',
|
||||
status: 'error',
|
||||
responseTime: 'N/A',
|
||||
timestamp: '2024-01-15 14:22:45',
|
||||
knowledgeBase: '培训资料',
|
||||
},
|
||||
];
|
||||
|
||||
const Dashboard: React.FC = () => {
|
||||
const [timeRange, setTimeRange] = useState('24h');
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<PageHeader>
|
||||
<Typography variant="h4" fontWeight={600} color="#333">
|
||||
运营监控
|
||||
</Typography>
|
||||
<FormControl size="small" sx={{ minWidth: 120 }}>
|
||||
<InputLabel>时间范围</InputLabel>
|
||||
<Select
|
||||
value={timeRange}
|
||||
onChange={(e) => setTimeRange(e.target.value)}
|
||||
label="时间范围"
|
||||
>
|
||||
<MenuItem value="1h">最近1小时</MenuItem>
|
||||
<MenuItem value="24h">最近24小时</MenuItem>
|
||||
<MenuItem value="7d">最近7天</MenuItem>
|
||||
<MenuItem value="30d">最近30天</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</PageHeader>
|
||||
|
||||
{/* 关键指标卡片 */}
|
||||
<Grid container spacing={3} sx={{ mb: 3 }}>
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<MetricCard>
|
||||
<CardContent>
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between">
|
||||
<Box>
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
总查询数
|
||||
</Typography>
|
||||
<MetricValue color="primary">
|
||||
{mockMetrics.totalQueries.value.toLocaleString()}
|
||||
</MetricValue>
|
||||
<TrendIndicator trend={mockMetrics.totalQueries.trend}>
|
||||
{mockMetrics.totalQueries.trend === 'up' ?
|
||||
<TrendingUpIcon fontSize="small" /> :
|
||||
<TrendingDownIcon fontSize="small" />
|
||||
}
|
||||
{mockMetrics.totalQueries.change}
|
||||
</TrendIndicator>
|
||||
</Box>
|
||||
<AssessmentIcon color="primary" sx={{ fontSize: '3rem', opacity: 0.3 }} />
|
||||
</Box>
|
||||
</CardContent>
|
||||
</MetricCard>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<MetricCard>
|
||||
<CardContent>
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between">
|
||||
<Box>
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
平均响应时间
|
||||
</Typography>
|
||||
<MetricValue color="warning.main">
|
||||
{mockMetrics.avgResponseTime.value}
|
||||
</MetricValue>
|
||||
<TrendIndicator trend={mockMetrics.avgResponseTime.trend}>
|
||||
{mockMetrics.avgResponseTime.trend === 'up' ?
|
||||
<TrendingUpIcon fontSize="small" /> :
|
||||
<TrendingDownIcon fontSize="small" />
|
||||
}
|
||||
{mockMetrics.avgResponseTime.change}
|
||||
</TrendIndicator>
|
||||
</Box>
|
||||
<SpeedIcon color="warning" sx={{ fontSize: '3rem', opacity: 0.3 }} />
|
||||
</Box>
|
||||
</CardContent>
|
||||
</MetricCard>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<MetricCard>
|
||||
<CardContent>
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between">
|
||||
<Box>
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
成功率
|
||||
</Typography>
|
||||
<MetricValue color="success.main">
|
||||
{mockMetrics.successRate.value}
|
||||
</MetricValue>
|
||||
<TrendIndicator trend={mockMetrics.successRate.trend}>
|
||||
{mockMetrics.successRate.trend === 'up' ?
|
||||
<TrendingUpIcon fontSize="small" /> :
|
||||
<TrendingDownIcon fontSize="small" />
|
||||
}
|
||||
{mockMetrics.successRate.change}
|
||||
</TrendIndicator>
|
||||
</Box>
|
||||
<CheckCircleIcon color="success" sx={{ fontSize: '3rem', opacity: 0.3 }} />
|
||||
</Box>
|
||||
</CardContent>
|
||||
</MetricCard>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<MetricCard>
|
||||
<CardContent>
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between">
|
||||
<Box>
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
活跃用户
|
||||
</Typography>
|
||||
<MetricValue color="info.main">
|
||||
{mockMetrics.activeUsers.value.toLocaleString()}
|
||||
</MetricValue>
|
||||
<TrendIndicator trend={mockMetrics.activeUsers.trend}>
|
||||
{mockMetrics.activeUsers.trend === 'up' ?
|
||||
<TrendingUpIcon fontSize="small" /> :
|
||||
<TrendingDownIcon fontSize="small" />
|
||||
}
|
||||
{mockMetrics.activeUsers.change}
|
||||
</TrendIndicator>
|
||||
</Box>
|
||||
<AssessmentIcon color="info" sx={{ fontSize: '3rem', opacity: 0.3 }} />
|
||||
</Box>
|
||||
</CardContent>
|
||||
</MetricCard>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{/* 系统状态 */}
|
||||
<Grid container spacing={3} sx={{ mb: 3 }}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Card sx={{ border: '1px solid #E5E5E5' }}>
|
||||
<CardContent>
|
||||
<Typography variant="h6" fontWeight={600} mb={2}>
|
||||
系统状态
|
||||
</Typography>
|
||||
<Box mb={2}>
|
||||
<Box display="flex" justifyContent="space-between" mb={1}>
|
||||
<Typography variant="body2">CPU 使用率</Typography>
|
||||
<Typography variant="body2" fontWeight={600}>45%</Typography>
|
||||
</Box>
|
||||
<LinearProgress variant="determinate" value={45} sx={{ height: 8, borderRadius: 4 }} />
|
||||
</Box>
|
||||
<Box mb={2}>
|
||||
<Box display="flex" justifyContent="space-between" mb={1}>
|
||||
<Typography variant="body2">内存使用率</Typography>
|
||||
<Typography variant="body2" fontWeight={600}>67%</Typography>
|
||||
</Box>
|
||||
<LinearProgress variant="determinate" value={67} color="warning" sx={{ height: 8, borderRadius: 4 }} />
|
||||
</Box>
|
||||
<Box>
|
||||
<Box display="flex" justifyContent="space-between" mb={1}>
|
||||
<Typography variant="body2">存储使用率</Typography>
|
||||
<Typography variant="body2" fontWeight={600}>23%</Typography>
|
||||
</Box>
|
||||
<LinearProgress variant="determinate" value={23} color="success" sx={{ height: 8, borderRadius: 4 }} />
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={6}>
|
||||
<Card sx={{ border: '1px solid #E5E5E5' }}>
|
||||
<CardContent>
|
||||
<Typography variant="h6" fontWeight={600} mb={2}>
|
||||
知识库状态
|
||||
</Typography>
|
||||
<Box display="flex" flexDirection="column" gap={1}>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||
<Typography variant="body2">产品文档库</Typography>
|
||||
<StatusChip status="success" label="正常" size="small" />
|
||||
</Box>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||
<Typography variant="body2">客服FAQ</Typography>
|
||||
<StatusChip status="success" label="正常" size="small" />
|
||||
</Box>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||
<Typography variant="body2">法律合规</Typography>
|
||||
<StatusChip status="warning" label="同步中" size="small" />
|
||||
</Box>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||
<Typography variant="body2">培训资料</Typography>
|
||||
<StatusChip status="error" label="错误" size="small" />
|
||||
</Box>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</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>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{mockRecentQueries.map((query) => (
|
||||
<TableRow key={query.id} hover>
|
||||
<TableCell>
|
||||
<Typography variant="body2" sx={{ maxWidth: '300px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
{query.query}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>{query.user}</TableCell>
|
||||
<TableCell>{query.knowledgeBase}</TableCell>
|
||||
<TableCell>
|
||||
<StatusChip status={query.status} label={
|
||||
query.status === 'success' ? '成功' :
|
||||
query.status === 'warning' ? '警告' : '失败'
|
||||
} size="small" />
|
||||
</TableCell>
|
||||
<TableCell>{query.responseTime}</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{query.timestamp}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
218
src/pages/Home.tsx
Normal file
218
src/pages/Home.tsx
Normal file
@@ -0,0 +1,218 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
Grid,
|
||||
LinearProgress,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Typography,
|
||||
styled
|
||||
} from '@mui/material';
|
||||
|
||||
const StyledCard = styled(Card)({
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
|
||||
borderRadius: '8px',
|
||||
});
|
||||
|
||||
const CardTitle = styled(Typography)({
|
||||
fontSize: '1rem',
|
||||
fontWeight: 'bold',
|
||||
marginBottom: '16px',
|
||||
});
|
||||
|
||||
const MetricsContainer = styled(Box)({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: '16px',
|
||||
});
|
||||
|
||||
const Metric = styled(Box)({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
});
|
||||
|
||||
const MetricValue = styled(Typography)({
|
||||
fontSize: '1.5rem',
|
||||
fontWeight: 'bold',
|
||||
});
|
||||
|
||||
const MetricLabel = styled(Typography)({
|
||||
fontSize: '0.75rem',
|
||||
color: '#666',
|
||||
});
|
||||
|
||||
const ProgressContainer = styled(Box)({
|
||||
marginBottom: '16px',
|
||||
});
|
||||
|
||||
const ProgressLabel = styled(Box)({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
fontSize: '0.65rem',
|
||||
marginBottom: '4px',
|
||||
});
|
||||
|
||||
const StyledLinearProgress = styled(LinearProgress)({
|
||||
height: '8px',
|
||||
borderRadius: '4px',
|
||||
});
|
||||
|
||||
const StatusPill = styled(Box)<{ status?: string }>(({ status }) => ({
|
||||
display: 'inline-block',
|
||||
padding: '4px 8px',
|
||||
borderRadius: '12px',
|
||||
fontSize: '0.65rem',
|
||||
fontWeight: 'bold',
|
||||
backgroundColor: status === 'OK' ? '#e6f7ed' : '#ffebee',
|
||||
color: status === 'OK' ? '#00a389' : '#d32f2f',
|
||||
}));
|
||||
|
||||
const InlineNote = styled('span')({
|
||||
fontSize: '0.75rem',
|
||||
color: '#666',
|
||||
fontWeight: 'normal',
|
||||
marginLeft: '4px',
|
||||
});
|
||||
|
||||
// 模拟数据
|
||||
const recentQueries = [
|
||||
{ query: 'How to reset device firmware?', latency: 732, source: 'manual.pdf', time: '09:21', status: 'OK' },
|
||||
{ query: 'List authentication failure codes', latency: 801, source: 'auth_guide.html', time: '09:18', status: 'OK' },
|
||||
{ query: 'Can we purge stale vectors?', latency: 915, source: 'system_kb', time: '09:10', status: 'OK' },
|
||||
{ query: 'Explain retrieval scoring logic', latency: 845, source: 'design_notes', time: '08:57', status: 'OK' },
|
||||
{ query: 'Pipeline concurrency limits?', latency: 1042, source: 'ops_doc', time: '08:43', status: 'OK' },
|
||||
];
|
||||
|
||||
const Home = () => {
|
||||
return (
|
||||
<Box>
|
||||
<Grid container spacing={3}>
|
||||
{/* Knowledge Base Status Card */}
|
||||
<Grid size={{ xs: 12, md: 6, lg: 4 }}>
|
||||
<StyledCard>
|
||||
<CardContent>
|
||||
<CardTitle>Knowledge Base Status</CardTitle>
|
||||
<MetricsContainer>
|
||||
<Metric>
|
||||
<MetricLabel>Documents</MetricLabel>
|
||||
<MetricValue>4,218</MetricValue>
|
||||
</Metric>
|
||||
<Metric>
|
||||
<MetricLabel>Sources</MetricLabel>
|
||||
<MetricValue>17</MetricValue>
|
||||
</Metric>
|
||||
<Metric>
|
||||
<MetricLabel>Vectors</MetricLabel>
|
||||
<MetricValue>1.2M</MetricValue>
|
||||
</Metric>
|
||||
</MetricsContainer>
|
||||
|
||||
<ProgressContainer>
|
||||
<ProgressLabel>
|
||||
<span>Sync Progress</span>
|
||||
<span>62%</span>
|
||||
</ProgressLabel>
|
||||
<StyledLinearProgress variant="determinate" value={62} />
|
||||
</ProgressContainer>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
fullWidth
|
||||
sx={{
|
||||
backgroundColor: '#1a1a2e',
|
||||
'&:hover': { backgroundColor: '#2a2a3e' }
|
||||
}}
|
||||
>
|
||||
Create New Knowledge Base
|
||||
</Button>
|
||||
</CardContent>
|
||||
</StyledCard>
|
||||
</Grid>
|
||||
|
||||
{/* Recent Activity Card */}
|
||||
<Grid size={{ xs: 12, md: 6, lg: 4 }}>
|
||||
<StyledCard>
|
||||
<CardContent>
|
||||
<CardTitle>
|
||||
Recent Activity <InlineNote>(latest 24h)</InlineNote>
|
||||
</CardTitle>
|
||||
<Box sx={{ fontSize: '0.7rem', lineHeight: 1.8 }}>
|
||||
<Box>152 user queries processed</Box>
|
||||
<Box>87 new documents ingested</Box>
|
||||
<Box>4 pipeline adjustments</Box>
|
||||
</Box>
|
||||
<Box sx={{ marginTop: 'auto', fontSize: '0.65rem', opacity: 0.75, mt: 2 }}>
|
||||
Latency stable at p95 820ms
|
||||
</Box>
|
||||
</CardContent>
|
||||
</StyledCard>
|
||||
</Grid>
|
||||
|
||||
{/* Model Overview Card */}
|
||||
<Grid size={{ xs: 12, md: 6, lg: 4 }}>
|
||||
<StyledCard>
|
||||
<CardContent>
|
||||
<CardTitle>Model Overview</CardTitle>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '0.4rem', fontSize: '0.7rem' }}>
|
||||
<Box>Embedding Model: <strong>text-embedding-3-large</strong></Box>
|
||||
<Box>Generator: <strong>gpt-4o-mini</strong></Box>
|
||||
<Box>Reranker: <strong>cross-encoder-v2</strong></Box>
|
||||
<Box>Chunking: 512 tokens</Box>
|
||||
<Box>Retriever Top-K: 8</Box>
|
||||
</Box>
|
||||
<StatusPill status="OK" sx={{ mt: 1 }}>Healthy</StatusPill>
|
||||
</CardContent>
|
||||
</StyledCard>
|
||||
</Grid>
|
||||
|
||||
{/* Recent RAG Queries Table */}
|
||||
<Grid size={12}>
|
||||
<StyledCard>
|
||||
<CardContent>
|
||||
<CardTitle>
|
||||
Recent RAG Queries <InlineNote>(latest 5)</InlineNote>
|
||||
</CardTitle>
|
||||
<TableContainer>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Query</TableCell>
|
||||
<TableCell>Latency (ms)</TableCell>
|
||||
<TableCell>Source</TableCell>
|
||||
<TableCell>Time</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{recentQueries.map((row, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell>{row.query}</TableCell>
|
||||
<TableCell>{row.latency}</TableCell>
|
||||
<TableCell>{row.source}</TableCell>
|
||||
<TableCell>{row.time}</TableCell>
|
||||
<TableCell>
|
||||
<StatusPill status={row.status}>{row.status}</StatusPill>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</CardContent>
|
||||
</StyledCard>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
||||
265
src/pages/KnowledgeBaseList.tsx
Normal file
265
src/pages/KnowledgeBaseList.tsx
Normal file
@@ -0,0 +1,265 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
Grid,
|
||||
Chip,
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuItem,
|
||||
TextField,
|
||||
InputAdornment,
|
||||
Fab,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Search as SearchIcon,
|
||||
Add as AddIcon,
|
||||
MoreVert as MoreVertIcon,
|
||||
Folder as FolderIcon,
|
||||
Description as DocumentIcon,
|
||||
} 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 SearchContainer = styled(Box)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
gap: '1rem',
|
||||
marginBottom: '1.5rem',
|
||||
}));
|
||||
|
||||
const KBCard = styled(Card)(({ theme }) => ({
|
||||
height: '100%',
|
||||
transition: 'all 0.2s ease-in-out',
|
||||
border: '1px solid #E5E5E5',
|
||||
'&:hover': {
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
|
||||
transform: 'translateY(-2px)',
|
||||
},
|
||||
}));
|
||||
|
||||
const StatusChip = styled(Chip)<{ status: string }>(({ status, theme }) => ({
|
||||
fontSize: '0.75rem',
|
||||
height: '24px',
|
||||
backgroundColor:
|
||||
status === 'active' ? '#E8F5E8' :
|
||||
status === 'processing' ? '#FFF3CD' : '#F8D7DA',
|
||||
color:
|
||||
status === 'active' ? '#155724' :
|
||||
status === 'processing' ? '#856404' : '#721C24',
|
||||
}));
|
||||
|
||||
const StatsBox = styled(Box)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
marginTop: '1rem',
|
||||
padding: '0.75rem',
|
||||
backgroundColor: '#F8F9FA',
|
||||
borderRadius: '6px',
|
||||
}));
|
||||
|
||||
const StatItem = styled(Box)(({ theme }) => ({
|
||||
textAlign: 'center',
|
||||
'& .number': {
|
||||
fontSize: '1.25rem',
|
||||
fontWeight: 600,
|
||||
color: theme.palette.primary.main,
|
||||
},
|
||||
'& .label': {
|
||||
fontSize: '0.75rem',
|
||||
color: '#666',
|
||||
marginTop: '0.25rem',
|
||||
},
|
||||
}));
|
||||
|
||||
const mockKnowledgeBases = [
|
||||
{
|
||||
id: 1,
|
||||
name: '产品文档库',
|
||||
description: '包含所有产品相关的技术文档和用户手册',
|
||||
status: 'active',
|
||||
documents: 156,
|
||||
size: '2.3 GB',
|
||||
lastUpdated: '2024-01-15',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '客服FAQ',
|
||||
description: '常见问题解答和客服对话记录',
|
||||
status: 'processing',
|
||||
documents: 89,
|
||||
size: '1.1 GB',
|
||||
lastUpdated: '2024-01-14',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '法律合规',
|
||||
description: '法律条文、合规要求和相关政策文档',
|
||||
status: 'active',
|
||||
documents: 234,
|
||||
size: '3.7 GB',
|
||||
lastUpdated: '2024-01-13',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '培训资料',
|
||||
description: '员工培训材料和学习资源',
|
||||
status: 'inactive',
|
||||
documents: 67,
|
||||
size: '890 MB',
|
||||
lastUpdated: '2024-01-10',
|
||||
},
|
||||
];
|
||||
|
||||
const KnowledgeBaseList: React.FC = () => {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [selectedKB, setSelectedKB] = useState<number | null>(null);
|
||||
|
||||
const handleMenuClick = (event: React.MouseEvent<HTMLElement>, kbId: number) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
setSelectedKB(kbId);
|
||||
};
|
||||
|
||||
const handleMenuClose = () => {
|
||||
setAnchorEl(null);
|
||||
setSelectedKB(null);
|
||||
};
|
||||
|
||||
const filteredKBs = mockKnowledgeBases.filter(kb =>
|
||||
kb.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
kb.description.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>
|
||||
|
||||
<SearchContainer>
|
||||
<TextField
|
||||
placeholder="搜索知识库..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon color="action" />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
sx={{ width: '400px' }}
|
||||
/>
|
||||
</SearchContainer>
|
||||
|
||||
<Grid container spacing={3}>
|
||||
{filteredKBs.map((kb) => (
|
||||
<Grid key={kb.id} size={{xs:12, sm:6, md:4}}>
|
||||
<KBCard>
|
||||
<CardContent>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="flex-start">
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<FolderIcon color="primary" />
|
||||
<Typography variant="h6" fontWeight={600}>
|
||||
{kb.name}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<StatusChip
|
||||
status={kb.status}
|
||||
label={
|
||||
kb.status === 'active' ? '活跃' :
|
||||
kb.status === 'processing' ? '处理中' : '未激活'
|
||||
}
|
||||
size="small"
|
||||
/>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={(e) => handleMenuClick(e, kb.id)}
|
||||
>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
sx={{ mt: 1, mb: 2 }}
|
||||
>
|
||||
{kb.description}
|
||||
</Typography>
|
||||
|
||||
<StatsBox>
|
||||
<StatItem>
|
||||
<div className="number">{kb.documents}</div>
|
||||
<div className="label">文档数量</div>
|
||||
</StatItem>
|
||||
<StatItem>
|
||||
<div className="number">{kb.size}</div>
|
||||
<div className="label">存储大小</div>
|
||||
</StatItem>
|
||||
</StatsBox>
|
||||
|
||||
<Typography variant="caption" color="text.secondary" sx={{ mt: 1, display: 'block' }}>
|
||||
最后更新: {kb.lastUpdated}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</KBCard>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={handleMenuClose}
|
||||
>
|
||||
<MenuItem onClick={handleMenuClose}>编辑</MenuItem>
|
||||
<MenuItem onClick={handleMenuClose}>查看详情</MenuItem>
|
||||
<MenuItem onClick={handleMenuClose}>导出</MenuItem>
|
||||
<MenuItem onClick={handleMenuClose} sx={{ color: 'error.main' }}>
|
||||
删除
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
|
||||
<Fab
|
||||
color="primary"
|
||||
aria-label="add"
|
||||
sx={{
|
||||
position: 'fixed',
|
||||
bottom: 24,
|
||||
right: 24,
|
||||
}}
|
||||
>
|
||||
<AddIcon />
|
||||
</Fab>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default KnowledgeBaseList;
|
||||
262
src/pages/Login.tsx
Normal file
262
src/pages/Login.tsx
Normal file
@@ -0,0 +1,262 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Checkbox,
|
||||
Container,
|
||||
FormControlLabel,
|
||||
TextField,
|
||||
Typography,
|
||||
Link,
|
||||
styled
|
||||
} from '@mui/material';
|
||||
|
||||
const LoginContainer = styled(Box)({
|
||||
height: '100vh',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
backgroundColor: '#f5f7fa',
|
||||
});
|
||||
|
||||
const TopBar = styled(Box)({
|
||||
height: '60px',
|
||||
backgroundColor: '#1a1a2e',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '0 20px',
|
||||
});
|
||||
|
||||
const BrandLogo = styled(Box)({
|
||||
width: '40px',
|
||||
height: '40px',
|
||||
backgroundColor: '#fff',
|
||||
color: '#1a1a2e',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: '50%',
|
||||
fontWeight: 'bold',
|
||||
fontSize: '1.2rem',
|
||||
});
|
||||
|
||||
const LoginMain = styled(Box)({
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
});
|
||||
|
||||
const LoginCard = styled(Box)({
|
||||
width: '400px',
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
|
||||
padding: '30px',
|
||||
});
|
||||
|
||||
const ServiceName = styled(Typography)({
|
||||
fontSize: '0.75rem',
|
||||
color: '#666',
|
||||
marginBottom: '10px',
|
||||
});
|
||||
|
||||
const LoginTitle = styled(Typography)({
|
||||
fontSize: '1.5rem',
|
||||
fontWeight: 'bold',
|
||||
marginBottom: '20px',
|
||||
});
|
||||
|
||||
const LoginForm = styled('form')({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '20px',
|
||||
});
|
||||
|
||||
const RememberRow = styled(Box)({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
});
|
||||
|
||||
const ActionButtons = styled(Box)({
|
||||
display: 'flex',
|
||||
gap: '10px',
|
||||
marginTop: '10px',
|
||||
});
|
||||
|
||||
const PrimaryButton = styled(Button)({
|
||||
backgroundColor: '#e20074',
|
||||
'&:hover': {
|
||||
backgroundColor: '#c10062',
|
||||
},
|
||||
});
|
||||
|
||||
const SecondaryButton = styled(Button)({
|
||||
color: '#333',
|
||||
backgroundColor: '#f5f5f5',
|
||||
'&:hover': {
|
||||
backgroundColor: '#e0e0e0',
|
||||
},
|
||||
});
|
||||
|
||||
const HelpSection = styled(Box)({
|
||||
marginTop: '20px',
|
||||
textAlign: 'center',
|
||||
fontSize: '0.875rem',
|
||||
});
|
||||
|
||||
const SocialLogin = styled(Box)({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
gap: '10px',
|
||||
marginTop: '15px',
|
||||
});
|
||||
|
||||
const SocialButton = styled(Box)({
|
||||
width: '30px',
|
||||
height: '30px',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: '#e0e0e0',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
});
|
||||
|
||||
const Footer = styled(Box)({
|
||||
height: '50px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '0 20px',
|
||||
borderTop: '1px solid #eee',
|
||||
fontSize: '0.75rem',
|
||||
color: '#666',
|
||||
});
|
||||
|
||||
const LegalLinks = styled(Box)({
|
||||
display: 'flex',
|
||||
gap: '15px',
|
||||
});
|
||||
|
||||
const Login = () => {
|
||||
const [username, setUsername] = useState('');
|
||||
const [rememberUser, setRememberUser] = useState(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [error, setError] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!username.trim()) {
|
||||
setError(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
setError(false);
|
||||
|
||||
// 模拟登录过程
|
||||
setTimeout(() => {
|
||||
navigate('/');
|
||||
}, 800);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
navigate('/');
|
||||
};
|
||||
|
||||
return (
|
||||
<LoginContainer>
|
||||
<TopBar>
|
||||
<BrandLogo>T</BrandLogo>
|
||||
</TopBar>
|
||||
|
||||
<LoginMain>
|
||||
<LoginCard>
|
||||
<ServiceName>Servicename</ServiceName>
|
||||
<LoginTitle>
|
||||
Enter Login <br/> Username
|
||||
</LoginTitle>
|
||||
|
||||
<LoginForm onSubmit={handleSubmit} noValidate>
|
||||
<TextField
|
||||
fullWidth
|
||||
id="username"
|
||||
name="username"
|
||||
placeholder="Username"
|
||||
autoComplete="username"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
error={error}
|
||||
required
|
||||
/>
|
||||
|
||||
<RememberRow>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={rememberUser}
|
||||
onChange={(e) => setRememberUser(e.target.checked)}
|
||||
id="rememberUser"
|
||||
/>
|
||||
}
|
||||
label="Remember username"
|
||||
/>
|
||||
<Link href="#" variant="caption">
|
||||
Forgot your username or password?
|
||||
</Link>
|
||||
</RememberRow>
|
||||
|
||||
<ActionButtons>
|
||||
<PrimaryButton
|
||||
type="submit"
|
||||
variant="contained"
|
||||
fullWidth
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? 'Processing...' : 'Next'}
|
||||
</PrimaryButton>
|
||||
<SecondaryButton
|
||||
type="button"
|
||||
variant="contained"
|
||||
fullWidth
|
||||
onClick={handleCancel}
|
||||
>
|
||||
Cancel
|
||||
</SecondaryButton>
|
||||
</ActionButtons>
|
||||
</LoginForm>
|
||||
|
||||
<HelpSection>
|
||||
<Link href="#">Do you need help?</Link>
|
||||
<Box mt={1}>
|
||||
No account? <Link href="#">Sign up</Link> or log in with your social network account.
|
||||
</Box>
|
||||
</HelpSection>
|
||||
|
||||
<SocialLogin>
|
||||
<SocialButton aria-label="Login with Facebook">
|
||||
<Link href="#" underline="none" color="inherit">f</Link>
|
||||
</SocialButton>
|
||||
<SocialButton aria-label="Login with Twitter">
|
||||
<Link href="#" underline="none" color="inherit">t</Link>
|
||||
</SocialButton>
|
||||
</SocialLogin>
|
||||
</LoginCard>
|
||||
</LoginMain>
|
||||
|
||||
<Footer>
|
||||
<Box>© Deutsche Telekom AG</Box>
|
||||
<LegalLinks>
|
||||
<Link href="#" color="inherit">Imprint</Link>
|
||||
<Link href="#" color="inherit">Data privacy</Link>
|
||||
</LegalLinks>
|
||||
</Footer>
|
||||
</LoginContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
516
src/pages/MCP.tsx
Normal file
516
src/pages/MCP.tsx
Normal file
@@ -0,0 +1,516 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Card,
|
||||
CardContent,
|
||||
Grid,
|
||||
Button,
|
||||
Switch,
|
||||
FormControlLabel,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Chip,
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuItem,
|
||||
TextField,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Alert,
|
||||
Tabs,
|
||||
Tab,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Add as AddIcon,
|
||||
MoreVert as MoreVertIcon,
|
||||
Settings as SettingsIcon,
|
||||
Link as LinkIcon,
|
||||
Security as SecurityIcon,
|
||||
Speed as SpeedIcon,
|
||||
CheckCircle as CheckCircleIcon,
|
||||
Error as ErrorIcon,
|
||||
} 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 StatusCard = 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 === 'connected' ? '#E8F5E8' :
|
||||
status === 'connecting' ? '#FFF3CD' :
|
||||
status === 'error' ? '#F8D7DA' : '#F8F9FA',
|
||||
color:
|
||||
status === 'connected' ? '#155724' :
|
||||
status === 'connecting' ? '#856404' :
|
||||
status === 'error' ? '#721C24' : '#666',
|
||||
}));
|
||||
|
||||
const mockMCPServers = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'File System Server',
|
||||
description: '文件系统操作服务器',
|
||||
url: 'mcp://localhost:3001',
|
||||
status: 'connected',
|
||||
version: '1.0.0',
|
||||
tools: ['read_file', 'write_file', 'list_directory'],
|
||||
lastPing: '2024-01-15 14:30:25',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Database Server',
|
||||
description: '数据库查询服务器',
|
||||
url: 'mcp://db.example.com:3002',
|
||||
status: 'connected',
|
||||
version: '1.2.1',
|
||||
tools: ['query_sql', 'execute_sql', 'get_schema'],
|
||||
lastPing: '2024-01-15 14:30:20',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Web Scraper',
|
||||
description: '网页抓取服务器',
|
||||
url: 'mcp://scraper.example.com:3003',
|
||||
status: 'connecting',
|
||||
version: '0.9.5',
|
||||
tools: ['scrape_url', 'extract_text', 'get_links'],
|
||||
lastPing: '2024-01-15 14:28:15',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'API Gateway',
|
||||
description: 'API网关服务器',
|
||||
url: 'mcp://api.example.com:3004',
|
||||
status: 'error',
|
||||
version: '2.1.0',
|
||||
tools: ['call_api', 'auth_token', 'rate_limit'],
|
||||
lastPing: '2024-01-15 14:25:10',
|
||||
},
|
||||
];
|
||||
|
||||
const MCP: React.FC = () => {
|
||||
const [tabValue, setTabValue] = useState(0);
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [selectedServer, setSelectedServer] = useState<number | null>(null);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [newServer, setNewServer] = useState({
|
||||
name: '',
|
||||
url: '',
|
||||
description: '',
|
||||
});
|
||||
|
||||
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
|
||||
setTabValue(newValue);
|
||||
};
|
||||
|
||||
const handleMenuClick = (event: React.MouseEvent<HTMLElement>, serverId: number) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
setSelectedServer(serverId);
|
||||
};
|
||||
|
||||
const handleMenuClose = () => {
|
||||
setAnchorEl(null);
|
||||
setSelectedServer(null);
|
||||
};
|
||||
|
||||
const handleAddServer = () => {
|
||||
setDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleDialogClose = () => {
|
||||
setDialogOpen(false);
|
||||
setNewServer({ name: '', url: '', description: '' });
|
||||
};
|
||||
|
||||
const handleSaveServer = () => {
|
||||
// 保存服务器逻辑
|
||||
console.log('添加服务器:', newServer);
|
||||
handleDialogClose();
|
||||
};
|
||||
|
||||
const connectedServers = mockMCPServers.filter(s => s.status === 'connected').length;
|
||||
const totalTools = mockMCPServers.reduce((acc, server) => acc + server.tools.length, 0);
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<PageHeader>
|
||||
<Box>
|
||||
<Typography variant="h4" fontWeight={600} color="#333">
|
||||
MCP 协议管理
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.5 }}>
|
||||
Model Context Protocol - 模型上下文协议服务器管理
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<AddIcon />}
|
||||
onClick={handleAddServer}
|
||||
sx={{ borderRadius: '6px' }}
|
||||
>
|
||||
添加服务器
|
||||
</Button>
|
||||
</PageHeader>
|
||||
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 3 }}>
|
||||
<Tabs value={tabValue} onChange={handleTabChange}>
|
||||
<Tab label="服务器管理" />
|
||||
<Tab label="工具配置" />
|
||||
<Tab label="协议监控" />
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
{tabValue === 0 && (
|
||||
<>
|
||||
{/* 状态概览 */}
|
||||
<Grid container spacing={3} sx={{ mb: 3 }}>
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<StatusCard>
|
||||
<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">
|
||||
{connectedServers}
|
||||
</Typography>
|
||||
</Box>
|
||||
<CheckCircleIcon color="success" sx={{ fontSize: '3rem', opacity: 0.3 }} />
|
||||
</Box>
|
||||
</CardContent>
|
||||
</StatusCard>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<StatusCard>
|
||||
<CardContent>
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between">
|
||||
<Box>
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
总服务器数
|
||||
</Typography>
|
||||
<Typography variant="h4" fontWeight={600} color="primary">
|
||||
{mockMCPServers.length}
|
||||
</Typography>
|
||||
</Box>
|
||||
<LinkIcon color="primary" sx={{ fontSize: '3rem', opacity: 0.3 }} />
|
||||
</Box>
|
||||
</CardContent>
|
||||
</StatusCard>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<StatusCard>
|
||||
<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">
|
||||
{totalTools}
|
||||
</Typography>
|
||||
</Box>
|
||||
<SettingsIcon color="info" sx={{ fontSize: '3rem', opacity: 0.3 }} />
|
||||
</Box>
|
||||
</CardContent>
|
||||
</StatusCard>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<StatusCard>
|
||||
<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">
|
||||
45ms
|
||||
</Typography>
|
||||
</Box>
|
||||
<SpeedIcon color="warning" sx={{ fontSize: '3rem', opacity: 0.3 }} />
|
||||
</Box>
|
||||
</CardContent>
|
||||
</StatusCard>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{/* 服务器列表 */}
|
||||
<Card sx={{ border: '1px solid #E5E5E5' }}>
|
||||
<CardContent>
|
||||
<Typography variant="h6" fontWeight={600} mb={2}>
|
||||
MCP 服务器列表
|
||||
</Typography>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>服务器名称</TableCell>
|
||||
<TableCell>URL</TableCell>
|
||||
<TableCell>状态</TableCell>
|
||||
<TableCell>版本</TableCell>
|
||||
<TableCell>工具数量</TableCell>
|
||||
<TableCell>最后心跳</TableCell>
|
||||
<TableCell>操作</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{mockMCPServers.map((server) => (
|
||||
<TableRow key={server.id} hover>
|
||||
<TableCell>
|
||||
<Box>
|
||||
<Typography variant="body2" fontWeight={600}>
|
||||
{server.name}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{server.description}
|
||||
</Typography>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2" fontFamily="monospace">
|
||||
{server.url}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<StatusChip
|
||||
status={server.status}
|
||||
label={
|
||||
server.status === 'connected' ? '已连接' :
|
||||
server.status === 'connecting' ? '连接中' : '错误'
|
||||
}
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={server.version}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{server.tools.length} 个工具
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{server.lastPing}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={(e) => handleMenuClick(e, server.id)}
|
||||
>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
|
||||
{tabValue === 1 && (
|
||||
<Card sx={{ border: '1px solid #E5E5E5' }}>
|
||||
<CardContent>
|
||||
<Typography variant="h6" fontWeight={600} mb={2}>
|
||||
工具配置
|
||||
</Typography>
|
||||
<Alert severity="info" sx={{ mb: 2 }}>
|
||||
配置各个 MCP 服务器提供的工具和权限设置
|
||||
</Alert>
|
||||
<Grid container spacing={2}>
|
||||
{mockMCPServers.map((server) => (
|
||||
<Grid item xs={12} md={6} key={server.id}>
|
||||
<Card variant="outlined">
|
||||
<CardContent>
|
||||
<Typography variant="subtitle1" fontWeight={600} mb={1}>
|
||||
{server.name}
|
||||
</Typography>
|
||||
<Box display="flex" flexDirection="column" gap={1}>
|
||||
{server.tools.map((tool, index) => (
|
||||
<FormControlLabel
|
||||
key={index}
|
||||
control={<Switch defaultChecked size="small" />}
|
||||
label={
|
||||
<Typography variant="body2" fontFamily="monospace">
|
||||
{tool}
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{tabValue === 2 && (
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Card sx={{ border: '1px solid #E5E5E5' }}>
|
||||
<CardContent>
|
||||
<Typography variant="h6" fontWeight={600} mb={2}>
|
||||
<SecurityIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
|
||||
安全监控
|
||||
</Typography>
|
||||
<Box display="flex" flexDirection="column" gap={2}>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||
<Typography variant="body2">SSL/TLS 加密</Typography>
|
||||
<CheckCircleIcon color="success" />
|
||||
</Box>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||
<Typography variant="body2">身份验证</Typography>
|
||||
<CheckCircleIcon color="success" />
|
||||
</Box>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||
<Typography variant="body2">访问控制</Typography>
|
||||
<CheckCircleIcon color="success" />
|
||||
</Box>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||
<Typography variant="body2">审计日志</Typography>
|
||||
<ErrorIcon color="error" />
|
||||
</Box>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={6}>
|
||||
<Card sx={{ border: '1px solid #E5E5E5' }}>
|
||||
<CardContent>
|
||||
<Typography variant="h6" fontWeight={600} mb={2}>
|
||||
<SpeedIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
|
||||
性能监控
|
||||
</Typography>
|
||||
<Box display="flex" flexDirection="column" gap={2}>
|
||||
<Box>
|
||||
<Box display="flex" justifyContent="space-between" mb={1}>
|
||||
<Typography variant="body2">平均响应时间</Typography>
|
||||
<Typography variant="body2" fontWeight={600}>45ms</Typography>
|
||||
</Box>
|
||||
<Box bgcolor="#F8F9FA" p={1} borderRadius="4px">
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
过去24小时内的平均值
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box>
|
||||
<Box display="flex" justifyContent="space-between" mb={1}>
|
||||
<Typography variant="body2">成功率</Typography>
|
||||
<Typography variant="body2" fontWeight={600} color="success.main">
|
||||
99.2%
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box bgcolor="#F8F9FA" p={1} borderRadius="4px">
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
过去24小时内的成功率
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{/* 菜单 */}
|
||||
<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>
|
||||
|
||||
{/* 添加服务器对话框 */}
|
||||
<Dialog open={dialogOpen} onClose={handleDialogClose} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>添加 MCP 服务器</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box display="flex" flexDirection="column" gap={2} pt={1}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="服务器名称"
|
||||
value={newServer.name}
|
||||
onChange={(e) => setNewServer(prev => ({ ...prev, name: e.target.value }))}
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="服务器 URL"
|
||||
placeholder="mcp://localhost:3000"
|
||||
value={newServer.url}
|
||||
onChange={(e) => setNewServer(prev => ({ ...prev, url: e.target.value }))}
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="描述"
|
||||
multiline
|
||||
rows={3}
|
||||
value={newServer.description}
|
||||
onChange={(e) => setNewServer(prev => ({ ...prev, description: e.target.value }))}
|
||||
/>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleDialogClose}>取消</Button>
|
||||
<Button onClick={handleSaveServer} variant="contained">
|
||||
添加
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default MCP;
|
||||
540
src/pages/ModelsResources.tsx
Normal file
540
src/pages/ModelsResources.tsx
Normal file
@@ -0,0 +1,540 @@
|
||||
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 item 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 item 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 item 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 item 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 item 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 item 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 item 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;
|
||||
349
src/pages/PipelineConfig.tsx
Normal file
349
src/pages/PipelineConfig.tsx
Normal file
@@ -0,0 +1,349 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Card,
|
||||
CardContent,
|
||||
Grid,
|
||||
Button,
|
||||
Switch,
|
||||
FormControlLabel,
|
||||
Slider,
|
||||
TextField,
|
||||
Select,
|
||||
MenuItem,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Chip,
|
||||
Divider,
|
||||
Alert,
|
||||
LinearProgress,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Settings as SettingsIcon,
|
||||
PlayArrow as PlayIcon,
|
||||
Stop as StopIcon,
|
||||
Refresh as RefreshIcon,
|
||||
Save as SaveIcon,
|
||||
} 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 ConfigCard = styled(Card)(({ theme }) => ({
|
||||
marginBottom: '1.5rem',
|
||||
border: '1px solid #E5E5E5',
|
||||
}));
|
||||
|
||||
const ConfigSection = styled(Box)(({ theme }) => ({
|
||||
marginBottom: '1.5rem',
|
||||
'&:last-child': {
|
||||
marginBottom: 0,
|
||||
},
|
||||
}));
|
||||
|
||||
const StatusIndicator = styled(Box)<{ status: 'running' | 'stopped' | 'error' }>(({ status, theme }) => ({
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.5rem',
|
||||
padding: '0.25rem 0.75rem',
|
||||
borderRadius: '12px',
|
||||
fontSize: '0.875rem',
|
||||
fontWeight: 500,
|
||||
backgroundColor:
|
||||
status === 'running' ? '#E8F5E8' :
|
||||
status === 'stopped' ? '#F8F9FA' : '#F8D7DA',
|
||||
color:
|
||||
status === 'running' ? '#155724' :
|
||||
status === 'stopped' ? '#666' : '#721C24',
|
||||
'&::before': {
|
||||
content: '""',
|
||||
width: '8px',
|
||||
height: '8px',
|
||||
borderRadius: '50%',
|
||||
backgroundColor:
|
||||
status === 'running' ? '#28A745' :
|
||||
status === 'stopped' ? '#6C757D' : '#DC3545',
|
||||
},
|
||||
}));
|
||||
|
||||
const PipelineConfig: React.FC = () => {
|
||||
const [pipelineStatus, setPipelineStatus] = useState<'running' | 'stopped' | 'error'>('stopped');
|
||||
const [config, setConfig] = useState({
|
||||
enabled: false,
|
||||
chunkSize: 512,
|
||||
chunkOverlap: 50,
|
||||
embeddingModel: 'text-embedding-ada-002',
|
||||
retrievalTopK: 5,
|
||||
temperature: 0.7,
|
||||
maxTokens: 2048,
|
||||
systemPrompt: '你是一个专业的AI助手,请基于提供的知识库内容回答用户问题。',
|
||||
});
|
||||
|
||||
const handleConfigChange = (key: string, value: any) => {
|
||||
setConfig(prev => ({ ...prev, [key]: value }));
|
||||
};
|
||||
|
||||
const handleStartPipeline = () => {
|
||||
setPipelineStatus('running');
|
||||
};
|
||||
|
||||
const handleStopPipeline = () => {
|
||||
setPipelineStatus('stopped');
|
||||
};
|
||||
|
||||
const handleSaveConfig = () => {
|
||||
// 保存配置逻辑
|
||||
console.log('保存配置:', config);
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<PageHeader>
|
||||
<Box>
|
||||
<Typography variant="h4" fontWeight={600} color="#333">
|
||||
RAG Pipeline 配置
|
||||
</Typography>
|
||||
<Box display="flex" alignItems="center" gap={2} mt={1}>
|
||||
<StatusIndicator status={pipelineStatus}>
|
||||
{pipelineStatus === 'running' ? '运行中' :
|
||||
pipelineStatus === 'stopped' ? '已停止' : '错误'}
|
||||
</StatusIndicator>
|
||||
{pipelineStatus === 'running' && (
|
||||
<LinearProgress sx={{ width: '200px', height: '6px', borderRadius: '3px' }} />
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box display="flex" gap={1}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<SaveIcon />}
|
||||
onClick={handleSaveConfig}
|
||||
>
|
||||
保存配置
|
||||
</Button>
|
||||
{pipelineStatus === 'running' ? (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="error"
|
||||
startIcon={<StopIcon />}
|
||||
onClick={handleStopPipeline}
|
||||
>
|
||||
停止
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<PlayIcon />}
|
||||
onClick={handleStartPipeline}
|
||||
>
|
||||
启动
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</PageHeader>
|
||||
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<ConfigCard>
|
||||
<CardContent>
|
||||
<Typography variant="h6" fontWeight={600} mb={2}>
|
||||
<SettingsIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
|
||||
基础配置
|
||||
</Typography>
|
||||
|
||||
<ConfigSection>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={config.enabled}
|
||||
onChange={(e) => handleConfigChange('enabled', e.target.checked)}
|
||||
/>
|
||||
}
|
||||
label="启用 RAG Pipeline"
|
||||
/>
|
||||
</ConfigSection>
|
||||
|
||||
<ConfigSection>
|
||||
<Typography gutterBottom>文档分块大小</Typography>
|
||||
<Slider
|
||||
value={config.chunkSize}
|
||||
onChange={(_, value) => handleConfigChange('chunkSize', value)}
|
||||
min={128}
|
||||
max={2048}
|
||||
step={64}
|
||||
marks={[
|
||||
{ value: 128, label: '128' },
|
||||
{ value: 512, label: '512' },
|
||||
{ value: 1024, label: '1024' },
|
||||
{ value: 2048, label: '2048' },
|
||||
]}
|
||||
valueLabelDisplay="on"
|
||||
/>
|
||||
</ConfigSection>
|
||||
|
||||
<ConfigSection>
|
||||
<Typography gutterBottom>分块重叠 (%)</Typography>
|
||||
<Slider
|
||||
value={config.chunkOverlap}
|
||||
onChange={(_, value) => handleConfigChange('chunkOverlap', value)}
|
||||
min={0}
|
||||
max={50}
|
||||
step={5}
|
||||
valueLabelDisplay="on"
|
||||
/>
|
||||
</ConfigSection>
|
||||
|
||||
<ConfigSection>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>嵌入模型</InputLabel>
|
||||
<Select
|
||||
value={config.embeddingModel}
|
||||
onChange={(e) => handleConfigChange('embeddingModel', e.target.value)}
|
||||
label="嵌入模型"
|
||||
>
|
||||
<MenuItem value="text-embedding-ada-002">text-embedding-ada-002</MenuItem>
|
||||
<MenuItem value="text-embedding-3-small">text-embedding-3-small</MenuItem>
|
||||
<MenuItem value="text-embedding-3-large">text-embedding-3-large</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</ConfigSection>
|
||||
</CardContent>
|
||||
</ConfigCard>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={6}>
|
||||
<ConfigCard>
|
||||
<CardContent>
|
||||
<Typography variant="h6" fontWeight={600} mb={2}>
|
||||
检索与生成配置
|
||||
</Typography>
|
||||
|
||||
<ConfigSection>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="检索文档数量 (Top-K)"
|
||||
type="number"
|
||||
value={config.retrievalTopK}
|
||||
onChange={(e) => handleConfigChange('retrievalTopK', parseInt(e.target.value))}
|
||||
inputProps={{ min: 1, max: 20 }}
|
||||
/>
|
||||
</ConfigSection>
|
||||
|
||||
<ConfigSection>
|
||||
<Typography gutterBottom>生成温度</Typography>
|
||||
<Slider
|
||||
value={config.temperature}
|
||||
onChange={(_, value) => handleConfigChange('temperature', value)}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.1}
|
||||
marks={[
|
||||
{ value: 0, label: '0' },
|
||||
{ value: 0.5, label: '0.5' },
|
||||
{ value: 1, label: '1' },
|
||||
]}
|
||||
valueLabelDisplay="on"
|
||||
/>
|
||||
</ConfigSection>
|
||||
|
||||
<ConfigSection>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="最大输出Token数"
|
||||
type="number"
|
||||
value={config.maxTokens}
|
||||
onChange={(e) => handleConfigChange('maxTokens', parseInt(e.target.value))}
|
||||
inputProps={{ min: 256, max: 4096 }}
|
||||
/>
|
||||
</ConfigSection>
|
||||
|
||||
<ConfigSection>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="系统提示词"
|
||||
multiline
|
||||
rows={4}
|
||||
value={config.systemPrompt}
|
||||
onChange={(e) => handleConfigChange('systemPrompt', e.target.value)}
|
||||
/>
|
||||
</ConfigSection>
|
||||
</CardContent>
|
||||
</ConfigCard>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<ConfigCard>
|
||||
<CardContent>
|
||||
<Typography variant="h6" fontWeight={600} mb={2}>
|
||||
Pipeline 状态监控
|
||||
</Typography>
|
||||
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<Box textAlign="center" p={2} bgcolor="#F8F9FA" borderRadius="6px">
|
||||
<Typography variant="h4" color="primary" fontWeight={600}>
|
||||
1,234
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
已处理文档
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<Box textAlign="center" p={2} bgcolor="#F8F9FA" borderRadius="6px">
|
||||
<Typography variant="h4" color="success.main" fontWeight={600}>
|
||||
98.5%
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
成功率
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<Box textAlign="center" p={2} bgcolor="#F8F9FA" borderRadius="6px">
|
||||
<Typography variant="h4" color="warning.main" fontWeight={600}>
|
||||
2.3s
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
平均响应时间
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<Box textAlign="center" p={2} bgcolor="#F8F9FA" borderRadius="6px">
|
||||
<Typography variant="h4" color="info.main" fontWeight={600}>
|
||||
156
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
今日查询数
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{pipelineStatus === 'error' && (
|
||||
<Alert severity="error" sx={{ mt: 2 }}>
|
||||
Pipeline 运行出现错误,请检查配置和日志。
|
||||
</Alert>
|
||||
)}
|
||||
</CardContent>
|
||||
</ConfigCard>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default PipelineConfig;
|
||||
32
src/routes/index.tsx
Normal file
32
src/routes/index.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Routes, Route, Navigate } from 'react-router-dom';
|
||||
import MainLayout from '../components/Layout/MainLayout';
|
||||
import Login from '../pages/Login';
|
||||
import Home from '../pages/Home';
|
||||
import KnowledgeBaseList from '../pages/KnowledgeBaseList';
|
||||
import PipelineConfig from '../pages/PipelineConfig';
|
||||
import Dashboard from '../pages/Dashboard';
|
||||
import ModelsResources from '../pages/ModelsResources';
|
||||
import MCP from '../pages/MCP';
|
||||
|
||||
const AppRoutes = () => {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/login" element={<Login />} />
|
||||
|
||||
{/* 使用MainLayout作为受保护路由的布局 */}
|
||||
<Route path="/" element={<MainLayout />}>
|
||||
<Route index element={<Home />} />
|
||||
<Route path="kb-list" element={<KnowledgeBaseList />} />
|
||||
<Route path="pipeline-config" element={<PipelineConfig />} />
|
||||
<Route path="dashboard" element={<Dashboard />} />
|
||||
<Route path="models-resources" element={<ModelsResources />} />
|
||||
<Route path="mcp" element={<MCP />} />
|
||||
</Route>
|
||||
|
||||
{/* 处理未匹配的路由 */}
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppRoutes;
|
||||
164
src/theme/index.ts
Normal file
164
src/theme/index.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import { createTheme } from '@mui/material/styles';
|
||||
|
||||
// Company branding colors extracted from web_prototype CSS
|
||||
const brandColors = {
|
||||
primary: '#E20074',
|
||||
primaryHover: '#C40062',
|
||||
background: '#F0F0F0',
|
||||
surface: '#FFFFFF',
|
||||
text: '#222',
|
||||
textSecondary: '#555',
|
||||
border: '#E2E2E2',
|
||||
borderStrong: '#CFCFCF',
|
||||
success: '#1E9E59',
|
||||
danger: '#D22C32',
|
||||
warning: '#D89200',
|
||||
};
|
||||
|
||||
export const theme = createTheme({
|
||||
palette: {
|
||||
primary: {
|
||||
main: brandColors.primary,
|
||||
dark: brandColors.primaryHover,
|
||||
contrastText: '#FFFFFF',
|
||||
},
|
||||
secondary: {
|
||||
main: brandColors.primary,
|
||||
dark: brandColors.primaryHover,
|
||||
},
|
||||
background: {
|
||||
default: brandColors.background,
|
||||
paper: brandColors.surface,
|
||||
},
|
||||
text: {
|
||||
primary: brandColors.text,
|
||||
secondary: brandColors.textSecondary,
|
||||
},
|
||||
success: {
|
||||
main: brandColors.success,
|
||||
},
|
||||
error: {
|
||||
main: brandColors.danger,
|
||||
},
|
||||
warning: {
|
||||
main: brandColors.warning,
|
||||
},
|
||||
divider: brandColors.border,
|
||||
},
|
||||
typography: {
|
||||
fontFamily: '"Segoe UI", system-ui, -apple-system, BlinkMacSystemFont, "Helvetica Neue", Arial, sans-serif',
|
||||
h1: {
|
||||
fontWeight: 700,
|
||||
letterSpacing: '0.5px',
|
||||
},
|
||||
h2: {
|
||||
fontWeight: 600,
|
||||
letterSpacing: '0.5px',
|
||||
},
|
||||
h3: {
|
||||
fontWeight: 600,
|
||||
letterSpacing: '0.3px',
|
||||
},
|
||||
body1: {
|
||||
fontSize: '0.875rem',
|
||||
},
|
||||
body2: {
|
||||
fontSize: '0.75rem',
|
||||
},
|
||||
button: {
|
||||
fontWeight: 600,
|
||||
letterSpacing: '0.3px',
|
||||
textTransform: 'none',
|
||||
},
|
||||
},
|
||||
shape: {
|
||||
borderRadius: 10,
|
||||
},
|
||||
shadows: [
|
||||
'none',
|
||||
'0 2px 4px rgba(0,0,0,0.08), 0 4px 12px rgba(0,0,0,0.06)',
|
||||
'0 2px 4px rgba(0,0,0,0.08), 0 4px 12px rgba(0,0,0,0.06)',
|
||||
'0 2px 4px rgba(0,0,0,0.08), 0 4px 12px rgba(0,0,0,0.06)',
|
||||
'0 4px 12px -2px rgba(226,0,116,0.45)',
|
||||
'0 6px 14px -2px rgba(226,0,116,0.5)',
|
||||
'0 2px 4px rgba(0,0,0,0.08), 0 4px 12px rgba(0,0,0,0.06)',
|
||||
'0 2px 4px rgba(0,0,0,0.08), 0 4px 12px rgba(0,0,0,0.06)',
|
||||
'0 2px 4px rgba(0,0,0,0.08), 0 4px 12px rgba(0,0,0,0.06)',
|
||||
'0 2px 4px rgba(0,0,0,0.08), 0 4px 12px rgba(0,0,0,0.06)',
|
||||
'0 2px 4px rgba(0,0,0,0.08), 0 4px 12px rgba(0,0,0,0.06)',
|
||||
'0 2px 4px rgba(0,0,0,0.08), 0 4px 12px rgba(0,0,0,0.06)',
|
||||
'0 2px 4px rgba(0,0,0,0.08), 0 4px 12px rgba(0,0,0,0.06)',
|
||||
'0 2px 4px rgba(0,0,0,0.08), 0 4px 12px rgba(0,0,0,0.06)',
|
||||
'0 2px 4px rgba(0,0,0,0.08), 0 4px 12px rgba(0,0,0,0.06)',
|
||||
'0 2px 4px rgba(0,0,0,0.08), 0 4px 12px rgba(0,0,0,0.06)',
|
||||
'0 2px 4px rgba(0,0,0,0.08), 0 4px 12px rgba(0,0,0,0.06)',
|
||||
'0 2px 4px rgba(0,0,0,0.08), 0 4px 12px rgba(0,0,0,0.06)',
|
||||
'0 2px 4px rgba(0,0,0,0.08), 0 4px 12px rgba(0,0,0,0.06)',
|
||||
'0 2px 4px rgba(0,0,0,0.08), 0 4px 12px rgba(0,0,0,0.06)',
|
||||
'0 2px 4px rgba(0,0,0,0.08), 0 4px 12px rgba(0,0,0,0.06)',
|
||||
'0 2px 4px rgba(0,0,0,0.08), 0 4px 12px rgba(0,0,0,0.06)',
|
||||
'0 2px 4px rgba(0,0,0,0.08), 0 4px 12px rgba(0,0,0,0.06)',
|
||||
'0 2px 4px rgba(0,0,0,0.08), 0 4px 12px rgba(0,0,0,0.06)',
|
||||
'0 2px 4px rgba(0,0,0,0.08), 0 4px 12px rgba(0,0,0,0.06)',
|
||||
],
|
||||
components: {
|
||||
MuiButton: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderRadius: '8px',
|
||||
padding: '0.65rem 1.1rem',
|
||||
fontSize: '0.8rem',
|
||||
fontWeight: 600,
|
||||
letterSpacing: '0.3px',
|
||||
},
|
||||
contained: {
|
||||
boxShadow: '0 4px 10px -2px rgba(226,0,116,0.45)',
|
||||
'&:hover': {
|
||||
transform: 'translateY(-2px)',
|
||||
boxShadow: '0 6px 14px -2px rgba(226,0,116,0.5)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiCard: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderRadius: '10px',
|
||||
border: `1px solid ${brandColors.border}`,
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.08), 0 4px 12px rgba(0,0,0,0.06)',
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiTextField: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: '8px',
|
||||
'&:hover .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: brandColors.primary,
|
||||
},
|
||||
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: brandColors.primary,
|
||||
boxShadow: `0 0 0 3px rgba(226,0,116,0.25)`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiLinearProgress: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
height: '8px',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: '#ECECEC',
|
||||
},
|
||||
bar: {
|
||||
borderRadius: '4px',
|
||||
background: `linear-gradient(90deg, ${brandColors.primary}, #FF4DA8)`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default theme;
|
||||
Reference in New Issue
Block a user