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:
2025-10-09 17:23:15 +08:00
parent 446b422a12
commit 5f93573e57
15 changed files with 3521 additions and 31 deletions

View File

@@ -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;

View 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;

View 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;

View 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
View 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
View 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;

View 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
View 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
View 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;

View 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;

View 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
View 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
View 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;