first-update
This commit is contained in:
180
easy-dataset-main/components/text-split/BatchEditChunkDialog.js
Normal file
180
easy-dataset-main/components/text-split/BatchEditChunkDialog.js
Normal file
@@ -0,0 +1,180 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Button,
|
||||
TextField,
|
||||
RadioGroup,
|
||||
FormControlLabel,
|
||||
Radio,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Box,
|
||||
Typography,
|
||||
Alert,
|
||||
CircularProgress
|
||||
} from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
/**
|
||||
* 批量编辑文本块对话框
|
||||
* @param {Object} props
|
||||
* @param {boolean} props.open - 对话框是否打开
|
||||
* @param {Function} props.onClose - 关闭对话框的回调
|
||||
* @param {Function} props.onConfirm - 确认编辑的回调
|
||||
* @param {Array} props.selectedChunks - 选中的文本块ID数组
|
||||
* @param {number} props.totalChunks - 文本块总数
|
||||
* @param {boolean} props.loading - 是否正在处理
|
||||
*/
|
||||
export default function BatchEditChunksDialog({
|
||||
open,
|
||||
onClose,
|
||||
onConfirm,
|
||||
selectedChunks = [],
|
||||
totalChunks = 0,
|
||||
loading = false
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [position, setPosition] = useState('start'); // 'start' 或 'end'
|
||||
const [content, setContent] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
|
||||
// 处理位置变更
|
||||
const handlePositionChange = event => {
|
||||
setPosition(event.target.value);
|
||||
};
|
||||
|
||||
// 处理内容变更
|
||||
const handleContentChange = event => {
|
||||
setContent(event.target.value);
|
||||
if (error) setError('');
|
||||
};
|
||||
|
||||
// 处理确认
|
||||
const handleConfirm = () => {
|
||||
if (!content.trim()) {
|
||||
setError(t('batchEdit.contentRequired'));
|
||||
return;
|
||||
}
|
||||
|
||||
onConfirm({
|
||||
position,
|
||||
content: content.trim(),
|
||||
chunkIds: selectedChunks
|
||||
});
|
||||
};
|
||||
|
||||
// 处理关闭
|
||||
const handleClose = () => {
|
||||
if (!loading) {
|
||||
setContent('');
|
||||
setError('');
|
||||
setPosition('start');
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={handleClose} maxWidth="md" fullWidth disableEscapeKeyDown={loading}>
|
||||
<DialogTitle>{t('batchEdit.title')}</DialogTitle>
|
||||
|
||||
<DialogContent>
|
||||
<Box sx={{ py: 1 }}>
|
||||
{/* 选择提示 */}
|
||||
<Alert severity="info" sx={{ mb: 3 }}>
|
||||
<Typography variant="body2">
|
||||
{selectedChunks.length === totalChunks
|
||||
? t('batchEdit.allChunksSelected', { count: totalChunks })
|
||||
: t('batchEdit.selectedChunks', {
|
||||
selected: selectedChunks.length,
|
||||
total: totalChunks
|
||||
})}
|
||||
</Typography>
|
||||
</Alert>
|
||||
|
||||
{/* 位置选择 */}
|
||||
<FormControl component="fieldset" sx={{ mb: 3, width: '100%' }}>
|
||||
<FormLabel component="legend" sx={{ mb: 1 }}>
|
||||
{t('batchEdit.position')}
|
||||
</FormLabel>
|
||||
<RadioGroup value={position} onChange={handlePositionChange} row>
|
||||
<FormControlLabel value="start" control={<Radio />} label={t('batchEdit.atBeginning')} />
|
||||
<FormControlLabel value="end" control={<Radio />} label={t('batchEdit.atEnd')} />
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
|
||||
{/* 内容输入 */}
|
||||
<TextField
|
||||
fullWidth
|
||||
label={t('batchEdit.contentToAdd')}
|
||||
multiline
|
||||
rows={6}
|
||||
value={content}
|
||||
onChange={handleContentChange}
|
||||
placeholder={t('batchEdit.contentPlaceholder')}
|
||||
error={!!error}
|
||||
helperText={error || t('batchEdit.contentHelp')}
|
||||
disabled={loading}
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
|
||||
{/* 预览示例 */}
|
||||
{content.trim() && (
|
||||
<Box
|
||||
sx={{
|
||||
p: 2,
|
||||
border: '1px solid',
|
||||
borderColor: 'divider',
|
||||
borderRadius: 1,
|
||||
bgcolor: 'background.default',
|
||||
mb: 2
|
||||
}}
|
||||
>
|
||||
<Typography variant="subtitle2" sx={{ mb: 1 }}>
|
||||
{t('batchEdit.preview')}:
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '0.875rem',
|
||||
whiteSpace: 'pre-wrap',
|
||||
color: 'text.secondary'
|
||||
}}
|
||||
>
|
||||
{position === 'start' ? (
|
||||
<>
|
||||
<span style={{ backgroundColor: '#e3f2fd', padding: '2px 4px' }}>{content}</span>
|
||||
{'\n\n[原始文本块内容...]'}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{'[原始文本块内容...]\n\n'}
|
||||
<span style={{ backgroundColor: '#e3f2fd', padding: '2px 4px' }}>{content}</span>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose} disabled={loading}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleConfirm}
|
||||
variant="contained"
|
||||
disabled={loading || !content.trim() || selectedChunks.length === 0}
|
||||
startIcon={loading ? <CircularProgress size={20} /> : null}
|
||||
>
|
||||
{loading ? t('batchEdit.processing') : t('batchEdit.applyToChunks', { count: selectedChunks.length })}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogActions,
|
||||
Button,
|
||||
CircularProgress
|
||||
} from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default function ChunkBatchDeleteDialog({ open, onClose, onConfirm, loading, count }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={loading ? undefined : onClose}
|
||||
aria-labelledby="batch-delete-dialog-title"
|
||||
aria-describedby="batch-delete-dialog-description"
|
||||
>
|
||||
<DialogTitle id="batch-delete-dialog-title">
|
||||
{t('textSplit.batchDeleteChunksConfirmTitle', { defaultValue: '确认批量删除' })}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText id="batch-delete-dialog-description">
|
||||
{t('textSplit.batchDeleteChunksConfirmMessage', {
|
||||
count,
|
||||
defaultValue: `您确定要删除选中的 ${count} 个文本块吗?此操作不可恢复。`
|
||||
})}
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} disabled={loading}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button onClick={onConfirm} color="error" variant="contained" disabled={loading}>
|
||||
{loading ? <CircularProgress size={20} /> : t('common.confirm')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
449
easy-dataset-main/components/text-split/ChunkCard.js
Normal file
449
easy-dataset-main/components/text-split/ChunkCard.js
Normal file
@@ -0,0 +1,449 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
IconButton,
|
||||
Chip,
|
||||
Checkbox,
|
||||
Tooltip,
|
||||
Card,
|
||||
CardContent,
|
||||
CardActions,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Button,
|
||||
TextField,
|
||||
CircularProgress
|
||||
} from '@mui/material';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||
import QuizIcon from '@mui/icons-material/Quiz';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import CleaningServicesIcon from '@mui/icons-material/CleaningServices';
|
||||
import AssignmentIcon from '@mui/icons-material/Assignment';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
// 编辑文本块对话框组件
|
||||
const EditChunkDialog = ({ open, chunk, onClose, onSave }) => {
|
||||
const [content, setContent] = useState(chunk?.content || '');
|
||||
const { t } = useTranslation();
|
||||
|
||||
// 当文本块变化时更新内容
|
||||
useEffect(() => {
|
||||
if (chunk?.content) {
|
||||
setContent(chunk.content);
|
||||
}
|
||||
}, [chunk]);
|
||||
|
||||
const handleSave = () => {
|
||||
onSave(content);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
|
||||
<DialogTitle>{t('textSplit.editChunk', { chunkId: chunk?.name })}</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<TextField
|
||||
fullWidth
|
||||
multiline
|
||||
rows={15}
|
||||
value={content}
|
||||
onChange={e => setContent(e.target.value)}
|
||||
variant="outlined"
|
||||
sx={{ mt: 1 }}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>{t('common.cancel')}</Button>
|
||||
<Button onClick={handleSave} variant="contained" color="primary">
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default function ChunkCard({
|
||||
chunk,
|
||||
selected,
|
||||
onSelect,
|
||||
onView,
|
||||
onDelete,
|
||||
onGenerateQuestions,
|
||||
onDataCleaning,
|
||||
onEdit,
|
||||
onGenerateEvalQuestions, // 新增:生成测评题目的回调
|
||||
projectId,
|
||||
selectedModel // 添加selectedModel参数
|
||||
}) {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const [editDialogOpen, setEditDialogOpen] = useState(false);
|
||||
const [chunkForEdit, setChunkForEdit] = useState(null);
|
||||
const [generatingQuestions, setGeneratingQuestions] = useState(false);
|
||||
const [generatingEval, setGeneratingEval] = useState(false);
|
||||
|
||||
// 获取文本预览
|
||||
const getTextPreview = (content, maxLength = 150) => {
|
||||
if (!content) return '';
|
||||
return content.length > maxLength ? `${content.substring(0, maxLength)}...` : content;
|
||||
};
|
||||
|
||||
// 检查是否有已生成的问题
|
||||
const hasQuestions = chunk.questions && chunk.questions.length > 0;
|
||||
|
||||
// 处理编辑按钮点击
|
||||
const handleEditClick = async () => {
|
||||
try {
|
||||
// 显示加载状态
|
||||
console.log('正在获取文本块完整内容...');
|
||||
console.log('projectId:', projectId, 'chunkId:', chunk.id);
|
||||
|
||||
// 先获取完整的文本块内容,使用从外部传入的 projectId
|
||||
const response = await fetch(`/api/projects/${projectId}/chunks/${encodeURIComponent(chunk.id)}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(t('textSplit.fetchChunkFailed'));
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('获取文本块完整内容成功:', data);
|
||||
|
||||
// 先设置完整数据,再打开对话框(与 ChunkList.js 中的实现一致)
|
||||
setChunkForEdit(data);
|
||||
setEditDialogOpen(true);
|
||||
} catch (error) {
|
||||
console.error(t('textSplit.fetchChunkError'), error);
|
||||
// 如果出错,使用原始预览数据
|
||||
alert(t('textSplit.fetchChunkError'));
|
||||
}
|
||||
};
|
||||
|
||||
// 处理保存编辑内容
|
||||
const handleSaveEdit = newContent => {
|
||||
if (onEdit) {
|
||||
onEdit(chunk.id, newContent);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理生成单个问题 - 后台执行,不阻塞UI
|
||||
const handleGenerateQuestionsClick = async () => {
|
||||
setGeneratingQuestions(true);
|
||||
try {
|
||||
await onGenerateQuestions([chunk.id]);
|
||||
} finally {
|
||||
// Always release loading state, even when generation fails.
|
||||
setTimeout(() => {
|
||||
setGeneratingQuestions(false);
|
||||
}, 500);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理生成测评题目
|
||||
const handleGenerateEvalQuestionsClick = async () => {
|
||||
if (!onGenerateEvalQuestions) return;
|
||||
|
||||
setGeneratingEval(true);
|
||||
try {
|
||||
await onGenerateEvalQuestions(chunk.id);
|
||||
} finally {
|
||||
// 延迟关闭加载状态
|
||||
setTimeout(() => {
|
||||
setGeneratingEval(false);
|
||||
}, 500);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card
|
||||
variant="outlined"
|
||||
sx={{
|
||||
mb: 1,
|
||||
position: 'relative',
|
||||
transition: 'all 0.2s ease-in-out',
|
||||
borderColor: selected ? theme.palette.primary.main : theme.palette.divider,
|
||||
bgcolor: selected ? `${theme.palette.primary.main}10` : 'transparent',
|
||||
borderRadius: 2,
|
||||
'&:hover': {
|
||||
borderColor: theme.palette.primary.main,
|
||||
transform: 'translateY(-2px)',
|
||||
boxShadow: `0 4px 12px ${theme.palette.mode === 'dark' ? 'rgba(0,0,0,0.3)' : 'rgba(0,0,0,0.1)'}`
|
||||
}
|
||||
}}
|
||||
>
|
||||
<CardContent sx={{ pt: 2.5, px: 2.5, pb: '16px !important' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-start' }}>
|
||||
<Checkbox
|
||||
checked={selected}
|
||||
onChange={onSelect}
|
||||
sx={{
|
||||
mr: 1,
|
||||
'&.Mui-checked': {
|
||||
color: theme.palette.primary.main
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Box sx={{ flexGrow: 1 }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
mb: 1.5,
|
||||
flexWrap: 'wrap',
|
||||
gap: 1
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
fontWeight="600"
|
||||
sx={{
|
||||
color: theme.palette.mode === 'dark' ? theme.palette.primary.light : theme.palette.primary.dark
|
||||
}}
|
||||
>
|
||||
{chunk.name}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
|
||||
<Chip
|
||||
label={`${chunk.fileName || t('textSplit.unknownFile')}`}
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
sx={{
|
||||
borderRadius: 1,
|
||||
fontWeight: 500,
|
||||
'& .MuiChip-label': { px: 1 }
|
||||
}}
|
||||
/>
|
||||
<Chip
|
||||
label={`${chunk.size || 0} ${t('textSplit.characters')}`}
|
||||
size="small"
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
sx={{
|
||||
borderRadius: 1,
|
||||
fontWeight: 500,
|
||||
'& .MuiChip-label': { px: 1 }
|
||||
}}
|
||||
/>
|
||||
{chunk.Questions.length > 0 && (
|
||||
<Tooltip
|
||||
title={
|
||||
<Box sx={{ p: 1 }} style={{ maxHeight: '200px', overflow: 'auto' }}>
|
||||
{chunk.Questions.map((q, index) => (
|
||||
<Typography key={index} variant="body2" sx={{ mb: 0.5 }}>
|
||||
{index + 1}. {q.question}
|
||||
</Typography>
|
||||
))}
|
||||
</Box>
|
||||
}
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<Chip
|
||||
label={`${t('textSplit.generatedQuestions', { count: chunk.Questions.length })}`}
|
||||
size="small"
|
||||
color="success"
|
||||
variant="outlined"
|
||||
sx={{
|
||||
borderRadius: 1,
|
||||
fontWeight: 500,
|
||||
'& .MuiChip-label': { px: 1 }
|
||||
}}
|
||||
onClick={() => {
|
||||
if (!projectId) return;
|
||||
router.push(`/projects/${projectId}/questions`);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
{chunk.EvalDatasets && chunk.EvalDatasets.length > 0 && (
|
||||
<Chip
|
||||
label={`${t('textSplit.generatedEvalQuestions', { count: chunk.EvalDatasets.length })}`}
|
||||
size="small"
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
sx={{
|
||||
borderRadius: 1,
|
||||
fontWeight: 500,
|
||||
'& .MuiChip-label': { px: 1 }
|
||||
}}
|
||||
onClick={() => {
|
||||
if (!projectId) return;
|
||||
router.push(`/projects/${projectId}/eval-datasets`);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="textSecondary"
|
||||
sx={{
|
||||
mb: 1,
|
||||
lineHeight: 1.6,
|
||||
opacity: 0.85
|
||||
}}
|
||||
>
|
||||
{getTextPreview(chunk.content)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</CardContent>
|
||||
|
||||
<CardActions
|
||||
sx={{
|
||||
justifyContent: 'flex-end',
|
||||
px: 2.5,
|
||||
pb: 2,
|
||||
gap: 1,
|
||||
'& .MuiIconButton-root': {
|
||||
transition: 'all 0.2s',
|
||||
'&:hover': {
|
||||
transform: 'scale(1.1)'
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Tooltip title={t('datasets.viewDetails')}>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="primary"
|
||||
onClick={onView}
|
||||
sx={{
|
||||
bgcolor: theme.palette.mode === 'dark' ? 'rgba(144, 202, 249, 0.08)' : 'rgba(33, 150, 243, 0.08)'
|
||||
}}
|
||||
>
|
||||
<VisibilityIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip
|
||||
title={
|
||||
selectedModel?.id
|
||||
? t('textSplit.generateQuestions')
|
||||
: t('textSplit.selectModelFirst', { defaultValue: '请先在右上角选择模型' })
|
||||
}
|
||||
>
|
||||
<span>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="info"
|
||||
onClick={handleGenerateQuestionsClick}
|
||||
disabled={!selectedModel?.id || generatingQuestions}
|
||||
sx={{
|
||||
bgcolor: theme.palette.mode === 'dark' ? 'rgba(41, 182, 246, 0.08)' : 'rgba(2, 136, 209, 0.08)',
|
||||
'&.Mui-disabled': {
|
||||
opacity: 0.6,
|
||||
pointerEvents: 'auto' // 允许鼠标悬停显示tooltip
|
||||
}
|
||||
}}
|
||||
>
|
||||
{generatingQuestions ? <CircularProgress size={20} color="inherit" /> : <QuizIcon fontSize="small" />}
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip
|
||||
title={
|
||||
selectedModel?.id
|
||||
? t('textSplit.generateEvalQuestions', { defaultValue: '生成测试集' })
|
||||
: t('textSplit.selectModelFirst', { defaultValue: '请先在右上角选择模型' })
|
||||
}
|
||||
>
|
||||
<span>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="secondary"
|
||||
onClick={handleGenerateEvalQuestionsClick}
|
||||
disabled={!selectedModel?.id || generatingEval}
|
||||
sx={{
|
||||
bgcolor: theme.palette.mode === 'dark' ? 'rgba(156, 39, 176, 0.08)' : 'rgba(123, 31, 162, 0.08)',
|
||||
'&.Mui-disabled': {
|
||||
opacity: 0.6,
|
||||
pointerEvents: 'auto'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{generatingEval ? <CircularProgress size={20} color="inherit" /> : <AssignmentIcon fontSize="small" />}
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip
|
||||
title={
|
||||
selectedModel?.id
|
||||
? t('textSplit.dataCleaning', { defaultValue: '数据清洗' })
|
||||
: t('textSplit.selectModelFirst', { defaultValue: '请先在右上角选择模型' })
|
||||
}
|
||||
>
|
||||
<span>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="success"
|
||||
onClick={onDataCleaning}
|
||||
disabled={!selectedModel?.id}
|
||||
sx={{
|
||||
bgcolor: theme.palette.mode === 'dark' ? 'rgba(76, 175, 80, 0.08)' : 'rgba(46, 125, 50, 0.08)',
|
||||
'&.Mui-disabled': {
|
||||
opacity: 0.6,
|
||||
pointerEvents: 'auto' // 允许鼠标悬停显示tooltip
|
||||
}
|
||||
}}
|
||||
>
|
||||
<CleaningServicesIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title={t('textSplit.editChunk', { chunkId: chunk.name })}>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="warning"
|
||||
onClick={handleEditClick}
|
||||
sx={{
|
||||
bgcolor: theme.palette.mode === 'dark' ? 'rgba(255, 152, 0, 0.08)' : 'rgba(237, 108, 2, 0.08)'
|
||||
}}
|
||||
>
|
||||
<EditIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title={t('common.delete')}>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="error"
|
||||
onClick={onDelete}
|
||||
sx={{
|
||||
bgcolor: theme.palette.mode === 'dark' ? 'rgba(244, 67, 54, 0.08)' : 'rgba(211, 47, 47, 0.08)'
|
||||
}}
|
||||
>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</CardActions>
|
||||
</Card>
|
||||
|
||||
{/* 编辑文本块对话框 */}
|
||||
<EditChunkDialog
|
||||
open={editDialogOpen}
|
||||
chunk={chunkForEdit || chunk}
|
||||
onClose={() => {
|
||||
setEditDialogOpen(false);
|
||||
setChunkForEdit(null);
|
||||
}}
|
||||
onSave={handleSaveEdit}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
27
easy-dataset-main/components/text-split/ChunkDeleteDialog.js
Normal file
27
easy-dataset-main/components/text-split/ChunkDeleteDialog.js
Normal file
@@ -0,0 +1,27 @@
|
||||
'use client';
|
||||
|
||||
import { Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions, Button } from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default function ChunkDeleteDialog({ open, onClose, onConfirm }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
aria-labelledby="delete-dialog-title"
|
||||
aria-describedby="delete-dialog-description"
|
||||
>
|
||||
<DialogTitle id="delete-dialog-title">{t('common.confirmDelete')}?</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText id="delete-dialog-description">{t('common.confirmDelete')}?</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>{t('common.cancel')}</Button>
|
||||
<Button onClick={onConfirm} color="error" variant="contained">
|
||||
{t('common.confirm')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
124
easy-dataset-main/components/text-split/ChunkFilterDialog.js
Normal file
124
easy-dataset-main/components/text-split/ChunkFilterDialog.js
Normal file
@@ -0,0 +1,124 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Button,
|
||||
Box,
|
||||
TextField,
|
||||
Typography,
|
||||
Slider,
|
||||
FormControlLabel,
|
||||
Checkbox
|
||||
} from '@mui/material';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default function ChunkFilterDialog({ open, onClose, onApply, initialFilters = {} }) {
|
||||
const { t } = useTranslation();
|
||||
const [contentKeyword, setContentKeyword] = useState(initialFilters.contentKeyword || '');
|
||||
const [sizeRange, setSizeRange] = useState(initialFilters.sizeRange || [0, 10000]);
|
||||
const [hasQuestions, setHasQuestions] = useState(initialFilters.hasQuestions || null);
|
||||
|
||||
// 重置筛选条件
|
||||
const handleReset = () => {
|
||||
setContentKeyword('');
|
||||
setSizeRange([0, 10000]);
|
||||
setHasQuestions(null);
|
||||
};
|
||||
|
||||
// 应用筛选
|
||||
const handleApply = () => {
|
||||
onApply({
|
||||
contentKeyword,
|
||||
sizeRange,
|
||||
hasQuestions
|
||||
});
|
||||
onClose();
|
||||
};
|
||||
|
||||
// 处理大小范围变化
|
||||
const handleSizeRangeChange = (event, newValue) => {
|
||||
setSizeRange(newValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>{t('datasets.moreFilters', { defaultValue: '更多筛选' })}</DialogTitle>
|
||||
<DialogContent dividers sx={{ display: 'flex', flexDirection: 'column', gap: 3, py: 3 }}>
|
||||
{/* 文本块内容筛选 */}
|
||||
<Box>
|
||||
<Typography variant="subtitle2" sx={{ mb: 1, fontWeight: 600 }}>
|
||||
{t('textSplit.contentKeyword', { defaultValue: '文本块内容' })}
|
||||
</Typography>
|
||||
<TextField
|
||||
fullWidth
|
||||
size="small"
|
||||
placeholder={t('textSplit.contentKeywordPlaceholder', { defaultValue: '输入关键词搜索文本块内容' })}
|
||||
value={contentKeyword}
|
||||
onChange={e => setContentKeyword(e.target.value)}
|
||||
variant="outlined"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* 字数范围筛选 */}
|
||||
<Box>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
|
||||
{t('textSplit.characterRange', { defaultValue: '字数范围' })}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
{sizeRange[0]} - {sizeRange[1]}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Slider
|
||||
value={sizeRange}
|
||||
onChange={handleSizeRangeChange}
|
||||
valueLabelDisplay="auto"
|
||||
min={0}
|
||||
max={10000}
|
||||
step={100}
|
||||
marks={[
|
||||
{ value: 0, label: '0' },
|
||||
{ value: 10000, label: '10000' }
|
||||
]}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* 是否有问题的筛选 */}
|
||||
<Box>
|
||||
<Typography variant="subtitle2" sx={{ mb: 1, fontWeight: 600 }}>
|
||||
{t('textSplit.questionStatus', { defaultValue: '问题状态' })}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<FormControlLabel
|
||||
control={<Checkbox checked={hasQuestions === null} onChange={() => setHasQuestions(null)} />}
|
||||
label={t('textSplit.allChunks', { defaultValue: '全部' })}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={<Checkbox checked={hasQuestions === true} onChange={() => setHasQuestions(true)} />}
|
||||
label={t('textSplit.generatedQuestions2', { defaultValue: '已生成问题' })}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={<Checkbox checked={hasQuestions === false} onChange={() => setHasQuestions(false)} />}
|
||||
label={t('textSplit.ungeneratedQuestions', { defaultValue: '未生成问题' })}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ p: 2 }}>
|
||||
<Button onClick={handleReset} color="inherit">
|
||||
{t('common.reset', { defaultValue: '重置' })}
|
||||
</Button>
|
||||
<Button onClick={onClose} color="inherit">
|
||||
{t('common.cancel', { defaultValue: '取消' })}
|
||||
</Button>
|
||||
<Button onClick={handleApply} variant="contained" color="primary">
|
||||
{t('common.apply', { defaultValue: '应用' })}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
413
easy-dataset-main/components/text-split/ChunkList.js
Normal file
413
easy-dataset-main/components/text-split/ChunkList.js
Normal file
@@ -0,0 +1,413 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { Box, Paper, Typography, CircularProgress, Pagination, Grid } from '@mui/material';
|
||||
import ChunkListHeader from './ChunkListHeader';
|
||||
import ChunkCard from './ChunkCard';
|
||||
import ChunkViewDialog from './ChunkViewDialog';
|
||||
import ChunkDeleteDialog from './ChunkDeleteDialog';
|
||||
import BatchEditChunksDialog from './BatchEditChunkDialog';
|
||||
import ChunkBatchDeleteDialog from './ChunkBatchDeleteDialog';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
/**
|
||||
* Chunk list component
|
||||
* @param {Object} props
|
||||
* @param {string} props.projectId - Project ID
|
||||
* @param {Array} props.chunks - Chunk array
|
||||
* @param {Function} props.onDelete - Delete callback
|
||||
* @param {Function} props.onEdit - Edit callback
|
||||
* @param {Function} props.onGenerateQuestions - Generate questions callback
|
||||
* @param {Function} props.onDataCleaning - Data cleaning callback
|
||||
* @param {string} props.questionFilter - Question filter
|
||||
* @param {Function} props.onQuestionFilterChange - Question filter change callback
|
||||
* @param {Object} props.selectedModel - 閫変腑鐨勬ā鍨嬩俊鎭?
|
||||
*/
|
||||
export default function ChunkList({
|
||||
projectId,
|
||||
chunks = [],
|
||||
onDelete,
|
||||
onEdit,
|
||||
onGenerateQuestions,
|
||||
onGenerateEvalQuestions,
|
||||
onDataCleaning,
|
||||
loading = false,
|
||||
questionFilter,
|
||||
setQuestionFilter,
|
||||
selectedModel,
|
||||
onChunksUpdate
|
||||
}) {
|
||||
const theme = useTheme();
|
||||
const [page, setPage] = useState(1);
|
||||
const [selectedChunks, setSelectedChunks] = useState([]);
|
||||
const [viewChunk, setViewChunk] = useState(null);
|
||||
const [viewDialogOpen, setViewDialogOpen] = useState(false);
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [chunkToDelete, setChunkToDelete] = useState(null);
|
||||
const [batchEditDialogOpen, setBatchEditDialogOpen] = useState(false);
|
||||
const [batchEditLoading, setBatchEditLoading] = useState(false);
|
||||
const [batchDeleteDialogOpen, setBatchDeleteDialogOpen] = useState(false);
|
||||
const [batchDeleteLoading, setBatchDeleteLoading] = useState(false);
|
||||
|
||||
// 娣诲姞楂樼骇绛涢€夌姸鎬?
|
||||
const [advancedFilters, setAdvancedFilters] = useState({
|
||||
contentKeyword: '',
|
||||
sizeRange: [0, 10000],
|
||||
hasQuestions: null
|
||||
});
|
||||
|
||||
// 璁$畻娲昏穬绛涢€夋潯浠舵暟
|
||||
const activeFilterCount = useMemo(() => {
|
||||
let count = 0;
|
||||
if (advancedFilters.contentKeyword) count++;
|
||||
if (advancedFilters.sizeRange[0] > 0 || advancedFilters.sizeRange[1] < 10000) count++;
|
||||
if (advancedFilters.hasQuestions !== null) count++;
|
||||
return count;
|
||||
}, [advancedFilters]);
|
||||
|
||||
const sortedChunks = useMemo(
|
||||
() =>
|
||||
[...chunks].sort((a, b) => {
|
||||
if (a.fileId !== b.fileId) {
|
||||
return a.fileId.localeCompare(b.fileId);
|
||||
}
|
||||
|
||||
const getPartNumber = name => {
|
||||
const match = name.match(/part-(\d+)/);
|
||||
return match ? parseInt(match[1], 10) : 0;
|
||||
};
|
||||
|
||||
const numA = getPartNumber(a.name);
|
||||
const numB = getPartNumber(b.name);
|
||||
|
||||
return numA - numB;
|
||||
}),
|
||||
[chunks]
|
||||
);
|
||||
|
||||
const filteredChunks = useMemo(() => {
|
||||
return sortedChunks.filter(chunk => {
|
||||
if (advancedFilters.contentKeyword) {
|
||||
const keyword = advancedFilters.contentKeyword.toLowerCase();
|
||||
if (!chunk.content?.toLowerCase().includes(keyword)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const size = chunk.size || 0;
|
||||
if (size < advancedFilters.sizeRange[0] || size > advancedFilters.sizeRange[1]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (advancedFilters.hasQuestions !== null) {
|
||||
const hasQuestions = chunk.Questions && chunk.Questions.length > 0;
|
||||
if (advancedFilters.hasQuestions !== hasQuestions) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}, [sortedChunks, advancedFilters]);
|
||||
|
||||
// 褰撶瓫閫夋潯浠跺彉鍖栨椂锛屾竻闄や笉鍦ㄧ瓫閫夌粨鏋滀腑鐨勯€変腑椤?
|
||||
useEffect(() => {
|
||||
const filteredChunkIds = filteredChunks.map(chunk => chunk.id);
|
||||
setSelectedChunks(prev => prev.filter(id => filteredChunkIds.includes(id)));
|
||||
}, [filteredChunks]);
|
||||
|
||||
const itemsPerPage = 5;
|
||||
const displayedChunks = useMemo(() => {
|
||||
const startIndex = (page - 1) * itemsPerPage;
|
||||
return filteredChunks.slice(startIndex, startIndex + itemsPerPage);
|
||||
}, [filteredChunks, page]);
|
||||
const totalPages = useMemo(() => Math.ceil(filteredChunks.length / itemsPerPage), [filteredChunks.length]);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handlePageChange = (event, value) => {
|
||||
setPage(value);
|
||||
};
|
||||
|
||||
const handleViewChunk = async chunkId => {
|
||||
try {
|
||||
const response = await fetch(`/api/projects/${projectId}/chunks/${chunkId}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(t('textSplit.fetchChunksFailed'));
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
setViewChunk(data);
|
||||
setViewDialogOpen(true);
|
||||
} catch (error) {
|
||||
console.error(t('textSplit.fetchChunksError'), error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCloseViewDialog = () => {
|
||||
setViewDialogOpen(false);
|
||||
};
|
||||
|
||||
const handleOpenDeleteDialog = chunkId => {
|
||||
setChunkToDelete(chunkId);
|
||||
setDeleteDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleCloseDeleteDialog = () => {
|
||||
setDeleteDialogOpen(false);
|
||||
setChunkToDelete(null);
|
||||
};
|
||||
|
||||
const handleConfirmDelete = () => {
|
||||
if (chunkToDelete && onDelete) {
|
||||
onDelete(chunkToDelete);
|
||||
}
|
||||
handleCloseDeleteDialog();
|
||||
};
|
||||
|
||||
// 澶勭悊缂栬緫鏂囨湰鍧?
|
||||
const handleEditChunk = async (chunkId, newContent) => {
|
||||
if (onEdit) {
|
||||
onEdit(chunkId, newContent);
|
||||
onChunksUpdate();
|
||||
}
|
||||
};
|
||||
|
||||
// 澶勭悊閫夋嫨鏂囨湰鍧?
|
||||
const handleSelectChunk = chunkId => {
|
||||
setSelectedChunks(prev => {
|
||||
if (prev.includes(chunkId)) {
|
||||
return prev.filter(id => id !== chunkId);
|
||||
} else {
|
||||
return [...prev, chunkId];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleSelectAll = () => {
|
||||
if (selectedChunks.length === filteredChunks.length) {
|
||||
setSelectedChunks([]);
|
||||
} else {
|
||||
setSelectedChunks(filteredChunks.map(chunk => chunk.id));
|
||||
}
|
||||
};
|
||||
|
||||
const handleBatchGenerateQuestions = () => {
|
||||
if (onGenerateQuestions && selectedChunks.length > 0) {
|
||||
onGenerateQuestions(selectedChunks);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBatchEdit = async editData => {
|
||||
try {
|
||||
setBatchEditLoading(true);
|
||||
|
||||
// 璋冪敤鎵归噺缂栬緫API
|
||||
const response = await fetch(`/api/projects/${projectId}/chunks/batch-edit`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
position: editData.position,
|
||||
content: editData.content,
|
||||
chunkIds: editData.chunkIds
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('鎵归噺缂栬緫澶辫触');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
// 缂栬緫鎴愬姛鍚庯紝鍒锋柊鏂囨湰鍧楁暟鎹?
|
||||
if (onChunksUpdate) {
|
||||
onChunksUpdate();
|
||||
}
|
||||
|
||||
// 娓呯┖閫変腑鐘舵€?
|
||||
setSelectedChunks([]);
|
||||
|
||||
// 鍏抽棴瀵硅瘽妗?
|
||||
setBatchEditDialogOpen(false);
|
||||
|
||||
// 鏄剧ず鎴愬姛娑堟伅
|
||||
console.log(`鎴愬姛鏇存柊浜?${result.updatedCount} 涓枃鏈潡`);
|
||||
} else {
|
||||
throw new Error(result.message || '鎵归噺缂栬緫澶辫触');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('鎵归噺缂栬緫澶辫触:', error);
|
||||
// 杩欓噷鍙互娣诲姞閿欒鎻愮ず
|
||||
} finally {
|
||||
setBatchEditLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 鎵撳紑鎵归噺缂栬緫瀵硅瘽妗?
|
||||
const handleOpenBatchEdit = () => {
|
||||
setBatchEditDialogOpen(true);
|
||||
};
|
||||
|
||||
// 鍏抽棴鎵归噺缂栬緫瀵硅瘽妗?
|
||||
const handleCloseBatchEdit = () => {
|
||||
setBatchEditDialogOpen(false);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', p: 4 }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// 澶勭悊绛涢€夊彉鍖?
|
||||
const handleFilterChange = filters => {
|
||||
setAdvancedFilters(filters);
|
||||
setPage(1); // 閲嶇疆鍒扮涓€椤?
|
||||
};
|
||||
|
||||
// 鎵撳紑鎵归噺鍒犻櫎瀵硅瘽妗?
|
||||
const handleOpenBatchDelete = () => {
|
||||
setBatchDeleteDialogOpen(true);
|
||||
};
|
||||
|
||||
// 鍏抽棴鎵归噺鍒犻櫎瀵硅瘽妗?
|
||||
const handleCloseBatchDelete = () => {
|
||||
setBatchDeleteDialogOpen(false);
|
||||
};
|
||||
|
||||
// 纭鎵归噺鍒犻櫎
|
||||
const handleConfirmBatchDelete = async () => {
|
||||
if (selectedChunks.length === 0) return;
|
||||
|
||||
try {
|
||||
setBatchDeleteLoading(true);
|
||||
|
||||
let successCount = 0;
|
||||
let failCount = 0;
|
||||
|
||||
// 寰幆璋冪敤鍗曚釜鍒犻櫎鎺ュ彛
|
||||
for (const chunkId of selectedChunks) {
|
||||
try {
|
||||
await onDelete(chunkId);
|
||||
successCount++;
|
||||
} catch (error) {
|
||||
console.error(`鍒犻櫎鏂囨湰鍧?${chunkId} 澶辫触:`, error);
|
||||
failCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// 鏄剧ず鍒犻櫎缁撴灉
|
||||
if (failCount === 0) {
|
||||
console.log(`鎴愬姛鍒犻櫎 ${successCount} 涓枃鏈潡`);
|
||||
} else {
|
||||
console.log(`删除完成:成功 ${successCount} 个,失败 ${failCount} 个`);
|
||||
}
|
||||
|
||||
// 娓呯┖閫変腑鐘舵€?
|
||||
setSelectedChunks([]);
|
||||
|
||||
// 鍒锋柊鏁版嵁
|
||||
if (onChunksUpdate) {
|
||||
onChunksUpdate();
|
||||
}
|
||||
|
||||
// 鍏抽棴瀵硅瘽妗?
|
||||
setBatchDeleteDialogOpen(false);
|
||||
} catch (error) {
|
||||
console.error('鎵归噺鍒犻櫎澶辫触:', error);
|
||||
} finally {
|
||||
setBatchDeleteLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<ChunkListHeader
|
||||
projectId={projectId}
|
||||
totalChunks={filteredChunks.length}
|
||||
selectedChunks={selectedChunks}
|
||||
onSelectAll={handleSelectAll}
|
||||
onBatchGenerateQuestions={handleBatchGenerateQuestions}
|
||||
onBatchEditChunks={handleOpenBatchEdit}
|
||||
onBatchDeleteChunks={handleOpenBatchDelete}
|
||||
questionFilter={questionFilter}
|
||||
setQuestionFilter={event => setQuestionFilter(event.target.value)}
|
||||
chunks={chunks}
|
||||
selectedModel={selectedModel}
|
||||
onFilterChange={handleFilterChange}
|
||||
activeFilterCount={activeFilterCount}
|
||||
/>
|
||||
|
||||
<Grid container spacing={2}>
|
||||
{displayedChunks.map(chunk => (
|
||||
<Grid item xs={12} key={chunk.id}>
|
||||
<ChunkCard
|
||||
chunk={chunk}
|
||||
selected={selectedChunks.includes(chunk.id)}
|
||||
onSelect={() => handleSelectChunk(chunk.id)}
|
||||
onView={() => handleViewChunk(chunk.id)}
|
||||
onDelete={() => handleOpenDeleteDialog(chunk.id)}
|
||||
onEdit={handleEditChunk}
|
||||
onGenerateQuestions={() => onGenerateQuestions && onGenerateQuestions([chunk.id])}
|
||||
onGenerateEvalQuestions={() => onGenerateEvalQuestions && onGenerateEvalQuestions(chunk.id)}
|
||||
onDataCleaning={() => onDataCleaning && onDataCleaning([chunk.id])}
|
||||
projectId={projectId}
|
||||
selectedModel={selectedModel}
|
||||
/>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
{chunks.length === 0 && (
|
||||
<Paper
|
||||
sx={{
|
||||
p: 4,
|
||||
textAlign: 'center',
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: 2
|
||||
}}
|
||||
>
|
||||
<Typography variant="body1" color="textSecondary">
|
||||
{t('textSplit.noChunks')}
|
||||
</Typography>
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
{totalPages > 1 && (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 3 }}>
|
||||
<Pagination count={totalPages} page={page} onChange={handlePageChange} color="primary" />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* 鏂囨湰鍧楄鎯呭璇濇 */}
|
||||
<ChunkViewDialog open={viewDialogOpen} chunk={viewChunk} onClose={handleCloseViewDialog} />
|
||||
|
||||
{/* 鍒犻櫎纭瀵硅瘽妗?*/}
|
||||
<ChunkDeleteDialog open={deleteDialogOpen} onClose={handleCloseDeleteDialog} onConfirm={handleConfirmDelete} />
|
||||
|
||||
{/* 鎵归噺缂栬緫瀵硅瘽妗?*/}
|
||||
<BatchEditChunksDialog
|
||||
open={batchEditDialogOpen}
|
||||
onClose={handleCloseBatchEdit}
|
||||
onConfirm={handleBatchEdit}
|
||||
selectedChunks={selectedChunks}
|
||||
totalChunks={chunks.length}
|
||||
loading={batchEditLoading}
|
||||
/>
|
||||
|
||||
{/* 鎵归噺鍒犻櫎纭瀵硅瘽妗?*/}
|
||||
<ChunkBatchDeleteDialog
|
||||
open={batchDeleteDialogOpen}
|
||||
onClose={handleCloseBatchDelete}
|
||||
onConfirm={handleConfirmBatchDelete}
|
||||
loading={batchDeleteLoading}
|
||||
count={selectedChunks.length}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
400
easy-dataset-main/components/text-split/ChunkListHeader.js
Normal file
400
easy-dataset-main/components/text-split/ChunkListHeader.js
Normal file
@@ -0,0 +1,400 @@
|
||||
'use client';
|
||||
|
||||
import { Box, Typography, Checkbox, Button, Select, MenuItem, Tooltip, Menu, IconButton, Badge } from '@mui/material';
|
||||
import QuizIcon from '@mui/icons-material/Quiz';
|
||||
import DownloadIcon from '@mui/icons-material/Download';
|
||||
import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh';
|
||||
import CleaningServicesIcon from '@mui/icons-material/CleaningServices';
|
||||
import AssessmentIcon from '@mui/icons-material/Assessment';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import FilterListIcon from '@mui/icons-material/FilterList';
|
||||
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
||||
import axios from 'axios';
|
||||
import { toast } from 'sonner';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useState } from 'react';
|
||||
import ChunkFilterDialog from './ChunkFilterDialog';
|
||||
|
||||
export default function ChunkListHeader({
|
||||
projectId,
|
||||
totalChunks,
|
||||
selectedChunks,
|
||||
onSelectAll,
|
||||
onBatchGenerateQuestions,
|
||||
onBatchEditChunks,
|
||||
onBatchDeleteChunks,
|
||||
questionFilter,
|
||||
setQuestionFilter,
|
||||
chunks = [], // 添加chunks参数,用于导出文本块
|
||||
selectedModel = {},
|
||||
onFilterChange = null,
|
||||
activeFilterCount = 0
|
||||
}) {
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
// 添加更多菜单的状态和锚点
|
||||
const [moreMenuAnchorEl, setMoreMenuAnchorEl] = useState(null);
|
||||
const isMoreMenuOpen = Boolean(moreMenuAnchorEl);
|
||||
|
||||
// 添加筛选对话框状态
|
||||
const [filterDialogOpen, setFilterDialogOpen] = useState(false);
|
||||
|
||||
// 自动任务菜单状态
|
||||
const [autoTasksMenuAnchorEl, setAutoTasksMenuAnchorEl] = useState(null);
|
||||
const isAutoTasksMenuOpen = Boolean(autoTasksMenuAnchorEl);
|
||||
|
||||
const handleAutoTasksClick = event => {
|
||||
setAutoTasksMenuAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleAutoTasksClose = () => {
|
||||
setAutoTasksMenuAnchorEl(null);
|
||||
};
|
||||
|
||||
// 打开更多菜单
|
||||
const handleMoreMenuClick = event => {
|
||||
setMoreMenuAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
// 关闭更多菜单
|
||||
const handleMoreMenuClose = () => {
|
||||
setMoreMenuAnchorEl(null);
|
||||
};
|
||||
|
||||
// 处理批量编辑,关闭菜单并调用原有函数
|
||||
const handleBatchEdit = () => {
|
||||
handleMoreMenuClose();
|
||||
onBatchEditChunks();
|
||||
};
|
||||
|
||||
// 处理批量删除,关闭菜单并调用原有函数
|
||||
const handleBatchDelete = () => {
|
||||
handleMoreMenuClose();
|
||||
onBatchDeleteChunks();
|
||||
};
|
||||
|
||||
// 处理导出文本块,关闭菜单并调用原有函数
|
||||
const handleExport = () => {
|
||||
handleMoreMenuClose();
|
||||
handleExportChunks();
|
||||
};
|
||||
|
||||
// 创建自动提取问题任务
|
||||
const handleCreateAutoQuestionTask = async () => {
|
||||
if (!projectId || !selectedModel?.id) {
|
||||
toast.error(t('textSplit.selectModelFirst', { defaultValue: '请先选择模型' }));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 调用创建任务接口
|
||||
const response = await axios.post(`/api/projects/${projectId}/tasks`, {
|
||||
taskType: 'question-generation',
|
||||
modelInfo: selectedModel,
|
||||
language: i18n.language,
|
||||
detail: '批量生成问题任务'
|
||||
});
|
||||
|
||||
if (response.data?.code === 0) {
|
||||
toast.success(t('tasks.createSuccess', { defaultValue: '后台任务已创建,系统将自动处理未生成问题的文本块' }));
|
||||
} else {
|
||||
toast.error(t('tasks.createFailed', { defaultValue: '创建任务失败' }) + ': ' + response.data?.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('创建自动提取问题任务失败:', error);
|
||||
toast.error(t('tasks.createFailed', { defaultValue: '创建任务失败' }) + ': ' + error.message);
|
||||
}
|
||||
};
|
||||
|
||||
// 创建自动数据清洗任务
|
||||
const handleCreateAutoDataCleaningTask = async () => {
|
||||
if (!projectId || !selectedModel?.id) {
|
||||
toast.error(t('textSplit.selectModelFirst', { defaultValue: '请先选择模型' }));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 调用创建任务接口
|
||||
const response = await axios.post(`/api/projects/${projectId}/tasks`, {
|
||||
taskType: 'data-cleaning',
|
||||
modelInfo: selectedModel,
|
||||
language: i18n.language,
|
||||
detail: '批量数据清洗任务'
|
||||
});
|
||||
|
||||
if (response.data?.code === 0) {
|
||||
toast.success(
|
||||
t('tasks.createSuccess', { defaultValue: '后台任务已创建,系统将自动处理所有文本块进行数据清洗' })
|
||||
);
|
||||
} else {
|
||||
toast.error(t('tasks.createFailed', { defaultValue: '创建任务失败' }) + ': ' + response.data?.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('创建自动数据清洗任务失败:', error);
|
||||
toast.error(t('tasks.createFailed', { defaultValue: '创建任务失败' }) + ': ' + error.message);
|
||||
}
|
||||
};
|
||||
|
||||
// 创建自动生成评估数据集任务
|
||||
const handleCreateAutoEvalGenerationTask = async () => {
|
||||
if (!projectId || !selectedModel?.id) {
|
||||
toast.error(t('textSplit.selectModelFirst', { defaultValue: '请先选择模型' }));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 调用创建任务接口
|
||||
const response = await axios.post(`/api/projects/${projectId}/tasks`, {
|
||||
taskType: 'eval-generation',
|
||||
modelInfo: selectedModel,
|
||||
language: i18n.language,
|
||||
detail: '批量生成评估数据集任务'
|
||||
});
|
||||
|
||||
if (response.data?.code === 0) {
|
||||
toast.success(
|
||||
t('tasks.createSuccess', {
|
||||
defaultValue: '后台任务已创建,系统将自动为所有未生成评估题目的文本块生成评估数据集'
|
||||
})
|
||||
);
|
||||
} else {
|
||||
toast.error(t('tasks.createFailed', { defaultValue: '创建任务失败' }) + ': ' + response.data?.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('创建自动生成评估数据集任务失败:', error);
|
||||
toast.error(t('tasks.createFailed', { defaultValue: '创建任务失败' }) + ': ' + error.message);
|
||||
}
|
||||
};
|
||||
|
||||
// 导出文本块为JSON文件的函数
|
||||
const handleExportChunks = () => {
|
||||
if (!chunks || chunks.length === 0) return;
|
||||
|
||||
// 创建要导出的数据对象
|
||||
const exportData = chunks.map(chunk => ({
|
||||
name: chunk.name,
|
||||
projectId: chunk.projectId,
|
||||
fileName: chunk.fileName,
|
||||
content: chunk.content,
|
||||
summary: chunk.summary,
|
||||
size: chunk.size
|
||||
}));
|
||||
|
||||
// 将数据转换为JSON字符串
|
||||
const jsonString = JSON.stringify(exportData, null, 2);
|
||||
|
||||
// 创建Blob对象
|
||||
const blob = new Blob([jsonString], { type: 'application/json' });
|
||||
|
||||
// 创建下载链接
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `text-chunks-export-${new Date().toISOString().split('T')[0]}.json`;
|
||||
|
||||
// 触发下载
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
|
||||
// 清理
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: { xs: 'column', md: 'row' },
|
||||
justifyContent: 'space-between',
|
||||
alignItems: { xs: 'flex-start', md: 'center' },
|
||||
gap: 2,
|
||||
mb: 3
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Checkbox
|
||||
checked={selectedChunks.length === totalChunks}
|
||||
indeterminate={selectedChunks.length > 0 && selectedChunks.length < totalChunks}
|
||||
onChange={onSelectAll}
|
||||
/>
|
||||
<Typography variant="body1">{t('textSplit.selectedCount', { count: selectedChunks.length })}</Typography>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: { xs: 'column', sm: 'row' },
|
||||
alignItems: { xs: 'flex-start', sm: 'center' },
|
||||
flexWrap: 'wrap',
|
||||
gap: 1.5,
|
||||
width: { xs: '100%', md: 'auto' }
|
||||
}}
|
||||
>
|
||||
{/* 更多筛选按钮 */}
|
||||
<Tooltip title={t('datasets.moreFilters', { defaultValue: '更多筛选' })}>
|
||||
<Badge badgeContent={activeFilterCount} color="error" overlap="circular">
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<FilterListIcon />}
|
||||
onClick={() => setFilterDialogOpen(true)}
|
||||
size="small"
|
||||
sx={{ borderRadius: 1 }}
|
||||
>
|
||||
{t('datasets.moreFilters', { defaultValue: '更多筛选' })}
|
||||
</Button>
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: 1.5,
|
||||
mt: { xs: 1, sm: 0 },
|
||||
width: { xs: '100%', sm: 'auto' }
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<QuizIcon />}
|
||||
disabled={selectedChunks.length === 0}
|
||||
onClick={onBatchGenerateQuestions}
|
||||
size="medium"
|
||||
sx={{ minWidth: { xs: '48%', sm: 'auto' } }}
|
||||
>
|
||||
{t('textSplit.batchGenerateQuestions')}
|
||||
</Button>
|
||||
|
||||
{/* 自动任务下拉菜单 */}
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
startIcon={<AutoFixHighIcon />}
|
||||
endIcon={<KeyboardArrowDownIcon />}
|
||||
onClick={handleAutoTasksClick}
|
||||
disabled={!projectId || !selectedModel?.id}
|
||||
size="medium"
|
||||
sx={{ minWidth: { xs: '48%', sm: 'auto' } }}
|
||||
>
|
||||
{t('textSplit.autoTasks', { defaultValue: '自动任务' })}
|
||||
</Button>
|
||||
|
||||
<Menu
|
||||
anchorEl={autoTasksMenuAnchorEl}
|
||||
open={isAutoTasksMenuOpen}
|
||||
onClose={handleAutoTasksClose}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'right'
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right'
|
||||
}}
|
||||
>
|
||||
<Tooltip
|
||||
title={t('textSplit.autoGenerateQuestionsTip', {
|
||||
defaultValue: '创建后台批量处理任务:自动查询待生成问题的文本块并提取问题'
|
||||
})}
|
||||
placement="left"
|
||||
>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
handleCreateAutoQuestionTask();
|
||||
handleAutoTasksClose();
|
||||
}}
|
||||
>
|
||||
<QuizIcon fontSize="small" sx={{ mr: 1, color: 'secondary.main' }} />
|
||||
{t('textSplit.autoGenerateQuestions')}
|
||||
</MenuItem>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip
|
||||
title={t('textSplit.autoEvalGenerationTip', {
|
||||
defaultValue: '创建后台批量处理任务:自动为所有未生成评估题目的文本块生成评估数据集'
|
||||
})}
|
||||
placement="left"
|
||||
>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
handleCreateAutoEvalGenerationTask();
|
||||
handleAutoTasksClose();
|
||||
}}
|
||||
>
|
||||
<AssessmentIcon fontSize="small" sx={{ mr: 1, color: 'secondary.main' }} />
|
||||
{t('textSplit.autoEvalGeneration', { defaultValue: '自动生成评估集' })}
|
||||
</MenuItem>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip
|
||||
title={t('textSplit.autoDataCleaningTip', {
|
||||
defaultValue: '创建后台批量处理任务:自动对所有文本块进行数据清洗'
|
||||
})}
|
||||
placement="left"
|
||||
>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
handleCreateAutoDataCleaningTask();
|
||||
handleAutoTasksClose();
|
||||
}}
|
||||
>
|
||||
<CleaningServicesIcon fontSize="small" sx={{ mr: 1, color: 'success.main' }} />
|
||||
{t('textSplit.autoDataCleaning', { defaultValue: '自动数据清洗' })}
|
||||
</MenuItem>
|
||||
</Tooltip>
|
||||
</Menu>
|
||||
|
||||
{/* 更多菜单按钮 */}
|
||||
<Tooltip title={t('common.more', { defaultValue: '更多操作' })}>
|
||||
<IconButton
|
||||
onClick={handleMoreMenuClick}
|
||||
color="primary"
|
||||
size="medium"
|
||||
sx={{
|
||||
border: '1px solid',
|
||||
borderColor: 'divider'
|
||||
}}
|
||||
>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
{/* 更多操作下拉菜单 */}
|
||||
<Menu
|
||||
anchorEl={moreMenuAnchorEl}
|
||||
open={isMoreMenuOpen}
|
||||
onClose={handleMoreMenuClose}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'right'
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right'
|
||||
}}
|
||||
>
|
||||
<MenuItem onClick={handleBatchEdit} disabled={selectedChunks.length === 0}>
|
||||
<EditIcon fontSize="small" sx={{ mr: 1 }} />
|
||||
{t('batchEdit.batchEdit', { defaultValue: '批量编辑' })}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleBatchDelete} disabled={selectedChunks.length === 0}>
|
||||
<DeleteIcon fontSize="small" sx={{ mr: 1, color: 'error.main' }} />
|
||||
{t('textSplit.batchDeleteChunks', { defaultValue: '批量删除' })}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleExport} disabled={chunks.length === 0}>
|
||||
<DownloadIcon fontSize="small" sx={{ mr: 1 }} />
|
||||
{t('textSplit.exportChunks', { defaultValue: '导出文本块' })}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* 筛选对话框 */}
|
||||
<ChunkFilterDialog open={filterDialogOpen} onClose={() => setFilterDialogOpen(false)} onApply={onFilterChange} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
31
easy-dataset-main/components/text-split/ChunkViewDialog.js
Normal file
31
easy-dataset-main/components/text-split/ChunkViewDialog.js
Normal file
@@ -0,0 +1,31 @@
|
||||
'use client';
|
||||
|
||||
import { Box, Button, Dialog, DialogTitle, DialogContent, DialogActions, CircularProgress } from '@mui/material';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import 'github-markdown-css/github-markdown-light.css';
|
||||
|
||||
export default function ChunkViewDialog({ open, chunk, onClose }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
|
||||
<DialogTitle>{t('textSplit.chunkDetails', { chunkId: chunk?.name })}</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
{chunk ? (
|
||||
<Box sx={{ maxHeight: '60vh', overflow: 'auto' }}>
|
||||
<div className="markdown-body">
|
||||
<ReactMarkdown>{chunk.content}</ReactMarkdown>
|
||||
</div>
|
||||
</Box>
|
||||
) : (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', p: 4 }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>{t('common.close')}</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
560
easy-dataset-main/components/text-split/DomainAnalysis.js
Normal file
560
easy-dataset-main/components/text-split/DomainAnalysis.js
Normal file
@@ -0,0 +1,560 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Box,
|
||||
Paper,
|
||||
Typography,
|
||||
Divider,
|
||||
CircularProgress,
|
||||
Tabs,
|
||||
Tab,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
Collapse,
|
||||
IconButton,
|
||||
TextField,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogTitle,
|
||||
Tooltip,
|
||||
Menu,
|
||||
MenuItem
|
||||
} from '@mui/material';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import TabPanel from './components/TabPanel';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import ExpandLess from '@mui/icons-material/ExpandLess';
|
||||
import ExpandMore from '@mui/icons-material/ExpandMore';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import axios from 'axios';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import 'github-markdown-css/github-markdown-light.css';
|
||||
|
||||
/**
|
||||
* 领域分析组件
|
||||
* @param {Object} props
|
||||
* @param {string} props.projectId - 项目ID
|
||||
* @param {Array} props.toc - 目录结构数组
|
||||
* @param {Array} props.tags - 标签树数组
|
||||
* @param {boolean} props.loading - 是否加载中
|
||||
* @param {Function} props.onTagsUpdate - 标签更新回调
|
||||
*/
|
||||
|
||||
// 领域树节点组件
|
||||
function TreeNode({ node, level = 0, onEdit, onDelete, onAddChild }) {
|
||||
const [open, setOpen] = useState(true);
|
||||
const theme = useTheme();
|
||||
const hasChildren = node.child && node.child.length > 0;
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
const menuOpen = Boolean(anchorEl);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleClick = () => {
|
||||
if (hasChildren) {
|
||||
setOpen(!open);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMenuOpen = event => {
|
||||
event.stopPropagation();
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleMenuClose = event => {
|
||||
if (event) event.stopPropagation();
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const handleEdit = event => {
|
||||
event.stopPropagation();
|
||||
onEdit(node);
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
const handleDelete = event => {
|
||||
event.stopPropagation();
|
||||
onDelete(node);
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
const handleAddChild = event => {
|
||||
event.stopPropagation();
|
||||
onAddChild(node);
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ListItem
|
||||
button
|
||||
onClick={handleClick}
|
||||
sx={{
|
||||
pl: level * 2 + 1,
|
||||
bgcolor: level === 0 ? theme.palette.primary.light : 'transparent',
|
||||
color: level === 0 ? theme.palette.primary.contrastText : 'inherit',
|
||||
'&:hover': {
|
||||
bgcolor: level === 0 ? theme.palette.primary.main : theme.palette.action.hover
|
||||
},
|
||||
borderRadius: '4px',
|
||||
mb: 0.5,
|
||||
pr: 1
|
||||
}}
|
||||
>
|
||||
<ListItemText
|
||||
primary={node.label}
|
||||
primaryTypographyProps={{
|
||||
fontWeight: level === 0 ? 600 : 400,
|
||||
fontSize: level === 0 ? '1rem' : '0.9rem'
|
||||
}}
|
||||
/>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={handleMenuOpen}
|
||||
sx={{
|
||||
color: level === 0 ? 'inherit' : theme.palette.text.secondary,
|
||||
'&:hover': { bgcolor: 'rgba(255, 255, 255, 0.1)' }
|
||||
}}
|
||||
>
|
||||
<MoreVertIcon fontSize="small" />
|
||||
</IconButton>
|
||||
{hasChildren && (open ? <ExpandLess /> : <ExpandMore />)}
|
||||
</Box>
|
||||
|
||||
<Menu anchorEl={anchorEl} open={menuOpen} onClose={handleMenuClose} onClick={e => e.stopPropagation()}>
|
||||
<MenuItem onClick={handleEdit}>
|
||||
<EditIcon fontSize="small" sx={{ mr: 1 }} />
|
||||
{t('textSplit.editTag')}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleDelete}>
|
||||
<DeleteIcon fontSize="small" sx={{ mr: 1 }} />
|
||||
{t('textSplit.deleteTag')}
|
||||
</MenuItem>
|
||||
{level === 0 && (
|
||||
<MenuItem onClick={handleAddChild}>
|
||||
<AddIcon fontSize="small" sx={{ mr: 1 }} />
|
||||
{t('textSplit.addTag')}
|
||||
</MenuItem>
|
||||
)}
|
||||
</Menu>
|
||||
</ListItem>
|
||||
|
||||
{hasChildren && (
|
||||
<Collapse in={open} timeout="auto" unmountOnExit>
|
||||
<List component="div" disablePadding>
|
||||
{node.child.map((childNode, index) => (
|
||||
<TreeNode
|
||||
key={index}
|
||||
node={childNode}
|
||||
level={level + 1}
|
||||
onEdit={onEdit}
|
||||
onDelete={onDelete}
|
||||
onAddChild={onAddChild}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
</Collapse>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// 领域树组件
|
||||
function DomainTree({ tags, onEdit, onDelete, onAddChild }) {
|
||||
return (
|
||||
<List component="nav" aria-label="domain tree">
|
||||
{tags.map((node, index) => (
|
||||
<TreeNode key={index} node={node} onEdit={onEdit} onDelete={onDelete} onAddChild={onAddChild} />
|
||||
))}
|
||||
</List>
|
||||
);
|
||||
}
|
||||
|
||||
export default function DomainAnalysis({ projectId, toc = '', loading = false }) {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [currentNode, setCurrentNode] = useState(null);
|
||||
const [parentNode, setParentNode] = useState('');
|
||||
const [dialogMode, setDialogMode] = useState('add');
|
||||
const [labelValue, setLabelValue] = useState({});
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [tags, setTags] = useState([]);
|
||||
const [snackbar, setSnackbar] = useState({
|
||||
open: false,
|
||||
message: '',
|
||||
severity: 'success'
|
||||
});
|
||||
|
||||
const handleCloseSnackbar = () => {
|
||||
setSnackbar(prev => ({ ...prev, open: false }));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getTags();
|
||||
}, []);
|
||||
const getTags = async () => {
|
||||
const response = await axios.get(`/api/projects/${projectId}/tags`);
|
||||
setTags(response.data.tags);
|
||||
};
|
||||
// 处理标签切换
|
||||
const handleTabChange = (event, newValue) => {
|
||||
setActiveTab(newValue);
|
||||
};
|
||||
|
||||
// 打开添加标签对话框
|
||||
const handleAddTag = () => {
|
||||
setDialogMode('add');
|
||||
setCurrentNode(null);
|
||||
setParentNode(null);
|
||||
setLabelValue({});
|
||||
setDialogOpen(true);
|
||||
};
|
||||
|
||||
// 打开编辑标签对话框
|
||||
const handleEditTag = node => {
|
||||
setDialogMode('edit');
|
||||
setCurrentNode({ id: node.id, label: node.label });
|
||||
setLabelValue({ id: node.id, label: node.label });
|
||||
setDialogOpen(true);
|
||||
};
|
||||
|
||||
// 打开添加子标签对话框
|
||||
const handleAddChildTag = parentNode => {
|
||||
setDialogMode('addChild');
|
||||
setParentNode(parentNode.label);
|
||||
setLabelValue({ parentId: parentNode.id });
|
||||
setDialogOpen(true);
|
||||
};
|
||||
|
||||
// 打开删除标签对话框
|
||||
const handleDeleteTag = node => {
|
||||
setCurrentNode(node);
|
||||
setDeleteDialogOpen(true);
|
||||
};
|
||||
|
||||
// 关闭对话框
|
||||
const handleCloseDialog = () => {
|
||||
setDialogOpen(false);
|
||||
setDeleteDialogOpen(false);
|
||||
};
|
||||
|
||||
// 查找并更新节点
|
||||
const findAndUpdateNode = (nodes, targetNode, newLabel) => {
|
||||
return nodes.map(node => {
|
||||
if (node === targetNode) {
|
||||
return { ...node, label: newLabel };
|
||||
}
|
||||
if (node.child && node.child.length > 0) {
|
||||
return { ...node, child: findAndUpdateNode(node.child, targetNode, newLabel) };
|
||||
}
|
||||
return node;
|
||||
});
|
||||
};
|
||||
|
||||
// 查找并删除节点
|
||||
const findAndDeleteNode = (nodes, targetNode) => {
|
||||
return nodes
|
||||
.filter(node => node !== targetNode)
|
||||
.map(node => {
|
||||
if (node.child && node.child.length > 0) {
|
||||
return { ...node, child: findAndDeleteNode(node.child, targetNode) };
|
||||
}
|
||||
return node;
|
||||
});
|
||||
};
|
||||
|
||||
// 查找并添加子节点
|
||||
const findAndAddChildNode = (nodes, parentNode, childLabel) => {
|
||||
return nodes.map(node => {
|
||||
if (node === parentNode) {
|
||||
const childArray = node.child || [];
|
||||
return {
|
||||
...node,
|
||||
child: [...childArray, { label: childLabel, child: [] }]
|
||||
};
|
||||
}
|
||||
if (node.child && node.child.length > 0) {
|
||||
return { ...node, child: findAndAddChildNode(node.child, parentNode, childLabel) };
|
||||
}
|
||||
return node;
|
||||
});
|
||||
};
|
||||
|
||||
// 保存标签更改
|
||||
const saveTagChanges = async updatedTags => {
|
||||
console.log('保存标签更改:', updatedTags);
|
||||
setSaving(true);
|
||||
try {
|
||||
const response = await fetch(`/api/projects/${projectId}/tags`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ tags: updatedTags })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || t('domain.errors.saveFailed'));
|
||||
}
|
||||
getTags();
|
||||
setSnackbar({
|
||||
open: true,
|
||||
message: t('domain.messages.updateSuccess'),
|
||||
severity: 'success'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('保存标签失败:', error);
|
||||
setSnackbar({
|
||||
open: true,
|
||||
message: error.message || '保存标签失败',
|
||||
severity: 'error'
|
||||
});
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!labelValue.label.trim()) {
|
||||
setSnackbar({
|
||||
open: true,
|
||||
message: '标签名称不能为空',
|
||||
severity: 'error'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await saveTagChanges(labelValue);
|
||||
handleCloseDialog();
|
||||
};
|
||||
|
||||
const handleConfirmDelete = async () => {
|
||||
if (!currentNode) return;
|
||||
|
||||
const res = await axios.delete(`/api/projects/${projectId}/tags?id=${currentNode.id}`);
|
||||
if (res.status === 200) {
|
||||
toast.success('删除成功');
|
||||
getTags();
|
||||
}
|
||||
|
||||
setDeleteDialogOpen(false);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', p: 4 }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (toc.length === 0) {
|
||||
return (
|
||||
<Paper
|
||||
sx={{
|
||||
p: 4,
|
||||
textAlign: 'center',
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: 2
|
||||
}}
|
||||
>
|
||||
<Typography variant="body1" color="textSecondary">
|
||||
{t('domain.noToc')}
|
||||
</Typography>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Paper
|
||||
sx={{
|
||||
p: 0,
|
||||
mb: 3,
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: 2,
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
>
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onChange={handleTabChange}
|
||||
variant="fullWidth"
|
||||
indicatorColor="secondary"
|
||||
sx={{
|
||||
borderBottom: 1,
|
||||
borderColor: 'divider',
|
||||
bgcolor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 0, 0, 0.03)',
|
||||
borderTopLeftRadius: 2,
|
||||
borderTopRightRadius: 2
|
||||
}}
|
||||
>
|
||||
<Tab label={t('domain.tabs.tree')} />
|
||||
<Tab label={t('domain.tabs.structure')} />
|
||||
</Tabs>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
p: 3,
|
||||
bgcolor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.02)' : 'rgba(255, 255, 255, 0.8)',
|
||||
borderBottomLeftRadius: 2,
|
||||
borderBottomRightRadius: 2,
|
||||
boxShadow: 'inset 0 2px 4px rgba(0,0,0,0.03)'
|
||||
}}
|
||||
>
|
||||
<TabPanel value={activeTab} index={0}>
|
||||
<Box>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
||||
<Typography variant="h6">{t('domain.tabs.tree')}</Typography>
|
||||
<Tooltip title="添加一级标签">
|
||||
<Button variant="outlined" size="small" startIcon={<AddIcon />} onClick={handleAddTag}>
|
||||
{t('domain.addRootTag')}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<Divider sx={{ mb: 2 }} />
|
||||
<Box
|
||||
sx={{
|
||||
p: 2,
|
||||
bgcolor: theme.palette.background.paper,
|
||||
borderRadius: 1,
|
||||
maxHeight: '800px',
|
||||
overflow: 'auto'
|
||||
}}
|
||||
>
|
||||
{tags && tags.length > 0 ? (
|
||||
<DomainTree
|
||||
tags={tags}
|
||||
onEdit={handleEditTag}
|
||||
onDelete={handleDeleteTag}
|
||||
onAddChild={handleAddChildTag}
|
||||
/>
|
||||
) : (
|
||||
<Box sx={{ textAlign: 'center', py: 4 }}>
|
||||
<Typography variant="body2" color="textSecondary" gutterBottom>
|
||||
{t('domain.noTags')}
|
||||
</Typography>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
startIcon={<AddIcon />}
|
||||
onClick={handleAddTag}
|
||||
sx={{ mt: 1 }}
|
||||
>
|
||||
{t('domain.addFirstTag')}
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</TabPanel>
|
||||
<TabPanel value={activeTab} index={1}>
|
||||
<Box>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
{t('domain.docStructure')}
|
||||
</Typography>
|
||||
<Divider sx={{ mb: 2 }} />
|
||||
<Box
|
||||
sx={{
|
||||
p: 2,
|
||||
bgcolor: theme.palette.background.paper,
|
||||
borderRadius: 1,
|
||||
maxHeight: '600px',
|
||||
overflow: 'auto'
|
||||
}}
|
||||
>
|
||||
<div className="markdown-body">
|
||||
<ReactMarkdown
|
||||
components={{
|
||||
root: ({ children }) => (
|
||||
<div
|
||||
style={{
|
||||
fontFamily: 'monospace',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word'
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
>
|
||||
{toc}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
</Box>
|
||||
</Box>
|
||||
</TabPanel>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
{/* 添加/编辑标签对话框 */}
|
||||
<Dialog open={dialogOpen} onClose={handleCloseDialog} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>
|
||||
{dialogMode === 'add'
|
||||
? t('domain.dialog.addTitle')
|
||||
: dialogMode === 'edit'
|
||||
? t('domain.dialog.editTitle')
|
||||
: t('domain.dialog.addChildTitle')}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText sx={{ mb: 2 }}>
|
||||
{dialogMode === 'add'
|
||||
? t('domain.dialog.inputRoot')
|
||||
: dialogMode === 'edit'
|
||||
? t('domain.dialog.inputEdit')
|
||||
: t('domain.dialog.inputChild', { label: parentNode })}
|
||||
</DialogContentText>
|
||||
<TextField
|
||||
autoFocus
|
||||
margin="dense"
|
||||
label={t('domain.dialog.labelName')}
|
||||
type="text"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={labelValue.label}
|
||||
onChange={e => setLabelValue({ ...labelValue, label: e.target.value })}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleCloseDialog}>{t('common.cancel')}</Button>
|
||||
<Button onClick={handleSubmit} variant="contained" disabled={saving || !labelValue?.label?.trim()}>
|
||||
{saving ? t('common.saving') : t('common.save')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
{/* 删除确认对话框 */}
|
||||
<Dialog open={deleteDialogOpen} onClose={handleCloseDialog}>
|
||||
<DialogTitle>{t('common.confirmDelete')}</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
{t('domain.dialog.deleteConfirm', { label: currentNode?.label })}
|
||||
{currentNode?.child && currentNode.child.length > 0 && t('domain.dialog.deleteWarning')}
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleCloseDialog}>{t('common.cancel')}</Button>
|
||||
<Button onClick={handleConfirmDelete} color="error" variant="contained">
|
||||
{saving ? t('common.deleting') : t('common.delete')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
360
easy-dataset-main/components/text-split/FileUploader.js
Normal file
360
easy-dataset-main/components/text-split/FileUploader.js
Normal file
@@ -0,0 +1,360 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Paper, Grid } from '@mui/material';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { useAtomValue } from 'jotai/index';
|
||||
import { selectedModelInfoAtom } from '@/lib/store';
|
||||
import UploadArea from './components/UploadArea';
|
||||
import FileList from './components/FileList';
|
||||
import DeleteConfirmDialog from './components/DeleteConfirmDialog';
|
||||
import PdfProcessingDialog from './components/PdfProcessingDialog';
|
||||
import DomainTreeActionDialog from './components/DomainTreeActionDialog';
|
||||
import FileLoadingProgress from './components/FileLoadingProgress';
|
||||
import { fileApi, taskApi } from '@/lib/api';
|
||||
import { getContent, checkMaxSize, checkInvalidFiles, getvalidFiles } from '@/lib/file/file-process';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export default function FileUploader({
|
||||
projectId,
|
||||
onUploadSuccess,
|
||||
onFileDeleted,
|
||||
sendToPages,
|
||||
setPdfStrategy,
|
||||
pdfStrategy,
|
||||
selectedViosnModel,
|
||||
setSelectedViosnModel,
|
||||
setPageLoading,
|
||||
taskFileProcessing,
|
||||
fileTask
|
||||
}) {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const [files, setFiles] = useState([]);
|
||||
const [pdfFiles, setPdfFiles] = useState([]);
|
||||
const [uploadedFiles, setUploadedFiles] = useState({});
|
||||
const selectedModelInfo = useAtomValue(selectedModelInfoAtom);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
|
||||
const [pdfProcessConfirmOpen, setpdfProcessConfirmOpen] = useState(false);
|
||||
const [fileToDelete, setFileToDelete] = useState({});
|
||||
const [domainTreeActionOpen, setDomainTreeActionOpen] = useState(false);
|
||||
const [domainTreeAction, setDomainTreeAction] = useState('');
|
||||
const [isFirstUpload, setIsFirstUpload] = useState(false);
|
||||
const [pendingAction, setPendingAction] = useState(null);
|
||||
const [taskSettings, setTaskSettings] = useState(null);
|
||||
const [visionModels, setVisionModels] = useState([]);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize] = useState(10);
|
||||
const [searchFileName, setSearchFileName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
fetchUploadedFiles();
|
||||
}, [currentPage, searchFileName]);
|
||||
|
||||
/**
|
||||
* 处理 PDF 处理方式选择
|
||||
*/
|
||||
const handleRadioChange = event => {
|
||||
const modelId = event.target.selectedVision;
|
||||
setPdfStrategy(event.target.value);
|
||||
|
||||
if (event.target.value === 'mineru') {
|
||||
toast.success(t('textSplit.mineruSelected'));
|
||||
} else if (event.target.value === 'mineru-local') {
|
||||
toast.success(t('textSplit.mineruLocalSelected'));
|
||||
} else if (event.target.value === 'vision') {
|
||||
const model = visionModels.find(item => item.id === modelId);
|
||||
toast.success(
|
||||
t('textSplit.customVisionModelSelected', {
|
||||
name: model.modelName,
|
||||
provider: model.projectName
|
||||
})
|
||||
);
|
||||
} else {
|
||||
toast.success(t('textSplit.defaultSelected'));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取上传的文件列表
|
||||
* @param {*} page
|
||||
* @param {*} size
|
||||
* @param {*} fileName
|
||||
*/
|
||||
const fetchUploadedFiles = async (page = currentPage, size = pageSize, fileName = searchFileName) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await fileApi.getFiles({ projectId, page, size, fileName, t });
|
||||
setUploadedFiles(data);
|
||||
|
||||
setIsFirstUpload(data.total === 0);
|
||||
|
||||
const taskData = await taskApi.getProjectTasks(projectId);
|
||||
setTaskSettings(taskData);
|
||||
|
||||
//使用Jotai会出现数据获取的延迟,导致这里模型获取不到,改用localStorage获取模型信息
|
||||
const model = JSON.parse(localStorage.getItem('modelConfigList'));
|
||||
|
||||
//过滤出视觉模型
|
||||
const visionItems = model.filter(item => item.type === 'vision');
|
||||
|
||||
//先默认选择第一个配置的视觉模型
|
||||
if (visionItems.length > 0) {
|
||||
setSelectedViosnModel(visionItems[0].id);
|
||||
}
|
||||
|
||||
setVisionModels(visionItems);
|
||||
} catch (error) {
|
||||
toast.error(error.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理文件选择
|
||||
*/
|
||||
const handleFileSelect = event => {
|
||||
const selectedFiles = Array.from(event.target.files);
|
||||
|
||||
checkMaxSize(selectedFiles);
|
||||
checkInvalidFiles(selectedFiles);
|
||||
|
||||
const validFiles = getvalidFiles(selectedFiles);
|
||||
|
||||
if (validFiles.length > 0) {
|
||||
setFiles(prev => [...prev, ...validFiles]);
|
||||
}
|
||||
const hasPdfFiles = selectedFiles.filter(file => file.name.endsWith('.pdf'));
|
||||
if (hasPdfFiles.length > 0) {
|
||||
setpdfProcessConfirmOpen(true);
|
||||
setPdfFiles(hasPdfFiles);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 从待上传文件列表中移除文件
|
||||
*/
|
||||
const removeFile = index => {
|
||||
const fileToRemove = files[index];
|
||||
setFiles(prev => prev.filter((_, i) => i !== index));
|
||||
if (fileToRemove && fileToRemove.name.toLowerCase().endsWith('.pdf')) {
|
||||
setPdfFiles(prevPdfFiles => prevPdfFiles.filter(pdfFile => pdfFile.name !== fileToRemove.name));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
*/
|
||||
const uploadFiles = async () => {
|
||||
if (files.length === 0) return;
|
||||
|
||||
// 如果是第一次上传,直接走默认逻辑
|
||||
if (isFirstUpload) {
|
||||
handleStartUpload('rebuild');
|
||||
return;
|
||||
}
|
||||
|
||||
// 否则打开领域树操作选择对话框
|
||||
setDomainTreeAction('upload');
|
||||
setPendingAction({ type: 'upload' });
|
||||
setDomainTreeActionOpen(true);
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理领域树操作选择
|
||||
*/
|
||||
const handleDomainTreeAction = action => {
|
||||
setDomainTreeActionOpen(false);
|
||||
|
||||
// 执行挂起的操作
|
||||
if (pendingAction && pendingAction.type === 'upload') {
|
||||
handleStartUpload(action);
|
||||
} else if (pendingAction && pendingAction.type === 'delete') {
|
||||
handleDeleteFile(action);
|
||||
}
|
||||
|
||||
// 清除挂起的操作
|
||||
setPendingAction(null);
|
||||
};
|
||||
|
||||
/**
|
||||
* 开始上传文件
|
||||
*/
|
||||
const handleStartUpload = async domainTreeActionType => {
|
||||
setUploading(true);
|
||||
try {
|
||||
const uploadedFileInfos = [];
|
||||
for (const file of files) {
|
||||
const { fileContent, fileName } = await getContent(file);
|
||||
const data = await fileApi.uploadFile({ file, projectId, fileContent, fileName, t });
|
||||
uploadedFileInfos.push({ fileName: data.fileName, fileId: data.fileId });
|
||||
}
|
||||
toast.success(t('textSplit.uploadSuccess', { count: files.length }));
|
||||
setFiles([]);
|
||||
setCurrentPage(1);
|
||||
await fetchUploadedFiles();
|
||||
if (onUploadSuccess) {
|
||||
await onUploadSuccess(uploadedFileInfos, pdfFiles, domainTreeActionType);
|
||||
}
|
||||
} catch (err) {
|
||||
toast.error(err.message || t('textSplit.uploadFailed'));
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 打开删除确认对话框
|
||||
const openDeleteConfirm = (fileId, fileName) => {
|
||||
setFileToDelete({ fileId, fileName });
|
||||
setDeleteConfirmOpen(true);
|
||||
};
|
||||
|
||||
// 关闭删除确认对话框
|
||||
const closeDeleteConfirm = () => {
|
||||
setDeleteConfirmOpen(false);
|
||||
setFileToDelete(null);
|
||||
};
|
||||
|
||||
// 删除文件前确认领域树操作
|
||||
const confirmDeleteFile = () => {
|
||||
setDeleteConfirmOpen(false);
|
||||
|
||||
// 如果没有其他文件了(删除后会变为空),直接删除
|
||||
if (uploadedFiles.total <= 1) {
|
||||
handleDeleteFile('keep');
|
||||
return;
|
||||
}
|
||||
|
||||
// 否则打开领域树操作选择对话框
|
||||
setDomainTreeAction('delete');
|
||||
setPendingAction({ type: 'delete' });
|
||||
setDomainTreeActionOpen(true);
|
||||
};
|
||||
|
||||
// 处理删除文件
|
||||
const handleDeleteFile = async domainTreeActionType => {
|
||||
if (!fileToDelete) return;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
closeDeleteConfirm();
|
||||
|
||||
await fileApi.deleteFile({
|
||||
fileToDelete,
|
||||
projectId,
|
||||
domainTreeActionType,
|
||||
modelInfo: selectedModelInfo || {},
|
||||
t
|
||||
});
|
||||
await fetchUploadedFiles();
|
||||
|
||||
if (onFileDeleted) {
|
||||
const filesLength = uploadedFiles.total;
|
||||
onFileDeleted(fileToDelete, filesLength);
|
||||
}
|
||||
|
||||
if (uploadedFiles.data && uploadedFiles.data.length <= 1 && currentPage > 1) {
|
||||
setCurrentPage(1);
|
||||
}
|
||||
|
||||
toast.success(t('textSplit.deleteSuccess', { fileName: fileToDelete.fileName }));
|
||||
} catch (error) {
|
||||
console.error('Error deleting file:', error);
|
||||
toast.error(error.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setFileToDelete(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
p: 3,
|
||||
mb: 3,
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: 2
|
||||
}}
|
||||
>
|
||||
{taskFileProcessing ? (
|
||||
<FileLoadingProgress fileTask={fileTask} />
|
||||
) : (
|
||||
<>
|
||||
<Grid container spacing={3}>
|
||||
{/* 左侧:上传文件区域 */}
|
||||
<Grid item xs={10} md={5} sx={{ maxWidth: '100%', width: '100%' }}>
|
||||
<UploadArea
|
||||
theme={theme}
|
||||
files={files}
|
||||
uploading={uploading}
|
||||
uploadedFiles={uploadedFiles}
|
||||
onFileSelect={handleFileSelect}
|
||||
onRemoveFile={removeFile}
|
||||
onUpload={uploadFiles}
|
||||
selectedModel={selectedModelInfo}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
{/* 右侧:已上传文件列表 */}
|
||||
<Grid item xs={14} md={7} sx={{ maxWidth: '100%', width: '100%' }}>
|
||||
<FileList
|
||||
theme={theme}
|
||||
files={uploadedFiles}
|
||||
loading={loading}
|
||||
setPageLoading={setPageLoading}
|
||||
sendToFileUploader={array => sendToPages(array)}
|
||||
onDeleteFile={openDeleteConfirm}
|
||||
projectId={projectId}
|
||||
currentPage={currentPage}
|
||||
onPageChange={(page, fileName) => {
|
||||
if (fileName !== undefined) {
|
||||
// 搜索时更新搜索关键词和页码
|
||||
setSearchFileName(fileName);
|
||||
setCurrentPage(page);
|
||||
} else {
|
||||
// 翻页时只更新页码
|
||||
setCurrentPage(page);
|
||||
}
|
||||
}}
|
||||
onRefresh={fetchUploadedFiles} // 传递刷新函数
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<DeleteConfirmDialog
|
||||
open={deleteConfirmOpen}
|
||||
fileName={fileToDelete?.fileName}
|
||||
onClose={closeDeleteConfirm}
|
||||
onConfirm={confirmDeleteFile}
|
||||
/>
|
||||
|
||||
{/* 领域树操作选择对话框 */}
|
||||
<DomainTreeActionDialog
|
||||
open={domainTreeActionOpen}
|
||||
onClose={() => setDomainTreeActionOpen(false)}
|
||||
onConfirm={handleDomainTreeAction}
|
||||
isFirstUpload={isFirstUpload}
|
||||
action={domainTreeAction}
|
||||
/>
|
||||
{/* 检测到pdf的处理框 */}
|
||||
<PdfProcessingDialog
|
||||
open={pdfProcessConfirmOpen}
|
||||
onClose={() => setpdfProcessConfirmOpen(false)}
|
||||
onRadioChange={handleRadioChange}
|
||||
value={pdfStrategy}
|
||||
projectId={projectId}
|
||||
taskSettings={taskSettings}
|
||||
visionModels={visionModels}
|
||||
selectedViosnModel={selectedViosnModel}
|
||||
setSelectedViosnModel={setSelectedViosnModel}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
114
easy-dataset-main/components/text-split/LoadingBackdrop.js
Normal file
114
easy-dataset-main/components/text-split/LoadingBackdrop.js
Normal file
@@ -0,0 +1,114 @@
|
||||
'use client';
|
||||
|
||||
import { Backdrop, Paper, CircularProgress, Typography, Box, LinearProgress } from '@mui/material';
|
||||
|
||||
export default function LoadingBackdrop({ open, title, description, progress = null }) {
|
||||
return (
|
||||
<Backdrop
|
||||
sx={{
|
||||
color: '#fff',
|
||||
zIndex: theme => theme.zIndex.drawer + 1,
|
||||
position: 'fixed',
|
||||
backdropFilter: 'blur(5px)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
open={open}
|
||||
>
|
||||
<Paper
|
||||
elevation={4}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
p: 4,
|
||||
borderRadius: 3,
|
||||
bgcolor: 'background.paper',
|
||||
minWidth: 280,
|
||||
maxWidth: '90%',
|
||||
textAlign: 'center',
|
||||
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.12)',
|
||||
border: '1px solid rgba(0, 0, 0, 0.05)'
|
||||
}}
|
||||
>
|
||||
<CircularProgress
|
||||
size={48}
|
||||
thickness={4}
|
||||
sx={{
|
||||
mb: 3,
|
||||
color: theme => theme.palette.primary.main
|
||||
}}
|
||||
/>
|
||||
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
mb: 1,
|
||||
textAlign: 'center',
|
||||
width: '100%'
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
variant="body1"
|
||||
color="text.secondary"
|
||||
sx={{
|
||||
mb: 2,
|
||||
textAlign: 'center',
|
||||
width: '100%',
|
||||
mx: 'auto'
|
||||
}}
|
||||
>
|
||||
{description}
|
||||
</Typography>
|
||||
|
||||
{progress && progress.total > 0 && (
|
||||
<Box sx={{ width: '100%', mt: 2 }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
mb: 1.5
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
sx={{
|
||||
textAlign: 'center',
|
||||
mb: 0.5
|
||||
}}
|
||||
>
|
||||
{progress.completed}/{progress.total} ({progress.percentage}%)
|
||||
</Typography>
|
||||
|
||||
{progress.questionCount > 0 && (
|
||||
<Typography variant="body2" color="text.secondary" sx={{ textAlign: 'center' }}>
|
||||
已生成问题数: {progress.questionCount}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={progress.percentage}
|
||||
sx={{
|
||||
height: 6,
|
||||
borderRadius: 3,
|
||||
'& .MuiLinearProgress-bar': {
|
||||
borderRadius: 3
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Paper>
|
||||
</Backdrop>
|
||||
);
|
||||
}
|
||||
433
easy-dataset-main/components/text-split/MarkdownViewDialog.js
Normal file
433
easy-dataset-main/components/text-split/MarkdownViewDialog.js
Normal file
@@ -0,0 +1,433 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
CircularProgress,
|
||||
Typography,
|
||||
Divider,
|
||||
Chip,
|
||||
Switch,
|
||||
FormControlLabel,
|
||||
Alert,
|
||||
DialogContentText
|
||||
} from '@mui/material';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import SaveIcon from '@mui/icons-material/Save';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import 'github-markdown-css/github-markdown-light.css';
|
||||
|
||||
export default function MarkdownViewDialog({ open, text, onClose, projectId, onSaveSuccess }) {
|
||||
const { t } = useTranslation();
|
||||
const [customSplitMode, setCustomSplitMode] = useState(false);
|
||||
const [splitPoints, setSplitPoints] = useState([]);
|
||||
const [selectedText, setSelectedText] = useState('');
|
||||
const [savedMessage, setSavedMessage] = useState('');
|
||||
const [confirmDialogOpen, setConfirmDialogOpen] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const contentRef = useRef(null);
|
||||
const [chunksPreview, setChunksPreview] = useState([]);
|
||||
|
||||
// 根据分块点计算每个块的字数
|
||||
const calculateChunksPreview = points => {
|
||||
if (!text || !text.content) return [];
|
||||
|
||||
const content = text.content;
|
||||
const sortedPoints = [...points].sort((a, b) => a.position - b.position);
|
||||
|
||||
const chunks = [];
|
||||
let startPos = 0;
|
||||
|
||||
// 计算每个分块
|
||||
for (let i = 0; i < sortedPoints.length; i++) {
|
||||
const endPos = sortedPoints[i].position;
|
||||
const chunkContent = content.substring(startPos, endPos);
|
||||
|
||||
if (chunkContent.trim().length > 0) {
|
||||
chunks.push({
|
||||
index: i + 1,
|
||||
length: chunkContent.length,
|
||||
preview: chunkContent.substring(0, 20) + (chunkContent.length > 20 ? '...' : '')
|
||||
});
|
||||
}
|
||||
|
||||
startPos = endPos;
|
||||
}
|
||||
|
||||
// 添加最后一个分块
|
||||
const lastChunkContent = content.substring(startPos);
|
||||
if (lastChunkContent.trim().length > 0) {
|
||||
chunks.push({
|
||||
index: chunks.length + 1,
|
||||
length: lastChunkContent.length,
|
||||
preview: lastChunkContent.substring(0, 20) + (lastChunkContent.length > 20 ? '...' : '')
|
||||
});
|
||||
}
|
||||
|
||||
return chunks;
|
||||
};
|
||||
|
||||
// 重置组件状态
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setSplitPoints([]);
|
||||
setCustomSplitMode(false);
|
||||
setSelectedText('');
|
||||
setSavedMessage('');
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
// 当分块点变化时更新预览
|
||||
useEffect(() => {
|
||||
if (splitPoints.length > 0 && text?.content) {
|
||||
const preview = calculateChunksPreview(splitPoints);
|
||||
setChunksPreview(preview);
|
||||
} else {
|
||||
setChunksPreview([]);
|
||||
}
|
||||
}, [splitPoints, text?.content]);
|
||||
|
||||
// 处理用户选择文本事件
|
||||
const handleTextSelection = () => {
|
||||
if (!customSplitMode) return;
|
||||
|
||||
const selection = window.getSelection();
|
||||
if (!selection.toString().trim()) return;
|
||||
|
||||
// 获取选择的文本内容和位置
|
||||
const selectedContent = selection.toString();
|
||||
|
||||
// 计算选择位置在文档中的偏移量
|
||||
const range = selection.getRangeAt(0);
|
||||
const preCaretRange = range.cloneRange();
|
||||
preCaretRange.selectNodeContents(contentRef.current);
|
||||
preCaretRange.setEnd(range.endContainer, range.endOffset);
|
||||
const position = preCaretRange.toString().length;
|
||||
|
||||
// 添加到分割点列表
|
||||
const newPoint = {
|
||||
id: Date.now(),
|
||||
position,
|
||||
preview: selectedContent.substring(0, 40) + (selectedContent.length > 40 ? '...' : '')
|
||||
};
|
||||
|
||||
setSplitPoints(prev => [...prev, newPoint].sort((a, b) => a.position - b.position));
|
||||
setSelectedText('');
|
||||
};
|
||||
|
||||
// 删除分割点
|
||||
const handleDeletePoint = id => {
|
||||
setSplitPoints(prev => prev.filter(point => point.id !== id));
|
||||
};
|
||||
|
||||
// 弹出确认对话框
|
||||
const handleConfirmSave = () => {
|
||||
setConfirmDialogOpen(true);
|
||||
};
|
||||
|
||||
// 取消保存
|
||||
const handleCancelSave = () => {
|
||||
setConfirmDialogOpen(false);
|
||||
};
|
||||
|
||||
// 确认并执行保存
|
||||
const handleSavePoints = async () => {
|
||||
// 输出调试信息
|
||||
console.log('保存分块点时的数据:', {
|
||||
projectId,
|
||||
text: text
|
||||
? {
|
||||
fileId: text.fileId,
|
||||
fileName: text.fileName,
|
||||
contentLength: text.content ? text.content.length : 0
|
||||
}
|
||||
: null,
|
||||
splitPointsCount: splitPoints.length
|
||||
});
|
||||
|
||||
if (!text) {
|
||||
setError(t('textSplit.missingRequiredData') + ': text 为空');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!text.fileId) {
|
||||
setError(t('textSplit.missingRequiredData') + ': fileId 不存在');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!text.fileName) {
|
||||
setError(t('textSplit.missingRequiredData') + ': fileName 不存在');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!text.content) {
|
||||
setError(t('textSplit.missingRequiredData') + ': content 不存在');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!projectId) {
|
||||
setError(t('textSplit.missingRequiredData') + ': projectId 不存在');
|
||||
return;
|
||||
}
|
||||
|
||||
setConfirmDialogOpen(false);
|
||||
setSaving(true);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
// 准备要发送的数据
|
||||
const customSplitData = {
|
||||
fileId: text.fileId,
|
||||
fileName: text.fileName,
|
||||
content: text.content,
|
||||
splitPoints: splitPoints.map(point => ({
|
||||
position: point.position,
|
||||
preview: point.preview
|
||||
}))
|
||||
};
|
||||
|
||||
// 发送请求到待创建的API接口
|
||||
const response = await fetch(`/api/projects/${projectId}/custom-split`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(customSplitData)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || t('textSplit.customSplitFailed'));
|
||||
}
|
||||
|
||||
// 保存成功
|
||||
setSavedMessage(t('textSplit.customSplitSuccess'));
|
||||
|
||||
// 短暂显示成功消息后关闭对话框并刷新列表
|
||||
setTimeout(() => {
|
||||
setSavedMessage('');
|
||||
|
||||
// 关闭对话框
|
||||
onClose();
|
||||
|
||||
// 调用父组件的刷新方法(如果提供了)
|
||||
if (typeof onSaveSuccess === 'function') {
|
||||
onSaveSuccess();
|
||||
}
|
||||
}, 1500);
|
||||
} catch (err) {
|
||||
console.error('保存自定义分块出错:', err);
|
||||
setError(err.message || t('textSplit.customSplitFailed'));
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
|
||||
<DialogTitle sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Typography variant="h6">{text ? text.fileName : ''}</Typography>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch checked={customSplitMode} onChange={e => setCustomSplitMode(e.target.checked)} color="primary" />
|
||||
}
|
||||
label={t('textSplit.customSplitMode')}
|
||||
sx={{ ml: 2 }}
|
||||
/>
|
||||
</DialogTitle>
|
||||
|
||||
{customSplitMode && (
|
||||
<Box sx={{ px: 3, py: 1, bgcolor: 'action.hover' }}>
|
||||
<Typography variant="subtitle2" color="textSecondary" gutterBottom>
|
||||
{t('textSplit.customSplitInstructions')}
|
||||
</Typography>
|
||||
|
||||
{/* 分割点列表 */}
|
||||
{splitPoints.length > 0 && (
|
||||
<Box sx={{ mt: 1, mb: 2 }}>
|
||||
<Typography variant="body2" sx={{ mb: 1 }}>
|
||||
{t('textSplit.splitPointsList')} ({splitPoints.length}):
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
|
||||
{splitPoints.map((point, index) => (
|
||||
<Chip
|
||||
key={point.id}
|
||||
label={`${index + 1}. ${point.preview}`}
|
||||
onDelete={() => handleDeletePoint(point.id)}
|
||||
deleteIcon={<DeleteIcon />}
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
{/* 文本块字数预览 */}
|
||||
{chunksPreview.length > 0 && (
|
||||
<Box
|
||||
sx={{
|
||||
mt: 2,
|
||||
p: 1,
|
||||
bgcolor: 'background.paper',
|
||||
borderRadius: 1,
|
||||
border: '1px dashed',
|
||||
borderColor: 'divider'
|
||||
}}
|
||||
>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
{t('textSplit.chunksPreview')}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
|
||||
{chunksPreview.map(chunk => (
|
||||
<Chip
|
||||
key={chunk.index}
|
||||
size="small"
|
||||
label={`${t('textSplit.chunk')} ${chunk.index}: ${chunk.length}${t('textSplit.characters')}`}
|
||||
color="info"
|
||||
variant="outlined"
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* 保存按钮 */}
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mt: 1 }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<SaveIcon />}
|
||||
disabled={splitPoints.length === 0 || saving}
|
||||
onClick={handleConfirmSave}
|
||||
size="small"
|
||||
>
|
||||
{saving ? t('common.saving') : t('textSplit.saveSplitPoints')}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{/* 提示消息 */}
|
||||
{savedMessage && (
|
||||
<Alert severity="success" sx={{ mt: 1 }}>
|
||||
{savedMessage}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ mt: 1 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Divider />
|
||||
|
||||
<DialogContent dividers>
|
||||
{text ? (
|
||||
<Box
|
||||
sx={{
|
||||
maxHeight: '60vh',
|
||||
overflow: 'auto',
|
||||
cursor: customSplitMode ? 'text' : 'default',
|
||||
position: 'relative',
|
||||
'::selection': {
|
||||
backgroundColor: customSplitMode ? 'primary.light' : 'inherit',
|
||||
color: customSplitMode ? 'primary.contrastText' : 'inherit'
|
||||
}
|
||||
}}
|
||||
onMouseUp={handleTextSelection}
|
||||
ref={contentRef}
|
||||
>
|
||||
{/* 渲染带有分割点标记的内容 */}
|
||||
{customSplitMode && splitPoints.length > 0 ? (
|
||||
<Box>
|
||||
<pre style={{ whiteSpace: 'pre-wrap', wordWrap: 'break-word', fontFamily: 'inherit' }}>
|
||||
{text.content.split('').map((char, index) => {
|
||||
const isSplitPoint = splitPoints.some(point => point.position === index);
|
||||
const splitPointIndex = splitPoints.findIndex(point => point.position === index);
|
||||
|
||||
if (isSplitPoint) {
|
||||
return (
|
||||
<React.Fragment key={index}>
|
||||
<span
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
width: '100%',
|
||||
borderTop: '2px dashed #1976d2',
|
||||
marginTop: '8px',
|
||||
marginBottom: '8px',
|
||||
position: 'relative'
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: '0',
|
||||
top: '-15px',
|
||||
backgroundColor: '#1976d2',
|
||||
color: 'white',
|
||||
padding: '0 6px',
|
||||
borderRadius: '4px',
|
||||
fontSize: '12px'
|
||||
}}
|
||||
>
|
||||
{splitPointIndex + 1}
|
||||
</span>
|
||||
</span>
|
||||
{char}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
return char;
|
||||
})}
|
||||
</pre>
|
||||
</Box>
|
||||
) : (
|
||||
<Box>
|
||||
<div className="markdown-body">
|
||||
<ReactMarkdown>{text.content}</ReactMarkdown>
|
||||
</div>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
) : (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', p: 4 }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
)}
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>{t('common.close')}</Button>
|
||||
</DialogActions>
|
||||
|
||||
{/* 确认对话框 */}
|
||||
<Dialog
|
||||
open={confirmDialogOpen}
|
||||
onClose={handleCancelSave}
|
||||
aria-labelledby="alert-dialog-title"
|
||||
aria-describedby="alert-dialog-description"
|
||||
>
|
||||
<DialogTitle id="alert-dialog-title">{t('textSplit.confirmCustomSplitTitle')}</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText id="alert-dialog-description">
|
||||
{t('textSplit.confirmCustomSplitMessage')}
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleCancelSave}>{t('common.cancel')}</Button>
|
||||
<Button onClick={handleSavePoints} color="primary" variant="contained" autoFocus>
|
||||
{t('common.confirm')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
43
easy-dataset-main/components/text-split/PdfSettings.js
Normal file
43
easy-dataset-main/components/text-split/PdfSettings.js
Normal file
@@ -0,0 +1,43 @@
|
||||
'use client';
|
||||
|
||||
import { Box, Select, MenuItem, Typography, FormControl, InputLabel } from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default function PdfSettings({ pdfStrategy, setPdfStrategy, selectedViosnModel, setSelectedViosnModel }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'row', gap: 2, mt: 2 }}>
|
||||
<FormControl sx={{ minWidth: 200 }}>
|
||||
<InputLabel id="pdf-strategy-label">{t('textSplit.pdfStrategy')}</InputLabel>
|
||||
<Select
|
||||
labelId="pdf-strategy-label"
|
||||
value={pdfStrategy}
|
||||
onChange={e => setPdfStrategy(e.target.value)}
|
||||
label={t('textSplit.pdfStrategy')}
|
||||
size="small"
|
||||
>
|
||||
<MenuItem value="default">{t('textSplit.defaultStrategy')}</MenuItem>
|
||||
<MenuItem value="vision">{t('textSplit.visionStrategy')}</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
{pdfStrategy === 'vision' && (
|
||||
<FormControl sx={{ minWidth: 200 }}>
|
||||
<InputLabel id="vision-model-label">{t('textSplit.visionModel')}</InputLabel>
|
||||
<Select
|
||||
labelId="vision-model-label"
|
||||
value={selectedViosnModel}
|
||||
onChange={e => setSelectedViosnModel(e.target.value)}
|
||||
label={t('textSplit.visionModel')}
|
||||
size="small"
|
||||
>
|
||||
<MenuItem value="gpt-4-vision-preview">GPT-4 Vision</MenuItem>
|
||||
<MenuItem value="claude-3-opus">Claude-3 Opus</MenuItem>
|
||||
<MenuItem value="claude-3-sonnet">Claude-3 Sonnet</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogActions,
|
||||
Button,
|
||||
Typography,
|
||||
Box,
|
||||
Alert
|
||||
} from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default function DeleteConfirmDialog({ open, fileName, onClose, onConfirm }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
aria-labelledby="delete-dialog-title"
|
||||
aria-describedby="delete-dialog-description"
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle id="delete-dialog-title">
|
||||
{t('common.confirmDelete')}「{fileName}」?
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText id="delete-dialog-description">{t('common.confirmDeleteDescription')}</DialogContentText>
|
||||
|
||||
<Alert severity="warning" sx={{ my: 2 }}>
|
||||
<Typography variant="body2" component="div" fontWeight="medium">
|
||||
{t('textSplit.deleteFileWarning')}
|
||||
</Typography>
|
||||
<Box sx={{ mt: 1 }}>
|
||||
<Typography variant="body2" component="div">
|
||||
• {t('textSplit.deleteFileWarningChunks')}
|
||||
</Typography>
|
||||
<Typography variant="body2" component="div">
|
||||
• {t('textSplit.deleteFileWarningQuestions')}
|
||||
</Typography>
|
||||
<Typography variant="body2" component="div">
|
||||
• {t('textSplit.deleteFileWarningDatasets')}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Alert>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} color="primary">
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button onClick={onConfirm} color="error" variant="contained">
|
||||
{t('common.delete')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
'use client';
|
||||
|
||||
import { Box, List, ListItem, ListItemIcon, ListItemText, Collapse, IconButton } from '@mui/material';
|
||||
import FolderIcon from '@mui/icons-material/Folder';
|
||||
import ArticleIcon from '@mui/icons-material/Article';
|
||||
import ExpandLess from '@mui/icons-material/ExpandLess';
|
||||
import ExpandMore from '@mui/icons-material/ExpandMore';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
|
||||
/**
|
||||
* 目录结构组件
|
||||
* @param {Object} props
|
||||
* @param {Array} props.items - 目录项数组
|
||||
* @param {Object} props.expandedItems - 展开状态对象
|
||||
* @param {Function} props.onToggleItem - 展开/折叠回调
|
||||
* @param {number} props.level - 当前层级
|
||||
* @param {string} props.parentId - 父级ID
|
||||
*/
|
||||
export default function DirectoryView({ items, expandedItems, onToggleItem, level = 0, parentId = '' }) {
|
||||
const theme = useTheme();
|
||||
|
||||
if (!items || items.length === 0) return null;
|
||||
|
||||
return (
|
||||
<List sx={{ pl: level > 0 ? 2 : 0 }}>
|
||||
{items.map((item, index) => {
|
||||
const itemId = `${parentId}-${index}`;
|
||||
const hasChildren = item.children && item.children.length > 0;
|
||||
const isExpanded = expandedItems[itemId] || false;
|
||||
|
||||
return (
|
||||
<Box key={itemId}>
|
||||
<ListItem
|
||||
sx={{
|
||||
pl: level * 2,
|
||||
borderLeft: level > 0 ? `1px solid ${theme.palette.divider}` : 'none',
|
||||
ml: level > 0 ? 1 : 0
|
||||
}}
|
||||
>
|
||||
<ListItemIcon sx={{ minWidth: 36 }}>
|
||||
{hasChildren ? <FolderIcon color="primary" /> : <ArticleIcon color="info" />}
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={item.text}
|
||||
primaryTypographyProps={{
|
||||
fontWeight: level === 0 ? 'bold' : 'normal',
|
||||
variant: level === 0 ? 'subtitle1' : 'body2'
|
||||
}}
|
||||
/>
|
||||
{hasChildren && (
|
||||
<IconButton size="small" onClick={() => onToggleItem(itemId)}>
|
||||
{isExpanded ? <ExpandLess /> : <ExpandMore />}
|
||||
</IconButton>
|
||||
)}
|
||||
</ListItem>
|
||||
|
||||
{hasChildren && (
|
||||
<Collapse in={isExpanded} timeout="auto" unmountOnExit>
|
||||
<DirectoryView
|
||||
items={item.children}
|
||||
expandedItems={expandedItems}
|
||||
onToggleItem={onToggleItem}
|
||||
level={level + 1}
|
||||
parentId={itemId}
|
||||
/>
|
||||
</Collapse>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Button,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
FormControlLabel,
|
||||
FormControl,
|
||||
Typography
|
||||
} from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
/**
|
||||
* 领域树操作选择对话框
|
||||
* 提供三种选项:修订领域树、重建领域树、不更改领域树
|
||||
*/
|
||||
export default function DomainTreeActionDialog({ open, onClose, onConfirm, isFirstUpload, action }) {
|
||||
const { t } = useTranslation();
|
||||
const [value, setValue] = useState(isFirstUpload ? 'rebuild' : 'revise');
|
||||
|
||||
// 处理选项变更
|
||||
const handleChange = event => {
|
||||
setValue(event.target.value);
|
||||
};
|
||||
|
||||
// 确认选择
|
||||
const handleConfirm = () => {
|
||||
onConfirm(value);
|
||||
};
|
||||
|
||||
// 获取对话框标题
|
||||
const getDialogTitle = () => {
|
||||
if (isFirstUpload) {
|
||||
return t('textSplit.domainTree.firstUploadTitle');
|
||||
}
|
||||
return action === 'upload' ? t('textSplit.domainTree.uploadTitle') : t('textSplit.domainTree.deleteTitle');
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>{getDialogTitle()}</DialogTitle>
|
||||
<DialogContent>
|
||||
<FormControl component="fieldset">
|
||||
<RadioGroup value={value} onChange={handleChange}>
|
||||
{!isFirstUpload && (
|
||||
<FormControlLabel
|
||||
value="revise"
|
||||
control={<Radio />}
|
||||
label={
|
||||
<>
|
||||
<Typography variant="subtitle1">{t('textSplit.domainTree.reviseOption')}</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t('textSplit.domainTree.reviseDesc')}
|
||||
</Typography>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<FormControlLabel
|
||||
value="rebuild"
|
||||
control={<Radio />}
|
||||
label={
|
||||
<>
|
||||
<Typography variant="subtitle1">{t('textSplit.domainTree.rebuildOption')}</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t('textSplit.domainTree.rebuildDesc')}
|
||||
</Typography>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
{!isFirstUpload && (
|
||||
<FormControlLabel
|
||||
value="keep"
|
||||
control={<Radio />}
|
||||
label={
|
||||
<>
|
||||
<Typography variant="subtitle1">{t('textSplit.domainTree.keepOption')}</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t('textSplit.domainTree.keepDesc')}
|
||||
</Typography>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>{t('common.cancel')}</Button>
|
||||
<Button onClick={handleConfirm} variant="contained" color="primary">
|
||||
{t('common.confirm')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
'use client';
|
||||
|
||||
import { Box } from '@mui/material';
|
||||
import { TreeView, TreeItem } from '@mui/lab';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
|
||||
|
||||
/**
|
||||
* 领域知识树组件
|
||||
* @param {Object} props
|
||||
* @param {Array} props.nodes - 树节点数组
|
||||
*/
|
||||
export default function DomainTreeView({ nodes = [] }) {
|
||||
if (!nodes || nodes.length === 0) return null;
|
||||
|
||||
const renderTreeItems = nodes => {
|
||||
return nodes.map((node, index) => (
|
||||
<TreeItem key={`node-${index}`} nodeId={`node-${index}`} label={node.text} sx={{ mb: 1 }}>
|
||||
{node.children && node.children.length > 0 && renderTreeItems(node.children)}
|
||||
</TreeItem>
|
||||
));
|
||||
};
|
||||
|
||||
return (
|
||||
<TreeView
|
||||
defaultCollapseIcon={<ExpandMoreIcon />}
|
||||
defaultExpandIcon={<ChevronRightIcon />}
|
||||
sx={{ flexGrow: 1, overflowY: 'auto' }}
|
||||
>
|
||||
{renderTreeItems(nodes)}
|
||||
</TreeView>
|
||||
);
|
||||
}
|
||||
1068
easy-dataset-main/components/text-split/components/FileList.js
Normal file
1068
easy-dataset-main/components/text-split/components/FileList.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,208 @@
|
||||
'use client';
|
||||
|
||||
import { Box, Typography, keyframes, Paper } from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { handleLongFileName } from '@/lib/file/file-process';
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
// 定义动画效果
|
||||
const pulse = keyframes`
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(32, 76, 255, 0.2);
|
||||
}
|
||||
70% {
|
||||
box-shadow: 0 0 0 15px rgba(32, 76, 255, 0);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(32, 76, 255, 0);
|
||||
}
|
||||
`;
|
||||
|
||||
const rotateAnimation = keyframes`
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
`;
|
||||
|
||||
const shimmer = keyframes`
|
||||
0% { background-position: -200% 0; }
|
||||
100% { background-position: 200% 0; }
|
||||
`;
|
||||
|
||||
/**
|
||||
* 文件处理进度展示组件 - 美化版
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {Object} props.fileTask - 文件处理任务信息
|
||||
*/
|
||||
export default function FileLoadingProgress({ fileTask }) {
|
||||
const { t } = useTranslation();
|
||||
const [animationStep, setAnimationStep] = useState(0);
|
||||
|
||||
// 创建动态效果
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setAnimationStep(prev => (prev + 1) % 4);
|
||||
}, 600);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
if (!fileTask) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const pageProgress = (fileTask.current.processedPage / fileTask.current.totalPage) * 100;
|
||||
const filesProgress = (fileTask.processedFiles / fileTask.totalFiles) * 100;
|
||||
|
||||
// 生成进度指示器文本
|
||||
const getProgressIndicator = () => {
|
||||
const dots = '.';
|
||||
return dots.repeat(animationStep + 1);
|
||||
};
|
||||
|
||||
return (
|
||||
<Paper
|
||||
elevation={3}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: 'auto',
|
||||
minHeight: '25vh',
|
||||
width: '80%',
|
||||
maxWidth: '600px',
|
||||
margin: '0 auto',
|
||||
padding: 4,
|
||||
borderRadius: 3,
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
background: 'linear-gradient(45deg, #f9f9f9 0%, #ffffff 100%)',
|
||||
animation: `${pulse} 2s infinite`
|
||||
}}
|
||||
>
|
||||
{/* 背景动画元素 */}
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: '-50%',
|
||||
left: '-50%',
|
||||
width: '200%',
|
||||
height: '200%',
|
||||
background: 'radial-gradient(circle, rgba(32,76,255,0.05) 0%, rgba(255,255,255,0) 70%)',
|
||||
animation: `${rotateAnimation} 15s linear infinite`,
|
||||
zIndex: 0
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 主标题 */}
|
||||
<Typography
|
||||
variant="h5"
|
||||
fontWeight="bold"
|
||||
sx={{
|
||||
mb: 3,
|
||||
position: 'relative',
|
||||
zIndex: 1,
|
||||
background: 'linear-gradient(90deg, #3a7bd5 0%, #00d2ff 100%)',
|
||||
backgroundClip: 'text',
|
||||
textFillColor: 'transparent',
|
||||
WebkitBackgroundClip: 'text',
|
||||
WebkitTextFillColor: 'transparent'
|
||||
}}
|
||||
>
|
||||
{t('textSplit.pdfProcessingLoading')}
|
||||
{getProgressIndicator()}
|
||||
</Typography>
|
||||
|
||||
{/* 处理进度显示区域 */}
|
||||
<Box sx={{ width: '90%', mt: 2, mb: 3, position: 'relative', zIndex: 1 }}>
|
||||
{/* 当前文件进度 */}
|
||||
<ProgressSection
|
||||
label={t('textSplit.pdfPageProcessStatus', {
|
||||
fileName: handleLongFileName(fileTask.current.fileName),
|
||||
total: fileTask.current.totalPage,
|
||||
completed: fileTask.current.processedPage
|
||||
})}
|
||||
progress={pageProgress}
|
||||
color="#3a7bd5"
|
||||
/>
|
||||
|
||||
{/* 总文件进度 */}
|
||||
<ProgressSection
|
||||
label={t('textSplit.pdfProcessStatus', {
|
||||
total: fileTask.totalFiles,
|
||||
completed: fileTask.processedFiles
|
||||
})}
|
||||
progress={filesProgress}
|
||||
color="#00d2ff"
|
||||
mt={3}
|
||||
/>
|
||||
</Box>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 进度条区域组件
|
||||
*/
|
||||
function ProgressSection({ label, progress, color, mt = 0 }) {
|
||||
return (
|
||||
<Box sx={{ mt }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
mb: 1,
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="body2"
|
||||
fontWeight="medium"
|
||||
sx={{
|
||||
color: 'text.primary',
|
||||
fontSize: '0.9rem'
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="h6"
|
||||
fontWeight="bold"
|
||||
sx={{
|
||||
color,
|
||||
fontSize: '1.1rem'
|
||||
}}
|
||||
>
|
||||
{Math.round(progress)}%
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* 自定义进度条 */}
|
||||
<Box
|
||||
sx={{
|
||||
height: 10,
|
||||
borderRadius: 5,
|
||||
background: '#f0f0f0',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
boxShadow: 'inset 0 1px 2px rgba(0,0,0,0.1)'
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
height: '100%',
|
||||
width: `${progress}%`,
|
||||
borderRadius: 5,
|
||||
background: `linear-gradient(90deg, ${color} 0%, ${color}80 100%)`,
|
||||
transition: 'width 0.5s ease',
|
||||
backgroundSize: '200% 100%',
|
||||
animation: `${shimmer} 2s infinite linear`
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
Card,
|
||||
CardContent,
|
||||
Typography,
|
||||
Box,
|
||||
Stack,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Select,
|
||||
MenuItem
|
||||
} from '@mui/material';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import ArticleOutlinedIcon from '@mui/icons-material/ArticleOutlined';
|
||||
import ScienceOutlinedIcon from '@mui/icons-material/ScienceOutlined';
|
||||
import LaunchOutlinedIcon from '@mui/icons-material/LaunchOutlined';
|
||||
import SmartToyOutlinedIcon from '@mui/icons-material/SmartToyOutlined';
|
||||
import ChangeCircleOutlinedIcon from '@mui/icons-material/ChangeCircleOutlined';
|
||||
|
||||
const StyledCard = styled(Card)(({ theme, disabled }) => ({
|
||||
cursor: disabled ? 'not-allowed' : 'pointer',
|
||||
opacity: disabled ? 0.6 : 1,
|
||||
transition: 'transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out',
|
||||
'&:hover': disabled
|
||||
? {}
|
||||
: {
|
||||
transform: 'translateY(-4px)',
|
||||
boxShadow: theme.shadows[4]
|
||||
}
|
||||
}));
|
||||
|
||||
const OptionCard = ({
|
||||
icon,
|
||||
title,
|
||||
description,
|
||||
disabled,
|
||||
onClick,
|
||||
selected,
|
||||
isVisionEnabled,
|
||||
visionModels,
|
||||
selectorName,
|
||||
handleSettingChange,
|
||||
selectedViosnModel
|
||||
}) => (
|
||||
<StyledCard
|
||||
disabled={disabled}
|
||||
onClick={disabled ? undefined : onClick}
|
||||
sx={{
|
||||
height: '100%',
|
||||
border: selected ? '2px solid primary.main' : '1px solid divider',
|
||||
backgroundColor: selected ? 'action.selected' : 'background.paper'
|
||||
}}
|
||||
>
|
||||
<CardContent>
|
||||
<Stack spacing={1}>
|
||||
<Box sx={{ color: 'primary.main', mb: 1 }}>{icon}</Box>
|
||||
<Typography variant="h6" component="div">
|
||||
{title}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{description}
|
||||
</Typography>
|
||||
{isVisionEnabled && (
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>{selectorName}</InputLabel>
|
||||
<Select
|
||||
label={selectorName}
|
||||
value={selectedViosnModel}
|
||||
onChange={e => handleSettingChange(e)}
|
||||
name="vision"
|
||||
>
|
||||
{visionModels.map(item => (
|
||||
<MenuItem key={item.id} value={item.id}>
|
||||
{item.modelName} ({item.providerName})
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
</Stack>
|
||||
</CardContent>
|
||||
</StyledCard>
|
||||
);
|
||||
|
||||
export default function PdfProcessingDialog({
|
||||
open,
|
||||
onClose,
|
||||
onRadioChange,
|
||||
value,
|
||||
taskSettings,
|
||||
visionModels,
|
||||
selectedViosnModel,
|
||||
setSelectedViosnModel
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
//检查配置中是否启用MinerU
|
||||
const isMinerUEnabled = taskSettings && taskSettings.minerUToken ? true : false;
|
||||
|
||||
const isMinerULocalEnabled = taskSettings && taskSettings.minerULocalUrl ? true : false;
|
||||
|
||||
//检查配置中是否启用Vision策略
|
||||
const isVisionEnabled = visionModels.length > 0 ? true : false;
|
||||
|
||||
//用于传递到父组件,显示当前选中的模型
|
||||
let selectedModel = selectedViosnModel;
|
||||
|
||||
const handleOptionClick = optionValue => {
|
||||
if (optionValue === 'mineru-web') {
|
||||
window.open('https://mineru.net/OpenSourceTools/Extractor', '_blank');
|
||||
} else {
|
||||
onRadioChange({ target: { value: optionValue, selectedVision: selectedModel } });
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
// 处理设置变更
|
||||
const handleSettingChange = e => {
|
||||
const { value } = e.target;
|
||||
selectedModel = value;
|
||||
setSelectedViosnModel(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} fullWidth maxWidth="md">
|
||||
<DialogTitle>{t('textSplit.pdfProcess')}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))',
|
||||
gap: 2,
|
||||
p: 1
|
||||
}}
|
||||
>
|
||||
<OptionCard
|
||||
icon={<ArticleOutlinedIcon fontSize="large" />}
|
||||
title={t('textSplit.basicPdfParsing')}
|
||||
description={t('textSplit.basicPdfParsingDesc')}
|
||||
onClick={() => handleOptionClick('default')}
|
||||
selected={value === 'default'}
|
||||
/>
|
||||
<OptionCard
|
||||
icon={<ScienceOutlinedIcon fontSize="large" />}
|
||||
title="MinerU API"
|
||||
description={isMinerUEnabled ? t('textSplit.mineruApiDesc') : t('textSplit.mineruApiDescDisabled')}
|
||||
disabled={!isMinerUEnabled}
|
||||
onClick={() => handleOptionClick('mineru')}
|
||||
selected={value === 'mineru'}
|
||||
/>
|
||||
<OptionCard
|
||||
icon={<ChangeCircleOutlinedIcon fontSize="large" />}
|
||||
title="MinerU Local"
|
||||
description={isMinerULocalEnabled ? t('textSplit.mineruLocalDesc') : t('textSplit.mineruLocalDisabled')}
|
||||
disabled={!isMinerULocalEnabled}
|
||||
onClick={() => handleOptionClick('mineru-local')}
|
||||
selected={value === 'mineru-local'}
|
||||
/>
|
||||
<OptionCard
|
||||
icon={<LaunchOutlinedIcon fontSize="large" />}
|
||||
title={t('textSplit.mineruWebPlatform')}
|
||||
description={t('textSplit.mineruWebPlatformDesc')}
|
||||
onClick={() => handleOptionClick('mineru-web')}
|
||||
/>
|
||||
<OptionCard
|
||||
icon={<SmartToyOutlinedIcon fontSize="large" />}
|
||||
title={t('textSplit.customVisionModel')}
|
||||
description={t('textSplit.customVisionModelDesc')}
|
||||
disabled={!isVisionEnabled}
|
||||
onClick={() => handleOptionClick('vision')}
|
||||
selected={value === 'vision'}
|
||||
isVisionEnabled={isVisionEnabled}
|
||||
visionModels={visionModels}
|
||||
selectorName={t('settings.vision')}
|
||||
selectedViosnModel={selectedViosnModel}
|
||||
handleSettingChange={handleSettingChange}
|
||||
/>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
'use client';
|
||||
|
||||
import { Box } from '@mui/material';
|
||||
|
||||
/**
|
||||
* 标签页面板组件
|
||||
* @param {Object} props
|
||||
* @param {number} props.value - 当前激活的标签索引
|
||||
* @param {number} props.index - 当前面板对应的索引
|
||||
* @param {ReactNode} props.children - 子组件
|
||||
*/
|
||||
export default function TabPanel({ value, index, children }) {
|
||||
return (
|
||||
<Box
|
||||
role="tabpanel"
|
||||
hidden={value !== index}
|
||||
id={`domain-tabpanel-${index}`}
|
||||
aria-labelledby={`domain-tab-${index}`}
|
||||
sx={{ height: '100%' }}
|
||||
>
|
||||
{value === index && <Box sx={{ height: '100%' }}>{children}</Box>}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
207
easy-dataset-main/components/text-split/components/UploadArea.js
Normal file
207
easy-dataset-main/components/text-split/components/UploadArea.js
Normal file
@@ -0,0 +1,207 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Typography,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
Divider,
|
||||
CircularProgress,
|
||||
Tooltip
|
||||
} from '@mui/material';
|
||||
import UploadFileIcon from '@mui/icons-material/UploadFile';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import { alpha } from '@mui/material/styles';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import React, { useRef, useState } from 'react';
|
||||
|
||||
export default function UploadArea({
|
||||
theme,
|
||||
files,
|
||||
uploading,
|
||||
uploadedFiles,
|
||||
onFileSelect,
|
||||
onRemoveFile,
|
||||
onUpload,
|
||||
selectedModel
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [dragActive, setDragActive] = useState(false);
|
||||
const inputRef = useRef(null);
|
||||
|
||||
// 拖拽进入
|
||||
const handleDragOver = e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!dragActive) setDragActive(true);
|
||||
};
|
||||
// 拖拽离开
|
||||
const handleDragLeave = e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setDragActive(false);
|
||||
};
|
||||
// 拖拽释放
|
||||
const handleDrop = e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setDragActive(false);
|
||||
if (!selectedModel?.id || uploading) return;
|
||||
const files = e.dataTransfer.files;
|
||||
if (files && files.length > 0) {
|
||||
// 构造一个模拟的 event 以复用 onFileSelect
|
||||
const event = { target: { files } };
|
||||
onFileSelect(event);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
p: 3,
|
||||
height: '100%',
|
||||
border: `2px dashed ${dragActive ? theme.palette.primary.main : alpha(theme.palette.primary.main, 0.2)}`,
|
||||
borderRadius: 2,
|
||||
bgcolor: dragActive ? alpha(theme.palette.primary.main, 0.12) : alpha(theme.palette.primary.main, 0.05),
|
||||
transition: 'all 0.3s ease',
|
||||
'&:hover': {
|
||||
bgcolor: alpha(theme.palette.primary.main, 0.08),
|
||||
borderColor: alpha(theme.palette.primary.main, 0.3)
|
||||
},
|
||||
cursor: uploading || !selectedModel?.id ? 'not-allowed' : 'pointer',
|
||||
position: 'relative'
|
||||
}}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
{dragActive && (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
bgcolor: alpha(theme.palette.primary.main, 0.3),
|
||||
zIndex: 10,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
pointerEvents: 'none',
|
||||
borderRadius: 2,
|
||||
border: `3px solid ${theme.palette.primary.main}`,
|
||||
backdropFilter: 'blur(2px)'
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
bgcolor: 'rgba(255, 255, 255, 0.9)',
|
||||
p: 3,
|
||||
borderRadius: 1,
|
||||
boxShadow: '0 4px 8px rgba(0,0,0,0.1)',
|
||||
textAlign: 'center',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
border: `1px solid ${theme.palette.primary.main}`
|
||||
}}
|
||||
>
|
||||
<UploadFileIcon color="primary" sx={{ fontSize: 40, mb: 1 }} />
|
||||
<Typography variant="h6" color="primary" sx={{ fontWeight: 'bold' }}>
|
||||
{t('textSplit.dragToUpload', { defaultValue: '拖拽文件到此处上传' })}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
<Typography variant="subtitle1" gutterBottom>
|
||||
{t('textSplit.uploadNewDocument')}
|
||||
</Typography>
|
||||
|
||||
<Tooltip
|
||||
title={!selectedModel?.id ? t('textSplit.selectModelFirst', { defaultValue: '请先在右上角选择模型' }) : ''}
|
||||
>
|
||||
<span>
|
||||
<Button
|
||||
component="label"
|
||||
variant="contained"
|
||||
startIcon={<UploadFileIcon />}
|
||||
sx={{ mb: 2, mt: 2 }}
|
||||
disabled={!selectedModel?.id || uploading}
|
||||
>
|
||||
{t('textSplit.selectFile')}
|
||||
<input
|
||||
type="file"
|
||||
hidden
|
||||
accept=".md,.txt,.docx,.pdf,.epub"
|
||||
multiple
|
||||
onChange={onFileSelect}
|
||||
disabled={!selectedModel?.id || uploading}
|
||||
/>
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
{uploadedFiles.total > 0 ? t('textSplit.mutilFileMessage') : t('textSplit.supportedFormats')}
|
||||
</Typography>
|
||||
|
||||
{files.length > 0 && (
|
||||
<Box sx={{ mt: 3, width: '100%' }}>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
{t('textSplit.selectedFiles', { count: files.length })}
|
||||
</Typography>
|
||||
|
||||
<List sx={{ bgcolor: theme.palette.background.paper, borderRadius: 1, maxHeight: '200px', overflow: 'auto' }}>
|
||||
{files.map((file, index) => (
|
||||
<Box key={index}>
|
||||
<ListItem
|
||||
secondaryAction={
|
||||
<Button
|
||||
size="small"
|
||||
color="error"
|
||||
startIcon={<DeleteIcon />}
|
||||
onClick={() => onRemoveFile(index)}
|
||||
disabled={uploading}
|
||||
>
|
||||
{t('common.delete')}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<ListItemText primary={file.name} secondary={`${(file.size / 1024).toFixed(2)} KB`} />
|
||||
</ListItem>
|
||||
{index < files.length - 1 && <Divider />}
|
||||
</Box>
|
||||
))}
|
||||
</List>
|
||||
|
||||
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'center' }}>
|
||||
<Tooltip
|
||||
title={
|
||||
!selectedModel?.id ? t('textSplit.selectModelFirst', { defaultValue: '请先在右上角选择模型' }) : ''
|
||||
}
|
||||
>
|
||||
<span>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={onUpload}
|
||||
disabled={uploading || !selectedModel?.id}
|
||||
sx={{ minWidth: 120 }}
|
||||
>
|
||||
{uploading ? <CircularProgress size={24} /> : t('textSplit.uploadAndProcess')}
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user