first-update

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

View File

@@ -0,0 +1,112 @@
'use client';
import React from 'react';
import { Box, IconButton, Tooltip } from '@mui/material';
import { useTranslation } from 'react-i18next';
import LightModeOutlinedIcon from '@mui/icons-material/LightModeOutlined';
import DarkModeOutlinedIcon from '@mui/icons-material/DarkModeOutlined';
import HelpOutlineIcon from '@mui/icons-material/HelpOutline';
import GitHubIcon from '@mui/icons-material/GitHub';
import BarChartIcon from '@mui/icons-material/BarChart';
import LanguageSwitcher from '../LanguageSwitcher';
import UpdateChecker from '../UpdateChecker';
import TaskIcon from '../TaskIcon';
import ModelSelect from '../ModelSelect';
import * as styles from './styles';
/**
* ActionButtons 组件
* 右侧操作区按钮语言切换、主题切换、文档、GitHub、更新检查
*/
export default function ActionButtons({
theme,
resolvedTheme,
toggleTheme,
isProjectDetail,
currentProject,
onActionAreaEnter
}) {
const { t, i18n } = useTranslation();
const isZhLanguage = String(i18n.language || '')
.toLowerCase()
.startsWith('zh');
return (
<Box sx={styles.actionAreaStyles} onMouseEnter={onActionAreaEnter}>
{isProjectDetail && <ModelSelect projectId={currentProject} />}
{isProjectDetail && <TaskIcon theme={theme} projectId={currentProject} />}
{/* Monitoring Dashboard - Only visible on Home page */}
{!isProjectDetail && (
<Tooltip title={t('monitoring.title', 'Resource Monitoring')}>
<IconButton component="a" href="/monitoring" size="medium" sx={styles.getIconButtonStyles(theme)}>
<BarChartIcon />
</IconButton>
</Tooltip>
)}
{/* Language Switcher - Always visible */}
<LanguageSwitcher />
{/* Theme Toggle - Always visible */}
<Tooltip
title={
resolvedTheme === 'dark'
? t('theme.switchToLight', 'Switch to light mode')
: t('theme.switchToDark', 'Switch to dark mode')
}
>
<IconButton
onClick={toggleTheme}
size="medium"
aria-label={
resolvedTheme === 'dark'
? t('theme.switchToLight', 'Switch to light mode')
: t('theme.switchToDark', 'Switch to dark mode')
}
sx={styles.getIconButtonStyles(theme)}
>
{resolvedTheme === 'dark' ? (
<LightModeOutlinedIcon fontSize="small" />
) : (
<DarkModeOutlinedIcon fontSize="small" />
)}
</IconButton>
</Tooltip>
{/* Documentation - Hide below xl */}
<Tooltip title={t('documentation')}>
<IconButton
component="a"
href={isZhLanguage ? 'https://docs.easy-dataset.com/' : 'https://docs.easy-dataset.com/ed/en'}
target="_blank"
rel="noopener noreferrer"
size="medium"
sx={styles.getIconButtonStyles(theme)}
>
<HelpOutlineIcon fontSize="small" />
</IconButton>
</Tooltip>
{/* GitHub - Hide at larger tablet screens and below */}
<Tooltip title={t('common.visitGitHub', 'View on GitHub')}>
<IconButton
component="a"
href="https://github.com/ConardLi/easy-dataset"
target="_blank"
rel="noopener noreferrer"
size="medium"
aria-label={t('common.visitGitHub', 'Open GitHub repository')}
sx={styles.getIconButtonStyles(theme)}
>
<GitHubIcon fontSize="small" />
</IconButton>
</Tooltip>
{/* Update Checker - Hide below xl */}
<Box sx={{ display: { xs: 'none', xl: 'flex' } }}>
<UpdateChecker />
</Box>
</Box>
);
}

View File

@@ -0,0 +1,175 @@
'use client';
import React, { useState } from 'react';
import {
Box,
Chip,
Typography,
useTheme,
Menu,
MenuItem,
ListItemIcon,
ListItemText,
Paper,
Tooltip
} from '@mui/material';
import { useTranslation } from 'react-i18next';
import { useRouter } from 'next/navigation';
import { useSetAtom } from 'jotai';
import { modelConfigListAtom, selectedModelInfoAtom } from '@/lib/store';
import { toast } from 'sonner';
import axios from 'axios';
// Icons
import FolderIcon from '@mui/icons-material/Folder';
import CheckIcon from '@mui/icons-material/Check';
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
// 样式
import * as styles from './contextBarStyles';
export default function ContextBar({ projects = [], currentProjectId, onMouseLeave }) {
const { t } = useTranslation();
const theme = useTheme();
const router = useRouter();
// State
const [projectMenuAnchor, setProjectMenuAnchor] = useState(null);
// Jotai atoms
const setConfigList = useSetAtom(modelConfigListAtom);
const setSelectedModelInfo = useSetAtom(selectedModelInfoAtom);
// Get current project
const currentProject = projects.find(p => p.id === currentProjectId);
// Handlers
const handleProjectMenuOpen = event => {
event.preventDefault();
setProjectMenuAnchor(event.currentTarget);
};
const handleProjectMenuClose = () => {
setProjectMenuAnchor(null);
// 菜单关闭时,如果提供了 onMouseLeave 回调,则调用它
if (onMouseLeave) {
onMouseLeave();
}
};
const handleProjectChange = async newProjectId => {
handleProjectMenuClose();
try {
// Fetch model config for new project
const response = await axios.get(`/api/projects/${newProjectId}/model-config`);
setConfigList(response.data.data);
if (response.data.defaultModelConfigId) {
const defaultModel = response.data.data.find(item => item.id === response.data.defaultModelConfigId);
setSelectedModelInfo(defaultModel || null);
} else {
setSelectedModelInfo(null);
}
// Navigate to the new project's text-split page
router.push(`/projects/${newProjectId}/text-split`);
} catch (error) {
console.error('Error switching project:', error);
toast.error(t('common.error', 'Error switching project'));
}
};
if (!currentProjectId || !currentProject) {
return null;
}
return (
<Paper
elevation={0}
component="nav"
aria-label={t('common.contextNavigation', 'Context navigation')}
sx={styles.getContextBarPaperStyles(theme)}
>
<Box sx={styles.contextBarContainerStyles}>
{/* Project Selector */}
<Box sx={styles.selectorContainerStyles}>
<Typography variant="caption" sx={styles.labelTypographyStyles}>
{t('common.project', 'Project')}:
</Typography>
<Tooltip
title={currentProject?.name || t('projects.selectProject', 'Select Project')}
placement="bottom-start"
arrow
>
<Chip
icon={<FolderIcon fontSize="small" />}
label={
<Box sx={styles.chipLabelBoxStyles}>
<Typography variant="body2" noWrap sx={styles.chipTextStyles}>
{currentProject?.name || t('projects.selectProject', 'Select Project')}
</Typography>
<ArrowDropDownIcon fontSize="small" sx={styles.chipArrowStyles} />
</Box>
}
onClick={handleProjectMenuOpen}
clickable
variant="outlined"
size="medium"
sx={styles.getProjectChipStyles(theme)}
aria-label={t('projects.selectProject', 'Select project')}
aria-controls={projectMenuAnchor ? 'project-menu' : undefined}
aria-haspopup="true"
aria-expanded={Boolean(projectMenuAnchor)}
/>
</Tooltip>
</Box>
</Box>
{/* Project Menu */}
<Menu
id="project-menu"
anchorEl={projectMenuAnchor}
open={Boolean(projectMenuAnchor)}
onClose={handleProjectMenuClose}
role="menu"
aria-label={t('projects.projectMenu', 'Project menu')}
PaperProps={{
elevation: 8,
sx: styles.getMenuPaperStyles(theme)
}}
transformOrigin={{ horizontal: 'left', vertical: 'top' }}
anchorOrigin={{ horizontal: 'left', vertical: 'bottom' }}
MenuListProps={{
'aria-labelledby': 'project-selector',
...styles.menuListPropsStyles
}}
>
<Typography variant="caption" sx={styles.menuHeaderTypographyStyles}>
{t('projects.allProjects', 'All Projects')}
</Typography>
{projects.map((project, index) => (
<MenuItem
key={project.id}
onClick={() => handleProjectChange(project.id)}
selected={project.id === currentProjectId}
role="menuitem"
sx={styles.getMenuItemStyles(theme)}
>
<ListItemIcon sx={styles.menuItemIconStyles}>
{project.id === currentProjectId ? (
<CheckIcon fontSize="small" color="primary" />
) : (
<FolderIcon fontSize="small" />
)}
</ListItemIcon>
<ListItemText
primary={project.name}
primaryTypographyProps={styles.getMenuItemTextPrimaryProps(project.id === currentProjectId)}
/>
</MenuItem>
))}
</Menu>
</Paper>
);
}

