Files
MemoryBear/web/src/views/KnowledgeBase/[knowledgeBaseId]/CreateDataset.tsx
2026-05-06 14:13:13 +08:00

914 lines
38 KiB
TypeScript
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.
import { useMemo,useRef, useState, useEffect } from 'react';
import { Button, Flex, Radio, Steps, Modal, Input, Checkbox, Select, Form, Progress, App } from 'antd';
import { useTranslation } from 'react-i18next';
import { useLocation, useNavigate, useParams } from 'react-router-dom';
import './Private.css';
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 type { UploadFile } from 'antd';
import UploadFiles from '@/components/Upload/UploadFiles';
import type { UploadRequestOption } from 'rc-upload/lib/interface';
import { uploadFile, uploadQaFile, getDocumentList, parseDocument, updateDocument, deleteDocument, createDocumentAndUpload } from '@/api/knowledgeBase';
import exitIcon from '@/assets/images/knowledgeBase/exit.png';
import SliderInput from '@/components/SliderInput';
import DelimiterSelector from '../components/DelimiterSelector';
const { TextArea } = Input;
const style: React.CSSProperties = {
display: 'flex',
gap: 16,
};
const radioWrapperBaseStyle: React.CSSProperties = {
display: 'flex',
alignItems: 'flex-start',
columnGap: 14, // Wider gap between dot and text
width: '100%',
border: '1px solid #E5E5E5',
borderRadius: 12,
padding: 16,
};
const getActiveRadioStyle = (active: boolean): React.CSSProperties => ({
...radioWrapperBaseStyle,
border: active ? '1px solid #171719' : radioWrapperBaseStyle.border,
backgroundColor: active ? '#FAFAFA' : 'transparent',
});
type SourceType = 'local' | 'link' | 'text' | 'csv';
type ProcessingMethod = 'directBlock' | 'qaExtract';
type ParameterSettings = 'defaultSettings' | 'customSettings';
const stepKeys = ['selectFile', 'parameterSettings', 'dataPreview', 'confirmUpload'] as const;
type StepKey = typeof stepKeys[number];
const stepIndexMap: Record<StepKey, number> = {
selectFile: 0,
parameterSettings: 1,
dataPreview: 2,
confirmUpload: 3,
};
interface CreateDatasetLocationState {
source?: SourceType;
knowledgeBaseId?: string;
parentId?: string;
startStep?: StepKey;
fileId?: string | string[];
fileIds?: string | string[];
}
interface ContentFormData {
title: string;
content: string;
}
const fileType = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'csv', 'md', 'htm', 'html', 'json', 'ppt', 'pptx', 'txt', 'png', 'jpg', 'mp3', 'mp4', 'mov', 'wav']
const csvFileType = ['csv']
const CreateDataset = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const { modal, message: messageApi } = App.useApp()
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<number>(stepIndexMap[initialStepKey]);
const tableRef = useRef<TableRef>(null);
const [form] = Form.useForm<ContentFormData>();
const [data, setData] = useState<KnowledgeBaseDocumentData[]>([]);
const [rechunkFileIds, setRechunkFileIds] = useState<string[]>(initialFileIds);
const [textFormValid, setTextFormValid] = useState<boolean>(false);
const [pollingLoading, setPollingLoading] = useState<boolean>(false);
const pollingTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
const [delimiter, setDelimiter] = useState<string | undefined>(undefined);
const [blockSize, setBlockSize] = useState<number>(130);
const [qaPrompt, setQaPrompt] = useState<string | undefined>()
console.log('qaPrompt', qaPrompt)
const [processingMethod, setProcessingMethod] = useState<ProcessingMethod>('directBlock');
const [parameterSettings, setParameterSettings] = useState<ParameterSettings>('defaultSettings');
const [pdfEnhancementEnabled, setPdfEnhancementEnabled] = useState<boolean>(true);
const [pdfEnhancementMethod, setPdfEnhancementMethod] = useState<string>('mineru');
const steps = useMemo(
() => [
{ title: t('knowledgeBase.selectFile') },
{ title: t('knowledgeBase.parameterSettings') },
// { title: t('knowledgeBase.dataPreview') }, // Temporarily hide step 3
{ title: t('knowledgeBase.confirmUpload') },
],
[t],
);
// 存储每个文件的 AbortController用于取消上传
const abortControllersRef = useRef<Map<string, AbortController>>(new Map());
const uploadRef = useRef<{ fileList: UploadFile[]; clearFiles: () => void }>(null);
console.log('Upload files', uploadRef.current?.fileList.length)
const handleNext = async () => {
// Temporarily hide step 3: adjust step index (0->1->2 corresponds to select file->parameter settings->confirm upload)
let nextStep = current + 1;
if (current === 0 && source === 'csv') {
return
}
if((nextStep === 1 && source === 'local') || (nextStep === 2 && source === 'csv')) {
// Check if files have been uploaded
if (rechunkFileIds.length === 0) {
// If no files, prompt user to upload first
Modal.warning({
title: t('common.warning') || 'Warning',
content: t('knowledgeBase.pleaseUploadFileFirst') || 'Please upload files first',
});
return; // Don't proceed to next step
}
}else if(nextStep === 1 && source === 'text'){
try {
const values = await form.validateFields();
// setLoading(true);
// TODO: Need to call corresponding API to save content here
const params = {
// ...values,
kb_id: knowledgeBaseId,
parent_id: parentId,
};
const response = await createDocumentAndUpload(values, params)
if(response) {
setRechunkFileIds([response.id])
}
} catch (err) {
messageApi.error(t('knowledgeBase.createContentError'));
} finally {
// setLoading(false);
}
}
// 从参数设置进入确认上传时的处理
if(current === 1 && nextStep === 2) {
// debugger
// handlePreview(data[0],0)
if(parameterSettings === 'customSettings' || processingMethod === 'qaExtract' || pdfEnhancementEnabled){
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,
qa_prompt: qaPrompt
}
}
updateDocument(id, params)
})
}
// Execute once immediately to load document list for preview (don't auto-return)
pollDocumentStatus(false);
}
// Limit max step to 2 (confirm upload)
setCurrent(Math.min(nextStep, 2));
};
const handlePrev = () => setCurrent((c) => Math.max(c - 1, 0));
// Start upload: trigger document parsing and start polling
const handleStartUpload = () => {
if (rechunkFileIds.length === 0) {
Modal.warning({
title: t('common.warning') || 'Warning',
content: t('knowledgeBase.pleaseUploadFileFirst') || 'Please upload files first',
});
return;
}
// 显示确认弹框
modal.confirm({
title: t('knowledgeBase.startUploadConfirmTitle') || 'Start processing documents',
content: t('knowledgeBase.startUploadConfirmContent') || 'Document processing will proceed in the background. You can choose to return to the list page immediately or stay on this page to view processing progress.',
okText: t('knowledgeBase.returnToList') || 'Return to list',
cancelText: t('knowledgeBase.stayOnPage') || 'Stay on this page',
onOk: () => {
// User chose to return to list - don't show loading, navigate directly
startProcessing(true);
},
onCancel: () => {
// User chose to stay on current page - show loading and start polling
console.log('User chose to stay, starting to show loading');
setPollingLoading(true);
// Delay a bit to let user see loading effect, then start processing
setTimeout(() => {
startProcessing(false);
}, 100);
},
});
};
// Function to actually start processing
const startProcessing = (autoReturnToList: boolean) => {
// Trigger document parsing
rechunkFileIds.map((id) => {
parseDocument(id, {});
});
if (autoReturnToList) {
// User chose to return immediately, navigate directly (no loading shown)
console.log('User chose to return to list page immediately');
handleBack();
} else {
// User chose to stay, start polling to view progress (loading already set in onCancel)
console.log('User chose to stay and view progress');
// Execute polling once immediately (enable auto-return)
pollDocumentStatus(true);
// Then execute every 3 seconds (enable auto-return)
pollingTimerRef.current = setInterval(() => {
pollDocumentStatus(true);
}, 3000);
}
};
const handleDelete = (record: AnyObject) => {
modal.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('Delete cancelled');
},
});
}
// Table column configuration
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) => {
// When value >= 1 it's complete, when 01 show progress bar
if (value >= 1) {
return (
<span className="rb:text-xs rb:border rb:border-[#DFE4ED] rb:bg-[#FBFDFF] rb:rounded rb:items-center rb:text-[#212332] rb:py-1 rb:px-2">
<span className="rb:inline-block rb:w-[5px] rb:h-[5px] rb:mr-2 rb:rounded-full" style={{ backgroundColor: '#369F21' }}></span>
<span>{t('knowledgeBase.completed')}</span>
</span>
);
} else if (value >= 0 && value < 1) {
// Processing, show progress bar
return (
<div className="rb:flex rb:items-center rb:gap-2">
<Progress
percent={Math.round(value * 100)}
size="small"
status="active"
strokeColor={{
'0%': '#108ee9',
'100%': '#87d068',
}}
style={{ width: '120px' }}
/>
</div>
);
} else {
// value = 0 or other cases, show pending
return (
<span className="rb:text-xs rb:border rb:border-[#DFE4ED] rb:bg-[#FBFDFF] rb:rounded rb:items-center rb:text-[#212332] rb:py-1 rb:px-2">
<span className="rb:inline-block rb:w-[5px] rb:h-[5px] rb:mr-2 rb:rounded-full" style={{ backgroundColor: '#FF8A4C' }}></span>
<span>{t('knowledgeBase.pending')}</span>
</span>
);
}
}
},
{
title: t('common.operation'),
key: 'action',
render: (_, record) => (
<Button type='text' danger onClick={() => handleDelete(record)}>{t('common.delete')}</Button>
),
},
];
// Helper function to check media file duration
const checkMediaDuration = (file: File): Promise<number> => {
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(`${t('knowledgeBase.unableReadFile')}`));
};
media.src = url;
});
};
// Upload file
const handleUpload = async (options: UploadRequestOption) => {
const { file, onSuccess, onError, onProgress, filename = 'file' } = options;
// Create AbortController for cancelling upload
const abortController = new AbortController();
const fileUid = (file as any).uid;
abortControllersRef.current.set(fileUid, abortController);
// Get file extension
const fileExtension = (file as File).name.split('.').pop()?.toLowerCase();
const mediaExtensions = ['mp3', 'mp4', 'mov', 'wav'];
// If media file, check size and duration
if (fileExtension && mediaExtensions.includes(fileExtension)) {
const fileSizeInMB = (file as File).size / (1024 * 1024);
// 检查文件大小50MB限制
if (fileSizeInMB > 100) {
messageApi.error(`${t('knowledgeBase.sizeLimitError')}: ${fileSizeInMB.toFixed(2)}MB`);
onError?.(new Error(`${t('knowledgeBase.fileSizeExceeds')}`));
abortControllersRef.current.delete(fileUid);
return;
}
try {
// Check media duration (150 second limit)
const duration = await checkMediaDuration(file as File);
if (duration > 150) {
messageApi.error(`${t('knowledgeBase.fileDurationLimitError')}: ${Math.round(duration)}s`);
onError?.(new Error(`${t('knowledgeBase.fileDurationExceeds')}`));
abortControllersRef.current.delete(fileUid);
return;
}
} catch (error) {
messageApi.error(`${t('knowledgeBase.unableReadFile')}`);
onError?.(error as Error);
abortControllersRef.current.delete(fileUid);
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);
}
if (source === 'csv') {
uploadQaFile(formData, {
kb_id: knowledgeBaseId,
parent_id: parentId,
signal: abortController.signal,
})
.then((res: UploadFileResponse) => {
// Upload successful, remove AbortController
abortControllersRef.current.delete(fileUid);
onSuccess?.(res, new XMLHttpRequest());
messageApi.success(t('knowledgeBase.uploadSuccess'))
handleBack()
})
.catch((error) => {
// Remove AbortController
abortControllersRef.current.delete(fileUid);
// If user actively cancelled, don't show error message
if (error.name === 'AbortError' || error.code === 'ERR_CANCELED') {
console.log('Upload cancelled:', (file as File).name);
return;
}
onError?.(error as Error);
});
} else {
uploadFile(formData, {
kb_id: knowledgeBaseId,
parent_id: parentId,
signal: abortController.signal,
onUploadProgress: (event) => {
if (!event.total) return;
const percent = Math.round((event.loaded / event.total) * 100);
onProgress?.({ percent }, file);
},
})
.then((res: UploadFileResponse) => {
// Upload successful, remove AbortController
abortControllersRef.current.delete(fileUid);
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) => {
// Remove AbortController
abortControllersRef.current.delete(fileUid);
// If user actively cancelled, don't show error message
if (error.name === 'AbortError' || error.code === 'ERR_CANCELED') {
console.log('Upload cancelled:', (file as File).name);
return;
}
onError?.(error as Error);
});
}
};
// 轮询检查文档处理状态
// autoReturn: whether to automatically return to list page when all documents are completed
const pollDocumentStatus = (autoReturn: boolean = false) => {
console.log('Start polling document status, current pollingLoading:', pollingLoading);
if (!knowledgeBaseId || !parentId || rechunkFileIds.length === 0) {
console.log('Polling conditions not met, exiting');
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);
// Check if all documents have progress of 1
const allCompleted = documents.every((doc: KnowledgeBaseDocumentData) => doc.progress === 1);
console.log('Polling status:', allCompleted);
// 检查是否所有文档都完成了
// debugger
if (allCompleted) {
// 清除定时器和 loading 状态
if (pollingTimerRef.current) {
clearInterval(pollingTimerRef.current);
pollingTimerRef.current = null;
}
// 延迟清除 loading让用户看到完成状态
setTimeout(() => {
setPollingLoading(false);
}, 1000);
// Only auto-return when autoReturn is true
if (autoReturn) {
// Delay 2 seconds before navigating to let user see completion status
console.log('All documents processed, returning to list page in 2 seconds');
setTimeout(() => {
handleBack();
}, 2000);
} else {
console.log('All documents processed, user can operate manually');
}
} else {
// If documents are still processing, keep loading state
console.log('Documents still processing, maintaining loading state');
}
})
.catch((error) => {
console.error('Failed to poll document status:', 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('Missing route parameters, unable to return');
}
};
const handleChange = (value: number | null) =>{
if (value !== null) {
setBlockSize(value);
}
}
// 删除已上传的文件
const handleDeleteFile = async (fileId: string) => {
try {
await deleteDocument(fileId);
// Delete successful, remove the id from rechunkFileIds
setRechunkFileIds((prev) => prev.filter((id) => id !== fileId));
console.log(`${t('common.deleteSuccess')}`);
} catch (error) {
messageApi.error(`${t('common.deleteFailed')}`);
}
};
// When navigating from other pages with fileIds, load corresponding document data
// useEffect(() => {
// if (initialFileIds.length > 0 && initialStepKey !== 'selectFile' && knowledgeBaseId && parentId) {
// // Load document list data
// getDocumentList(knowledgeBaseId,{
// document_ids: initialFileIds.join(','),
// })
// .then((res: any) => {
// const documents = res.items || [];
// setData(documents);
// })
// .catch((error) => {
// console.error('Failed to load document list:', error);
// });
// }
// }, []);
// Cleanup function: clear timer and loading state when component unmounts
useEffect(() => {
return () => {
if (pollingTimerRef.current) {
clearInterval(pollingTimerRef.current);
pollingTimerRef.current = null;
}
setPollingLoading(false);
};
}, []);
// Watch for route changes, ensure state is cleaned up when page switches
useEffect(() => {
return () => {
// Clean up state when page unmounts
if (pollingTimerRef.current) {
clearInterval(pollingTimerRef.current);
pollingTimerRef.current = null;
}
setPollingLoading(false);
};
}, [location.pathname]);
return (<>
<div className='rb:p-3 rb:pt-2 rb:h-full rb:flex rb:flex-col'>
{/* <Typography.Title level={4} className='rb:!m-0 rb:!mb-4'>
{t('knowledgeBase.createA') + ' ' + t('knowledgeBase.dataset')}
</Typography.Title> */}
<div className='rb:flex rb:items-center rb:gap-2 rb:mb-4 rb:cursor-pointer' onClick={handleBack}>
<img src={exitIcon} alt='exit' className='rb:w-4 rb:h-4' />
<span className='rb:text-gray-500 rb:text-sm'>{t('common.exit')}</span>
</div>
{source !== 'csv' && <div className='rb:px-24 rb:py-5 rb:bg-white rb:rounded-xl'>
<Steps current={current} items={steps} className="custom-steps" />
</div> }
<div className='rb:bg-white rb:rounded-xl rb:flex-1 rb:mt-3'>
{current === 0 && (
<div className='rb:flex rb:w-full rb:p-6'>
{source && (source === 'local' || source === 'csv') && (
<UploadFiles
ref={uploadRef}
isCanDrag={true}
fileSize={100}
multiple={source !== 'csv'}
maxCount={source === 'csv' ? 1 : 99}
fileType={source === 'csv' ? csvFileType : fileType}
customRequest={handleUpload}
onChange={(fileList) => {
console.log('File list changed:', fileList);
}}
onRemove={async (file) => {
// 如果文件正在上传,取消上传
const fileUid = file.uid;
const abortController = abortControllersRef.current.get(fileUid);
if (abortController) {
abortController.abort();
abortControllersRef.current.delete(fileUid);
console.log('Upload cancelled:', (file as any).name);
// 取消上传后直接返回 true允许移除文件
return true;
}
// Only delete server file when file upload was successful (has response.id)
if (file.response?.id) {
try {
await deleteDocument(file.response.id);
setRechunkFileIds(prev => prev.filter(id => id !== file.response.id));
console.log('Server file deleted:', file.response.id);
return true;
} catch (error) {
console.error('Failed to delete file:', error);
messageApi.error(t('common.deleteFailed') || 'Failed to delete file');
return false; // Don't remove file when deletion fails
}
}
// Also allow removal in other cases (such as failed uploads)
return true;
}} />
)}
{source && source === 'link' && (
<div className='rb:flex rb:w-full rb:flex-col rb:mt-10 rb:px-40'>
<div className='rb:text-sm rb:font-medium rb:text-gray-800 rb:mb-3'>
{t('knowledgeBase.webLink')}
</div>
<TextArea rows={6} placeholder={t('knowledgeBase.webLinkPlaceholder')} />
<div className='rb:text-sm rb:text-gray-500 rb:mt-3'>
{t('knowledgeBase.webLinkDesc',{count: 5})}
</div>
<div className='rb:text-sm rb:font-medium rb:text-gray-800 rb:mt-10 rb:mb-3'>
{t('knowledgeBase.selectorTutorial')}
</div>
<Input className='rb:w-full' placeholder={t('knowledgeBase.webLinkPlaceholder')}/>
</div>
)}
{source && source === 'text' && (
<div className='rb:flex rb:w-full rb:flex-col rb:mt-10 rb:px-20'>
<Form
form={form}
layout="vertical"
onValuesChange={() => {
// 检查表单字段是否都已填写
const values = form.getFieldsValue();
const isValid = !!(values.title?.trim() && values.content?.trim());
setTextFormValid(isValid);
}}
>
<Form.Item
name="title"
label={t('knowledgeBase.title')}
rules={[{ required: true, message: t('knowledgeBase.pleaseEnterTitle') }]}
>
<Input placeholder={t('knowledgeBase.pleaseEnterTitle')} />
</Form.Item>
<Form.Item
name="content"
label={t('knowledgeBase.customContent')}
rules={[{ required: true, message: t('knowledgeBase.pleaseEnterContent') }]}
>
<Input.TextArea
placeholder={t('knowledgeBase.pleaseEnterContent')}
rows={8}
showCount
maxLength={5000}
/>
</Form.Item>
</Form>
{/* <div className='rb:text-sm rb:font-medium rb:text-gray-800 rb:mb-3'>
{t('knowledgeBase.customText')}
</div>
<Input className='rb:w-full' placeholder={t('knowledgeBase.webLinkPlaceholder')}/>
<div className='rb:text-sm rb:font-medium rb:text-gray-800 rb:mt-10 rb:mb-3'>
{t('knowledgeBase.customContent')}
</div>
<TextArea rows={6} placeholder={t('knowledgeBase.webLinkPlaceholder')} /> */}
</div>
)}
</div>
)}
{current === 1 && (
<div className='rb:flex rb:flex-col rb:mt-10 rb:px-40'>
{rechunkFileIds.length > 0 && (
<div className='rb:bg-[#F0F3F8] rb:border rb:border-[#DFE4ED] rb:rounded-[8px] rb:px-3 rb:py-2 rb:mb-4 rb:text-xs rb:text-gray-600 rb:flex rb:items-center rb:flex-wrap rb:gap-2'>
<span className='rb:text-gray-700 rb:font-medium'>{t('knowledgeBase.rechunking')}:</span>
{rechunkFileIds.map((id) => (
<span key={id} className='rb:px-2 rb:py-0.5 rb:bg-white rb:border rb:border-[#DFE4ED] rb:rounded'>{id}</span>
))}
</div>
)}
<div className='rb:text-base rb:font-medium rb:text-gray-800 rb:mt-4'>
{t('knowledgeBase.fileParsingSettings')}
</div>
<div className='rb:mt-4'>
<div
className={`rb:flex rb:items-center rb:justify-between rb:w-full rb:border rb:rounded-xl rb:p-4 rb:cursor-pointer ${
pdfEnhancementEnabled ? 'rb:border-[#171719] rb:bg-[#FAFAFA]' : 'rb-border'
}`}
// onClick={() => setPdfEnhancementEnabled(!pdfEnhancementEnabled)}
>
<Checkbox
checked={pdfEnhancementEnabled}
onChange={(e) => setPdfEnhancementEnabled(e.target.checked)}
className='rb:mr-3'
>
<span className='rb:text-base rb:font-medium rb:text-gray-800 rb:pl-[22px]'>
{t('knowledgeBase.pdfEnhancementAnalysis')}
</span>
</Checkbox>
{pdfEnhancementEnabled && (
<div className='rb:ml-10'>
<Select
value={pdfEnhancementMethod}
onChange={(value) => setPdfEnhancementMethod(value)}
className='rb:w-[300px]'
options={[
{ value: 'deepdoc', label: 'DeepDoc' },
{ value: 'mineru', label: 'MinerU' },
{ value: 'textln', label: 'TextLN' }
]}
/>
</div>
)}
</div>
</div>
<div className='rb:text-base rb:font-medium rb:text-gray-800 rb:mt-6'>
{t('knowledgeBase.dataProcessingSettings')}
</div>
<div className='rb:font-medium rb:text-gray-500 rb:mt-4 rb:mb-3'>
{t('knowledgeBase.processingMethod')}
</div>
<Radio.Group
value={processingMethod}
onChange={(e) => setProcessingMethod(e.target.value)}
style={style}
>
<Radio value='directBlock' style={getActiveRadioStyle(processingMethod === 'directBlock')}>
<Flex gap='small' vertical>
<span className='rb:text-base rb:font-medium rb:text-gray-800'>
{t('knowledgeBase.directBlock')}
</span>
</Flex>
</Radio>
<Radio value='qaExtract' style={getActiveRadioStyle(processingMethod === 'qaExtract')}>
<Flex gap='small' vertical>
<span className='rb:text-base rb:font-medium rb:text-gray-800'>
{t('knowledgeBase.qaExtract')}
</span>
</Flex>
</Radio>
</Radio.Group>
<div className='rb:font-medium rb:text-gray-500 rb:mt-4 rb:mb-3'>
{t('knowledgeBase.parameterSettings')}
</div>
<Radio.Group
value={parameterSettings}
onChange={(e) => setParameterSettings(e.target.value)}
style={style}
>
<Radio value='defaultSettings' style={getActiveRadioStyle(parameterSettings === 'defaultSettings')}>
<Flex gap='small' vertical>
<span className='rb:text-base rb:font-medium rb:text-gray-800'>
{t('knowledgeBase.default')}
</span>
<span className='rb:text-3 rb:text-gray-500'>{t('knowledgeBase.defaultSettings')}</span>
</Flex>
</Radio>
<Radio value='customSettings' style={getActiveRadioStyle(parameterSettings === 'customSettings')}>
<Flex gap='small' vertical>
<span className='rb:text-base rb:font-medium rb:text-gray-800'>
{t('knowledgeBase.customize')}
</span>
<span className='rb:text-3 rb:text-gray-500'>{t('knowledgeBase.customSettings')}</span>
</Flex>
</Radio>
</Radio.Group>
{parameterSettings === 'customSettings' && (<>
<div className='rb:grid rb:grid-cols-2 rb:mt-5 rb-border rb:rounded-xl rb:px-6 rb:py-4 rb:gap-10'>
<div>
<div className='rb:w-full rb:text-[#5B6167] rb:leading-5 rb:mb-2'>
{t('knowledgeBase.delimiter')}
</div>
<DelimiterSelector value={delimiter} onChange={setDelimiter} />
</div>
<SliderInput label={t('knowledgeBase.suggestedBlockSize')} max={1024} min={1} step={1} value={blockSize} onChange={handleChange} />
</div>
<div>
<div className='rb:w-full rb:text-[#5B6167] rb:leading-5 rb:mb-2 rb:mt-4'>
{t('knowledgeBase.qaPrompt')}
</div>
<Input.TextArea value={qaPrompt} rows={6} onChange={(e) => setQaPrompt(e.target.value)} />
</div>
</>)}
</div>
)}
{/* 暂时隐藏第三步:数据预览 */}
{/* {current === stepIndexMap.dataPreview && (
<div className='rb:grid rb:grid-cols-2 rb:rounded-xl rb:border rb:border-[#DFE4ED] rb:h-[calc(100%-160px)] rb:bg-[#FBFDFF] rb:mt-4'>
<div className='rb:border-r rb:h-full rb:overflow-hidden rb:border-[#DFE4ED]'>
<div className='rb:h-11 rb:w-full rb:text-sm rb:font-medium rb:text-gray-800 rb:px-4 rb:py-3 rb:border-b rb:border-[#DFE4ED]'>
{t('knowledgeBase.fileList')}
</div>
<div className='rb:flex rb:flex-col rb:h-[calc(100%-44px)] rb:overflow-y-auto'>
{data.map((item, index) => (
<div key={index} className={`rb:h-11 rb:w-full rb:text-sm rb:text-gray-800 rb:px-4 rb:py-3 rb:hover:text-[#155EEF] rb:cursor-pointer ${curSelectedFileId === index ? styles.textBg + ' ' + styles.active : ''}`}
onClick={() => handlePreview(item, index)}>
{item.file_name}
</div>
))
}
</div>
</div>
<div className='rb:h-full rb:overflow-hidden'>
<div className='rb:flex rb:items-center rb:justify-between rb:h-11 rb:w-full rb:text-sm rb:font-medium rb:text-gray-800 rb:px-4 rb:py-3 rb:border-b rb:border-[#DFE4ED]'>
{t('knowledgeBase.dataPreview')}
<span className='rb:text-sm rb:text-gray-500'>{t('knowledgeBase.maxPreviewChunks', {count: total, max: chunkData.length})}</span>
</div>
<Spin spinning={previewLoading}>
<div className='rb:flex rb:flex-col rb:h-[calc(100%-44px)] rb:overflow-y-auto'>
{chunkData.length > 0 ? (
chunkData.map((item, index) => (
<div key={index} className='rb:text-sm rb:text-gray-800 rb:px-4 rb:py-3'
dangerouslySetInnerHTML={{ __html: item.page_content }}
/>
))
) : (
<NoData title={t('knowledgeBase.noChunksToPreview')}
subTitle={t('knowledgeBase.clickToPreview')}
image={noDataIcon}
/>
)}
</div>
</Spin>
</div>
</div>
)} */}
{current === 2 && (
// <Spin spinning={pollingLoading} tip={t('knowledgeBase.processingDocuments') || '正在处理文档...'}>
<div className='rb:text-sm rb:text-gray-500 rb:mt-4 rb:h-[calc(100%-160px)] rb:overflow-y-auto rb:px-6 rb:py-6'>
{rechunkFileIds.length > 0 ? (
<Table
ref={tableRef}
apiUrl={`/documents/${knowledgeBaseId}/documents`}
apiParams={{
document_ids: rechunkFileIds.join(','),
}}
columns={columns}
rowKey="id"
/>
) : (
<Table
ref={tableRef}
columns={columns}
rowKey="id"
initialData={[]}
/>
)}
</div>
// </Spin>
)}
<div className={`rb:flex rb:p-6 rb:gap-3 rb:mt-6 ${current === 1 || (source == 'link' && current === 0) || (source == 'text' && current === 0) ? 'rb:pl-28 rb:mt-10' : ''}`}>
{current !== 0 && (
<Button onClick={handlePrev} disabled={current === 0 || pollingLoading}>
{t('common.previous') || 'Prev'}
</Button>
)}
{source !== 'csv' && <Button
type='primary'
onClick={current === 2 ? handleStartUpload : handleNext}
disabled={
pollingLoading ||
(current === 0 && source === 'local' && rechunkFileIds.length === 0) ||
(current === 0 && source === 'text' && !textFormValid)
}
>
{current === 2 ? t('knowledgeBase.startUploading') || 'Start Upload' : t('common.next') || 'Next'}
</Button>}
</div>
</div>
</div>
</>);
};
export default CreateDataset;