first-update

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

View File

@@ -0,0 +1,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>
);
}

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

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