View File

@@ -0,0 +1,315 @@
'use client';
import React from 'react';
import { Menu, MenuItem, ListItemIcon, ListItemText, Divider } from '@mui/material';
import { useTranslation } from 'react-i18next';
import Link from 'next/link';
import DescriptionOutlinedIcon from '@mui/icons-material/DescriptionOutlined';
import ImageIcon from '@mui/icons-material/Image';
import DatasetOutlinedIcon from '@mui/icons-material/DatasetOutlined';
import ChatIcon from '@mui/icons-material/Chat';
import SettingsOutlinedIcon from '@mui/icons-material/SettingsOutlined';
import ScienceOutlinedIcon from '@mui/icons-material/ScienceOutlined';
import StorageIcon from '@mui/icons-material/Storage';
import AssessmentOutlinedIcon from '@mui/icons-material/AssessmentOutlined';
import PlaylistPlayIcon from '@mui/icons-material/PlaylistPlay';
import VisibilityIcon from '@mui/icons-material/Visibility';
import * as styles from './styles';
/**
* DesktopMenus 缂備礁瀚▎?
* 婵℃鐭傚鎵博椤栨稑浜鹃柛瀣矎瑜板秹宕¢弴顏嗙闁告牕鎳庨幆鍫ュ极閻楀牆绁︽繝褎鍔戦埀顑跨劍閺嗙喖骞戦鈧▔锔剧不閿涘嫭鍊為柕鍡曠劍濞叉寧寰勫顐ょ憦濞戞搩浜hぐ宥夊础?
*/
export default function DesktopMenus({
theme,
menuState,
isMenuOpen,
handleMenuClose,
currentProject,
onNavigateStart
}) {
const { t } = useTranslation();
return (
<>
{/* 闁轰胶澧楀畵浣糕攦閹邦垰缍呴柛?*/}
<Menu
anchorEl={menuState.anchorEl}
open={isMenuOpen('source')}
onClose={handleMenuClose}
hideBackdrop
disableScrollLock
sx={{ pointerEvents: 'none' }}
aria-label={t('common.dataSource', 'Data source menu')}
PaperProps={{
elevation: 8,
sx: {
...styles.getMenuPaperStyles(theme),
pointerEvents: 'auto'
},
onMouseLeave: handleMenuClose
}}
transformOrigin={{ horizontal: 'center', vertical: 'top' }}
anchorOrigin={{ horizontal: 'center', vertical: 'bottom' }}
MenuListProps={{
dense: false,
onMouseLeave: handleMenuClose,
sx: styles.menuListStyles,
role: 'menu'
}}
transitionDuration={200}
>
<MenuItem
component={Link}
href={`/projects/${currentProject}/text-split`}
onClick={() => {
onNavigateStart?.();
handleMenuClose();
}}
role="menuitem"
sx={styles.getMenuItemStyles(theme)}
>
<ListItemIcon sx={styles.listItemIconStyles}>
<DescriptionOutlinedIcon fontSize="small" sx={styles.getPrimaryIconColorStyles(theme)} />
</ListItemIcon>
<ListItemText primary={t('textSplit.title')} primaryTypographyProps={styles.smallListItemTextStyles} />
</MenuItem>
<Divider sx={{ my: 0.75, mx: 1.5 }} />
<MenuItem
component={Link}
href={`/projects/${currentProject}/images`}
onClick={() => {
onNavigateStart?.();
handleMenuClose();
}}
role="menuitem"
sx={styles.getMenuItemStyles(theme)}
>
<ListItemIcon sx={styles.listItemIconStyles}>
<ImageIcon fontSize="small" sx={styles.getPrimaryIconColorStyles(theme)} />
</ListItemIcon>
<ListItemText primary={t('images.title')} primaryTypographyProps={styles.smallListItemTextStyles} />
</MenuItem>
</Menu>
{/* 闁轰胶澧楀畵渚€姊块崱娆樺悁闁荤偛妫滆ぐ宥夊础?*/}
<Menu
anchorEl={menuState.anchorEl}
open={isMenuOpen('dataset')}
onClose={handleMenuClose}
hideBackdrop
disableScrollLock
sx={{ pointerEvents: 'none' }}
PaperProps={{
elevation: 8,
sx: {
...styles.getSimpleMenuPaperStyles(theme),
pointerEvents: 'auto'
},
onMouseLeave: handleMenuClose
}}
transformOrigin={{ horizontal: 'center', vertical: 'top' }}
anchorOrigin={{ horizontal: 'center', vertical: 'bottom' }}
MenuListProps={{
dense: true,
onMouseLeave: handleMenuClose,
sx: styles.simpleMenuListStyles
}}
>
<MenuItem
component={Link}
href={`/projects/${currentProject}/datasets`}
onClick={() => {
onNavigateStart?.();
handleMenuClose();
}}
sx={styles.getSimpleMenuItemStyles(theme)}
>
<ListItemIcon sx={styles.smallListItemIconStyles}>
<DatasetOutlinedIcon fontSize="small" sx={styles.getPrimaryIconColorStyles(theme)} />
</ListItemIcon>
<ListItemText
primary={t('datasets.singleTurn', '单轮问答数据集')}
primaryTypographyProps={styles.smallListItemTextStyles}
/>
</MenuItem>
<Divider sx={{ my: 0.5, mx: 1 }} />
<MenuItem
component={Link}
href={`/projects/${currentProject}/multi-turn`}
onClick={() => {
onNavigateStart?.();
handleMenuClose();
}}
sx={styles.getSimpleMenuItemStyles(theme)}
>
<ListItemIcon sx={styles.smallListItemIconStyles}>
<ChatIcon fontSize="small" sx={styles.getPrimaryIconColorStyles(theme)} />
</ListItemIcon>
<ListItemText
primary={t('datasets.multiTurn', '多轮对话数据集')}
primaryTypographyProps={styles.smallListItemTextStyles}
/>
</MenuItem>
<Divider sx={{ my: 0.5, mx: 1 }} />
<MenuItem
component={Link}
href={`/projects/${currentProject}/image-datasets`}
onClick={() => {
onNavigateStart?.();
handleMenuClose();
}}
sx={styles.getSimpleMenuItemStyles(theme)}
>
<ListItemIcon sx={styles.smallListItemIconStyles}>
<ImageIcon fontSize="small" sx={styles.getPrimaryIconColorStyles(theme)} />
</ListItemIcon>
<ListItemText
primary={t('datasets.imageQA', '图片问答数据集')}
primaryTypographyProps={styles.smallListItemTextStyles}
/>
</MenuItem>
</Menu>
{/* 閻犲洤瀚崣濠囨嚕濠婂啫绀?*/}
<Menu
anchorEl={menuState.anchorEl}
open={isMenuOpen('eval')}
onClose={handleMenuClose}
hideBackdrop
disableScrollLock
sx={{ pointerEvents: 'none' }}
PaperProps={{
elevation: 8,
sx: {
...styles.getSimpleMenuPaperStyles(theme),
pointerEvents: 'auto'
},
onMouseLeave: handleMenuClose
}}
transformOrigin={{ horizontal: 'center', vertical: 'top' }}
anchorOrigin={{ horizontal: 'center', vertical: 'bottom' }}
MenuListProps={{
dense: true,
onMouseLeave: handleMenuClose,
sx: styles.simpleMenuListStyles
}}
>
<MenuItem
component={Link}
href={`/projects/${currentProject}/eval-datasets`}
onClick={() => {
onNavigateStart?.();
handleMenuClose();
}}
sx={styles.getSimpleMenuItemStyles(theme)}
>
<ListItemIcon sx={styles.smallListItemIconStyles}>
<AssessmentOutlinedIcon fontSize="small" sx={styles.getPrimaryIconColorStyles(theme)} />
</ListItemIcon>
<ListItemText primary={t('eval.datasets')} primaryTypographyProps={styles.smallListItemTextStyles} />
</MenuItem>
<Divider sx={{ my: 0.5, mx: 1 }} />
<MenuItem
component={Link}
href={`/projects/${currentProject}/eval-tasks`}
onClick={() => {
onNavigateStart?.();
handleMenuClose();
}}
sx={styles.getSimpleMenuItemStyles(theme)}
>
<ListItemIcon sx={styles.smallListItemIconStyles}>
<PlaylistPlayIcon fontSize="small" sx={styles.getPrimaryIconColorStyles(theme)} />
</ListItemIcon>
<ListItemText primary={t('eval.tasks')} primaryTypographyProps={styles.smallListItemTextStyles} />
</MenuItem>
<Divider sx={{ my: 0.5, mx: 1 }} />
<MenuItem
component={Link}
href={`/projects/${currentProject}/blind-test-tasks`}
onClick={() => {
onNavigateStart?.();
handleMenuClose();
}}
sx={styles.getSimpleMenuItemStyles(theme)}
>
<ListItemIcon sx={styles.smallListItemIconStyles}>
<VisibilityIcon fontSize="small" sx={styles.getPrimaryIconColorStyles(theme)} />
</ListItemIcon>
<ListItemText primary={t('blindTest.title')} primaryTypographyProps={styles.smallListItemTextStyles} />
</MenuItem>
</Menu>
{/* 闁哄洦娼欓ˇ鍧楁嚕濠婂啫绀?*/}
<Menu
anchorEl={menuState.anchorEl}
open={isMenuOpen('more')}
onClose={handleMenuClose}
hideBackdrop
disableScrollLock
sx={{ pointerEvents: 'none' }}
PaperProps={{
elevation: 8,
sx: {
...styles.getSimpleMenuPaperStyles(theme),
pointerEvents: 'auto'
},
onMouseLeave: handleMenuClose
}}
transformOrigin={{ horizontal: 'center', vertical: 'top' }}
anchorOrigin={{ horizontal: 'center', vertical: 'bottom' }}
MenuListProps={{
dense: true,
onMouseLeave: handleMenuClose,
sx: styles.simpleMenuListStyles
}}
>
<MenuItem
component={Link}
href={`/projects/${currentProject}/settings`}
onClick={() => {
onNavigateStart?.();
handleMenuClose();
}}
sx={styles.getSimpleMenuItemStyles(theme)}
>
<ListItemIcon sx={styles.smallListItemIconStyles}>
<SettingsOutlinedIcon fontSize="small" sx={styles.getPrimaryIconColorStyles(theme)} />
</ListItemIcon>
<ListItemText primary={t('settings.title')} primaryTypographyProps={styles.smallListItemTextStyles} />
</MenuItem>
<Divider sx={{ my: 0.5, mx: 1 }} />
<MenuItem
component={Link}
href={`/projects/${currentProject}/playground`}
onClick={() => {
onNavigateStart?.();
handleMenuClose();
}}
sx={styles.getSimpleMenuItemStyles(theme)}
>
<ListItemIcon sx={styles.smallListItemIconStyles}>
<ScienceOutlinedIcon fontSize="small" sx={styles.getPrimaryIconColorStyles(theme)} />
</ListItemIcon>
<ListItemText primary={t('playground.title')} primaryTypographyProps={styles.smallListItemTextStyles} />
</MenuItem>
<Divider sx={{ my: 0.5, mx: 1 }} />
<MenuItem
component={Link}
href="/dataset-square"
onClick={() => {
onNavigateStart?.();
handleMenuClose();
}}
sx={styles.getSimpleMenuItemStyles(theme)}
>
<ListItemIcon sx={styles.smallListItemIconStyles}>
<StorageIcon fontSize="small" sx={styles.getPrimaryIconColorStyles(theme)} />
</ListItemIcon>
<ListItemText primary={t('datasetSquare.title')} primaryTypographyProps={styles.smallListItemTextStyles} />
</MenuItem>
</Menu>
</>
);
}

