import { useMemo,useRef, useState, useEffect } from 'react'; import { Button, Flex, Radio, Steps, Modal, Input, Spin, message, Checkbox, Select} from 'antd'; import { useTranslation } from 'react-i18next'; import { useLocation, useNavigate, useParams } from 'react-router-dom'; import Table, { type TableRef } from '@/components/Table' import type { AnyObject } from 'antd/es/_util/type'; import type { UploadFileResponse,KnowledgeBaseDocumentData } from '@/views/KnowledgeBase/types'; import type { ColumnsType } from 'antd/es/table'; import UploadFiles from '@/components/Upload/UploadFiles'; import type { UploadRequestOption } from 'rc-upload/lib/interface'; import { uploadFile, getDocumentList, parseDocument, updateDocument, deleteDocument } from '@/api/knowledgeBase'; import exitIcon from '@/assets/images/knowledgeBase/exit.png'; import SliderInput from '@/components/SliderInput'; import DelimiterSelector from '../components/DelimiterSelector'; const { confirm } = Modal const { TextArea } = Input; const style: React.CSSProperties = { display: 'flex', gap: 16, }; const radioWrapperBaseStyle: React.CSSProperties = { display: 'flex', alignItems: 'flex-start', columnGap: 14, // 点与文字更宽的间距 width: '100%', border: '1px solid #E5E5E5', borderRadius: 8, padding: 16, }; const getActiveRadioStyle = (active: boolean): React.CSSProperties => ({ ...radioWrapperBaseStyle, border: active ? '1px solid #1677ff' : radioWrapperBaseStyle.border, }); type SourceType = 'local' | 'link' | 'text'; type ProcessingMethod = 'directBlock' | 'qaExtract'; type ParameterSettings = 'defaultSettings' | 'customSettings'; const stepKeys = ['selectFile', 'parameterSettings', 'dataPreview', 'confirmUpload'] as const; type StepKey = typeof stepKeys[number]; const stepIndexMap: Record = { selectFile: 0, parameterSettings: 1, dataPreview: 2, confirmUpload: 3, }; interface CreateDatasetLocationState { source?: SourceType; knowledgeBaseId?: string; parentId?: string; startStep?: StepKey; fileId?: string | string[]; fileIds?: string | string[]; } const CreateDataset = () => { const { t } = useTranslation(); const navigate = useNavigate(); const { knowledgeBaseId: routeKnowledgeBaseId } = useParams<{ knowledgeBaseId: string }>(); const location = useLocation(); const locationState = (location.state ?? {}) as CreateDatasetLocationState; const source = (locationState.source ?? 'local') as SourceType; const knowledgeBaseId = locationState.knowledgeBaseId || routeKnowledgeBaseId; const parentId = locationState.parentId; const initialStepKey = locationState.startStep ?? 'selectFile'; const initialFileIds = (() => { const fileIds = locationState.fileIds || locationState.fileId; if (!fileIds) return []; return Array.isArray(fileIds) ? fileIds : [fileIds]; })(); const [current, setCurrent] = useState(stepIndexMap[initialStepKey]); const tableRef = useRef(null); const [data, setData] = useState([]); const [rechunkFileIds, setRechunkFileIds] = useState(initialFileIds); const [pollingLoading, setPollingLoading] = useState(false); const pollingTimerRef = useRef | null>(null); const [delimiter, setDelimiter] = useState(undefined); const [blockSize, setBlockSize] = useState(130); const [processingMethod, setProcessingMethod] = useState('directBlock'); const [parameterSettings, setParameterSettings] = useState('defaultSettings'); const [pdfEnhancementEnabled, setPdfEnhancementEnabled] = useState(true); const [pdfEnhancementMethod, setPdfEnhancementMethod] = useState('deepdoc'); const [messageApi, contextHolder] = message.useMessage(); const fileType = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'csv', 'md', 'htm', 'html', 'json', 'ppt', 'pptx', 'txt','png','jpg','mp3','mp4','mov','wav'] const steps = useMemo( () => [ { title: t('knowledgeBase.selectFile') }, { title: t('knowledgeBase.parameterSettings') }, // { title: t('knowledgeBase.dataPreview') }, // 暂时隐藏第三步 { title: t('knowledgeBase.confirmUpload') }, ], [t], ); const handleNext = () => { // 暂时隐藏第三步:调整步骤索引(0->1->2 对应 选择文件->参数设置->确认上传) let nextStep = current + 1; if(nextStep === 1) { // 检查是否有文件已上传 if (rechunkFileIds.length === 0) { // 如果没有文件,提示用户先上传文件 Modal.warning({ title: t('common.warning') || '提示', content: t('knowledgeBase.pleaseUploadFileFirst') || '请先上传文件', }); return; // 不进入下一步 } } // 从参数设置进入确认上传时的处理 if(current === 1 && nextStep === 2) { // debugger // handlePreview(data[0],0) if(parameterSettings === 'customSettings' || processingMethod === 'qaExtract'){ rechunkFileIds.map((id) => { const params = { progress: 0, parser_config: { layout_recognize: pdfEnhancementMethod || 'DeepDOC', delimiter: delimiter, chunk_token_num: blockSize, auto_questions: processingMethod === 'directBlock' ? 0 : 1, } } updateDocument(id, params) }) } // 立即执行一次,加载文档列表用于预览(不自动返回) pollDocumentStatus(false); } // 限制最大步骤为 2(确认上传) setCurrent(Math.min(nextStep, 2)); }; const handlePrev = () => setCurrent((c) => Math.max(c - 1, 0)); // 开始上传:触发文档解析并启动轮询 const handleStartUpload = () => { if (rechunkFileIds.length === 0) { Modal.warning({ title: t('common.warning') || '提示', content: t('knowledgeBase.pleaseUploadFileFirst') || '请先上传文件', }); return; } // 显示确认弹框 confirm({ title: t('knowledgeBase.startUploadConfirmTitle') || '开始处理文档', content: t('knowledgeBase.startUploadConfirmContent') || '文档处理将在后台进行,您可以选择立即返回列表页或停留在此页面查看处理进度。', okText: t('knowledgeBase.returnToList') || '返回列表页', cancelText: t('knowledgeBase.stayOnPage') || '停留在此页', onOk: () => { // 用户选择返回列表页 - 不显示 loading,直接跳转 startProcessing(true); }, onCancel: () => { // 用户选择停留在当前页 - 显示 loading 并开始轮询 console.log('用户选择停留,开始显示 loading'); setPollingLoading(true); // 延迟一点时间让用户看到 loading 效果,然后开始处理 setTimeout(() => { startProcessing(false); }, 100); }, }); }; // 实际开始处理的函数 const startProcessing = (autoReturnToList: boolean) => { // 触发文档解析 rechunkFileIds.map((id) => { parseDocument(id, {}); }); if (autoReturnToList) { // 用户选择立即返回,直接跳转(不显示 loading) console.log('用户选择立即返回列表页'); handleBack(); } else { // 用户选择停留,启动轮询查看进度(loading 已在 onCancel 中设置) console.log('用户选择停留查看进度'); // 立即执行一次轮询(启用自动返回) pollDocumentStatus(true); // 然后每3秒执行一次(启用自动返回) pollingTimerRef.current = setInterval(() => { pollDocumentStatus(true); }, 3000); } }; const handleDelete = (record: AnyObject) => { confirm({ title: t('common.deleteWarning'), content: t('common.deleteWarningContent', { content: record.name }), onOk: async () => { await deleteDocument(record.id); // 删除成功,从 rechunkFileIds 中移除该 id setRechunkFileIds((prev) => prev.filter((id) => id !== record.id)); // 刷新列表 messageApi.success(t('common.deleteSuccess')); tableRef.current?.loadData(); }, onCancel: () => { console.log('取消删除'); }, }); } // 表格列配置 const columns: ColumnsType = [ { title: t('knowledgeBase.name'), dataIndex: 'file_name', key: 'file_name' }, { title: t('knowledgeBase.status'), dataIndex: 'progress', key: 'progress', render: (value: number, record: any) => { return ( {value === 1 ? t('knowledgeBase.completed') : value === 0 ? t('knowledgeBase.pending') : t('knowledgeBase.processing')} ); } }, { title: t('common.operation'), key: 'action', render: (_, record) => ( ), }, ]; // 检查媒体文件时长的辅助函数 const checkMediaDuration = (file: File): Promise => { return new Promise((resolve, reject) => { const url = URL.createObjectURL(file); const media = document.createElement(file.type.startsWith('video/') ? 'video' : 'audio'); media.onloadedmetadata = () => { URL.revokeObjectURL(url); resolve(media.duration); }; media.onerror = () => { URL.revokeObjectURL(url); reject(new Error('无法读取媒体文件')); }; media.src = url; }); }; // 上传文件 const handleUpload = async (options: UploadRequestOption) => { const { file, onSuccess, onError, onProgress, filename = 'file' } = options; // 获取文件扩展名 const fileExtension = (file as File).name.split('.').pop()?.toLowerCase(); const mediaExtensions = ['mp3', 'mp4', 'mov', 'wav']; // 如果是媒体文件,进行大小和时长检查 if (fileExtension && mediaExtensions.includes(fileExtension)) { const fileSizeInMB = (file as File).size / (1024 * 1024); // 检查文件大小(256MB限制) if (fileSizeInMB > 256) { messageApi.error(`${t('knowledgeBase.sizeLimitError')}:${fileSizeInMB.toFixed(2)}MB`); onError?.(new Error(`${t('knowledgeBase.fileSizeExceeds')}`)); return; } try { // 检查媒体时长(150秒限制) const duration = await checkMediaDuration(file as File); if (duration > 150) { messageApi.error(`${t('knowledgeBase.fileDurationLimitError')}:${Math.round(duration)}秒`); onError?.(new Error(`${t('knowledgeBase.fileDurationExceeds')}`)); return; } } catch (error) { messageApi.error(`${t('knowledgeBase.unableReadFile')}`); onError?.(error as Error); return; } } const formData = new FormData(); formData.append(filename, file as File); if (knowledgeBaseId) { formData.append('kb_id', knowledgeBaseId); } if (parentId) { formData.append('parent_id', parentId); } uploadFile(formData, { kb_id: knowledgeBaseId, parent_id: parentId, onUploadProgress: (event) => { if (!event.total) return; const percent = Math.round((event.loaded / event.total) * 100); onProgress?.({ percent }, file); }, }) .then((res: UploadFileResponse) => { onSuccess?.(res, new XMLHttpRequest()); if (res?.id) { setRechunkFileIds((prev) => { if (prev.includes(res.id)) return prev; const next = [...prev, res.id]; return next; }); } }) .catch((error) => { onError?.(error as Error); }); }; // 轮询检查文档处理状态 // autoReturn: 是否在所有文档完成时自动返回列表页 const pollDocumentStatus = (autoReturn: boolean = false) => { console.log('开始轮询文档状态,当前 pollingLoading:', pollingLoading); if (!knowledgeBaseId || !parentId || rechunkFileIds.length === 0) { console.log('轮询条件不满足,退出'); return; } // 获取文档列表检查是否全部完成,并刷新表格数据 getDocumentList(knowledgeBaseId, { document_ids: rechunkFileIds.join(','), }) .then((res: any) => { const documents = res.items || []; setData(documents); // 只在 confirmUpload 步骤刷新表格数据 if (current === 2) { tableRef.current?.loadData(); } console.log('documents', documents); // 检查是否所有文档的 progress 都为 1 const allCompleted = documents.every((doc: KnowledgeBaseDocumentData) => doc.progress === 1); console.log('轮询状态:', allCompleted); // 检查是否所有文档都完成了 // debugger if (allCompleted) { // 清除定时器和 loading 状态 if (pollingTimerRef.current) { clearInterval(pollingTimerRef.current); pollingTimerRef.current = null; } // 延迟清除 loading,让用户看到完成状态 setTimeout(() => { setPollingLoading(false); }, 1000); // 只有在 autoReturn 为 true 时才自动返回 if (autoReturn) { // 延迟 2 秒后跳转,让用户看到完成状态 console.log('所有文档处理完成,2秒后返回列表页'); setTimeout(() => { handleBack(); }, 2000); } else { console.log('所有文档处理完成,用户可手动操作'); } } else { // 如果还有文档在处理中,确保 loading 状态保持 console.log('还有文档在处理中,保持 loading 状态'); } }) .catch((error) => { console.error('轮询文档状态失败:', error); setPollingLoading(false); }); }; const handleBack = () => { if (knowledgeBaseId) { navigate(`/knowledge-base/${knowledgeBaseId}/private`, { state: { refresh: true, timestamp: Date.now(), // 添加时间戳确保每次都是新的 state // 保持返回到原来的文档文件夹位置 navigateToDocumentFolder: parentId !== knowledgeBaseId ? parentId : undefined, }, }); } else { console.warn('缺少路由参数,无法返回'); } }; const handleChange = (value: number | null) =>{ if (value !== null) { setBlockSize(value); } } // 当从其他页面跳转过来且带有 fileIds 时,加载对应的文档数据 useEffect(() => { if (initialFileIds.length > 0 && initialStepKey !== 'selectFile' && knowledgeBaseId && parentId) { // 加载文档列表数据 getDocumentList(knowledgeBaseId,{ document_ids: initialFileIds.join(','), }) .then((res: any) => { const documents = res.items || []; setData(documents); }) .catch((error) => { console.error('加载文档列表失败:', error); }); } }, []); // 清理函数:组件卸载时清除定时器和 loading 状态 useEffect(() => { return () => { if (pollingTimerRef.current) { clearInterval(pollingTimerRef.current); pollingTimerRef.current = null; } setPollingLoading(false); }; }, []); // 监听路由变化,确保在页面切换时清理状态 useEffect(() => { return () => { // 页面卸载时清理状态 if (pollingTimerRef.current) { clearInterval(pollingTimerRef.current); pollingTimerRef.current = null; } setPollingLoading(false); }; }, [location.pathname]); return ( <> {contextHolder}
{/* {t('knowledgeBase.createA') + ' ' + t('knowledgeBase.dataset')} */}
exit {t('common.exit')}
{current === 0 && (
{source && source === 'local' && ( )} {source && source === 'link' && (
{t('knowledgeBase.webLink')}