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,33 @@
'use client';
import { Box, Button } from '@mui/material';
import AssessmentIcon from '@mui/icons-material/Assessment';
import FileUploadIcon from '@mui/icons-material/FileUpload';
import FileDownloadIcon from '@mui/icons-material/FileDownload';
import { useTranslation } from 'react-i18next';
const ActionBar = ({ onBatchEvaluate, onImport, onExport, batchEvaluating = false }) => {
const { t } = useTranslation();
return (
<Box sx={{ display: 'flex', gap: 2 }}>
<Button
variant="outlined"
startIcon={<AssessmentIcon />}
sx={{ borderRadius: 2 }}
onClick={onBatchEvaluate}
disabled={batchEvaluating}
>
{batchEvaluating ? t('datasets.evaluating', '评估中...') : t('datasets.batchEvaluate', '批量评估')}
</Button>
<Button variant="outlined" startIcon={<FileUploadIcon />} sx={{ borderRadius: 2 }} onClick={onImport}>
{t('import.title', '导入')}
</Button>
<Button variant="outlined" startIcon={<FileDownloadIcon />} sx={{ borderRadius: 2 }} onClick={onExport}>
{t('export.title')}
</Button>
</Box>
);
};
export default ActionBar;

View File

@@ -0,0 +1,422 @@
'use client';
import {
Box,
Typography,
IconButton,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Chip,
Divider,
useTheme,
alpha,
Tooltip,
Checkbox,
TablePagination,
TextField,
Card,
CircularProgress
} from '@mui/material';
import DeleteIcon from '@mui/icons-material/Delete';
import VisibilityIcon from '@mui/icons-material/Visibility';
import AssessmentIcon from '@mui/icons-material/Assessment';
import StarIcon from '@mui/icons-material/Star';
import { useTranslation } from 'react-i18next';
import { getRatingConfigI18n, formatScore } from '@/components/datasets/utils/ratingUtils';
// 数据集列表组件
const DatasetList = ({
datasets,
onViewDetails,
onDelete,
onEvaluate,
page,
rowsPerPage,
onPageChange,
onRowsPerPageChange,
total,
selectedIds,
onSelectAll,
onSelectItem,
evaluatingIds = [],
loading = false
}) => {
const theme = useTheme();
const { t } = useTranslation();
const bgColor = theme.palette.mode === 'dark' ? theme.palette.primary.dark : theme.palette.primary.light;
const color =
theme.palette.mode === 'dark'
? theme.palette.getContrastText(theme.palette.primary.main)
: theme.palette.getContrastText(theme.palette.primary.contrastText);
const RatingChip = ({ score }) => {
const config = getRatingConfigI18n(score, t);
return (
<Chip
icon={<StarIcon sx={{ fontSize: '14px !important' }} />}
label={`${formatScore(score)} ${config.label}`}
size="small"
sx={{
backgroundColor: config.backgroundColor,
color: config.color,
fontWeight: 'medium',
'& .MuiChip-icon': {
color: config.color
}
}}
/>
);
};
return (
<Card elevation={2}>
<Box sx={{ position: 'relative' }}>
<TableContainer sx={{ overflowX: 'auto' }}>
<Table sx={{ minWidth: 900 }}>
<TableHead>
<TableRow>
<TableCell
padding="checkbox"
sx={{
backgroundColor: bgColor,
color: color,
borderBottom: `2px solid ${theme.palette.divider}`
}}
>
<Checkbox
color="primary"
indeterminate={selectedIds.length > 0 && selectedIds.length < total}
checked={total > 0 && selectedIds.length === total}
onChange={onSelectAll}
/>
</TableCell>
<TableCell
sx={{
backgroundColor: bgColor,
color: color,
fontWeight: 'bold',
padding: '16px 8px',
borderBottom: `2px solid ${theme.palette.divider}`,
minWidth: 200
}}
>
{t('datasets.question')}
</TableCell>
<TableCell
sx={{
backgroundColor: bgColor,
color: color,
fontWeight: 'bold',
padding: '16px 8px',
borderBottom: `2px solid ${theme.palette.divider}`,
width: 120
}}
>
{t('datasets.rating', '评分')}
</TableCell>
<TableCell
sx={{
backgroundColor: bgColor,
color: color,
fontWeight: 'bold',
padding: '16px 8px',
borderBottom: `2px solid ${theme.palette.divider}`,
width: 100
}}
>
{t('datasets.model')}
</TableCell>
<TableCell
sx={{
backgroundColor: bgColor,
color: color,
fontWeight: 'bold',
padding: '16px 8px',
borderBottom: `2px solid ${theme.palette.divider}`,
width: 100
}}
>
{t('datasets.domainTag')}
</TableCell>
<TableCell
sx={{
backgroundColor: bgColor,
color: color,
fontWeight: 'bold',
padding: '16px 8px',
borderBottom: `2px solid ${theme.palette.divider}`,
width: 120
}}
>
{t('datasets.createdAt')}
</TableCell>
<TableCell
sx={{
backgroundColor: bgColor,
color: color,
fontWeight: 'bold',
padding: '16px 8px',
borderBottom: `2px solid ${theme.palette.divider}`,
width: 120
}}
>
{t('common.actions')}
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{datasets.map((dataset, index) => (
<>
<TableRow
key={dataset.id}
sx={{
'&:nth-of-type(odd)': { backgroundColor: alpha(theme.palette.primary.light, 0.05) },
'&:hover': { backgroundColor: alpha(theme.palette.primary.light, 0.1) },
cursor: 'pointer'
}}
onClick={() => onViewDetails(dataset.id)}
>
<TableCell
padding="checkbox"
sx={{
borderLeft: `4px solid ${theme.palette.primary.main}`
}}
>
<Checkbox
color="primary"
checked={selectedIds.includes(dataset.id)}
onChange={e => {
e.stopPropagation();
onSelectItem(dataset.id);
}}
onClick={e => e.stopPropagation()}
/>
</TableCell>
<TableCell sx={{ py: 2 }}>
<Box>
<Typography
variant="body2"
fontWeight="medium"
sx={{
overflow: 'hidden',
textOverflow: 'ellipsis',
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
lineHeight: 1.4,
mb: 0.5
}}
>
{dataset.question}
</Typography>
{dataset.confirmed && (
<Chip
label={t('datasets.confirmed')}
size="small"
sx={{
backgroundColor: alpha(theme.palette.success.main, 0.1),
color: theme.palette.success.dark,
fontWeight: 'medium',
height: 20,
fontSize: '0.7rem',
mt: 1
}}
/>
)}
</Box>
</TableCell>
<TableCell>
<RatingChip score={dataset.score || 0} />
</TableCell>
<TableCell>
<Chip
label={dataset.model}
size="small"
sx={{
backgroundColor: alpha(theme.palette.info.main, 0.1),
color: theme.palette.info.dark,
fontWeight: 'medium',
maxWidth: '100%',
'& .MuiChip-label': {
overflow: 'hidden',
textOverflow: 'ellipsis'
}
}}
/>
</TableCell>
<TableCell>
{dataset.questionLabel ? (
<Chip
label={dataset.questionLabel}
size="small"
sx={{
backgroundColor: alpha(theme.palette.primary.main, 0.1),
color: theme.palette.primary.dark,
fontWeight: 'medium',
maxWidth: '100%',
'& .MuiChip-label': {
overflow: 'hidden',
textOverflow: 'ellipsis'
}
}}
/>
) : (
<Typography variant="body2" color="text.disabled" fontSize="0.75rem">
{t('datasets.noTag')}
</Typography>
)}
</TableCell>
<TableCell>
<Typography variant="body2" color="text.secondary" fontSize="0.75rem">
{new Date(dataset.createAt).toLocaleDateString('zh-CN')}
</Typography>
</TableCell>
<TableCell>
<Box sx={{ display: 'flex', gap: 0.5 }}>
<Tooltip title={t('datasets.viewDetails')}>
<IconButton
size="small"
onClick={e => {
e.stopPropagation();
onViewDetails(dataset.id);
}}
sx={{
color: theme.palette.primary.main,
'&:hover': { backgroundColor: alpha(theme.palette.primary.main, 0.1) }
}}
>
<VisibilityIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title={t('datasets.evaluate')}>
<IconButton
size="small"
disabled={evaluatingIds.includes(dataset.id)}
onClick={e => {
e.stopPropagation();
onEvaluate && onEvaluate(dataset);
}}
sx={{
color: theme.palette.secondary.main,
'&:hover': { backgroundColor: alpha(theme.palette.secondary.main, 0.1) }
}}
>
{evaluatingIds.includes(dataset.id) ? (
<CircularProgress size={20} sx={{ color: theme.palette.secondary.main }} />
) : (
<AssessmentIcon fontSize="small" />
)}
</IconButton>
</Tooltip>
<Tooltip title={t('common.delete')}>
<IconButton
size="small"
onClick={e => {
e.stopPropagation();
onDelete(dataset);
}}
sx={{
color: theme.palette.error.main,
'&:hover': { backgroundColor: alpha(theme.palette.error.main, 0.1) }
}}
>
<DeleteIcon fontSize="small" />
</IconButton>
</Tooltip>
</Box>
</TableCell>
</TableRow>
</>
))}
{datasets.length === 0 && (
<TableRow>
<TableCell colSpan={8} align="center" sx={{ py: 6 }}>
<Typography variant="body1" color="text.secondary">
{t('datasets.noData')}
</Typography>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
{loading && (
<Box
sx={{
position: 'absolute',
inset: 0,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: alpha(theme.palette.background.paper, 0.6),
backdropFilter: 'blur(2px)',
zIndex: 1
}}
>
<CircularProgress size={32} />
<Typography variant="body2" sx={{ mt: 1 }} color="text.secondary">
{t('datasets.loading')}
</Typography>
</Box>
)}
</Box>
<Divider />
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
px: 2,
py: 1,
borderTop: `1px solid ${theme.palette.divider}`
}}
>
<TablePagination
component="div"
count={total}
page={page - 1}
onPageChange={onPageChange}
rowsPerPage={rowsPerPage}
onRowsPerPageChange={onRowsPerPageChange}
labelRowsPerPage={t('datasets.rowsPerPage')}
labelDisplayedRows={({ from, to, count }) => t('datasets.pagination', { from, to, count })}
sx={{
'.MuiTablePagination-selectLabel, .MuiTablePagination-displayedRows': {
fontWeight: 'medium'
},
border: 'none'
}}
/>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="body2">{t('common.jumpTo')}:</Typography>
<TextField
size="small"
type="number"
inputProps={{
min: 1,
max: Math.ceil(total / rowsPerPage),
style: { padding: '4px 8px', width: '50px' }
}}
onKeyPress={e => {
if (e.key === 'Enter') {
const pageNum = parseInt(e.target.value, 10);
if (pageNum >= 1 && pageNum <= Math.ceil(total / rowsPerPage)) {
onPageChange(null, pageNum - 1);
e.target.value = '';
}
}
}}
/>
</Box>
</Box>
</Card>
);
};
export default DatasetList;