View File

@@ -0,0 +1,42 @@
'use client';
import React from 'react';
import { Box, Typography, Tooltip } from '@mui/material';
import { useTranslation } from 'react-i18next';
import * as styles from './styles';
/**
* Logo 组件
* 显示应用 Logo 和标题,支持点击跳转到首页
*/
export default function Logo({ theme }) {
const { t } = useTranslation();
return (
<Tooltip title={t('common.goHome', 'Go to Home')} placement="bottom">
<Box
component="a"
href="/"
role="link"
aria-label={t('common.goToHomePage', 'Go to home page')}
tabIndex={0}
sx={styles.getLogoLinkStyles(theme)}
onClick={e => {
e.preventDefault();
window.location.href = '/';
}}
onKeyDown={e => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
window.location.href = '/';
}
}}
>
<Box component="img" src="/imgs/logo.svg" alt="Easy Dataset Logo" sx={styles.logoImageStyles} />
<Typography variant="h6" sx={styles.getLogoTextStyles(theme)}>
Easy DataSet
</Typography>
</Box>
</Tooltip>
);
}

View File

@@ -0,0 +1,405 @@
'use client';
import React from 'react';
import {
Drawer,
Box,
Typography,
IconButton,
Tooltip,
List,
ListItem,
ListItemButton,
ListItemIcon,
ListItemText,
Collapse
} from '@mui/material';
import { useTranslation } from 'react-i18next';
import Link from 'next/link';
import CloseIcon from '@mui/icons-material/Close';
import DescriptionOutlinedIcon from '@mui/icons-material/DescriptionOutlined';
import TokenOutlinedIcon from '@mui/icons-material/TokenOutlined';
import QuestionAnswerOutlinedIcon from '@mui/icons-material/QuestionAnswerOutlined';
import DatasetOutlinedIcon from '@mui/icons-material/DatasetOutlined';
import SettingsOutlinedIcon from '@mui/icons-material/SettingsOutlined';
import ScienceOutlinedIcon from '@mui/icons-material/ScienceOutlined';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import ChatIcon from '@mui/icons-material/Chat';
import ImageIcon from '@mui/icons-material/Image';
import StorageIcon from '@mui/icons-material/Storage';
import HelpOutlineIcon from '@mui/icons-material/HelpOutline';
import GitHubIcon from '@mui/icons-material/GitHub';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
import AssessmentOutlinedIcon from '@mui/icons-material/AssessmentOutlined';
import PlaylistPlayIcon from '@mui/icons-material/PlaylistPlay';
import VisibilityIcon from '@mui/icons-material/Visibility';
import UpdateChecker from '../UpdateChecker';
import * as styles from './styles';
/**
* MobileDrawer 组件
* 移动端抽屉菜单,包含所有导航项
*/
export default function MobileDrawer({
theme,
drawerOpen,
toggleDrawer,
expandedMenu,
toggleMobileSubmenu,
currentProject,
onNavigateStart
}) {
const { t, i18n } = useTranslation();
const handleNavigateStart = () => {
onNavigateStart?.();
toggleDrawer();
};
return (
<Drawer
id="mobile-navigation-drawer"
anchor="left"
open={drawerOpen}
onClose={toggleDrawer}
PaperProps={{
role: 'navigation',
'aria-label': t('common.mobileNavigation', 'Mobile navigation menu'),
sx: styles.getDrawerPaperStyles(theme)
}}
ModalProps={{
keepMounted: true // Better mobile performance
}}
transitionDuration={300}
SlideProps={{
easing: 'cubic-bezier(0.4, 0, 0.2, 1)'
}}
>
{/* Drawer Header */}
<Box sx={styles.getDrawerHeaderStyles(theme)}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
<Box component="img" src="/imgs/logo.svg" alt="Easy Dataset Logo" sx={{ width: 32, height: 32 }} />
<Typography variant="h6" component="h2" sx={{ fontWeight: 700, fontSize: '1.15rem' }}>
{t('common.navigation', 'Navigation')}
</Typography>
</Box>
<Tooltip title={t('common.closeMenu', 'Close menu')}>
<IconButton
onClick={toggleDrawer}
aria-label={t('common.closeMenu', 'Close menu')}
size="medium"
sx={styles.getDrawerCloseButtonStyles(theme)}
>
<CloseIcon />
</IconButton>
</Tooltip>
</Box>
{/* Drawer Menu List */}
<List sx={styles.drawerListStyles} role="menu">
{/* 数据源菜单 */}
<ListItem disablePadding sx={{ mb: 0.5 }}>
<ListItemButton
onClick={() => toggleMobileSubmenu('source')}
aria-expanded={expandedMenu === 'source'}
aria-controls="source-submenu"
role="menuitem"
sx={styles.getDrawerListItemButtonStyles(theme)}
>
<ListItemIcon sx={styles.listItemIconStyles}>
<DescriptionOutlinedIcon sx={styles.getIconColorStyles(theme)} />
</ListItemIcon>
<ListItemText primary={t('common.dataSource')} primaryTypographyProps={styles.listItemTextStyles} />
{expandedMenu === 'source' ? <ExpandLessIcon /> : <ExpandMoreIcon />}
</ListItemButton>
</ListItem>
<Collapse id="source-submenu" in={expandedMenu === 'source'} timeout="auto" unmountOnExit>
<List component="div" disablePadding role="menu" sx={styles.getDrawerSubmenuContainerStyles(theme)}>
<ListItemButton
role="menuitem"
sx={styles.getDrawerSubmenuItemStyles(theme)}
component={Link}
href={`/projects/${currentProject}/text-split`}
onClick={toggleDrawer}
>
<ListItemIcon sx={styles.smallListItemIconStyles}>
<DescriptionOutlinedIcon fontSize="small" />
</ListItemIcon>
<ListItemText primary={t('textSplit.title')} primaryTypographyProps={styles.smallListItemTextStyles} />
</ListItemButton>
<ListItemButton
role="menuitem"
sx={styles.getDrawerSubmenuItemStyles(theme)}
component={Link}
href={`/projects/${currentProject}/images`}
onClick={toggleDrawer}
>
<ListItemIcon sx={styles.smallListItemIconStyles}>
<ImageIcon fontSize="small" />
</ListItemIcon>
<ListItemText primary={t('images.title')} primaryTypographyProps={styles.smallListItemTextStyles} />
</ListItemButton>
</List>
</Collapse>
{/* 数据蒸馏 */}
<ListItem disablePadding sx={{ mb: 0.5 }}>
<ListItemButton
component={Link}
href={`/projects/${currentProject}/distill`}
onClick={toggleDrawer}
role="menuitem"
sx={styles.getDrawerListItemButtonStyles(theme)}
>
<ListItemIcon sx={styles.listItemIconStyles}>
<TokenOutlinedIcon sx={styles.getIconColorStyles(theme)} />
</ListItemIcon>
<ListItemText primary={t('distill.title')} primaryTypographyProps={styles.listItemTextStyles} />
</ListItemButton>
</ListItem>
{/* 问题管理 */}
<ListItem disablePadding sx={{ mb: 0.5 }}>
<ListItemButton
component={Link}
href={`/projects/${currentProject}/questions`}
onClick={toggleDrawer}
role="menuitem"
sx={styles.getDrawerListItemButtonStyles(theme)}
>
<ListItemIcon sx={styles.listItemIconStyles}>
<QuestionAnswerOutlinedIcon sx={styles.getIconColorStyles(theme)} />
</ListItemIcon>
<ListItemText primary={t('questions.title')} primaryTypographyProps={styles.listItemTextStyles} />
</ListItemButton>
</ListItem>
{/* 数据集管理 */}
<ListItem disablePadding sx={{ mb: 0.5 }}>
<ListItemButton
onClick={() => toggleMobileSubmenu('dataset')}
role="menuitem"
aria-expanded={expandedMenu === 'dataset'}
aria-controls="dataset-submenu"
sx={styles.getDrawerListItemButtonStyles(theme)}
>
<ListItemIcon sx={styles.listItemIconStyles}>
<DatasetOutlinedIcon sx={styles.getIconColorStyles(theme)} />
</ListItemIcon>
<ListItemText primary={t('datasets.management')} primaryTypographyProps={styles.listItemTextStyles} />
{expandedMenu === 'dataset' ? <ExpandLessIcon /> : <ExpandMoreIcon />}
</ListItemButton>
</ListItem>
<Collapse in={expandedMenu === 'dataset'} timeout="auto" unmountOnExit id="dataset-submenu">
<List component="div" disablePadding sx={styles.getDrawerSubmenuContainerStyles(theme)}>
<ListItemButton
role="menuitem"
sx={styles.getDrawerSubmenuItemStyles(theme)}
component={Link}
href={`/projects/${currentProject}/datasets`}
onClick={toggleDrawer}
>
<ListItemIcon sx={styles.smallListItemIconStyles}>
<DatasetOutlinedIcon fontSize="small" />
</ListItemIcon>
<ListItemText
primary={t('datasets.singleTurn', '单轮问答数据集')}
primaryTypographyProps={styles.smallListItemTextStyles}
/>
</ListItemButton>
<ListItemButton
sx={styles.getDrawerSubmenuItemStyles(theme)}
component={Link}
href={`/projects/${currentProject}/multi-turn`}
onClick={handleNavigateStart}
>
<ListItemIcon sx={styles.smallListItemIconStyles}>
<ChatIcon fontSize="small" />
</ListItemIcon>
<ListItemText
primary={t('datasets.multiTurn', '多轮对话数据集')}
primaryTypographyProps={styles.smallListItemTextStyles}
/>
</ListItemButton>
<ListItemButton
sx={styles.getDrawerSubmenuItemStyles(theme)}
component={Link}
href={`/projects/${currentProject}/image-datasets`}
onClick={toggleDrawer}
>
<ListItemIcon sx={styles.smallListItemIconStyles}>
<ImageIcon fontSize="small" />
</ListItemIcon>
<ListItemText
primary={t('datasets.imageQA', '图片问答数据集')}
primaryTypographyProps={styles.smallListItemTextStyles}
/>
</ListItemButton>
</List>
</Collapse>
{/* 评估菜单 */}
<ListItem disablePadding sx={{ mb: 0.5 }}>
<ListItemButton
onClick={() => toggleMobileSubmenu('eval')}
role="menuitem"
aria-expanded={expandedMenu === 'eval'}
aria-controls="eval-submenu"
sx={styles.getDrawerListItemButtonStyles(theme)}
>
<ListItemIcon sx={styles.listItemIconStyles}>
<AssessmentOutlinedIcon sx={styles.getIconColorStyles(theme)} />
</ListItemIcon>
<ListItemText primary={t('eval.title')} primaryTypographyProps={styles.listItemTextStyles} />
{expandedMenu === 'eval' ? <ExpandLessIcon /> : <ExpandMoreIcon />}
</ListItemButton>
</ListItem>
<Collapse in={expandedMenu === 'eval'} timeout="auto" unmountOnExit id="eval-submenu">
<List component="div" disablePadding sx={styles.getDrawerSubmenuContainerStyles(theme)}>
<ListItemButton
role="menuitem"
sx={styles.getDrawerSubmenuItemStyles(theme)}
component={Link}
href={`/projects/${currentProject}/eval-datasets`}
onClick={handleNavigateStart}
>
<ListItemIcon sx={styles.smallListItemIconStyles}>
<AssessmentOutlinedIcon fontSize="small" />
</ListItemIcon>
<ListItemText primary={t('eval.datasets')} primaryTypographyProps={styles.smallListItemTextStyles} />
</ListItemButton>
<ListItemButton
role="menuitem"
sx={styles.getDrawerSubmenuItemStyles(theme)}
component={Link}
href={`/projects/${currentProject}/eval-tasks`}
onClick={handleNavigateStart}
>
<ListItemIcon sx={styles.smallListItemIconStyles}>
<PlaylistPlayIcon fontSize="small" />
</ListItemIcon>
<ListItemText primary={t('eval.tasks')} primaryTypographyProps={styles.smallListItemTextStyles} />
</ListItemButton>
<ListItemButton
role="menuitem"
sx={styles.getDrawerSubmenuItemStyles(theme)}
component={Link}
href={`/projects/${currentProject}/blind-test-tasks`}
onClick={toggleDrawer}
>
<ListItemIcon sx={styles.smallListItemIconStyles}>
<VisibilityIcon fontSize="small" />
</ListItemIcon>
<ListItemText primary={t('blindTest.title')} primaryTypographyProps={styles.smallListItemTextStyles} />
</ListItemButton>
</List>
</Collapse>
{/* 更多菜单 */}
<ListItem disablePadding sx={{ mb: 0.5 }}>
<ListItemButton
onClick={() => toggleMobileSubmenu('more')}
role="menuitem"
aria-expanded={expandedMenu === 'more'}
aria-controls="more-submenu"
sx={styles.getDrawerListItemButtonStyles(theme)}
>
<ListItemIcon sx={styles.listItemIconStyles}>
<MoreVertIcon sx={styles.getIconColorStyles(theme)} />
</ListItemIcon>
<ListItemText primary={t('common.more')} primaryTypographyProps={styles.listItemTextStyles} />
{expandedMenu === 'more' ? <ExpandLessIcon /> : <ExpandMoreIcon />}
</ListItemButton>
</ListItem>
<Collapse in={expandedMenu === 'more'} timeout="auto" unmountOnExit id="more-submenu">
<List component="div" disablePadding sx={styles.getDrawerSubmenuContainerStyles(theme)}>
<ListItemButton
sx={styles.getDrawerSubmenuItemStyles(theme)}
component={Link}
href={`/projects/${currentProject}/settings`}
onClick={toggleDrawer}
>
<ListItemIcon sx={styles.smallListItemIconStyles}>
<SettingsOutlinedIcon fontSize="small" />
</ListItemIcon>
<ListItemText primary={t('settings.title')} primaryTypographyProps={styles.smallListItemTextStyles} />
</ListItemButton>
<ListItemButton
sx={styles.getDrawerSubmenuItemStyles(theme)}
component={Link}
href={`/projects/${currentProject}/playground`}
onClick={toggleDrawer}
>
<ListItemIcon sx={styles.smallListItemIconStyles}>
<ScienceOutlinedIcon fontSize="small" />
</ListItemIcon>
<ListItemText primary={t('playground.title')} primaryTypographyProps={styles.smallListItemTextStyles} />
</ListItemButton>
<ListItemButton
sx={styles.getDrawerSubmenuItemStyles(theme)}
component={Link}
href="/dataset-square"
onClick={toggleDrawer}
>
<ListItemIcon sx={styles.smallListItemIconStyles}>
<StorageIcon fontSize="small" />
</ListItemIcon>
<ListItemText
primary={t('datasetSquare.title')}
primaryTypographyProps={styles.smallListItemTextStyles}
/>
</ListItemButton>
</List>
</Collapse>
{/* Utilities Section */}
<Box sx={styles.getDrawerUtilitiesStyles(theme)}>
<ListItem disablePadding sx={{ mb: 0.5 }}>
<ListItemButton
component="a"
href={
i18n.language === 'zh-CN' ? 'https://docs.easy-dataset.com/' : 'https://docs.easy-dataset.com/ed/en'
}
target="_blank"
rel="noopener noreferrer"
onClick={toggleDrawer}
sx={styles.getDrawerListItemButtonStyles(theme)}
>
<ListItemIcon sx={styles.listItemIconStyles}>
<HelpOutlineIcon sx={styles.getIconColorStyles(theme)} />
</ListItemIcon>
<ListItemText
primary={t('common.documentation', 'Documentation')}
primaryTypographyProps={styles.listItemTextStyles}
/>
</ListItemButton>
</ListItem>
<ListItem disablePadding sx={{ mb: 0.5 }}>
<ListItemButton
onClick={() => {
window.open('https://github.com/ConardLi/easy-dataset', '_blank');
toggleDrawer();
}}
sx={styles.getDrawerListItemButtonStyles(theme)}
>
<ListItemIcon sx={styles.listItemIconStyles}>
<GitHubIcon sx={styles.getIconColorStyles(theme)} />
</ListItemIcon>
<ListItemText
primary={t('common.viewOnGitHub', 'View on GitHub')}
primaryTypographyProps={styles.listItemTextStyles}
/>
</ListItemButton>
</ListItem>
<ListItem disablePadding sx={{ mb: 1 }}>
<Box sx={{ px: 1, width: '100%' }}>
<UpdateChecker />
</Box>
</ListItem>
</Box>
</List>
</Drawer>
);
}

