first-update
This commit is contained in:
440
easy-dataset-main/app/projects/[projectId]/text-split/page.js
Normal file
440
easy-dataset-main/app/projects/[projectId]/text-split/page.js
Normal file
@@ -0,0 +1,440 @@
|
||||
'use client';
|
||||
|
||||
import axios from 'axios';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Container,
|
||||
Box,
|
||||
Tabs,
|
||||
Tab,
|
||||
IconButton,
|
||||
Collapse,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Typography,
|
||||
LinearProgress,
|
||||
CircularProgress
|
||||
} from '@mui/material';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
|
||||
import FullscreenIcon from '@mui/icons-material/Fullscreen';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import FileUploader from '@/components/text-split/FileUploader';
|
||||
import FileList from '@/components/text-split/components/FileList';
|
||||
import DeleteConfirmDialog from '@/components/text-split/components/DeleteConfirmDialog';
|
||||
import PdfSettings from '@/components/text-split/PdfSettings';
|
||||
import ChunkList from '@/components/text-split/ChunkList';
|
||||
import DomainAnalysis from '@/components/text-split/DomainAnalysis';
|
||||
import useTaskSettings from '@/hooks/useTaskSettings';
|
||||
import { useAtomValue } from 'jotai/index';
|
||||
import { selectedModelInfoAtom } from '@/lib/store';
|
||||
import useChunks from './useChunks';
|
||||
import useQuestionGeneration from './useQuestionGeneration';
|
||||
import useDataCleaning from './useDataCleaning';
|
||||
import useEvalGeneration from './useEvalGeneration';
|
||||
import useFileProcessing from './useFileProcessing';
|
||||
import useFileProcessingStatus from '@/hooks/useFileProcessingStatus';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export default function TextSplitPage({ params }) {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const { projectId } = params;
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
const [renderedTab, setRenderedTab] = useState(0);
|
||||
const [tabSwitching, setTabSwitching] = useState(false);
|
||||
const tabSwitchTimerRef = useRef(null);
|
||||
const { taskSettings } = useTaskSettings(projectId);
|
||||
const [pdfStrategy, setPdfStrategy] = useState('default');
|
||||
const [questionFilter, setQuestionFilter] = useState('all'); // 'all', 'generated', 'ungenerated'
|
||||
const [selectedViosnModel, setSelectedViosnModel] = useState('');
|
||||
const selectedModelInfo = useAtomValue(selectedModelInfoAtom);
|
||||
const { taskFileProcessing, task } = useFileProcessingStatus();
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [uploadedFiles, setUploadedFiles] = useState({ data: [], total: 0 });
|
||||
const [searchFileName, setSearchFileName] = useState('');
|
||||
const [showLoadingBar, setShowLoadingBar] = useState(false);
|
||||
|
||||
// 娑撳﹣绱堕崠鍝勭厵閻ㄥ嫬鐫嶅鈧?閹舵ê褰旈悩鑸碘偓?
|
||||
const [uploaderExpanded, setUploaderExpanded] = useState(true);
|
||||
|
||||
// 閺傚洨灏為崚妤勩€?FileList)鐏炴洜銇氱€电鐦藉鍡欏Ц閹?
|
||||
const [fileListDialogOpen, setFileListDialogOpen] = useState(false);
|
||||
|
||||
// 娴h法鏁ら懛顏勭暰娑斿“ooks
|
||||
const { chunks, tocData, loading, fetchChunks, handleDeleteChunk, handleEditChunk, updateChunks, setLoading } =
|
||||
useChunks(projectId, questionFilter);
|
||||
|
||||
// 閼惧嘲褰囬弬鍥︽閸掓銆?
|
||||
const fetchUploadedFiles = async (page = currentPage, fileName = searchFileName) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
size: '10'
|
||||
});
|
||||
|
||||
if (fileName && fileName.trim()) {
|
||||
params.append('fileName', fileName.trim());
|
||||
}
|
||||
|
||||
const response = await axios.get(`/api/projects/${projectId}/files?${params}`);
|
||||
setUploadedFiles(response.data);
|
||||
} catch (error) {
|
||||
console.error('Error fetching files:', error);
|
||||
toast.error(error.message || '閼惧嘲褰囬弬鍥︽閸掓銆冩径杈Е');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 閸掔娀娅庨弬鍥︽绾喛顓荤€电鐦藉鍡欏Ц閹?
|
||||
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
|
||||
const [fileToDelete, setFileToDelete] = useState(null);
|
||||
|
||||
// 閹垫挸绱戦崚鐘绘珟绾喛顓荤€电鐦藉?
|
||||
const openDeleteConfirm = (fileId, fileName) => {
|
||||
setFileToDelete({ fileId, fileName });
|
||||
setDeleteConfirmOpen(true);
|
||||
};
|
||||
|
||||
// 閸忔娊妫撮崚鐘绘珟绾喛顓荤€电鐦藉?
|
||||
const closeDeleteConfirm = () => {
|
||||
setDeleteConfirmOpen(false);
|
||||
setFileToDelete(null);
|
||||
};
|
||||
|
||||
// 绾喛顓婚崚鐘绘珟閺傚洣娆?
|
||||
const confirmDeleteFile = async () => {
|
||||
if (!fileToDelete) return;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
closeDeleteConfirm();
|
||||
|
||||
await axios.delete(`/api/projects/${projectId}/files/${fileToDelete.fileId}`);
|
||||
await fetchUploadedFiles();
|
||||
fetchChunks();
|
||||
|
||||
toast.success(
|
||||
t('textSplit.deleteSuccess', { fileName: fileToDelete.fileName }) || `删除 ${fileToDelete.fileName} 成功`
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('删除文件出错:', error);
|
||||
toast.error(error.message || '删除文件失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setFileToDelete(null);
|
||||
}
|
||||
};
|
||||
|
||||
const { handleGenerateQuestions } = useQuestionGeneration(projectId, taskSettings);
|
||||
const { handleDataCleaning } = useDataCleaning(projectId, taskSettings);
|
||||
const { handleGenerateEvalQuestions } = useEvalGeneration(projectId);
|
||||
const { handleFileProcessing } = useFileProcessing(projectId);
|
||||
|
||||
// 文本块数据刷新:初始化 + 文件处理任务状态变化
|
||||
useEffect(() => {
|
||||
fetchChunks('all');
|
||||
}, [fetchChunks, taskFileProcessing]);
|
||||
|
||||
// 文件列表刷新:文件分页、搜索关键词变化时触发
|
||||
useEffect(() => {
|
||||
fetchUploadedFiles(currentPage, searchFileName);
|
||||
}, [projectId, currentPage, searchFileName]);
|
||||
|
||||
useEffect(() => {
|
||||
let timerId;
|
||||
if (loading) {
|
||||
timerId = setTimeout(() => setShowLoadingBar(true), 180);
|
||||
} else {
|
||||
setShowLoadingBar(false);
|
||||
}
|
||||
return () => {
|
||||
if (timerId) clearTimeout(timerId);
|
||||
};
|
||||
}, [loading]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (tabSwitchTimerRef.current) {
|
||||
clearTimeout(tabSwitchTimerRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleTabChange = (event, newValue) => {
|
||||
if (newValue === activeTab) return;
|
||||
|
||||
setActiveTab(newValue);
|
||||
setTabSwitching(true);
|
||||
|
||||
if (tabSwitchTimerRef.current) {
|
||||
clearTimeout(tabSwitchTimerRef.current);
|
||||
}
|
||||
|
||||
const switchContent = () => {
|
||||
setRenderedTab(newValue);
|
||||
tabSwitchTimerRef.current = null;
|
||||
if (typeof window !== 'undefined') {
|
||||
window.requestAnimationFrame(() => setTabSwitching(false));
|
||||
} else {
|
||||
setTabSwitching(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.requestAnimationFrame(() => {
|
||||
tabSwitchTimerRef.current = setTimeout(switchContent, 80);
|
||||
});
|
||||
} else {
|
||||
switchContent();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 鐎甸€涚瑐娴肩姴鎮楅惃鍕瀮娴犳儼绻樼悰灞筋槱閻?
|
||||
*/
|
||||
const handleUploadSuccess = async (fileNames, pdfFiles, domainTreeAction) => {
|
||||
try {
|
||||
await handleFileProcessing(fileNames, pdfStrategy, selectedViosnModel, domainTreeAction);
|
||||
location.reload();
|
||||
} catch (error) {
|
||||
toast.error('File upload failed' + error.message || '');
|
||||
}
|
||||
};
|
||||
|
||||
// 閸栧懓顥婇悽鐔稿灇闂傤噣顣介惃鍕槱閻炲棗鍤遍弫?
|
||||
const onGenerateQuestions = async chunkIds => {
|
||||
await handleGenerateQuestions(chunkIds, selectedModelInfo, fetchChunks);
|
||||
};
|
||||
|
||||
// 閸栧懓顥婇弫鐗堝祦濞撳懏绀傞惃鍕槱閻炲棗鍤遍弫?
|
||||
const onDataCleaning = async chunkIds => {
|
||||
await handleDataCleaning(chunkIds, selectedModelInfo, fetchChunks);
|
||||
};
|
||||
|
||||
// 閸栧懓顥婇悽鐔稿灇濞村鐦庢0妯兼窗閻ㄥ嫬顦╅悶鍡楀毐閺?
|
||||
const onGenerateEvalQuestions = async chunkId => {
|
||||
await handleGenerateEvalQuestions(chunkId, selectedModelInfo, () => {
|
||||
// 閹存劕濮涢崥搴″煕閺傛澘鍨悰?
|
||||
fetchChunks();
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const url = new URL(window.location.href);
|
||||
if (questionFilter !== 'all') {
|
||||
url.searchParams.set('filter', questionFilter);
|
||||
} else {
|
||||
url.searchParams.delete('filter');
|
||||
}
|
||||
window.history.replaceState({}, '', url);
|
||||
fetchChunks(questionFilter);
|
||||
}, [questionFilter]);
|
||||
|
||||
const handleSelected = array => {
|
||||
if (array.length > 0) {
|
||||
axios.post(`/api/projects/${projectId}/chunks`, { array }).then(response => {
|
||||
updateChunks(response.data);
|
||||
});
|
||||
} else {
|
||||
fetchChunks();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Container maxWidth="lg" sx={{ mt: 4, mb: 8, position: 'relative' }}>
|
||||
{/* 閺傚洣娆㈡稉濠佺炊缂佸嫪娆?*/}
|
||||
|
||||
<Box
|
||||
sx={{ position: 'absolute', top: -18, left: '50%', transform: 'translateX(-50%)', zIndex: 1, display: 'flex' }}
|
||||
>
|
||||
<IconButton
|
||||
onClick={() => setUploaderExpanded(!uploaderExpanded)}
|
||||
sx={{
|
||||
bgcolor: 'background.paper',
|
||||
boxShadow: 1,
|
||||
mr: uploaderExpanded ? 1 : 0 // 鐏炴洖绱戦弮鑸靛瘻闁筋喕绠i梻瀵告殌閻愬綊妫跨捄?
|
||||
}}
|
||||
size="small"
|
||||
>
|
||||
{uploaderExpanded ? <ExpandLessIcon /> : <ExpandMoreIcon />}
|
||||
</IconButton>
|
||||
|
||||
{/* 閺傚洨灏為崚妤勩€冮幍鈺佺潔閹稿鎸抽敍灞肩矌閸︺劋绗傞柈銊ュ隘閸╃喎鐫嶅鈧弮鑸垫▔缁€?*/}
|
||||
{uploaderExpanded && (
|
||||
<IconButton
|
||||
color="primary"
|
||||
onClick={() => setFileListDialogOpen(true)}
|
||||
sx={{ bgcolor: 'background.paper', boxShadow: 1 }}
|
||||
size="small"
|
||||
title={t('textSplit.expandFileList') || '扩展文件列表'}
|
||||
>
|
||||
<FullscreenIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Collapse in={uploaderExpanded}>
|
||||
<FileUploader
|
||||
projectId={projectId}
|
||||
onUploadSuccess={handleUploadSuccess}
|
||||
onFileDeleted={fetchChunks}
|
||||
setPageLoading={setLoading}
|
||||
sendToPages={handleSelected}
|
||||
setPdfStrategy={setPdfStrategy}
|
||||
pdfStrategy={pdfStrategy}
|
||||
selectedViosnModel={selectedViosnModel}
|
||||
setSelectedViosnModel={setSelectedViosnModel}
|
||||
taskFileProcessing={taskFileProcessing}
|
||||
fileTask={task}
|
||||
>
|
||||
<PdfSettings
|
||||
pdfStrategy={pdfStrategy}
|
||||
setPdfStrategy={setPdfStrategy}
|
||||
selectedViosnModel={selectedViosnModel}
|
||||
setSelectedViosnModel={setSelectedViosnModel}
|
||||
/>
|
||||
</FileUploader>
|
||||
</Collapse>
|
||||
|
||||
{/* 閺嶅洨顒锋い?*/}
|
||||
<Box sx={{ width: '100%', mb: 3 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onChange={handleTabChange}
|
||||
variant="fullWidth"
|
||||
sx={{ borderBottom: 1, borderColor: 'divider', flexGrow: 1 }}
|
||||
>
|
||||
<Tab label={t('textSplit.tabs.smartSplit')} />
|
||||
<Tab label={t('textSplit.tabs.domainAnalysis')} />
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
{/* 閺呴缚鍏橀崚鍡楀閺嶅洨顒烽崘鍛啇 */}
|
||||
{tabSwitching ? (
|
||||
<Box
|
||||
sx={{
|
||||
minHeight: 220,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column',
|
||||
gap: 1.5
|
||||
}}
|
||||
>
|
||||
<CircularProgress size={26} />
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t('common.loading')}
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<>
|
||||
{renderedTab === 0 && (
|
||||
<ChunkList
|
||||
projectId={projectId}
|
||||
chunks={chunks}
|
||||
onDelete={handleDeleteChunk}
|
||||
onEdit={handleEditChunk}
|
||||
onGenerateQuestions={onGenerateQuestions}
|
||||
onGenerateEvalQuestions={onGenerateEvalQuestions}
|
||||
onDataCleaning={onDataCleaning}
|
||||
loading={loading}
|
||||
questionFilter={questionFilter}
|
||||
setQuestionFilter={setQuestionFilter}
|
||||
selectedModel={selectedModelInfo}
|
||||
/>
|
||||
)}
|
||||
|
||||
{renderedTab === 1 && <DomainAnalysis projectId={projectId} toc={tocData} loading={loading} />}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* 閸旂姾娴囨稉顓℃寢閻?*/}
|
||||
{showLoadingBar && (
|
||||
<Box sx={{ position: 'sticky', bottom: 12, zIndex: 5, px: 1 }}>
|
||||
<Box
|
||||
sx={{
|
||||
bgcolor: 'background.paper',
|
||||
border: 1,
|
||||
borderColor: 'divider',
|
||||
borderRadius: 2,
|
||||
px: 1.5,
|
||||
py: 1,
|
||||
boxShadow: 1
|
||||
}}
|
||||
>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 0.5 }}>
|
||||
{t('textSplit.loading')}
|
||||
</Typography>
|
||||
<LinearProgress />
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* 婢跺嫮鎮婃稉顓℃寢閻?*/}
|
||||
|
||||
{/* 閺佺増宓佸〒鍛鏉╂稑瀹抽拏娆戝 */}
|
||||
|
||||
{/* 閺傚洣娆㈡径鍕倞鏉╂稑瀹抽拏娆戝 */}
|
||||
|
||||
{/* 閺傚洣娆㈤崚鐘绘珟绾喛顓荤€电鐦藉?*/}
|
||||
<DeleteConfirmDialog
|
||||
open={deleteConfirmOpen}
|
||||
fileName={fileToDelete?.fileName}
|
||||
onClose={closeDeleteConfirm}
|
||||
onConfirm={confirmDeleteFile}
|
||||
/>
|
||||
|
||||
{/* 閺傚洨灏為崚妤勩€冪€电鐦藉?*/}
|
||||
<Dialog
|
||||
open={fileListDialogOpen}
|
||||
onClose={() => setFileListDialogOpen(false)}
|
||||
maxWidth="lg"
|
||||
fullWidth
|
||||
sx={{ '& .MuiDialog-paper': { bgcolor: 'background.default' } }}
|
||||
>
|
||||
<DialogTitle sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', px: 3, py: 1 }}>
|
||||
<Typography variant="h6">{t('textSplit.fileList')}</Typography>
|
||||
<IconButton edge="end" color="inherit" onClick={() => setFileListDialogOpen(false)} aria-label="close">
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</DialogTitle>
|
||||
<DialogContent dividers sx={{ p: 3 }}>
|
||||
{/* 濮濄倕顦╂径宥囨暏 FileUploader 缂佸嫪娆㈡稉顓犳畱 FileList 闁劌鍨?*/}
|
||||
<Box sx={{ minHeight: '80vh' }}>
|
||||
{/* 閺傚洣娆㈤崚妤勩€冮崘鍛啇 */}
|
||||
<FileList
|
||||
theme={theme}
|
||||
files={uploadedFiles}
|
||||
loading={loading}
|
||||
setPageLoading={setLoading}
|
||||
sendToFileUploader={array => handleSelected(array)}
|
||||
onDeleteFile={(fileId, fileName) => openDeleteConfirm(fileId, fileName)}
|
||||
projectId={projectId}
|
||||
currentPage={currentPage}
|
||||
onPageChange={(page, fileName) => {
|
||||
if (fileName !== undefined) {
|
||||
// 閹兼粎鍌ㄩ弮鑸垫纯閺傜増鎮崇槐銏犲彠闁款喛鐦濋崪宀勩€夐惍?
|
||||
setSearchFileName(fileName);
|
||||
setCurrentPage(page);
|
||||
} else {
|
||||
// 缂堝銆夐弮璺哄涧閺囧瓨鏌婃い鐢电垳
|
||||
setCurrentPage(page);
|
||||
}
|
||||
}}
|
||||
onRefresh={fetchUploadedFiles} // 娴肩娀鈧帒鍩涢弬鏉垮毐閺?
|
||||
isFullscreen={true} // 閸︺劌顕拠婵囶攱娑擃厾些闂勩倝鐝惔锕傛閸?
|
||||
/>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
/**
|
||||
* 文本块管理的自定义Hook
|
||||
* @param {string} projectId - 项目ID
|
||||
* @param {string} [currentFilter='all'] - 当前筛选条件
|
||||
* @returns {Object} - 文本块状态和操作方法
|
||||
*/
|
||||
export default function useChunks(projectId, currentFilter = 'all') {
|
||||
const { t } = useTranslation();
|
||||
const [chunks, setChunks] = useState([]);
|
||||
const [tocData, setTocData] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
/**
|
||||
* 获取文本块列表
|
||||
* @param {string} filter - 筛选条件
|
||||
*/
|
||||
const fetchChunks = useCallback(
|
||||
async (filter = 'all') => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await fetch(`/api/projects/${projectId}/split?filter=${filter}`);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || t('textSplit.fetchChunksFailed'));
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
setChunks(data.chunks || []);
|
||||
|
||||
// 如果有文件结果,处理详细信息
|
||||
if (data.toc) {
|
||||
console.log(t('textSplit.fileResultReceived'), data.fileResult);
|
||||
// 如果有目录结构,设置目录数据
|
||||
setTocData(data.toc);
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(error.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[projectId, t, setLoading, setChunks, setTocData]
|
||||
);
|
||||
|
||||
/**
|
||||
* 处理删除文本块
|
||||
* @param {string} chunkId - 文本块ID
|
||||
*/
|
||||
const handleDeleteChunk = useCallback(
|
||||
async chunkId => {
|
||||
try {
|
||||
const response = await fetch(`/api/projects/${projectId}/chunks/${encodeURIComponent(chunkId)}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || t('textSplit.deleteChunkFailed'));
|
||||
}
|
||||
|
||||
// 更新文本块列表
|
||||
setChunks(prev => prev.filter(chunk => chunk.id !== chunkId));
|
||||
} catch (error) {
|
||||
toast.error(error.message);
|
||||
}
|
||||
},
|
||||
[projectId, t]
|
||||
);
|
||||
|
||||
/**
|
||||
* 处理文本块编辑
|
||||
* @param {string} chunkId - 文本块ID
|
||||
* @param {string} newContent - 新内容
|
||||
*/
|
||||
const handleEditChunk = useCallback(
|
||||
async (chunkId, newContent) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const response = await fetch(`/api/projects/${projectId}/chunks/${encodeURIComponent(chunkId)}`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ content: newContent })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || t('textSplit.editChunkFailed'));
|
||||
}
|
||||
|
||||
// 更新成功后使用当前筛选条件刷新文本块列表
|
||||
// 直接从 URL 获取当前筛选参数,确保获取到的是最新的值
|
||||
const url = new URL(window.location.href);
|
||||
const filterParam = url.searchParams.get('filter') || 'all';
|
||||
await fetchChunks(filterParam);
|
||||
|
||||
toast.success(t('textSplit.editChunkSuccess'));
|
||||
} catch (error) {
|
||||
toast.error(error.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[projectId, t, fetchChunks]
|
||||
);
|
||||
|
||||
/**
|
||||
* 设置文本块列表
|
||||
* @param {Array} data - 新的文本块列表
|
||||
*/
|
||||
const updateChunks = useCallback(data => {
|
||||
setChunks(data);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 添加新的文本块
|
||||
* @param {Array} newChunks - 新的文本块列表
|
||||
*/
|
||||
const addChunks = useCallback(newChunks => {
|
||||
setChunks(prev => {
|
||||
const updatedChunks = [...prev];
|
||||
newChunks.forEach(chunk => {
|
||||
if (!updatedChunks.find(c => c.id === chunk.id)) {
|
||||
updatedChunks.push(chunk);
|
||||
}
|
||||
});
|
||||
return updatedChunks;
|
||||
});
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 设置TOC数据
|
||||
* @param {string} toc - TOC数据
|
||||
*/
|
||||
const updateTocData = useCallback(toc => {
|
||||
if (toc) {
|
||||
setTocData(toc);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
chunks,
|
||||
tocData,
|
||||
loading,
|
||||
fetchChunks,
|
||||
handleDeleteChunk,
|
||||
handleEditChunk,
|
||||
updateChunks,
|
||||
addChunks,
|
||||
updateTocData,
|
||||
setLoading
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import i18n from '@/lib/i18n';
|
||||
import request from '@/lib/util/request';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export default function useDataCleaning(projectId) {
|
||||
const { t } = useTranslation();
|
||||
const [processing, setProcessing] = useState(false);
|
||||
const [progress, setProgress] = useState({
|
||||
total: 0,
|
||||
completed: 0,
|
||||
percentage: 0,
|
||||
cleanedCount: 0
|
||||
});
|
||||
|
||||
const resetProgress = useCallback(() => {
|
||||
setTimeout(() => {
|
||||
setProgress({
|
||||
total: 0,
|
||||
completed: 0,
|
||||
percentage: 0,
|
||||
cleanedCount: 0
|
||||
});
|
||||
}, 500);
|
||||
}, []);
|
||||
|
||||
const handleDataCleaning = useCallback(
|
||||
async (chunkIds, selectedModelInfo, fetchChunks) => {
|
||||
try {
|
||||
if (!chunkIds || chunkIds.length === 0) return;
|
||||
|
||||
if (!selectedModelInfo) {
|
||||
throw new Error(t('textSplit.selectModelFirst'));
|
||||
}
|
||||
|
||||
setProcessing(true);
|
||||
|
||||
if (chunkIds.length === 1) {
|
||||
const chunkId = chunkIds[0];
|
||||
const currentLanguage = i18n.language === 'zh-CN' ? '中文' : 'en';
|
||||
|
||||
const response = await request(`/api/projects/${projectId}/chunks/${chunkId}/clean`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: selectedModelInfo,
|
||||
language: currentLanguage
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || t('textSplit.dataCleaningFailed', { chunkId }));
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
toast.success(
|
||||
t('textSplit.dataCleaningSuccess', {
|
||||
originalLength: data.originalLength,
|
||||
cleanedLength: data.cleanedLength
|
||||
})
|
||||
);
|
||||
|
||||
if (fetchChunks) fetchChunks();
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await request(`/api/projects/${projectId}/tasks`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
taskType: 'data-cleaning',
|
||||
modelInfo: selectedModelInfo,
|
||||
language: i18n.language,
|
||||
detail: '批量数据清洗任务',
|
||||
note: { chunkIds }
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || t('tasks.createFailed'));
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (data?.code !== 0) {
|
||||
throw new Error(data?.message || t('tasks.createFailed'));
|
||||
}
|
||||
|
||||
toast.success(`${t('tasks.createSuccess')},${t('tasks.title')}查看进度`);
|
||||
} catch (error) {
|
||||
toast.error(error.message);
|
||||
} finally {
|
||||
setProcessing(false);
|
||||
resetProgress();
|
||||
}
|
||||
},
|
||||
[projectId, t, resetProgress]
|
||||
);
|
||||
|
||||
return {
|
||||
processing,
|
||||
progress,
|
||||
setProgress,
|
||||
setProcessing,
|
||||
handleDataCleaning,
|
||||
resetProgress
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import i18n from '@/lib/i18n';
|
||||
import request from '@/lib/util/request';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
/**
|
||||
* 测评题目生成的自定义Hook
|
||||
* @param {string} projectId - 项目ID
|
||||
* @returns {Object} - 测评题目生成状态和操作方法
|
||||
*/
|
||||
export default function useEvalGeneration(projectId) {
|
||||
const { t } = useTranslation();
|
||||
const [generating, setGenerating] = useState({});
|
||||
|
||||
/**
|
||||
* 为单个文本块生成测评题目
|
||||
* @param {string} chunkId - 文本块ID
|
||||
* @param {Object} selectedModelInfo - 选定的模型信息
|
||||
* @param {Function} onSuccess - 成功回调
|
||||
*/
|
||||
const handleGenerateEvalQuestions = useCallback(
|
||||
async (chunkId, selectedModelInfo, onSuccess) => {
|
||||
try {
|
||||
// 检查模型信息
|
||||
if (!selectedModelInfo) {
|
||||
throw new Error(t('textSplit.selectModelFirst'));
|
||||
}
|
||||
|
||||
// 设置生成状态
|
||||
setGenerating(prev => ({ ...prev, [chunkId]: true }));
|
||||
|
||||
// 获取当前语言环境
|
||||
const currentLanguage = i18n.language === 'zh-CN' ? 'zh-CN' : 'en';
|
||||
|
||||
// 调用API生成测评题目
|
||||
const response = await request(
|
||||
`/api/projects/${projectId}/chunks/${encodeURIComponent(chunkId)}/eval-questions`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: selectedModelInfo,
|
||||
language: currentLanguage
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || t('textSplit.generateEvalQuestionsFailed'));
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// 显示成功消息
|
||||
toast.success(
|
||||
t('textSplit.evalQuestionsGeneratedSuccess', {
|
||||
total: data.total,
|
||||
defaultValue: `成功生成 ${data.total} 道测评题目`
|
||||
})
|
||||
);
|
||||
|
||||
// 调用成功回调
|
||||
if (onSuccess) {
|
||||
onSuccess(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error generating eval questions:', error);
|
||||
toast.error(error.message || t('textSplit.generateEvalQuestionsFailed'));
|
||||
} finally {
|
||||
// 清除生成状态
|
||||
setGenerating(prev => {
|
||||
const newState = { ...prev };
|
||||
delete newState[chunkId];
|
||||
return newState;
|
||||
});
|
||||
}
|
||||
},
|
||||
[projectId, t]
|
||||
);
|
||||
|
||||
return {
|
||||
generating,
|
||||
handleGenerateEvalQuestions
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { selectedModelInfoAtom } from '@/lib/store';
|
||||
import { useAtomValue } from 'jotai/index';
|
||||
import { toast } from 'sonner';
|
||||
import i18n from '@/lib/i18n';
|
||||
import axios from 'axios';
|
||||
|
||||
/**
|
||||
* 文件处理的自定义Hook
|
||||
* @param {string} projectId - 项目ID
|
||||
* @returns {Object} - 文件处理状态和操作方法
|
||||
*/
|
||||
export default function useFileProcessing(projectId) {
|
||||
const { t } = useTranslation();
|
||||
const [fileProcessing, setFileProcessing] = useState(false);
|
||||
const [progress, setProgress] = useState({
|
||||
total: 0,
|
||||
completed: 0,
|
||||
percentage: 0,
|
||||
questionCount: 0
|
||||
});
|
||||
const model = useAtomValue(selectedModelInfoAtom);
|
||||
|
||||
/**
|
||||
* 重置进度状态
|
||||
*/
|
||||
const resetProgress = useCallback(() => {
|
||||
setTimeout(() => {
|
||||
setProgress({
|
||||
total: 0,
|
||||
completed: 0,
|
||||
percentage: 0,
|
||||
questionCount: 0
|
||||
});
|
||||
}, 1000); // 延迟重置,让用户看到完成的进度
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 处理文件
|
||||
* @param {Array} files - 文件列表
|
||||
* @param {string} pdfStrategy - PDF处理策略
|
||||
* @param {string} selectedViosnModel - 选定的视觉模型
|
||||
*/
|
||||
const handleFileProcessing = useCallback(
|
||||
async (files, pdfStrategy, selectedViosnModel, domainTreeAction) => {
|
||||
try {
|
||||
const currentLanguage = i18n.language === 'zh-CN' ? '中文' : 'en';
|
||||
|
||||
//获取到视觉策略要使用的模型
|
||||
const availableModels = JSON.parse(localStorage.getItem('modelConfigList'));
|
||||
const vsionModel = availableModels.find(m => m.id === selectedViosnModel);
|
||||
|
||||
const response = await axios.post(`/api/projects/${projectId}/tasks`, {
|
||||
taskType: 'file-processing',
|
||||
modelInfo: model,
|
||||
language: currentLanguage,
|
||||
detail: '文件处理任务',
|
||||
note: {
|
||||
vsionModel,
|
||||
projectId,
|
||||
fileList: files,
|
||||
strategy: pdfStrategy,
|
||||
domainTreeAction
|
||||
}
|
||||
});
|
||||
|
||||
if (response.data?.code !== 0) {
|
||||
throw new Error(t('textSplit.pdfProcessingFailed') + (response.data?.error || ''));
|
||||
}
|
||||
|
||||
//提示后台任务进行中
|
||||
toast.success(t('textSplit.pdfProcessingToast'));
|
||||
} catch (error) {
|
||||
toast.error(t('textSplit.pdfProcessingFailed') + error.message || '');
|
||||
}
|
||||
},
|
||||
[projectId, t, resetProgress, model]
|
||||
);
|
||||
|
||||
return {
|
||||
fileProcessing,
|
||||
progress,
|
||||
setFileProcessing,
|
||||
setProgress,
|
||||
handleFileProcessing,
|
||||
resetProgress
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import i18n from '@/lib/i18n';
|
||||
import request from '@/lib/util/request';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export default function useQuestionGeneration(projectId) {
|
||||
const { t } = useTranslation();
|
||||
const [processing, setProcessing] = useState(false);
|
||||
const [progress, setProgress] = useState({
|
||||
total: 0,
|
||||
completed: 0,
|
||||
percentage: 0,
|
||||
questionCount: 0
|
||||
});
|
||||
|
||||
const resetProgress = useCallback(() => {
|
||||
setTimeout(() => {
|
||||
setProgress({
|
||||
total: 0,
|
||||
completed: 0,
|
||||
percentage: 0,
|
||||
questionCount: 0
|
||||
});
|
||||
}, 500);
|
||||
}, []);
|
||||
|
||||
const handleGenerateQuestions = useCallback(
|
||||
async (chunkIds, selectedModelInfo, fetchChunks) => {
|
||||
try {
|
||||
if (!chunkIds || chunkIds.length === 0) return;
|
||||
|
||||
if (!selectedModelInfo) {
|
||||
throw new Error(t('textSplit.selectModelFirst'));
|
||||
}
|
||||
|
||||
setProcessing(true);
|
||||
|
||||
if (chunkIds.length === 1) {
|
||||
const chunkId = chunkIds[0];
|
||||
const currentLanguage = i18n.language === 'zh-CN' ? '中文' : 'en';
|
||||
|
||||
const response = await request(`/api/projects/${projectId}/chunks/${chunkId}/questions`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: selectedModelInfo,
|
||||
language: currentLanguage,
|
||||
enableGaExpansion: true
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || t('textSplit.generateQuestionsFailed', { chunkId }));
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
toast.success(
|
||||
t('textSplit.questionsGeneratedSuccess', {
|
||||
total: data.total
|
||||
})
|
||||
);
|
||||
|
||||
if (fetchChunks) fetchChunks();
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await request(`/api/projects/${projectId}/tasks`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
taskType: 'question-generation',
|
||||
modelInfo: selectedModelInfo,
|
||||
language: i18n.language,
|
||||
detail: '批量生成问题任务',
|
||||
note: { chunkIds }
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || t('tasks.createFailed'));
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (data?.code !== 0) {
|
||||
throw new Error(data?.message || t('tasks.createFailed'));
|
||||
}
|
||||
|
||||
toast.success(`${t('tasks.createSuccess')},${t('tasks.title')}查看进度`);
|
||||
} catch (error) {
|
||||
toast.error(error.message);
|
||||
} finally {
|
||||
setProcessing(false);
|
||||
resetProgress();
|
||||
}
|
||||
},
|
||||
[projectId, t, resetProgress]
|
||||
);
|
||||
|
||||
return {
|
||||
processing,
|
||||
progress,
|
||||
setProgress,
|
||||
setProcessing,
|
||||
handleGenerateQuestions,
|
||||
resetProgress
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user