Files
YG-Datasets/easy-dataset-main/components/text-split/components/FileList.js

1069 lines
35 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import {
Box,
Typography,
List,
ListItem,
ListItemText,
IconButton,
Tooltip,
Divider,
CircularProgress,
Checkbox,
Button,
Dialog,
DialogTitle,
DialogContent,
DialogContentText,
DialogActions,
FormControlLabel,
Switch,
Pagination,
TextField,
InputAdornment,
Grid,
Alert
} from '@mui/material';
import {
Visibility as VisibilityIcon,
Download,
Delete as DeleteIcon,
FilePresent as FileIcon,
Psychology as PsychologyIcon,
CheckBox as SelectAllIcon,
CheckBoxOutlineBlank as DeselectAllIcon,
Search as SearchIcon,
Clear as ClearIcon
} from '@mui/icons-material';
import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useAtomValue } from 'jotai';
import { selectedModelInfoAtom } from '@/lib/store';
import MarkdownViewDialog from '../MarkdownViewDialog';
import GaPairsIndicator from '../../mga/GaPairsIndicator';
import DomainTreeActionDialog from './DomainTreeActionDialog';
import i18n from '@/lib/i18n';
import { toast } from 'sonner';
export default function FileList({
theme,
files = {},
loading = false,
onDeleteFile,
sendToFileUploader,
projectId,
setPageLoading,
currentPage = 1,
onPageChange,
onRefresh, // 新增:刷新文件列表的回调函数
isFullscreen = false // 新增参数,用于控制是否处于全屏状态
}) {
const { t } = useTranslation();
// 现有的状态
const [array, setArray] = useState([]);
const [viewDialogOpen, setViewDialogOpen] = useState(false);
const [viewContent, setViewContent] = useState('');
// 新增的批量生成GA对相关状态
const [batchGenDialogOpen, setBatchGenDialogOpen] = useState(false);
const [generating, setGenerating] = useState(false);
const [genError, setGenError] = useState(null);
const [genResult, setGenResult] = useState(null);
const [projectModel, setProjectModel] = useState(null);
const [loadingModel, setLoadingModel] = useState(false);
const [appendMode, setAppendMode] = useState(false);
const [generationMode, setGenerationMode] = useState('ai'); // 'ai' 或 'manual'
const [manualGaPair, setManualGaPair] = useState({
genreTitle: '',
genreDesc: '',
audienceTitle: '',
audienceDesc: ''
});
// 批量删除相关状态
const [batchDeleteDialogOpen, setBatchDeleteDialogOpen] = useState(false);
const [domainTreeActionOpen, setDomainTreeActionOpen] = useState(false);
const [deleting, setDeleting] = useState(false);
// 搜索相关状态
const [searchTerm, setSearchTerm] = useState('');
const [searchLoading, setSearchLoading] = useState(false);
// 获取当前选中的模型信息
const selectedModelInfo = useAtomValue(selectedModelInfoAtom);
// 后端搜索功能
const handleSearch = async searchValue => {
if (typeof onPageChange === 'function') {
setSearchLoading(true);
try {
// 调用父组件的页面变更函数,传递搜索参数
await onPageChange(1, searchValue); // 搜索时重置到第一页
} catch (error) {
console.error('搜索失败:', error);
} finally {
setSearchLoading(false);
}
}
};
// 防抖搜索
useEffect(() => {
const timer = setTimeout(() => {
handleSearch(searchTerm);
}, 500); // 500ms 防抖
return () => clearTimeout(timer);
}, [searchTerm]);
// 清空搜索
const handleClearSearch = () => {
setSearchTerm('');
// 清空搜索时立即触发搜索
handleSearch('');
};
const handleCheckboxChange = (fileId, isChecked) => {
setArray(prevArray => {
let newArray;
const stringFileId = String(fileId);
if (isChecked) {
newArray = prevArray.includes(stringFileId) ? prevArray : [...prevArray, stringFileId];
} else {
newArray = prevArray.filter(item => item !== stringFileId);
}
if (typeof sendToFileUploader === 'function') {
sendToFileUploader(newArray);
}
return newArray;
});
};
// 全选文件(包括所有页面的文件)
const handleSelectAll = async () => {
try {
// 获取项目中所有文件的ID
const response = await fetch(`/api/projects/${projectId}/files?getAllIds=true`);
if (!response.ok) {
throw new Error('获取文件列表失败');
}
const data = await response.json();
const allFileIds = data.allFileIds || [];
setArray(allFileIds);
if (typeof sendToFileUploader === 'function') {
sendToFileUploader(allFileIds);
}
} catch (error) {
console.error('全选文件失败:', error);
// 如果API调用失败回退到选择当前页面的文件
if (files?.data?.length > 0) {
const currentPageFileIds = files.data.map(file => String(file.id));
setArray(currentPageFileIds);
if (typeof sendToFileUploader === 'function') {
sendToFileUploader(currentPageFileIds);
}
}
}
};
// 取消全选
const handleDeselectAll = () => {
setArray([]);
if (typeof sendToFileUploader === 'function') {
sendToFileUploader([]);
}
};
const handleCloseViewDialog = () => {
setViewDialogOpen(false);
};
// 刷新文本块列表
const refreshTextChunks = () => {
if (typeof setPageLoading === 'function') {
setPageLoading(true);
setTimeout(() => {
// 可能需要调用父组件的刷新方法
sendToFileUploader(array);
setPageLoading(false);
}, 500);
}
};
const handleViewContent = async fileId => {
getFileContent(fileId);
setViewDialogOpen(true);
};
const handleDownload = async (fileId, fileName) => {
setPageLoading(true);
const text = await getFileContent(fileId);
// Modify the filename if it ends with .pdf
let downloadName = fileName || 'download.txt';
if (downloadName.toLowerCase().endsWith('.pdf')) {
downloadName = downloadName.slice(0, -4) + '.md';
}
const blob = new Blob([text.content], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = downloadName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
setPageLoading(false);
};
const getFileContent = async fileId => {
try {
const response = await fetch(`/api/projects/${projectId}/preview/${fileId}`);
if (!response.ok) {
throw new Error(t('textSplit.fetchChunksFailed'));
}
const data = await response.json();
setViewContent(data);
return data;
} catch (error) {
console.error(t('textSplit.fetchChunksError'), error);
}
};
const formatFileSize = size => {
if (size < 1024) {
return size + 'B';
} else if (size < 1024 * 1024) {
return (size / 1024).toFixed(2) + 'KB';
} else if (size < 1024 * 1024 * 1024) {
return (size / 1024 / 1024).toFixed(2) + 'MB';
} else {
return (size / 1024 / 1024 / 1024).toFixed(2) + 'GB';
}
};
// 新增:获取项目特定的默认模型信息
const fetchProjectModel = async () => {
try {
setLoadingModel(true);
// 首先获取项目信息
const response = await fetch(`/api/projects/${projectId}`);
if (!response.ok) {
throw new Error(t('gaPairs.fetchProjectInfoFailed', { status: response.status }));
}
const projectData = await response.json();
// 获取模型配置
const modelResponse = await fetch(`/api/projects/${projectId}/model-config`);
if (!modelResponse.ok) {
throw new Error(t('gaPairs.fetchModelConfigFailed', { status: modelResponse.status }));
}
const modelConfigData = await modelResponse.json();
if (modelConfigData.data && Array.isArray(modelConfigData.data)) {
// 优先使用项目默认模型
let targetModel = null;
if (projectData.defaultModelConfigId) {
targetModel = modelConfigData.data.find(model => model.id === projectData.defaultModelConfigId);
}
// 如果没有默认模型,使用第一个可用的模型
if (!targetModel) {
targetModel = modelConfigData.data.find(
m => m.modelName && m.endpoint && (m.providerId === 'ollama' || m.apiKey)
);
}
if (targetModel) {
setProjectModel(targetModel);
}
}
} catch (error) {
console.error(t('gaPairs.fetchProjectModelError'), error);
} finally {
setLoadingModel(false);
}
};
// 新增批量生成GA对的处理函数
const handleBatchGenerateGAPairs = async () => {
if (array.length === 0) {
setGenError(t('gaPairs.selectAtLeastOneFile'));
return;
}
// 如果是手动添加模式,验证手动输入的 GA 对
if (generationMode === 'manual') {
if (!manualGaPair.genreTitle || !manualGaPair.audienceTitle) {
setGenError(t('gaPairs.manualGaPairRequired'));
return;
}
try {
setGenerating(true);
setGenError(null);
setGenResult(null);
const stringFileIds = array.map(id => String(id));
const requestData = {
fileIds: stringFileIds,
gaPair: manualGaPair,
appendMode: appendMode
};
const response = await fetch(`/api/projects/${projectId}/batch-add-manual-ga`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestData)
});
const responseText = await response.text();
if (!response.ok) {
const errorData = await response
.json()
.catch(() => ({ error: t('gaPairs.requestFailed', { status: response.status }) }));
throw new Error(errorData.error || t('gaPairs.requestFailed', { status: response.status }));
}
const result = JSON.parse(responseText);
if (result.success) {
setGenResult({
total: result.data?.length || 0,
success: result.data?.filter(r => r.success).length || 0
});
// 成功后清空选择状态和表单
setArray([]);
if (typeof sendToFileUploader === 'function') {
sendToFileUploader([]);
}
setManualGaPair({
genreTitle: '',
genreDesc: '',
audienceTitle: '',
audienceDesc: ''
});
// 发送全局刷新事件
const successfulFileIds = result.data?.filter(item => item.success)?.map(item => String(item.fileId)) || [];
if (successfulFileIds.length > 0) {
window.dispatchEvent(
new CustomEvent('refreshGaPairsIndicators', {
detail: {
projectId,
fileIds: successfulFileIds
}
})
);
}
} else {
setGenError(result.error || t('gaPairs.generationFailed'));
}
} catch (error) {
console.error(t('gaPairs.batchGenerationFailed'), error);
setGenError(t('gaPairs.generationError', { error: error.message || t('common.unknownError') }));
} finally {
setGenerating(false);
}
return;
}
// AI 生成模式
const modelToUse = projectModel || selectedModelInfo;
if (!modelToUse || !modelToUse.id) {
setGenError(t('gaPairs.noDefaultModel'));
return;
}
// 检查模型配置是否完整
if (!modelToUse.modelName || !modelToUse.endpoint) {
setGenError('模型配置不完整,请检查模型设置');
return;
}
// 检查API密钥除了ollama模型
if (modelToUse.providerId !== 'ollama' && !modelToUse.apiKey) {
setGenError(t('gaPairs.missingApiKey'));
return;
}
try {
setGenerating(true);
setGenError(null);
setGenResult(null);
const stringFileIds = array.map(id => String(id));
// 获取当前语言环境
const currentLanguage = i18n.language === 'en' ? 'en' : '中文';
const requestData = {
fileIds: stringFileIds,
modelConfigId: modelToUse.id,
language: currentLanguage,
appendMode: appendMode
};
const response = await fetch(`/api/projects/${projectId}/batch-generateGA`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestData)
});
const responseText = await response.text();
if (!response.ok) {
const errorData = await response
.json()
.catch(() => ({ error: t('gaPairs.requestFailed', { status: response.status }) }));
throw new Error(errorData.error || t('gaPairs.requestFailed', { status: response.status }));
}
const result = JSON.parse(responseText);
if (result.success) {
setGenResult({
total: result.data?.length || 0,
success: result.data?.filter(r => r.success).length || 0
});
// 成功后清空选择状态
setArray([]);
if (typeof sendToFileUploader === 'function') {
sendToFileUploader([]);
}
console.log(t('gaPairs.batchGenerationSuccess', { count: result.summary?.success || 0 }));
//发送全局刷新事件
const successfulFileIds = result.data?.filter(item => item.success)?.map(item => String(item.fileId)) || [];
if (successfulFileIds.length > 0) {
window.dispatchEvent(
new CustomEvent('refreshGaPairsIndicators', {
detail: {
projectId,
fileIds: successfulFileIds
}
})
);
}
} else {
setGenError(result.error || t('gaPairs.generationFailed'));
}
} catch (error) {
console.error(t('gaPairs.batchGenerationFailed'), error);
setGenError(t('gaPairs.generationError', { error: error.message || t('common.unknownError') }));
} finally {
setGenerating(false);
}
};
// 新增:打开批量生成对话框
const openBatchGenDialog = () => {
// 如果没有选中文件,自动选中所有文件
if (array.length === 0 && files?.data?.length > 0) {
const allFileIds = files.data.map(file => String(file.id));
setArray(allFileIds);
if (typeof sendToFileUploader === 'function') {
sendToFileUploader(allFileIds);
}
}
// 获取项目模型配置
fetchProjectModel();
setBatchGenDialogOpen(true);
};
// 新增:关闭批量生成对话框
const closeBatchGenDialog = () => {
setBatchGenDialogOpen(false);
setGenError(null);
setGenResult(null);
setAppendMode(false); // 重置追加模式
};
// 批量删除处理函数 - 第一步:打开确认对话框
const handleBatchDelete = () => {
if (array.length === 0) {
return;
}
setBatchDeleteDialogOpen(true);
};
// 确认批量删除 - 第二步:打开领域树选择对话框
const confirmBatchDelete = () => {
setBatchDeleteDialogOpen(false);
// 检查是否还有其他文件
const remainingFilesCount = files.total - array.length;
// 如果删除后没有文件了直接执行删除keep 模式)
if (remainingFilesCount === 0) {
executeBatchDelete('keep');
return;
}
// 否则打开领域树操作选择对话框
setDomainTreeActionOpen(true);
};
// 处理领域树操作选择
const handleDomainTreeAction = action => {
setDomainTreeActionOpen(false);
executeBatchDelete(action);
};
// 执行批量删除 - 第三步:实际删除操作
const executeBatchDelete = async domainTreeAction => {
if (array.length === 0) {
return;
}
setDeleting(true);
// 设置页面 loading 状态
if (typeof setPageLoading === 'function') {
setPageLoading(true);
}
try {
const response = await fetch(`/api/projects/${projectId}/batch-delete-files`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
fileIds: array,
domainTreeAction,
model: selectedModelInfo || {},
language: i18n.language === 'en' ? 'English' : '中文'
})
});
if (!response.ok) {
throw new Error('批量删除失败');
}
const result = await response.json();
// 清空选择
setArray([]);
if (typeof sendToFileUploader === 'function') {
sendToFileUploader([]);
}
// 刷新文件列表
if (typeof onRefresh === 'function') {
await onRefresh();
} else if (typeof onPageChange === 'function') {
// 回退方案:如果没有 onRefresh使用 onPageChange
await onPageChange(1);
}
toast.success(
t('textSplit.batchDeleteSuccess', {
count: result.deletedCount || array.length,
defaultValue: `成功删除 ${result.deletedCount || array.length} 个文件`
})
);
} catch (error) {
console.error('批量删除文件失败:', error);
toast.error(t('textSplit.batchDeleteFailed', { defaultValue: '批量删除失败' }));
} finally {
setDeleting(false);
// 清除页面 loading 状态
if (typeof setPageLoading === 'function') {
setPageLoading(false);
}
}
};
// 取消批量删除
const cancelBatchDelete = () => {
setBatchDeleteDialogOpen(false);
};
return (
<Box
sx={{
height: '100%',
p: 3,
border: `1px solid ${theme.palette.divider}`,
borderRadius: 2,
bgcolor: theme.palette.background.paper,
width: '100%',
maxWidth: '100%',
overflow: 'hidden'
}}
>
{/* 标题和按钮区域 */}
<Box sx={{ mb: 2 }}>
{/* 第一行:标题和按钮 */}
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
mb: isFullscreen && files.total > 0 ? 2 : 0
}}
>
<Typography variant="subtitle1">{t('textSplit.uploadedDocuments', { count: files.total })}</Typography>
{/* 批量操作按钮 */}
{files.total > 0 && (
<Box sx={{ display: 'flex', gap: 1 }}>
{/* 全选/取消全选按钮 */}
{array.length === files.total ? (
<Button
variant="outlined"
size="small"
startIcon={<SelectAllIcon />}
onClick={handleDeselectAll}
disabled={loading}
>
{t('gaPairs.deselectAllFiles')}
</Button>
) : (
<Button
variant="outlined"
size="small"
startIcon={<DeselectAllIcon />}
onClick={handleSelectAll}
disabled={loading}
>
{t('gaPairs.selectAllFiles')}
</Button>
)}
{/* 批量删除按钮 */}
{array.length > 0 && (
<Button
variant="outlined"
color="error"
size="small"
startIcon={<DeleteIcon />}
onClick={handleBatchDelete}
disabled={loading}
>
{t('textSplit.batchDelete', { count: array.length })}
</Button>
)}
{/* 批量生成GA对按钮 */}
<Button
variant="contained"
color="primary"
size="small"
startIcon={<PsychologyIcon />}
onClick={openBatchGenDialog}
disabled={loading}
>
{t('gaPairs.batchGenerate')}
</Button>
</Box>
)}
</Box>
{/* 第二行:搜索框 - 在全屏展示时显示,或者有搜索内容时显示 */}
{isFullscreen && (files.total > 0 || searchTerm) && (
<Box>
<TextField
size="small"
placeholder={t('textSplit.searchFiles', { defaultValue: '搜索文件名...' })}
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon color="action" />
</InputAdornment>
),
endAdornment: searchTerm && (
<InputAdornment position="end">
<IconButton size="small" onClick={handleClearSearch} edge="end">
<ClearIcon />
</IconButton>
</InputAdornment>
)
}}
sx={{ width: '100%', maxWidth: 400 }}
/>
{(searchTerm || searchLoading) && (
<Typography variant="body2" color="textSecondary" sx={{ mt: 1, textAlign: 'center' }}>
{searchLoading
? '搜索中...'
: searchTerm
? t('textSplit.searchResults', {
count: files?.data?.length || 0,
total: files.total,
defaultValue: `找到 ${files?.data?.length || 0} 个文件(共 ${files.total} 个)`
})
: null}
</Typography>
)}
</Box>
)}
</Box>
{loading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', p: 4 }}>
<CircularProgress size={24} />
</Box>
) : files.total === 0 ? (
<Box sx={{ p: 4, textAlign: 'center' }}>
<Typography variant="body2" color="textSecondary">
{searchTerm
? // 搜索无结果
t('textSplit.noSearchResults', {
searchTerm,
defaultValue: `未找到包含 "${searchTerm}" 的文件`
})
: // 真的没有上传文件
t('textSplit.noFilesUploaded', {
defaultValue: '暂未上传文件'
})}
</Typography>
</Box>
) : !files?.data || files.data.length === 0 ? (
<Box sx={{ p: 4, textAlign: 'center' }}>
<Typography variant="body2" color="textSecondary">
{searchTerm
? // 搜索有结果但当前页没数据
t('textSplit.noResultsOnCurrentPage', {
defaultValue: '当前页面没有搜索结果,请返回第一页查看'
})
: // 当前页没数据但总数不为0
t('textSplit.noDataOnCurrentPage', {
defaultValue: '当前页面没有数据'
})}
</Typography>
</Box>
) : (
<>
<List
sx={{
maxHeight: isFullscreen ? 'none' : '200px', // 根据 isFullscreen 控制最大高度
overflow: 'auto',
width: '100%'
}}
dense // 使列表项更紧凑,减少高度
>
{files?.data?.map((file, index) => (
<Box key={index}>
<ListItem
sx={{
display: 'flex',
justifyContent: 'space-between',
flexWrap: 'nowrap',
pr: 0 // 移除右侧内边距,便于自定义操作区域位置
}}
>
{/* 文件信息区域 */}
<Box
sx={{
display: 'flex',
alignItems: 'center',
overflow: 'hidden', // 隐藏溢出内容
maxWidth: '70%', // 限制文件信息区域最大宽度
flexGrow: 1,
mr: 2 // 与操作区域保持间距
}}
>
<FileIcon color="primary" sx={{ mr: 1, flexShrink: 0 }} />
<Tooltip title={`${file.fileName}${t('textSplit.viewDetails')}`}>
<ListItemText
style={{ cursor: 'pointer', overflow: 'hidden' }}
onClick={() => handleViewContent(file.id)}
primary={
<Typography
noWrap // 文本不换行并显示省略号
variant="body1"
component="div"
>
{file.fileName}
</Typography>
}
secondary={
<Typography
noWrap // 文本不换行并显示省略号
variant="body2"
color="textSecondary"
component="div"
>
{`${formatFileSize(file.size)} · ${new Date(file.createAt).toLocaleString()}`}
</Typography>
}
/>
</Tooltip>
</Box>
{/* 操作按钮区域 */}
<Box
sx={{
display: 'flex',
flexShrink: 0, // 防止操作区域被压缩
alignItems: 'center'
}}
>
<Checkbox
sx={{ ml: 1 }}
checked={array.includes(String(file.id))}
onChange={e => handleCheckboxChange(file.id, e.target.checked)}
/>
<GaPairsIndicator projectId={projectId} fileId={file.id} fileName={file.fileName} />
<Tooltip title={t('textSplit.download')}>
<IconButton color="primary" onClick={() => handleDownload(file.id, file.fileName)}>
<Download />
</IconButton>
</Tooltip>
<Tooltip title={t('textSplit.deleteFile')}>
<IconButton color="error" onClick={() => onDeleteFile(file.id, file.fileName)}>
<DeleteIcon />
</IconButton>
</Tooltip>
</Box>
</ListItem>
{index < files.data.length - 1 && <Divider />}
</Box>
))}
</List>
{/* 分页控件 */}
{files.total > 10 && (
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 2 }}>
<Pagination
count={Math.ceil(files.total / 10)} // 假设每页10个文件
page={currentPage}
onChange={(event, page) => onPageChange && onPageChange(page)}
color="primary"
showFirstButton
showLastButton
/>
</Box>
)}
</>
)}
{/* 现有的文本块详情对话框 */}
<MarkdownViewDialog
open={viewDialogOpen}
text={viewContent}
onClose={handleCloseViewDialog}
projectId={projectId}
onSaveSuccess={refreshTextChunks}
/>
{/* 新增批量生成GA对对话框 */}
<Dialog open={batchGenDialogOpen} onClose={closeBatchGenDialog} maxWidth="md" fullWidth>
<DialogTitle>{t('gaPairs.batchGenerateTitle')}</DialogTitle>
<DialogContent>
{!genResult && (
<DialogContentText>
{t('gaPairs.batchGenerateDescription', { count: array.length })}
{/* 生成方式选择 */}
<Box sx={{ mt: 2, mb: 2 }}>
<Typography variant="subtitle2" sx={{ mb: 1 }}>
{t('gaPairs.generationMode')}
</Typography>
<FormControlLabel
control={
<Switch
checked={generationMode === 'manual'}
onChange={e => setGenerationMode(e.target.checked ? 'manual' : 'ai')}
color="primary"
/>
}
label={generationMode === 'manual' ? t('gaPairs.manualAddMode') : t('gaPairs.aiGenerateMode')}
/>
</Box>
{/* AI 生成模式:显示模型信息 */}
{generationMode === 'ai' && (
<>
{loadingModel ? (
<Box sx={{ mt: 1, display: 'flex', alignItems: 'center' }}>
<CircularProgress size={16} sx={{ mr: 1 }} />
<Typography variant="body2">{t('gaPairs.loadingProjectModel')}</Typography>
</Box>
) : projectModel ? (
<Box sx={{ mt: 1 }}>
<Typography variant="body2" color="textSecondary">
{t('gaPairs.usingModel')}:{' '}
<strong>
{projectModel.providerName}: {projectModel.modelName}
</strong>
</Typography>
</Box>
) : (
<Box sx={{ mt: 1 }}>
<Typography variant="body2" color="error">
{t('gaPairs.noDefaultModel')}
</Typography>
</Box>
)}
</>
)}
{/* 手动添加模式:显示输入表单 */}
{generationMode === 'manual' && (
<Box sx={{ mt: 2 }}>
<Grid container spacing={2}>
<Grid item xs={12}>
<TextField
fullWidth
label={t('gaPairs.genreTitle')}
value={manualGaPair.genreTitle}
onChange={e => setManualGaPair({ ...manualGaPair, genreTitle: e.target.value })}
required
/>
</Grid>
<Grid item xs={12}>
<TextField
fullWidth
label={t('gaPairs.genreDesc')}
value={manualGaPair.genreDesc}
onChange={e => setManualGaPair({ ...manualGaPair, genreDesc: e.target.value })}
multiline
rows={2}
/>
</Grid>
<Grid item xs={12}>
<TextField
fullWidth
label={t('gaPairs.audienceTitle')}
value={manualGaPair.audienceTitle}
onChange={e => setManualGaPair({ ...manualGaPair, audienceTitle: e.target.value })}
required
/>
</Grid>
<Grid item xs={12}>
<TextField
fullWidth
label={t('gaPairs.audienceDesc')}
value={manualGaPair.audienceDesc}
onChange={e => setManualGaPair({ ...manualGaPair, audienceDesc: e.target.value })}
multiline
rows={2}
/>
</Grid>
</Grid>
</Box>
)}
{/* 追加模式选择 */}
<Box sx={{ mt: 2, mb: 2 }}>
<FormControlLabel
control={
<Switch checked={appendMode} onChange={e => setAppendMode(e.target.checked)} color="primary" />
}
label={`${t('gaPairs.appendMode')}${t('gaPairs.appendModeDescription')}`}
/>
</Box>
</DialogContentText>
)}
{genError && (
<Box sx={{ mt: 2, p: 2, bgcolor: 'error.light', borderRadius: 1 }}>
<Typography variant="body2" color="error.contrastText">
{genError}
</Typography>
</Box>
)}
{genResult && (
<Box sx={{ mt: 2, p: 2, bgcolor: 'success.light', borderRadius: 1 }}>
<Typography variant="body2" color="success.contrastText">
{t('gaPairs.batchGenCompleted', { success: genResult.success, total: genResult.total })}
</Typography>
</Box>
)}
</DialogContent>
<DialogActions>
<Button onClick={closeBatchGenDialog}>{genResult ? t('common.close') : t('common.cancel')}</Button>
{!genResult && (
<Button
onClick={handleBatchGenerateGAPairs}
variant="contained"
disabled={
generating ||
array.length === 0 ||
(generationMode === 'ai' && !projectModel) ||
(generationMode === 'manual' && (!manualGaPair.genreTitle || !manualGaPair.audienceTitle))
}
startIcon={generating ? <CircularProgress size={20} /> : <PsychologyIcon />}
>
{generating
? t('gaPairs.generating')
: generationMode === 'manual'
? t('gaPairs.batchAddManual')
: t('gaPairs.startGeneration')}
</Button>
)}
</DialogActions>
</Dialog>
{/* 批量删除确认对话框 */}
<Dialog open={batchDeleteDialogOpen} onClose={cancelBatchDelete} maxWidth="sm" fullWidth>
<DialogTitle>{t('textSplit.batchDeleteTitle')}</DialogTitle>
<DialogContent>
<DialogContentText>
{t('textSplit.batchDeleteConfirm', {
count: array.length,
defaultValue: `确定要删除选中的 ${array.length} 个文件吗?此操作不可恢复。`
})}
</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={cancelBatchDelete}>{t('common.cancel')}</Button>
<Button onClick={confirmBatchDelete} variant="contained" color="error">
{t('common.confirmDelete')}
</Button>
</DialogActions>
</Dialog>
{/* 领域树操作选择对话框 */}
<DomainTreeActionDialog
open={domainTreeActionOpen}
onClose={() => setDomainTreeActionOpen(false)}
onConfirm={handleDomainTreeAction}
isFirstUpload={false}
action="delete"
/>
</Box>
);
}