View File

@@ -0,0 +1,139 @@
'use client';
import React from 'react';
import { Box, Tabs, Tab } from '@mui/material';
import { useTranslation } from 'react-i18next';
import Link from 'next/link';
import DescriptionOutlinedIcon from '@mui/icons-material/DescriptionOutlined';
import TokenOutlinedIcon from '@mui/icons-material/TokenOutlined';
import QuestionAnswerOutlinedIcon from '@mui/icons-material/QuestionAnswerOutlined';
import DatasetOutlinedIcon from '@mui/icons-material/DatasetOutlined';
import AssessmentOutlinedIcon from '@mui/icons-material/AssessmentOutlined';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
import * as styles from './styles';
/**
* NavigationTabs 组件
* 桌面端导航 Tabs包含数据源、数据蒸馏、问题管理、数据集管理、更多等 Tab
*/
export default function NavigationTabs({
theme,
pathname,
currentProject,
handleMenuOpen,
handleMenuClose,
onNavigateStart
}) {
const { t } = useTranslation();
// 计算当前 Tab 值
const getCurrentTabValue = () => {
if (pathname.includes('/settings') || pathname.includes('/playground') || pathname.includes('/datasets-sq')) {
return 'more';
}
if (pathname.includes('/eval-datasets') || pathname.includes('/eval-tasks')) {
return 'eval';
}
if (pathname.includes('/datasets') || pathname.includes('/multi-turn') || pathname.includes('/image-datasets')) {
return 'datasets';
}
if (pathname.includes('/text-split') || pathname.includes('/images')) {
return 'source';
}
return pathname;
};
return (
<Box sx={styles.navContainerStyles}>
<Tabs
value={getCurrentTabValue()}
textColor="inherit"
indicatorColor="secondary"
variant="scrollable"
scrollButtons="auto"
allowScrollButtonsMobile
sx={styles.getTabsStyles(theme)}
>
<Tab
icon={<DescriptionOutlinedIcon fontSize="small" />}
iconPosition="start"
label={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.25 }}>
{t('common.dataSource')}
<ArrowDropDownIcon fontSize="small" sx={{ ml: 0.25 }} />
</Box>
}
value="source"
onMouseEnter={e => handleMenuOpen(e, 'source')}
sx={styles.tabIconWrapperStyles}
/>
<Tab
icon={<TokenOutlinedIcon fontSize="small" />}
iconPosition="start"
label={t('distill.title')}
value={`/projects/${currentProject}/distill`}
component={Link}
href={`/projects/${currentProject}/distill`}
onClick={() => {
onNavigateStart?.();
handleMenuClose();
}}
sx={styles.tabIconWrapperStyles}
/>
<Tab
icon={<QuestionAnswerOutlinedIcon fontSize="small" />}
iconPosition="start"
label={t('questions.title')}
value={`/projects/${currentProject}/questions`}
component={Link}
href={`/projects/${currentProject}/questions`}
onClick={() => {
onNavigateStart?.();
handleMenuClose();
}}
sx={styles.tabIconWrapperStyles}
/>
<Tab
icon={<DatasetOutlinedIcon fontSize="small" />}
iconPosition="start"
label={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.25 }}>
{t('datasets.management')}
<ArrowDropDownIcon fontSize="small" sx={{ ml: 0.25 }} />
</Box>
}
value="datasets"
onMouseEnter={e => handleMenuOpen(e, 'dataset')}
sx={styles.tabIconWrapperStyles}
/>
<Tab
icon={<AssessmentOutlinedIcon fontSize="small" />}
iconPosition="start"
label={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.25 }}>
{t('eval.title')}
<ArrowDropDownIcon fontSize="small" sx={{ ml: 0.25 }} />
</Box>
}
value="eval"
onMouseEnter={e => handleMenuOpen(e, 'eval')}
sx={styles.tabIconWrapperStyles}
/>
<Tab
icon={<MoreVertIcon fontSize="small" />}
iconPosition="start"
label={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.25 }}>
{t('common.more')}
<ArrowDropDownIcon fontSize="small" sx={{ ml: 0.25 }} />
</Box>
}
onMouseEnter={e => handleMenuOpen(e, 'more')}
value="more"
sx={styles.tabIconWrapperStyles}
/>
</Tabs>
</Box>
);
}