View File

@@ -0,0 +1,105 @@
'use client';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Typography,
Paper,
Box,
LinearProgress,
Button,
useTheme,
alpha
} from '@mui/material';
import { useTranslation } from 'react-i18next';
const DeleteConfirmDialog = ({ open, datasets, onClose, onConfirm, batch, progress, deleting }) => {
const theme = useTheme();
const { t } = useTranslation();
const dataset = datasets?.[0];
return (
<Dialog
open={open}
onClose={onClose}
PaperProps={{
elevation: 3,
sx: { borderRadius: 2 }
}}
>
<DialogTitle sx={{ pb: 1 }}>
<Typography variant="h6" fontWeight="bold">
{t('common.confirmDelete')}
</Typography>
</DialogTitle>
<DialogContent sx={{ pb: 2, pt: 1 }}>
<Typography variant="body1" sx={{ mb: 2 }}>
{batch
? t('datasets.batchconfirmDeleteMessage', {
count: datasets.length
})
: t('common.confirmDeleteDataSet')}
</Typography>
{batch ? (
''
) : (
<Paper
variant="outlined"
sx={{
p: 2,
backgroundColor: alpha(theme.palette.warning.light, 0.1),
borderColor: theme.palette.warning.light
}}
>
<Typography variant="subtitle2" color="text.secondary" fontWeight="bold">
{t('datasets.question')}
</Typography>
<Typography variant="body2">{dataset?.question}</Typography>
</Paper>
)}
{deleting && progress ? (
<Box sx={{ mb: 3 }}>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
<Typography variant="body1" sx={{ mr: 1 }}>
{progress.percentage}%
</Typography>
<Box sx={{ width: '100%' }}>
<LinearProgress
variant="determinate"
value={progress.percentage}
sx={{
height: 8,
borderRadius: 4,
backgroundColor: alpha(theme.palette.primary.main, 0.1),
'& .MuiLinearProgress-bar': {
borderRadius: 4,
backgroundColor: theme.palette.primary.main
}
}}
/>
</Box>
</Box>
<Typography variant="body2" color="text.secondary">
{t('datasets.deletingProgress', '正在删除 {{completed}}/{{total}} 个数据集...', {
completed: progress.completed,
total: progress.total
})}
</Typography>
</Box>
) : null}
</DialogContent>
<DialogActions sx={{ px: 3, pb: 2 }}>
<Button onClick={onClose} disabled={deleting} sx={{ borderRadius: 2 }}>
{t('common.cancel')}
</Button>
<Button onClick={onConfirm} variant="contained" color="error" disabled={deleting} sx={{ borderRadius: 2 }}>
{deleting ? t('common.deleting') : t('common.delete')}
</Button>
</DialogActions>
</Dialog>
);
};
export default DeleteConfirmDialog;

