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,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>
);
}

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

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

View 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" />;
}

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