View File

@@ -0,0 +1,247 @@
/**
* ContextBar 组件样式
* 包含项目选择器和模型选择器的所有样式
*/
import { alpha } from '@mui/material';
// ===== 主容器样式 =====
export const getContextBarPaperStyles = theme => ({
position: 'absolute',
top: 64, // Below navbar
left: 0,
zIndex: 1100,
borderBottom: 1,
borderColor: 'divider',
bgcolor:
theme.palette.mode === 'dark'
? alpha(theme.palette.background.paper, 0.9)
: alpha(theme.palette.background.paper, 0.95),
backdropFilter: 'blur(16px)',
WebkitBackdropFilter: 'blur(16px)',
px: { xs: 2, sm: 3, md: 4 },
py: { xs: 1.25, sm: 1.5 },
transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
boxShadow: theme.palette.mode === 'dark' ? '0 1px 3px rgba(0, 0, 0, 0.2)' : '0 1px 3px rgba(0, 0, 0, 0.08)',
width: 'auto'
});
export const contextBarContainerStyles = {
display: 'flex',
alignItems: 'center',
gap: { xs: 1, sm: 1.5, md: 2 },
flexWrap: 'nowrap',
width: 'auto'
};
// ===== 选择器容器样式 =====
export const selectorContainerStyles = {
display: 'flex',
alignItems: 'center',
gap: 1
};
// ===== 标签样式 =====
export const labelTypographyStyles = {
color: 'text.secondary',
fontWeight: 600,
textTransform: 'uppercase',
letterSpacing: '0.5px',
fontSize: '0.7rem',
display: { xs: 'none', sm: 'block' }
};
// ===== Chip 内部文本样式 =====
export const chipLabelBoxStyles = {
display: 'flex',
alignItems: 'center',
gap: 0.5
};
export const chipTextStyles = {
fontWeight: 600,
fontSize: { xs: '0.8rem', sm: '0.875rem' },
maxWidth: { xs: '80px', sm: '120px', md: '150px' },
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis'
};
export const chipArrowStyles = {
ml: -0.25,
flexShrink: 0
};
// ===== 项目选择器 Chip 样式 =====
export const getProjectChipStyles = theme => ({
minWidth: 'auto',
maxWidth: { xs: '120px', sm: '150px', md: '180px' },
height: { xs: 32, sm: 36 },
minWidth: { xs: 120, sm: 150, md: 180 },
maxWidth: { xs: '120px', sm: '150px', md: '180px' },
borderRadius: 1.5,
borderColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.23)' : 'rgba(0, 0, 0, 0.23)',
bgcolor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 0, 0, 0.02)',
transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
'&:hover': {
borderColor: 'primary.main',
bgcolor: theme.palette.mode === 'dark' ? 'rgba(144, 202, 249, 0.08)' : 'rgba(25, 118, 210, 0.04)',
transform: 'translateY(-1px)',
boxShadow:
theme.palette.mode === 'dark' ? '0 4px 12px rgba(144, 202, 249, 0.15)' : '0 4px 12px rgba(25, 118, 210, 0.15)'
},
'&:active': {
transform: 'translateY(0)'
},
'&:focus-visible': {
outline: `2px solid ${theme.palette.primary.main}`,
outlineOffset: 2
},
'& .MuiChip-icon': {
color: 'text.primary',
fontSize: '1.1rem',
ml: 0.5,
flexShrink: 0
},
'& .MuiChip-label': {
px: 1,
overflow: 'hidden'
}
});
// ===== 模型选择器 Chip 样式 =====
export const getModelChipStyles = theme => ({
minWidth: { xs: 140, sm: 160, md: 180 },
maxWidth: { xs: 200, sm: 280, md: 360 },
height: { xs: 36, sm: 40 },
borderRadius: 2,
bgcolor: theme.palette.mode === 'dark' ? 'rgba(144, 202, 249, 0.08)' : 'rgba(25, 118, 210, 0.04)',
transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
'&:hover': {
bgcolor: theme.palette.mode === 'dark' ? 'rgba(144, 202, 249, 0.15)' : 'rgba(25, 118, 210, 0.08)',
transform: 'translateY(-1px)',
boxShadow:
theme.palette.mode === 'dark' ? '0 4px 12px rgba(144, 202, 249, 0.25)' : '0 4px 12px rgba(25, 118, 210, 0.25)'
},
'&:active': {
transform: 'translateY(0)'
},
'&:focus-visible': {
outline: `2px solid ${theme.palette.primary.main}`,
outlineOffset: 2
},
'& .MuiChip-icon': {
color: 'primary.main',
fontSize: '1.1rem',
ml: 0.5,
flexShrink: 0
},
'& .MuiChip-label': {
px: 1,
overflow: 'hidden'
}
});
// ===== 菜单样式 =====
export const getMenuPaperStyles = theme => ({
mt: 1,
minWidth: 240,
maxWidth: 400,
maxHeight: 400,
borderRadius: 2,
overflow: 'visible',
bgcolor: theme.palette.mode === 'dark' ? 'background.paper' : 'background.paper',
backdropFilter: 'blur(20px)',
WebkitBackdropFilter: 'blur(20px)',
boxShadow:
theme.palette.mode === 'dark'
? '0 12px 40px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.1)'
: '0 12px 40px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(0, 0, 0, 0.05)',
'&::before': {
content: '""',
display: 'block',
position: 'absolute',
top: -6,
left: 24,
width: 12,
height: 12,
bgcolor: 'background.paper',
transform: 'translateY(-50%) rotate(45deg)',
zIndex: 0,
borderLeft: `1px solid ${theme.palette.divider}`,
borderTop: `1px solid ${theme.palette.divider}`
}
});
export const menuListPropsStyles = {
dense: false,
sx: { py: 1 }
};
// ===== 菜单标题样式 =====
export const menuHeaderTypographyStyles = {
px: 2,
py: 1,
display: 'block',
color: 'text.secondary',
fontWeight: 600,
textTransform: 'uppercase',
letterSpacing: '0.5px',
fontSize: '0.7rem'
};
// ===== 菜单项样式 =====
export const getMenuItemStyles = theme => ({
mx: 1,
borderRadius: 1.5,
minHeight: 44,
py: 1.25,
px: 1.5,
transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
'&:hover': {
bgcolor: theme.palette.mode === 'dark' ? 'rgba(144, 202, 249, 0.08)' : 'rgba(25, 118, 210, 0.04)',
transform: 'translateX(4px)'
},
'&.Mui-selected': {
bgcolor: theme.palette.mode === 'dark' ? 'rgba(144, 202, 249, 0.16)' : 'rgba(25, 118, 210, 0.08)',
'&:hover': {
bgcolor: theme.palette.mode === 'dark' ? 'rgba(144, 202, 249, 0.24)' : 'rgba(25, 118, 210, 0.12)'
}
}
});
export const menuItemIconStyles = {
minWidth: 36
};
export const getMenuItemTextPrimaryProps = isSelected => ({
variant: 'body2',
fontWeight: isSelected ? 600 : 400
});
export const menuItemTextSecondaryProps = {
variant: 'caption',
sx: { fontSize: '0.7rem' }
};
// ===== 模型图标样式 =====
export const modelIconStyles = {
width: 20,
height: 20,
objectFit: 'contain',
flexShrink: 0,
borderRadius: '50%',
mr: 1
};
// ===== 分组标题样式 =====
export const getProviderHeaderStyles = theme => ({
pl: 2,
color: theme.palette.text.secondary,
fontWeight: 600,
fontSize: '0.75rem',
textTransform: 'uppercase',
letterSpacing: '0.5px',
mt: 1,
mb: 0.5
});