View File

@@ -0,0 +1,198 @@
'use client';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Box,
Typography,
Select,
MenuItem,
Slider,
TextField,
Button,
InputAdornment
} from '@mui/material';
import SearchIcon from '@mui/icons-material/Search';
import { useTranslation } from 'react-i18next';
const FilterDialog = ({
open,
onClose,
filterConfirmed,
filterHasCot,
filterIsDistill,
filterScoreRange,
filterCustomTag,
filterNoteKeyword,
filterChunkName,
availableTags,
onFilterConfirmedChange,
onFilterHasCotChange,
onFilterIsDistillChange,
onFilterScoreRangeChange,
onFilterCustomTagChange,
onFilterNoteKeywordChange,
onFilterChunkNameChange,
onResetFilters,
onApplyFilters
}) => {
const { t } = useTranslation();
return (
<Dialog open={open} onClose={onClose} maxWidth="xs" fullWidth>
<DialogTitle>{t('datasets.filtersTitle')}</DialogTitle>
<DialogContent>
<Box sx={{ mb: 3, mt: 1 }}>
<Typography variant="subtitle2" gutterBottom>
{t('datasets.filterConfirmationStatus')}
</Typography>
<Select
value={filterConfirmed}
onChange={e => onFilterConfirmedChange(e.target.value)}
fullWidth
size="small"
sx={{ mt: 1 }}
>
<MenuItem value="all">{t('datasets.filterAll')}</MenuItem>
<MenuItem value="confirmed">{t('datasets.filterConfirmed')}</MenuItem>
<MenuItem value="unconfirmed">{t('datasets.filterUnconfirmed')}</MenuItem>
</Select>
</Box>
<Box sx={{ mb: 2 }}>
<Typography variant="subtitle2" gutterBottom>
{t('datasets.filterCotStatus')}
</Typography>
<Select
value={filterHasCot}
onChange={e => onFilterHasCotChange(e.target.value)}
fullWidth
size="small"
sx={{ mt: 1 }}
>
<MenuItem value="all">{t('datasets.filterAll')}</MenuItem>
<MenuItem value="yes">{t('datasets.filterHasCot')}</MenuItem>
<MenuItem value="no">{t('datasets.filterNoCot')}</MenuItem>
</Select>
</Box>
<Box sx={{ mb: 2 }}>
<Typography variant="subtitle2" gutterBottom>
{t('datasets.filterDistill')}
</Typography>
<Select
value={filterIsDistill}
onChange={e => onFilterIsDistillChange(e.target.value)}
fullWidth
size="small"
sx={{ mt: 1 }}
>
<MenuItem value="all">{t('datasets.filterAll')}</MenuItem>
<MenuItem value="yes">{t('datasets.filterDistillYes')}</MenuItem>
<MenuItem value="no">{t('datasets.filterDistillNo')}</MenuItem>
</Select>
</Box>
<Box sx={{ mb: 2 }}>
<Typography variant="subtitle2" gutterBottom>
{t('datasets.filterScoreRange')}
</Typography>
<Box sx={{ px: 1, mt: 2 }}>
<Slider
value={filterScoreRange}
onChange={(_, newValue) => onFilterScoreRangeChange(newValue)}
valueLabelDisplay="auto"
min={0}
max={5}
step={0.5}
marks={[
{ value: 0, label: '0' },
{ value: 2.5, label: '2.5' },
{ value: 5, label: '5' }
]}
sx={{ mt: 1 }}
/>
<Typography variant="caption" color="text.secondary">
{t('datasets.scoreRange', '{{min}} - {{max}} 分', {
min: filterScoreRange[0],
max: filterScoreRange[1]
})}
</Typography>
</Box>
</Box>
<Box sx={{ mb: 2 }}>
<Typography variant="subtitle2" gutterBottom>
{t('datasets.filterCustomTag')}
</Typography>
<Select
value={filterCustomTag}
onChange={e => onFilterCustomTagChange(e.target.value)}
fullWidth
size="small"
sx={{ mt: 1 }}
>
<MenuItem value="">{t('datasets.filterAll')}</MenuItem>
{availableTags.map(tag => (
<MenuItem key={tag} value={tag}>
{tag}
</MenuItem>
))}
</Select>
</Box>
<Box sx={{ mb: 2 }}>
<Typography variant="subtitle2" gutterBottom>
{t('datasets.filterNoteKeyword')}
</Typography>
<TextField
value={filterNoteKeyword}
onChange={e => onFilterNoteKeywordChange(e.target.value)}
placeholder={t('datasets.filterNoteKeywordPlaceholder')}
fullWidth
size="small"
sx={{ mt: 1 }}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon fontSize="small" />
</InputAdornment>
)
}}
/>
</Box>
<Box sx={{ mb: 2 }}>
<Typography variant="subtitle2" gutterBottom>
{t('datasets.filterChunkName')}
</Typography>
<TextField
value={filterChunkName}
onChange={e => onFilterChunkNameChange(e.target.value)}
placeholder={t('datasets.filterChunkNamePlaceholder')}
fullWidth
size="small"
sx={{ mt: 1 }}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon fontSize="small" />
</InputAdornment>
)
}}
/>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={onResetFilters}>{t('datasets.resetFilters')}</Button>
<Button onClick={onApplyFilters} variant="contained">
{t('datasets.applyFilters')}
</Button>
</DialogActions>
</Dialog>
);
};
export default FilterDialog;

