first-update
This commit is contained in:
27
easy-dataset-main/components/tasks/TaskActions.js
Normal file
27
easy-dataset-main/components/tasks/TaskActions.js
Normal file
@@ -0,0 +1,27 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { IconButton, Tooltip } from '@mui/material';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import StopCircleIcon from '@mui/icons-material/StopCircle';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
// 任务操作组件
|
||||
export default function TaskActions({ task, onAbort, onDelete }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// 处理中的任务显示中断按钮,其他状态显示删除按钮
|
||||
return task.status === 0 ? (
|
||||
<Tooltip title={t('tasks.actions.abort')} arrow>
|
||||
<IconButton size="small" onClick={() => onAbort(task.id)}>
|
||||
<StopCircleIcon fontSize="small" color="warning" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip title={t('tasks.actions.delete')} arrow>
|
||||
<IconButton size="small" onClick={() => onDelete(task.id)}>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
74
easy-dataset-main/components/tasks/TaskFilters.js
Normal file
74
easy-dataset-main/components/tasks/TaskFilters.js
Normal file
@@ -0,0 +1,74 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Select,
|
||||
MenuItem,
|
||||
OutlinedInput,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
CircularProgress
|
||||
} from '@mui/material';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default function TaskFilters({ statusFilter, setStatusFilter, typeFilter, setTypeFilter, loading, onRefresh }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const taskTypeOptions = [
|
||||
'text-processing',
|
||||
'file-processing',
|
||||
'pdf-processing',
|
||||
'question-generation',
|
||||
'answer-generation',
|
||||
'data-cleaning',
|
||||
'data-distillation',
|
||||
'eval-generation',
|
||||
'multi-turn-generation',
|
||||
'image-question-generation'
|
||||
];
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||
<FormControl size="small" sx={{ minWidth: 120 }}>
|
||||
<InputLabel>{t('tasks.filters.status')}</InputLabel>
|
||||
<Select
|
||||
value={statusFilter}
|
||||
onChange={e => setStatusFilter(e.target.value)}
|
||||
input={<OutlinedInput label={t('tasks.filters.status')} />}
|
||||
>
|
||||
<MenuItem value="all">{t('datasets.filterAll')}</MenuItem>
|
||||
<MenuItem value="0">{t('tasks.status.processing')}</MenuItem>
|
||||
<MenuItem value="1">{t('tasks.status.completed')}</MenuItem>
|
||||
<MenuItem value="2">{t('tasks.status.failed')}</MenuItem>
|
||||
<MenuItem value="3">{t('tasks.status.aborted')}</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<FormControl size="small" sx={{ minWidth: 160 }}>
|
||||
<InputLabel>{t('tasks.filters.type')}</InputLabel>
|
||||
<Select
|
||||
value={typeFilter}
|
||||
onChange={e => setTypeFilter(e.target.value)}
|
||||
input={<OutlinedInput label={t('tasks.filters.type')} />}
|
||||
>
|
||||
<MenuItem value="all">{t('datasets.filterAll')}</MenuItem>
|
||||
{taskTypeOptions.map(type => (
|
||||
<MenuItem key={type} value={type}>
|
||||
{t(`tasks.types.${type}`, { defaultValue: type })}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<Tooltip title={t('tasks.actions.refresh')}>
|
||||
<IconButton onClick={onRefresh} disabled={loading}>
|
||||
{loading ? <CircularProgress size={20} /> : <RefreshIcon />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
36
easy-dataset-main/components/tasks/TaskProgress.js
Normal file
36
easy-dataset-main/components/tasks/TaskProgress.js
Normal file
@@ -0,0 +1,36 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Stack, LinearProgress, Typography } from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
// 任务进度组件
|
||||
export default function TaskProgress({ task }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// 如果没有总数,则不显示进度条
|
||||
if (task.totalCount === 0) return '-';
|
||||
|
||||
// 计算进度百分比
|
||||
const progress = (task.completedCount / task.totalCount) * 100;
|
||||
|
||||
return (
|
||||
<Stack direction="column" spacing={0.5}>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={progress}
|
||||
sx={{
|
||||
height: 6,
|
||||
borderRadius: 3,
|
||||
width: 120,
|
||||
'& .MuiLinearProgress-bar': {
|
||||
transition: 'transform 0.5s ease'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{task.completedCount} / {task.totalCount} ({Math.round(progress)}%)
|
||||
</Typography>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
48
easy-dataset-main/components/tasks/TaskStatusChip.js
Normal file
48
easy-dataset-main/components/tasks/TaskStatusChip.js
Normal file
@@ -0,0 +1,48 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Chip, CircularProgress, Box } from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
// 任务状态显示组件
|
||||
export default function TaskStatusChip({ status }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// 状态映射配置
|
||||
const STATUS_CONFIG = {
|
||||
0: {
|
||||
label: t('tasks.status.processing'),
|
||||
color: 'warning',
|
||||
loading: true
|
||||
},
|
||||
1: {
|
||||
label: t('tasks.status.completed'),
|
||||
color: 'success'
|
||||
},
|
||||
2: {
|
||||
label: t('tasks.status.failed'),
|
||||
color: 'error'
|
||||
},
|
||||
3: {
|
||||
label: t('tasks.status.aborted'),
|
||||
color: 'default'
|
||||
}
|
||||
};
|
||||
|
||||
const statusInfo = STATUS_CONFIG[status] || {
|
||||
label: t('tasks.status.unknown'),
|
||||
color: 'default'
|
||||
};
|
||||
|
||||
// 处理中状态显示加载动画
|
||||
if (status === 0) {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<CircularProgress size={16} color="warning" />
|
||||
<Chip label={statusInfo.label} color={statusInfo.color} size="small" />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return <Chip label={statusInfo.label} color={statusInfo.color} size="small" />;
|
||||
}
|
||||
293
easy-dataset-main/components/tasks/TasksTable.js
Normal file
293
easy-dataset-main/components/tasks/TasksTable.js
Normal file
@@ -0,0 +1,293 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Paper,
|
||||
Typography,
|
||||
CircularProgress,
|
||||
Box,
|
||||
TablePagination,
|
||||
Tooltip
|
||||
} from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { zhCN, enUS } from 'date-fns/locale';
|
||||
|
||||
import TaskStatusChip from './TaskStatusChip';
|
||||
import TaskProgress from './TaskProgress';
|
||||
import TaskActions from './TaskActions';
|
||||
|
||||
export default function TasksTable({
|
||||
tasks,
|
||||
loading,
|
||||
handleAbortTask,
|
||||
handleDeleteTask,
|
||||
page,
|
||||
rowsPerPage,
|
||||
handleChangePage,
|
||||
handleChangeRowsPerPage,
|
||||
totalCount
|
||||
}) {
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
const formatDate = dateString => {
|
||||
if (!dateString) return '-';
|
||||
const date = new Date(dateString);
|
||||
return formatDistanceToNow(date, {
|
||||
addSuffix: true,
|
||||
locale: i18n.language === 'zh-CN' ? zhCN : enUS
|
||||
});
|
||||
};
|
||||
|
||||
const calculateDuration = (startTimeStr, endTimeStr) => {
|
||||
if (!startTimeStr || !endTimeStr) return '-';
|
||||
|
||||
try {
|
||||
const startTime = new Date(startTimeStr);
|
||||
const endTime = new Date(endTimeStr);
|
||||
const duration = endTime - startTime;
|
||||
const seconds = Math.floor(duration / 1000);
|
||||
|
||||
if (seconds < 60) {
|
||||
return t('tasks.duration.seconds', { seconds });
|
||||
}
|
||||
if (seconds < 3600) {
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = seconds % 60;
|
||||
return t('tasks.duration.minutes', { minutes, seconds: remainingSeconds });
|
||||
}
|
||||
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const remainingMinutes = Math.floor((seconds % 3600) / 60);
|
||||
return t('tasks.duration.hours', { hours, minutes: remainingMinutes });
|
||||
} catch (error) {
|
||||
console.error('Failed to calculate duration:', error);
|
||||
return '-';
|
||||
}
|
||||
};
|
||||
|
||||
const parseModelInfo = modelInfoString => {
|
||||
let modelInfo = '';
|
||||
try {
|
||||
const parsedModel = JSON.parse(modelInfoString);
|
||||
modelInfo = parsedModel.modelName || parsedModel.name || '-';
|
||||
} catch {
|
||||
modelInfo = modelInfoString || '-';
|
||||
}
|
||||
return modelInfo;
|
||||
};
|
||||
|
||||
const toTaskTypeLabel = taskType => {
|
||||
if (!taskType) return '-';
|
||||
return String(taskType)
|
||||
.split('-')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
};
|
||||
|
||||
const getLocalizedTaskType = taskType => {
|
||||
return t(`tasks.types.${taskType}`, { defaultValue: toTaskTypeLabel(taskType) });
|
||||
};
|
||||
|
||||
const parseJsonSafely = input => {
|
||||
if (!input || typeof input !== 'string') return null;
|
||||
try {
|
||||
return JSON.parse(input);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const formatTaskNote = task => {
|
||||
const note = String(task?.note || '').trim();
|
||||
if (!note) return '-';
|
||||
|
||||
const noteJson = parseJsonSafely(note);
|
||||
if (noteJson) {
|
||||
if (Array.isArray(noteJson.chunkIds)) {
|
||||
return t('tasks.notes.selectedChunks', { count: noteJson.chunkIds.length });
|
||||
}
|
||||
if (Array.isArray(noteJson.fileList)) {
|
||||
return t('tasks.notes.fileBatch', {
|
||||
count: noteJson.fileList.length,
|
||||
strategy: noteJson.strategy || '-'
|
||||
});
|
||||
}
|
||||
return t('tasks.notes.jsonParams');
|
||||
}
|
||||
|
||||
if (note === 'No chunks require question generation' || note.startsWith('No chunks require question gen')) {
|
||||
return t('tasks.notes.noChunksQuestion');
|
||||
}
|
||||
if (note === 'No chunks require cleaning' || note.startsWith('No chunks require clean')) {
|
||||
return t('tasks.notes.noChunksCleaning');
|
||||
}
|
||||
if (note.startsWith('Processing failed:')) {
|
||||
return t('tasks.notes.processingFailed', {
|
||||
error: note.replace('Processing failed:', '').trim()
|
||||
});
|
||||
}
|
||||
|
||||
const summaryMatch = note.match(/Processed:\s*(\d+)\/(\d+),\s*succeeded:\s*(\d+),\s*failed:\s*(\d+)/i);
|
||||
if (summaryMatch) {
|
||||
const [, processed, total, succeeded, failed] = summaryMatch;
|
||||
|
||||
const questionMatch = note.match(/questions generated:\s*(\d+)/i);
|
||||
if (questionMatch) {
|
||||
return t('tasks.notes.questionSummary', {
|
||||
processed,
|
||||
total,
|
||||
succeeded,
|
||||
failed,
|
||||
generated: questionMatch[1]
|
||||
});
|
||||
}
|
||||
|
||||
const datasetMatch = note.match(/datasets generated:\s*(\d+)/i);
|
||||
if (datasetMatch) {
|
||||
return t('tasks.notes.datasetSummary', {
|
||||
processed,
|
||||
total,
|
||||
succeeded,
|
||||
failed,
|
||||
generated: datasetMatch[1]
|
||||
});
|
||||
}
|
||||
|
||||
const cleaningMatch = note.match(/total original length:\s*(\d+),\s*total cleaned length:\s*(\d+)/i);
|
||||
if (cleaningMatch) {
|
||||
return t('tasks.notes.cleaningSummary', {
|
||||
processed,
|
||||
total,
|
||||
succeeded,
|
||||
failed,
|
||||
original: cleaningMatch[1],
|
||||
cleaned: cleaningMatch[2]
|
||||
});
|
||||
}
|
||||
|
||||
return t('tasks.notes.genericSummary', {
|
||||
processed,
|
||||
total,
|
||||
succeeded,
|
||||
failed
|
||||
});
|
||||
}
|
||||
|
||||
return note;
|
||||
};
|
||||
|
||||
const truncateNote = (note, maxLength = 48) => {
|
||||
if (!note) return '-';
|
||||
if (note.length <= maxLength) return note;
|
||||
return `${note.substring(0, maxLength)}...`;
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<TableContainer component={Paper} elevation={1} sx={{ borderRadius: 2, mb: 2 }}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>{t('tasks.table.type')}</TableCell>
|
||||
<TableCell>{t('tasks.table.status')}</TableCell>
|
||||
<TableCell>{t('tasks.table.progress')}</TableCell>
|
||||
<TableCell>{t('tasks.table.createTime')}</TableCell>
|
||||
<TableCell>{t('tasks.table.duration')}</TableCell>
|
||||
<TableCell>{t('tasks.table.model')}</TableCell>
|
||||
<TableCell>{t('tasks.table.note')}</TableCell>
|
||||
<TableCell align="right">{t('tasks.table.actions')}</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{loading && tasks.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={8} align="center" sx={{ py: 6 }}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
|
||||
<CircularProgress size={40} />
|
||||
<Typography variant="body2" sx={{ mt: 2 }}>
|
||||
{t('tasks.loading')}
|
||||
</Typography>
|
||||
</Box>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : tasks.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={8} align="center" sx={{ py: 6 }}>
|
||||
<Typography variant="body1">{t('tasks.empty')}</Typography>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
tasks.map(task => {
|
||||
const noteText = formatTaskNote(task);
|
||||
return (
|
||||
<TableRow key={task.id}>
|
||||
<TableCell>{getLocalizedTaskType(task.taskType)}</TableCell>
|
||||
<TableCell>
|
||||
<TaskStatusChip status={task.status} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<TaskProgress task={task} />
|
||||
</TableCell>
|
||||
|
||||
<TableCell>{formatDate(task.createAt)}</TableCell>
|
||||
<TableCell>{task.endTime ? calculateDuration(task.startTime, task.endTime) : '-'}</TableCell>
|
||||
<TableCell>{parseModelInfo(task.modelInfo)}</TableCell>
|
||||
<TableCell>
|
||||
{noteText !== '-' ? (
|
||||
<Tooltip title={noteText} arrow placement="top">
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
cursor: 'pointer',
|
||||
'&:hover': { color: 'primary.main' }
|
||||
}}
|
||||
>
|
||||
{truncateNote(noteText)}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<TaskActions task={task} onAbort={handleAbortTask} onDelete={handleDeleteTask} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
{tasks.length > 0 && (
|
||||
<TablePagination
|
||||
component="div"
|
||||
count={totalCount}
|
||||
page={page}
|
||||
onPageChange={handleChangePage}
|
||||
rowsPerPage={rowsPerPage}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage}
|
||||
rowsPerPageOptions={[5, 10, 25]}
|
||||
labelRowsPerPage={t('datasets.rowsPerPage')}
|
||||
labelDisplayedRows={({ count }) => {
|
||||
const calculatedFrom = page * rowsPerPage + 1;
|
||||
const calculatedTo = Math.min((page + 1) * rowsPerPage, count);
|
||||
return t('datasets.pagination', {
|
||||
from: calculatedFrom,
|
||||
to: calculatedTo,
|
||||
count
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user