View File

@@ -0,0 +1,257 @@
'use client';
import React, { useState, useRef, useEffect } from 'react';
import {
AppBar,
Toolbar,
Box,
IconButton,
useTheme as useMuiTheme,
Tooltip,
useMediaQuery,
LinearProgress
} from '@mui/material';
import { useTranslation } from 'react-i18next';
import { usePathname, useRouter } from 'next/navigation';
import { useTheme } from 'next-themes';
import MenuIcon from '@mui/icons-material/Menu';
// 样式
import * as styles from './styles';
// 子组件
import Logo from './Logo';
import ActionButtons from './ActionButtons';
import NavigationTabs from './NavigationTabs';
import MobileDrawer from './MobileDrawer';
import DesktopMenus from './DesktopMenus';
import ContextBar from './ContextBar';
export default function Navbar({ projects = [], currentProject }) {
const { t } = useTranslation();
const pathname = usePathname();
const router = useRouter();
const theme = useMuiTheme();
const { resolvedTheme, setTheme } = useTheme();
const isProjectDetail = pathname.includes('/projects/') && pathname.split('/').length > 3;
// 检测移动设备
const isMobile = useMediaQuery(theme.breakpoints.down('lg'));
// 移动端抽屉状态
const [drawerOpen, setDrawerOpen] = useState(false);
const [expandedMenu, setExpandedMenu] = useState(null);
// 桌面端菜单状态
const [menuState, setMenuState] = useState({ anchorEl: null, menuType: null });
const [navLoading, setNavLoading] = useState(false);
const navLoadingTimeoutRef = useRef(null);
// ContextBar 悬浮状态
const [contextBarHovered, setContextBarHovered] = useState(false);
const contextTriggerRef = useRef(null);
const contextBarRef = useRef(null);
useEffect(() => {
if (!contextBarHovered) return;
const handleOutsideClick = event => {
if (contextBarRef.current?.contains(event.target)) return;
if (contextTriggerRef.current?.contains(event.target)) return;
const projectMenuEl = document.getElementById('project-menu');
if (projectMenuEl?.contains(event.target)) return;
setContextBarHovered(false);
};
document.addEventListener('pointerdown', handleOutsideClick, true);
return () => {
document.removeEventListener('pointerdown', handleOutsideClick, true);
};
}, [contextBarHovered]);
useEffect(() => {
if (!menuState.menuType) return;
const handleOutsideMenuClick = event => {
if (menuState.anchorEl?.contains(event.target)) return;
if (event.target?.closest?.('.MuiMenu-root')) return;
setMenuState({ anchorEl: null, menuType: null });
};
document.addEventListener('pointerdown', handleOutsideMenuClick, true);
return () => {
document.removeEventListener('pointerdown', handleOutsideMenuClick, true);
};
}, [menuState.anchorEl, menuState.menuType]);
useEffect(() => {
setNavLoading(false);
if (navLoadingTimeoutRef.current) {
clearTimeout(navLoadingTimeoutRef.current);
navLoadingTimeoutRef.current = null;
}
}, [pathname]);
useEffect(() => {
if (!isProjectDetail || !currentProject) return;
const prefetchRoutes = [
`/projects/${currentProject}/multi-turn`,
`/projects/${currentProject}/eval-datasets`,
`/projects/${currentProject}/eval-tasks`
];
prefetchRoutes.forEach(route => router.prefetch(route));
}, [router, currentProject, isProjectDetail]);
useEffect(() => {
return () => {
if (navLoadingTimeoutRef.current) {
clearTimeout(navLoadingTimeoutRef.current);
}
};
}, []);
const handleNavigateStart = () => {
setNavLoading(true);
if (navLoadingTimeoutRef.current) {
clearTimeout(navLoadingTimeoutRef.current);
}
navLoadingTimeoutRef.current = setTimeout(() => {
setNavLoading(false);
navLoadingTimeoutRef.current = null;
}, 12000);
};
const handleMenuOpen = (event, menuType) => {
setMenuState({ anchorEl: event.currentTarget, menuType });
};
const handleMenuClose = () => {
setMenuState({ anchorEl: null, menuType: null });
};
const isMenuOpen = menuType => menuState.menuType === menuType;
const toggleDrawer = () => {
setDrawerOpen(!drawerOpen);
setExpandedMenu(null);
};
const toggleMobileSubmenu = menuType => {
setExpandedMenu(expandedMenu === menuType ? null : menuType);
};
const toggleTheme = () => {
setTheme(resolvedTheme === 'dark' ? 'light' : 'dark');
};
return (
<>
<AppBar
component="nav"
position="sticky"
elevation={0}
color={theme.palette.mode === 'dark' ? 'transparent' : 'primary'}
sx={styles.getAppBarStyles(theme)}
style={{ borderRadius: 0, zIndex: 1200 }}
role="navigation"
aria-label={t('common.mainNavigation', 'Main navigation')}
>
<Toolbar sx={styles.toolbarStyles}>
{/* 左侧: 汉堡菜单(移动端) + Logo */}
<Box
ref={contextTriggerRef}
sx={styles.logoContainerStyles}
onMouseEnter={() => isProjectDetail && setContextBarHovered(true)}
>
{/* 汉堡菜单按钮 */}
{isProjectDetail && isMobile && (
<Tooltip title={t('common.menu', 'Menu')} placement="bottom">
<IconButton
onClick={toggleDrawer}
size="medium"
aria-label={t('common.openMenu', 'Open navigation menu')}
aria-expanded={drawerOpen}
aria-controls="mobile-navigation-drawer"
sx={styles.getHamburgerButtonStyles(theme)}
>
<MenuIcon />
</IconButton>
</Tooltip>
)}
{/* Logo 组件 */}
<Logo theme={theme} />
</Box>
{/* 中间导航 - 仅桌面端 */}
{isProjectDetail && !isMobile && (
<NavigationTabs
theme={theme}
pathname={pathname}
currentProject={currentProject}
handleMenuOpen={handleMenuOpen}
handleMenuClose={handleMenuClose}
onNavigateStart={handleNavigateStart}
/>
)}
{/* 右侧操作区 */}
<ActionButtons
theme={theme}
resolvedTheme={resolvedTheme}
toggleTheme={toggleTheme}
isProjectDetail={isProjectDetail}
currentProject={currentProject}
onActionAreaEnter={!isMobile ? handleMenuClose : undefined}
/>
</Toolbar>
{isProjectDetail && (
<LinearProgress
color="secondary"
sx={{
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
height: 2,
opacity: navLoading ? 1 : 0,
transition: 'opacity 180ms ease'
}}
/>
)}
</AppBar>
{/* ContextBar - 在 Logo 或 ContextBar 悬浮时展示 */}
{isProjectDetail && contextBarHovered && (
<Box ref={contextBarRef} onMouseLeave={() => setContextBarHovered(false)}>
<ContextBar
projects={projects}
currentProjectId={currentProject}
onMouseLeave={() => setContextBarHovered(false)}
/>
</Box>
)}
{/* 移动端抽屉组件 */}
<MobileDrawer
theme={theme}
drawerOpen={drawerOpen}
toggleDrawer={toggleDrawer}
expandedMenu={expandedMenu}
toggleMobileSubmenu={toggleMobileSubmenu}
currentProject={currentProject}
onNavigateStart={handleNavigateStart}
/>
{/* 桌面端菜单组件 */}
<DesktopMenus
theme={theme}
menuState={menuState}
isMenuOpen={isMenuOpen}
handleMenuClose={handleMenuClose}
currentProject={currentProject}
onNavigateStart={handleNavigateStart}
/>
</>
);
}