View File

@@ -0,0 +1,68 @@
'use client';
import { Box, Paper, IconButton, InputBase, Select, MenuItem, Button, Badge } from '@mui/material';
import SearchIcon from '@mui/icons-material/Search';
import FilterListIcon from '@mui/icons-material/FilterList';
import { useTranslation } from 'react-i18next';
const SearchBar = ({
searchQuery,
searchField,
onSearchQueryChange,
onSearchFieldChange,
onMoreFiltersClick,
activeFilterCount = 0
}) => {
const { t } = useTranslation();
return (
<Box sx={{ display: 'flex', gap: 2 }}>
<Paper
component="form"
sx={{
p: '2px 4px',
display: 'flex',
alignItems: 'center',
width: 400,
borderRadius: 2
}}
>
<IconButton sx={{ p: '10px' }} aria-label="search">
<SearchIcon />
</IconButton>
<InputBase
sx={{ ml: 1, flex: 1 }}
placeholder={t('datasets.searchPlaceholder')}
value={searchQuery}
onChange={e => onSearchQueryChange(e.target.value)}
endAdornment={
<Select
value={searchField}
onChange={e => onSearchFieldChange(e.target.value)}
variant="standard"
sx={{
minWidth: 90,
'& .MuiInput-underline:before': { borderBottom: 'none' },
'& .MuiInput-underline:after': { borderBottom: 'none' },
'& .MuiInput-underline:hover:not(.Mui-disabled):before': { borderBottom: 'none' }
}}
disableUnderline
>
<MenuItem value="question">{t('datasets.fieldQuestion')}</MenuItem>
<MenuItem value="answer">{t('datasets.fieldAnswer')}</MenuItem>
<MenuItem value="cot">{t('datasets.fieldCOT')}</MenuItem>
<MenuItem value="questionLabel">{t('datasets.fieldLabel')}</MenuItem>
</Select>
}
/>
</Paper>
<Badge badgeContent={activeFilterCount} color="error" overlap="circular">
<Button variant="outlined" onClick={onMoreFiltersClick} startIcon={<FilterListIcon />} sx={{ borderRadius: 2 }}>
{t('datasets.moreFilters')}
</Button>
</Badge>
</Box>
);
};
export default SearchBar;