View File

@@ -0,0 +1,374 @@
/**
* Navbar 组件样式配置
*/
// AppBar 样式
export const getAppBarStyles = theme => ({
borderBottom: `1px solid ${theme.palette.divider}`,
bgcolor: theme.palette.mode === 'dark' ? 'background.paper' : 'primary.main',
backdropFilter: 'blur(20px)',
WebkitBackdropFilter: 'blur(20px)',
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
boxShadow: theme.palette.mode === 'dark' ? '0 1px 3px rgba(0, 0, 0, 0.3)' : '0 1px 3px rgba(0, 0, 0, 0.1)'
});
// Toolbar 样式
export const toolbarStyles = {
height: '64px',
minHeight: '64px !important',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
px: { xs: 2, sm: 2, md: 3 },
gap: 2
};
// Logo 容器样式
export const logoContainerStyles = {
display: 'flex',
alignItems: 'center',
gap: 1.5,
flexShrink: 0
};
// 汉堡菜单按钮样式
export const getHamburgerButtonStyles = theme => ({
color: theme.palette.mode === 'dark' ? 'inherit' : 'white',
minWidth: 44,
minHeight: 44,
transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
'&:hover': {
transform: 'scale(1.1)',
bgcolor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.08)' : 'rgba(255, 255, 255, 0.15)'
},
'&:active': {
transform: 'scale(0.95)'
},
'&:focus-visible': {
outline: `2px solid ${theme.palette.mode === 'dark' ? theme.palette.secondary.main : 'white'}`,
outlineOffset: 2
}
});
// Logo 链接样式
export const getLogoLinkStyles = theme => ({
display: 'flex',
alignItems: 'center',
cursor: 'pointer',
textDecoration: 'none',
transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
borderRadius: 1.5,
px: 0.5,
'&:hover': {
opacity: 0.85,
transform: 'translateY(-1px)'
},
'&:active': {
transform: 'translateY(0)'
},
'&:focus-visible': {
outline: `2px solid ${theme.palette.mode === 'dark' ? theme.palette.secondary.main : 'white'}`,
outlineOffset: 2
}
});
// Logo 图片样式
export const logoImageStyles = {
width: 32,
height: 32,
mr: 1.5,
transition: 'transform 0.2s ease'
};
// Logo 文字样式
export const getLogoTextStyles = theme => ({
fontWeight: 700,
letterSpacing: '-0.5px',
fontSize: '1.125rem',
display: { xs: 'none', md: 'block' },
color: 'white',
...(theme.palette.mode === 'dark' && {
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
backgroundClip: 'text'
})
});
// 中间导航容器样式
export const navContainerStyles = {
flexGrow: 1,
display: 'flex',
justifyContent: 'center',
mx: { lg: 1, xl: 3 },
overflow: 'hidden'
};
// Tabs 样式
export const getTabsStyles = theme => ({
minHeight: '64px',
'& .MuiTab-root': {
minWidth: 100,
maxWidth: 180,
fontSize: '0.875rem',
fontWeight: 500,
transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
color: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.7)' : 'rgba(255, 255, 255, 1)',
px: 2,
minHeight: '64px',
textTransform: 'none',
letterSpacing: '0.3px',
'&:hover': {
color: 'white',
bgcolor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.08)' : 'rgba(255, 255, 255, 0.15)'
}
},
'& .Mui-selected': {
color: 'white !important',
fontWeight: 600,
bgcolor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.12)' : 'rgba(255, 255, 255, 0.2)'
},
'& .MuiTabs-indicator': {
height: 3,
borderRadius: '3px 3px 0 0',
backgroundColor: theme.palette.mode === 'dark' ? theme.palette.secondary.main : 'white',
boxShadow: theme.palette.mode === 'dark' ? '0 0 8px rgba(103, 126, 234, 0.5)' : '0 0 8px rgba(255, 255, 255, 0.5)'
}
});
// Tab 图标包装器样式
export const tabIconWrapperStyles = {
'& .MuiTab-iconWrapper': { mr: 1 }
};
// 右侧操作区容器样式
export const actionAreaStyles = {
display: 'flex',
alignItems: 'center',
gap: 1,
flexShrink: 0
};
// 文档/GitHub 按钮样式
export const getIconButtonStyles = theme => ({
display: { xs: 'none', xl: 'flex' },
bgcolor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.08)' : 'rgba(255, 255, 255, 0.2)',
color: theme.palette.mode === 'dark' ? 'inherit' : 'white',
borderRadius: 1.5,
transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
'&:hover': {
bgcolor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.15)' : 'rgba(255, 255, 255, 0.35)'
},
'&:focus-visible': {
outline: `2px solid ${theme.palette.mode === 'dark' ? theme.palette.secondary.main : 'white'}`,
outlineOffset: 2
}
});
// Drawer Paper 样式
export const getDrawerPaperStyles = theme => ({
width: { xs: '85vw', sm: 320 },
maxWidth: 380,
bgcolor: theme.palette.mode === 'dark' ? 'background.paper' : 'background.default',
backgroundImage:
theme.palette.mode === 'dark' ? 'linear-gradient(rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.05))' : 'none',
boxShadow: theme.palette.mode === 'dark' ? '0 8px 32px rgba(0, 0, 0, 0.6)' : '0 8px 32px rgba(0, 0, 0, 0.15)'
});
// Drawer 头部样式
export const getDrawerHeaderStyles = theme => ({
p: 2.5,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
borderBottom: `1px solid ${theme.palette.divider}`,
minHeight: 64
});
// Drawer 关闭按钮样式
export const getDrawerCloseButtonStyles = theme => ({
minWidth: 44,
minHeight: 44,
transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
'&:hover': {
transform: 'rotate(90deg)',
bgcolor: 'action.hover'
},
'&:focus-visible': {
outline: `2px solid ${theme.palette.primary.main}`,
outlineOffset: 2
}
});
// Drawer 列表样式
export const drawerListStyles = {
pt: 1,
px: 1
};
// Drawer 列表项按钮样式
export const getDrawerListItemButtonStyles = theme => ({
borderRadius: '8px',
minHeight: 48,
transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
'&:hover': {
bgcolor: theme.palette.mode === 'dark' ? 'rgba(103, 126, 234, 0.12)' : 'rgba(103, 126, 234, 0.08)'
},
'&:focus-visible': {
outline: `2px solid ${theme.palette.primary.main}`,
outlineOffset: -2
}
});
// Drawer 子菜单容器样式
export const getDrawerSubmenuContainerStyles = theme => ({
bgcolor: theme.palette.mode === 'dark' ? 'rgba(0, 0, 0, 0.2)' : 'rgba(0, 0, 0, 0.02)',
borderRadius: '8px',
my: 0.5
});
// Drawer 子菜单项样式
export const getDrawerSubmenuItemStyles = theme => ({
pl: 4,
mx: 1,
borderRadius: '8px',
minHeight: 44,
py: 1.5,
transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
'&:hover': {
bgcolor: theme.palette.mode === 'dark' ? 'rgba(103, 126, 234, 0.08)' : 'rgba(103, 126, 234, 0.05)'
},
'&:focus-visible': {
outline: `2px solid ${theme.palette.primary.main}`,
outlineOffset: -2
}
});
// Drawer 工具区域样式
export const getDrawerUtilitiesStyles = theme => ({
mt: 'auto',
pt: 2,
borderTop: `1px solid ${theme.palette.divider}`
});
// Menu Paper 样式
export const getMenuPaperStyles = theme => ({
mt: 1.5,
borderRadius: '12px',
minWidth: 220,
overflow: 'visible',
bgcolor: theme.palette.mode === 'dark' ? 'rgba(30, 30, 30, 0.98)' : 'rgba(255, 255, 255, 0.98)',
backdropFilter: 'blur(20px)',
WebkitBackdropFilter: 'blur(20px)',
boxShadow:
theme.palette.mode === 'dark'
? '0 12px 40px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(255, 255, 255, 0.1)'
: '0 12px 40px rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(0, 0, 0, 0.05)',
'&::before': {
content: '""',
display: 'block',
position: 'absolute',
top: 0,
right: '50%',
width: 12,
height: 12,
bgcolor: theme.palette.mode === 'dark' ? 'rgba(30, 30, 30, 0.98)' : 'rgba(255, 255, 255, 0.98)',
transform: 'translateY(-50%) translateX(50%) rotate(45deg)',
zIndex: 0,
boxShadow: theme.palette.mode === 'dark' ? '-2px -2px 4px rgba(0, 0, 0, 0.3)' : '-2px -2px 4px rgba(0, 0, 0, 0.1)'
}
});
// Menu 列表样式
export const menuListStyles = {
py: 1.5
};
// Menu 项样式
export const getMenuItemStyles = theme => ({
mx: 1,
borderRadius: '8px',
py: 1.25,
minHeight: 44,
transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
'&:hover': {
bgcolor: theme.palette.mode === 'dark' ? 'rgba(103, 126, 234, 0.15)' : 'rgba(103, 126, 234, 0.1)',
transform: 'translateX(4px)'
},
'&:focus-visible': {
outline: `2px solid ${theme.palette.primary.main}`,
outlineOffset: -2
}
});
// Dataset/More Menu Paper 样式(简化版)
export const getSimpleMenuPaperStyles = theme => ({
mt: 1.5,
borderRadius: '12px',
minWidth: 220,
overflow: 'visible',
bgcolor: theme.palette.mode === 'dark' ? 'rgba(30, 30, 30, 0.98)' : 'rgba(255, 255, 255, 0.98)',
backdropFilter: 'blur(20px)',
boxShadow:
theme.palette.mode === 'dark'
? '0 8px 32px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.1)'
: '0 8px 32px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(0, 0, 0, 0.05)',
'&::before': {
content: '""',
display: 'block',
position: 'absolute',
top: 0,
right: '50%',
width: 12,
height: 12,
bgcolor: theme.palette.mode === 'dark' ? 'rgba(30, 30, 30, 0.98)' : 'rgba(255, 255, 255, 0.98)',
transform: 'translateY(-50%) translateX(50%) rotate(45deg)',
zIndex: 0
}
});
// 简化 Menu 列表样式
export const simpleMenuListStyles = {
py: 1
};
// 简化 Menu 项样式
export const getSimpleMenuItemStyles = theme => ({
mx: 0.75,
borderRadius: '8px',
py: 1,
transition: 'all 0.15s ease',
'&:hover': {
bgcolor: theme.palette.mode === 'dark' ? 'rgba(103, 126, 234, 0.15)' : 'rgba(103, 126, 234, 0.1)',
transform: 'translateX(4px)'
}
});
// ListItemIcon 样式
export const listItemIconStyles = {
minWidth: 40
};
export const smallListItemIconStyles = {
minWidth: 36
};
// ListItemText 样式
export const listItemTextStyles = {
fontWeight: 600,
fontSize: '0.95rem'
};
export const smallListItemTextStyles = {
fontSize: '0.9rem',
fontWeight: 500
};
// 图标颜色样式
export const getIconColorStyles = theme => ({
color: theme.palette.mode === 'dark' ? 'primary.light' : 'primary.main'
});
export const getPrimaryIconColorStyles = theme => ({
color: theme.palette.primary.main
});