feat(knowledgeBase): enhance file upload and dataset creation with abort support and improved UX
- Add AbortSignal support to uploadFile API for cancellable uploads - Implement custom onRemove callback in UploadFiles component with confirmation dialog - Add i18n translations for file removal confirmation and error messages - Update supported file types documentation to include IMAGE and MEDIA formats - Improve file removal UI with cursor pointer styling - Refactor getModelList API to remove unused type parameter - Add Form import and UploadFile type for better type safety in CreateDataset - Enhance error handling and user feedback for file operations
This commit is contained in:
@@ -64,8 +64,8 @@ export const getModelTypeList = async () => {
|
||||
return response as any[];
|
||||
};
|
||||
// 获取模型列表
|
||||
export const getModelList = async (type: string | string[], pageInfo: PageRequest) => {
|
||||
const response = await request.get(`${apiPrefix}/models`, { type, ...pageInfo });
|
||||
export const getModelList = async (pageInfo: PageRequest) => {
|
||||
const response = await request.get(`${apiPrefix}/models`, pageInfo);
|
||||
return response as any;
|
||||
};
|
||||
//获取模型提供者
|
||||
@@ -135,16 +135,18 @@ interface UploadFileOptions {
|
||||
kb_id?: string;
|
||||
parent_id?: string;
|
||||
onUploadProgress?: (event: AxiosProgressEvent) => void;
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
// 上传文件
|
||||
export const uploadFile = async (data: FormData, options?: UploadFileOptions) => {
|
||||
const { kb_id, parent_id, onUploadProgress } = options || {};
|
||||
const { kb_id, parent_id, onUploadProgress, signal } = options || {};
|
||||
const params: Record<string, string> = {};
|
||||
if (kb_id) params.kb_id = kb_id;
|
||||
if (parent_id) params.parent_id = parent_id;
|
||||
const response = await request.uploadFile(`${apiPrefix}/files/file`, data, {
|
||||
params,
|
||||
onUploadProgress,
|
||||
signal,
|
||||
});
|
||||
return response as UploadFileResponse;
|
||||
};
|
||||
|
||||
@@ -38,6 +38,8 @@ interface UploadFilesProps extends Omit<UploadProps, 'onChange'> {
|
||||
maxCount?: number;
|
||||
/** 是否支持拖拽上传,默认为false */
|
||||
isCanDrag?: boolean;
|
||||
/** 自定义移除文件回调 */
|
||||
onRemove?: (file: UploadFile) => boolean | void | Promise<boolean | void>;
|
||||
}
|
||||
const ALL_FILE_TYPE: {
|
||||
[key: string]: string;
|
||||
@@ -77,6 +79,7 @@ const UploadFiles = forwardRef<UploadFilesRef, UploadFilesProps>(({
|
||||
isAutoUpload = true,
|
||||
maxCount = 1,
|
||||
isCanDrag = false,
|
||||
onRemove: customOnRemove,
|
||||
...props
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
@@ -86,11 +89,20 @@ const UploadFiles = forwardRef<UploadFilesRef, UploadFilesProps>(({
|
||||
|
||||
// 处理文件移除
|
||||
const handleRemove = (file: UploadFile) => {
|
||||
// 如果有自定义的 onRemove 回调,先执行它
|
||||
if (customOnRemove) {
|
||||
const result = customOnRemove(file);
|
||||
// 如果返回 false,阻止移除
|
||||
if (result === false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
confirm({
|
||||
title: '确定要删除此文件吗?',
|
||||
okText: '确定',
|
||||
title: `${t('common.confirmRemoveFile')}`,
|
||||
okText: `${t('common.confirm')}`,
|
||||
okType: 'danger',
|
||||
cancelText: '取消',
|
||||
cancelText: `${t('common.cancel')}`,
|
||||
onOk: () => {
|
||||
const newFileList = fileList.filter((item) => item.uid !== file.uid);
|
||||
setFileList(newFileList);
|
||||
@@ -236,7 +248,7 @@ const UploadFiles = forwardRef<UploadFilesRef, UploadFilesProps>(({
|
||||
<div key={file.uid} className="rb:relative rb:w-full rb:pt-[8px] rb:pl-[10px] rb:pr-[10px] rb-pb-[10px] rb:border-1 rb:border-[#EBEBEB] rb:rounded rb:p-2 rb:mt-2 rb:bg-white">
|
||||
<div className="rb:text-[12px] rb:flex rb:items-center rb:justify-between rb:mb-[2px]">
|
||||
{file.name}
|
||||
<span className="rb:text-[#5B6167]" onClick={() => actions?.remove()}>Cancel</span>
|
||||
<span className="rb:text-[#5B6167] rb:cursor-pointer" onClick={() => actions?.remove()}>Cancel</span>
|
||||
</div>
|
||||
<Progress percent={file.percent || 0} strokeColor={file.status === 'error' ? '#FF5D34' : '#155EEF'} size="small" showInfo={false} />
|
||||
</div>
|
||||
|
||||
@@ -293,7 +293,9 @@ export const en = {
|
||||
add: 'Add',
|
||||
addOption: 'Add Option',
|
||||
viewDetail: 'View Detail',
|
||||
confirmRemoveFile: 'Are you sure you want to remove this file?',
|
||||
deleteSuccess: 'Delete successfully',
|
||||
deleteFailed: 'Delete Failure',
|
||||
foldUp: 'Collapse',
|
||||
expanded: 'Expand',
|
||||
clickUploadIcon: 'click on the upload icon',
|
||||
@@ -468,7 +470,7 @@ export const en = {
|
||||
knowledgeBase: 'Knowledge Base',
|
||||
selectDataSource: 'Select Source',
|
||||
localFile: 'Local File',
|
||||
uploadFileTypes: 'Upload PDF, TXT, DOCX and other format files',
|
||||
uploadFileTypes: 'Upload PDF, TXT, DOCX, IMAGE, MEDIA and other format files',
|
||||
webLink: 'Web Link',
|
||||
webLinkPlaceholder:'Please enter',
|
||||
webLinkDesc: 'Only static links are supported. If the uploaded data shows as empty, the link may not be readable. One per line, with a maximum of {{count}} links at a time',
|
||||
@@ -476,6 +478,7 @@ export const en = {
|
||||
readStaticWebPage: 'Read static web page content as dataset',
|
||||
customText: 'Custom Text',
|
||||
customContent: 'Custom Content',
|
||||
createContentError: 'Failed to create custom content',
|
||||
manuallyInputText: 'Manually input a text as dataset',
|
||||
importTemplate: 'Template Import',
|
||||
importBackup: 'Backup Import',
|
||||
|
||||
@@ -91,7 +91,7 @@ export const zh = {
|
||||
operation: '操作',
|
||||
selectDataSource: '选择来源',
|
||||
localFile: '本地文件',
|
||||
uploadFileTypes: '上传 PDF、TXT、DOCX 等格式的文件',
|
||||
uploadFileTypes: '上传 PDF、TXT、DOCX, IMAGE, MEDIA 等格式的文件',
|
||||
webLink: '网页链接',
|
||||
webLinkPlaceholder: '请输入',
|
||||
webLinkDesc: '仅支持静态链接。如果上传的数据显示为空,则该链接可能无法读取。每行一个,一次最多{{count}}个链接',
|
||||
@@ -99,6 +99,7 @@ export const zh = {
|
||||
readStaticWebPage: '读取静态网页内容作为数据集',
|
||||
customText: '自定义文本',
|
||||
customContent: '自定义内容',
|
||||
createContentError: '创建自定义文件失败',
|
||||
manuallyInputText: '手动输入一段文本作为数据集',
|
||||
openKnowledgeBase: '打开知识库',
|
||||
searchPlaceholder: '搜索',
|
||||
@@ -784,7 +785,9 @@ export const zh = {
|
||||
add: '添加',
|
||||
addOption: '添加选项',
|
||||
viewDetail: '查看详情',
|
||||
confirmRemoveFile: '确定要移除此文件吗?',
|
||||
deleteSuccess: '删除成功',
|
||||
deleteFailed: '删除失败',
|
||||
foldUp: '收起',
|
||||
expanded: '展开',
|
||||
clickUploadIcon: '点击上传图标',
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { useMemo,useRef, useState, useEffect } from 'react';
|
||||
import { Button, Flex, Radio, Steps, Modal, Input, Spin, message, Checkbox, Select} from 'antd';
|
||||
import { Button, Flex, Radio, Steps, Modal, Input, Spin, message, Checkbox, Select, Form} 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 type { UploadFile } from 'antd';
|
||||
import UploadFiles from '@/components/Upload/UploadFiles';
|
||||
import type { UploadRequestOption } from 'rc-upload/lib/interface';
|
||||
import { uploadFile, getDocumentList, parseDocument, updateDocument, deleteDocument } from '@/api/knowledgeBase';
|
||||
import { uploadFile, getDocumentList, parseDocument, updateDocument, deleteDocument, createDocumentAndUpload } from '@/api/knowledgeBase';
|
||||
import exitIcon from '@/assets/images/knowledgeBase/exit.png';
|
||||
|
||||
import SliderInput from '@/components/SliderInput';
|
||||
@@ -56,7 +57,10 @@ interface CreateDatasetLocationState {
|
||||
fileId?: string | string[];
|
||||
fileIds?: string | string[];
|
||||
}
|
||||
|
||||
interface ContentFormData {
|
||||
title: string;
|
||||
content: string;
|
||||
}
|
||||
const CreateDataset = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
@@ -75,7 +79,7 @@ const CreateDataset = () => {
|
||||
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);
|
||||
|
||||
@@ -98,12 +102,15 @@ const CreateDataset = () => {
|
||||
],
|
||||
[t],
|
||||
);
|
||||
|
||||
const handleNext = () => {
|
||||
// 存储每个文件的 AbortController,用于取消上传
|
||||
const abortControllersRef = useRef<Map<string, AbortController>>(new Map());
|
||||
const uploadRef = useRef<{ fileList: UploadFile[]; clearFiles: () => void }>(null);
|
||||
console.log('上传文件',uploadRef.current?.fileList.length)
|
||||
const handleNext = async () => {
|
||||
// 暂时隐藏第三步:调整步骤索引(0->1->2 对应 选择文件->参数设置->确认上传)
|
||||
let nextStep = current + 1;
|
||||
|
||||
if(nextStep === 1) {
|
||||
if(nextStep === 1 && source === 'local') {
|
||||
// 检查是否有文件已上传
|
||||
if (rechunkFileIds.length === 0) {
|
||||
// 如果没有文件,提示用户先上传文件
|
||||
@@ -113,6 +120,27 @@ const CreateDataset = () => {
|
||||
});
|
||||
return; // 不进入下一步
|
||||
}
|
||||
}else if(nextStep === 1 && source === 'text'){
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
// setLoading(true);
|
||||
|
||||
// TODO: 这里需要调用相应的API来保存内容
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// 从参数设置进入确认上传时的处理
|
||||
@@ -262,7 +290,7 @@ const CreateDataset = () => {
|
||||
|
||||
media.onerror = () => {
|
||||
URL.revokeObjectURL(url);
|
||||
reject(new Error('无法读取媒体文件'));
|
||||
reject(new Error(`${t('knowledgeBase.unableReadFile')}`));
|
||||
};
|
||||
|
||||
media.src = url;
|
||||
@@ -273,18 +301,24 @@ const CreateDataset = () => {
|
||||
const handleUpload = async (options: UploadRequestOption) => {
|
||||
const { file, onSuccess, onError, onProgress, filename = 'file' } = options;
|
||||
|
||||
// 创建 AbortController 用于取消上传
|
||||
const abortController = new AbortController();
|
||||
const fileUid = (file as any).uid;
|
||||
abortControllersRef.current.set(fileUid, abortController);
|
||||
|
||||
// 获取文件扩展名
|
||||
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);
|
||||
const fileSizeInMB = (file as File).size / (100 * 1024);
|
||||
|
||||
// 检查文件大小(256MB限制)
|
||||
if (fileSizeInMB > 256) {
|
||||
// 检查文件大小(50MB限制)
|
||||
if (fileSizeInMB > 100) {
|
||||
messageApi.error(`${t('knowledgeBase.sizeLimitError')}:${fileSizeInMB.toFixed(2)}MB`);
|
||||
onError?.(new Error(`${t('knowledgeBase.fileSizeExceeds')}`));
|
||||
abortControllersRef.current.delete(fileUid);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -294,11 +328,13 @@ const CreateDataset = () => {
|
||||
if (duration > 150) {
|
||||
messageApi.error(`${t('knowledgeBase.fileDurationLimitError')}:${Math.round(duration)}秒`);
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -315,6 +351,7 @@ const CreateDataset = () => {
|
||||
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);
|
||||
@@ -332,6 +369,14 @@ const CreateDataset = () => {
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
// 移除 AbortController
|
||||
abortControllersRef.current.delete(fileUid);
|
||||
|
||||
// 如果是用户主动取消,不显示错误信息
|
||||
if (error.name === 'AbortError' || error.code === 'ERR_CANCELED') {
|
||||
console.log('上传已取消:', (file as File).name);
|
||||
return;
|
||||
}
|
||||
onError?.(error as Error);
|
||||
});
|
||||
};
|
||||
@@ -419,23 +464,33 @@ const CreateDataset = () => {
|
||||
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);
|
||||
});
|
||||
// 删除已上传的文件
|
||||
const handleDeleteFile = async (fileId: string) => {
|
||||
try {
|
||||
await deleteDocument(fileId);
|
||||
// 删除成功,从 rechunkFileIds 中移除该 id
|
||||
setRechunkFileIds((prev) => prev.filter((id) => id !== fileId));
|
||||
console.log(`${t('common.deleteSuccess')}`);
|
||||
} catch (error) {
|
||||
messageApi.error(`${t('common.deleteFailed')}`);
|
||||
}
|
||||
}, []);
|
||||
};
|
||||
// 当从其他页面跳转过来且带有 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(() => {
|
||||
@@ -480,7 +535,40 @@ const CreateDataset = () => {
|
||||
{current === 0 && (
|
||||
<div className='rb:flex rb:w-full rb:mt-10'>
|
||||
{source && source === 'local' && (
|
||||
<UploadFiles isCanDrag={true} fileSize={50} multiple={true} maxCount={99} fileType={fileType} customRequest={handleUpload} />
|
||||
<UploadFiles
|
||||
ref={uploadRef}
|
||||
isCanDrag={true}
|
||||
fileSize={100}
|
||||
multiple={true}
|
||||
maxCount={99}
|
||||
fileType={fileType}
|
||||
customRequest={handleUpload}
|
||||
onChange={(fileList) => {
|
||||
console.log('文件列表变化:', fileList);
|
||||
}}
|
||||
onRemove={async (file) => {
|
||||
// 如果文件正在上传,取消上传
|
||||
const fileUid = file.uid;
|
||||
const abortController = abortControllersRef.current.get(fileUid);
|
||||
if (abortController) {
|
||||
abortController.abort();
|
||||
abortControllersRef.current.delete(fileUid);
|
||||
|
||||
}
|
||||
console.log('文件移除前:', uploadRef.current?.fileList);
|
||||
// 如果文件已经上传成功,删除服务器上的文件并从rechunkFileIds中移除对应的ID
|
||||
if (file.response?.id) {
|
||||
try {
|
||||
await deleteDocument(file.response.id);
|
||||
setRechunkFileIds(prev => prev.filter(id => id !== file.response.id));
|
||||
} catch (error) {
|
||||
console.error('删除文件失败:', error);
|
||||
messageApi.error('删除文件失败');
|
||||
}
|
||||
}
|
||||
|
||||
return true; // 允许移除文件
|
||||
}} />
|
||||
)}
|
||||
{source && source === 'link' && (
|
||||
<div className='rb:flex rb:w-full rb:flex-col rb:mt-10 rb:px-40'>
|
||||
@@ -500,15 +588,36 @@ const CreateDataset = () => {
|
||||
)}
|
||||
{source && source === 'text' && (
|
||||
<div className='rb:flex rb:w-full rb:flex-col rb:mt-10 rb:px-40'>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item
|
||||
name="title"
|
||||
label={t('knowledgeBase.title')}
|
||||
rules={[{ required: true, message: t('knowledgeBase.pleaseEnterTitle') }]}
|
||||
>
|
||||
<Input placeholder={t('knowledgeBase.pleaseEnterTitle')} />
|
||||
</Form.Item>
|
||||
|
||||
<div className='rb:text-sm rb:font-medium rb:text-gray-800 rb:mb-3'>
|
||||
<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')} />
|
||||
<TextArea rows={6} placeholder={t('knowledgeBase.webLinkPlaceholder')} /> */}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -700,7 +809,7 @@ const CreateDataset = () => {
|
||||
<Button
|
||||
type='primary'
|
||||
onClick={current === 2 ? handleStartUpload : handleNext}
|
||||
disabled={pollingLoading}
|
||||
disabled={pollingLoading || (current === 0 && rechunkFileIds.length === 0)}
|
||||
loading={pollingLoading}
|
||||
>
|
||||
{current === 2 ? t('knowledgeBase.startUploading') || 'Start Upload' : t('common.next') || 'Next'}
|
||||
|
||||
@@ -321,28 +321,27 @@ const Private: FC = () => {
|
||||
{
|
||||
key: '2',
|
||||
icon: <img src={textIcon} alt="text" style={{ width: 16, height: 16 }} />,
|
||||
label: (<span>{t('knowledgeBase.text')} {t('knowledgeBase.dataset')}</span>),
|
||||
label: (<span>{t('knowledgeBase.createA')} {t('knowledgeBase.dataset')}</span>),
|
||||
onClick: () => {
|
||||
datasetModalRef?.current?.handleOpen(knowledgeBase?.id,folder?.parent_id ?? knowledgeBase?.id ?? '');
|
||||
},
|
||||
},
|
||||
{
|
||||
key: '8',
|
||||
icon: <img src={blankIcon} alt="Custome Text" style={{ width: 16, height: 16 }} />,
|
||||
label: t('knowledgeBase.customTextDataset'),
|
||||
onClick: () => {
|
||||
createContentModalRef?.current?.handleOpen(knowledgeBase?.id ?? '', folder?.parent_id ?? knowledgeBase?.id ?? '');
|
||||
// handleCreate('folder'); // 传入 type: 'folder'
|
||||
},
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
icon: <img src={imageIcon} alt="image" style={{ width: 16, height: 16 }} />,
|
||||
label: t('knowledgeBase.mediaDataSet'),
|
||||
onClick: () => {
|
||||
createImageDataset?.current?.handleOpen(knowledgeBaseId || '', parentId || '')
|
||||
},
|
||||
},
|
||||
// {
|
||||
// key: '8',
|
||||
// icon: <img src={blankIcon} alt="Custome Text" style={{ width: 16, height: 16 }} />,
|
||||
// label: t('knowledgeBase.mediaDataSet'),
|
||||
// onClick: () => {
|
||||
// createContentModalRef?.current?.handleOpen(knowledgeBase?.id ?? '', folder?.parent_id ?? knowledgeBase?.id ?? '');
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// key: '3',
|
||||
// icon: <img src={imageIcon} alt="image" style={{ width: 16, height: 16 }} />,
|
||||
// label: t('knowledgeBase.imageDataSet'),
|
||||
// onClick: () => {
|
||||
// createImageDataset?.current?.handleOpen(knowledgeBaseId || '', parentId || '')
|
||||
// },
|
||||
// },
|
||||
// 暂时未实现
|
||||
// {
|
||||
// key: '4',
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* @Author: yujiangping
|
||||
* @Date: 2025-11-10 18:52:55
|
||||
* @LastEditors: yujiangping
|
||||
* @LastEditTime: 2025-11-24 11:23:33
|
||||
* @LastEditTime: 2025-12-29 16:09:13
|
||||
*/
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import type { RadioChangeEvent } from 'antd';
|
||||
@@ -51,10 +51,10 @@ const CreateDatasetModal = forwardRef<CreateDatasetModalRef,CreateDatasetModalRe
|
||||
// title: t('knowledgeBase.webLink'),
|
||||
// description: t('knowledgeBase.readStaticWebPage')
|
||||
// },
|
||||
// {
|
||||
// title: t('knowledgeBase.customText'),
|
||||
// description: t('knowledgeBase.manuallyInputText')
|
||||
// },
|
||||
{
|
||||
title: t('knowledgeBase.customText'),
|
||||
description: t('knowledgeBase.manuallyInputText')
|
||||
},
|
||||
]
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
const handleClose = () => {
|
||||
@@ -111,7 +111,7 @@ const CreateDatasetModal = forwardRef<CreateDatasetModalRef,CreateDatasetModalRe
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={t('knowledgeBase.createA') + ' ' + t('knowledgeBase.text') + ' ' + t('knowledgeBase.dataset')}
|
||||
title={t('knowledgeBase.createA') + ' ' + t('knowledgeBase.dataset')}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={t('common.create')}
|
||||
@@ -133,13 +133,13 @@ const CreateDatasetModal = forwardRef<CreateDatasetModalRef,CreateDatasetModalRe
|
||||
<span className='rb:text-base rb:font-medium rb:text-gray-800'>{items[1].title}</span>
|
||||
<span className='rb:text-xs rb:text-gray-500'>{items[1].description}</span>
|
||||
</Flex>
|
||||
</Radio>
|
||||
</Radio> */}
|
||||
<Radio value={2} style={getActiveRadioStyle(value === 2)} className='rb:w-full'>
|
||||
<Flex gap="small" align='start' justify='start' vertical>
|
||||
<span className='rb:text-base rb:font-medium rb:text-gray-800'>{items[2].title}</span>
|
||||
<span className='rb:text-xs rb:text-gray-500'>{items[2].description}</span>
|
||||
<span className='rb:text-base rb:font-medium rb:text-gray-800'>{items[1].title}</span>
|
||||
<span className='rb:text-xs rb:text-gray-500'>{items[1].description}</span>
|
||||
</Flex>
|
||||
</Radio> */}
|
||||
</Radio>
|
||||
</Radio.Group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { forwardRef, useImperativeHandle, useState, useRef } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { Form, Input, message } from 'antd';
|
||||
import { Form, message } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { UploadFile } from 'antd';
|
||||
import type { CreateSetModalRef, CreateSetMoealRefProps, UploadFileResponse } from '@/views/KnowledgeBase/types';
|
||||
import type { CreateSetModalRef, CreateSetMoealRefProps } from '@/views/KnowledgeBase/types';
|
||||
import type { UploadRequestOption } from 'rc-upload/lib/interface';
|
||||
import RbModal from '@/components/RbModal';
|
||||
import UploadFiles from '@/components/Upload/UploadFiles';
|
||||
import { uploadFile } from '@/api/knowledgeBase';
|
||||
import { uploadFile, deleteDocument } from '@/api/knowledgeBase';
|
||||
|
||||
interface ImageDatasetFormData {
|
||||
name: string;
|
||||
@@ -26,16 +26,26 @@ const CreateImageDataset = forwardRef<CreateSetModalRef, CreateSetMoealRefProps>
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [kbId, setKbId] = useState<string>('');
|
||||
const [parentId, setParentId] = useState<string>('');
|
||||
const [hasFiles, setHasFiles] = useState(false);
|
||||
const uploadRef = useRef<{ fileList: UploadFile[]; clearFiles: () => void }>(null);
|
||||
// 存储每个文件的 AbortController,用于取消上传
|
||||
const abortControllersRef = useRef<Map<string, AbortController>>(new Map());
|
||||
// const fileIds = [];
|
||||
|
||||
const handleClose = () => {
|
||||
// 取消所有正在进行的上传
|
||||
abortControllersRef.current.forEach((controller) => {
|
||||
controller.abort();
|
||||
});
|
||||
abortControllersRef.current.clear();
|
||||
|
||||
form.resetFields();
|
||||
uploadRef.current?.clearFiles();
|
||||
setLoading(false);
|
||||
setVisible(false);
|
||||
setKbId('');
|
||||
setParentId('');
|
||||
setHasFiles(false);
|
||||
};
|
||||
|
||||
const handleOpen = (kb_id: string, parent_id: string) => {
|
||||
@@ -43,6 +53,7 @@ const CreateImageDataset = forwardRef<CreateSetModalRef, CreateSetMoealRefProps>
|
||||
setParentId(parent_id);
|
||||
form.resetFields();
|
||||
uploadRef.current?.clearFiles();
|
||||
setHasFiles(false);
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
@@ -120,21 +131,38 @@ const CreateImageDataset = forwardRef<CreateSetModalRef, CreateSetMoealRefProps>
|
||||
media.src = url;
|
||||
});
|
||||
};
|
||||
// 删除已上传的文件
|
||||
const handleDeleteFile = async (fileId: string) => {
|
||||
try {
|
||||
await deleteDocument(fileId);
|
||||
console.log(`${t('common.deleteSuccess')}`);
|
||||
} catch (error) {
|
||||
messageApi.error(`${t('common.deleteFailed')}`);
|
||||
}
|
||||
};
|
||||
|
||||
// 上传文件
|
||||
const handleUpload = async (options: UploadRequestOption) => {
|
||||
const { file, onSuccess, onError, onProgress, filename = 'file' } = options;
|
||||
|
||||
// 创建 AbortController 用于取消上传
|
||||
const abortController = new AbortController();
|
||||
const fileUid = (file as any).uid;
|
||||
abortControllersRef.current.set(fileUid, abortController);
|
||||
|
||||
// 获取文件扩展名
|
||||
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);
|
||||
const fileSizeInMB = (file as File).size / (50 * 1024);
|
||||
|
||||
// 检查文件大小(256MB限制)
|
||||
if (fileSizeInMB > 256) {
|
||||
// 检查文件大小(50MB限制)
|
||||
if (fileSizeInMB > 50) {
|
||||
messageApi.error(`${t('knowledgeBase.sizeLimitError')}:${fileSizeInMB.toFixed(2)}MB`);
|
||||
onError?.(new Error(`${t('knowledgeBase.fileSizeExceeds')}`));
|
||||
abortControllersRef.current.delete(fileUid);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -144,11 +172,13 @@ const CreateImageDataset = forwardRef<CreateSetModalRef, CreateSetMoealRefProps>
|
||||
if (duration > 150) {
|
||||
messageApi.error(`${t('knowledgeBase.fileDurationLimitError')}:${Math.round(duration)}秒`);
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -162,36 +192,53 @@ const CreateImageDataset = forwardRef<CreateSetModalRef, CreateSetMoealRefProps>
|
||||
formData.append('parent_id', parentId);
|
||||
}
|
||||
|
||||
uploadFile(formData, {
|
||||
kb_id: kbId,
|
||||
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) {
|
||||
// 上传成功
|
||||
// fileIds.push(res.id)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
onError?.(error as Error);
|
||||
try {
|
||||
const res = await uploadFile(formData, {
|
||||
kb_id: kbId,
|
||||
parent_id: parentId,
|
||||
signal: abortController.signal,
|
||||
onUploadProgress: (event) => {
|
||||
if (!event.total) return;
|
||||
const percent = Math.round((event.loaded / event.total) * 100);
|
||||
onProgress?.({ percent }, file);
|
||||
},
|
||||
});
|
||||
|
||||
// 上传成功,移除 AbortController
|
||||
abortControllersRef.current.delete(fileUid);
|
||||
onSuccess?.(res, new XMLHttpRequest());
|
||||
|
||||
if (res?.id) {
|
||||
// 上传成功
|
||||
// fileIds.push(res.id)
|
||||
}
|
||||
} catch (error: any) {
|
||||
// 移除 AbortController
|
||||
abortControllersRef.current.delete(fileUid);
|
||||
|
||||
// 如果是用户主动取消,不显示错误信息
|
||||
if (error.name === 'AbortError' || error.code === 'ERR_CANCELED') {
|
||||
console.log('上传已取消:', (file as File).name);
|
||||
return;
|
||||
}
|
||||
|
||||
onError?.(error as Error);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{contextHolder}
|
||||
<RbModal
|
||||
title={`${t('knowledgeBase.createA')} ${t('knowledgeBase.imageDataSet')}`}
|
||||
title={`${t('knowledgeBase.createA')} ${t('knowledgeBase.mediaDataSet')}`}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={t('common.create')}
|
||||
onOk={handleSave}
|
||||
confirmLoading={loading}
|
||||
maskClosable={false}
|
||||
okButtonProps={{
|
||||
disabled: loading || !hasFiles
|
||||
}}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
{/* <Form.Item
|
||||
@@ -206,11 +253,31 @@ const CreateImageDataset = forwardRef<CreateSetModalRef, CreateSetMoealRefProps>
|
||||
<UploadFiles
|
||||
ref={uploadRef}
|
||||
isCanDrag={true}
|
||||
fileSize={100}
|
||||
fileSize={50}
|
||||
multiple={true}
|
||||
maxCount={99}
|
||||
fileType={['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'mp3', 'mp4', 'mov', 'wav']}
|
||||
customRequest={handleUpload}
|
||||
customRequest={handleUpload}
|
||||
onChange={(fileList) => {
|
||||
// 实时更新文件状态
|
||||
setHasFiles(fileList.length > 0);
|
||||
}}
|
||||
onRemove={async (file) => {
|
||||
// 如果文件正在上传,取消上传
|
||||
const fileUid = file.uid;
|
||||
const abortController = abortControllersRef.current.get(fileUid);
|
||||
if (abortController) {
|
||||
abortController.abort();
|
||||
abortControllersRef.current.delete(fileUid);
|
||||
}
|
||||
|
||||
// 如果文件已经上传成功,删除服务器上的文件
|
||||
if (file.response?.id) {
|
||||
await handleDeleteFile(file.response.id);
|
||||
}
|
||||
|
||||
return true; // 允许移除文件
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
@@ -7,6 +7,9 @@ import RbModal from '@/components/RbModal'
|
||||
const { TextArea } = Input;
|
||||
const { confirm } = Modal
|
||||
|
||||
// 全局模型数据常量
|
||||
let models: any = null;
|
||||
|
||||
const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
|
||||
refreshTable
|
||||
}, ref) => {
|
||||
@@ -46,20 +49,26 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
|
||||
};
|
||||
|
||||
const fetchModelLists = async (types: string[]) => {
|
||||
// 如果 types 中包含 'llm',也需要获取 'chat' 的数据
|
||||
const typesToFetch = types.includes('llm') ? [...types, 'chat'] : types;
|
||||
|
||||
const entries = await Promise.all(typesToFetch.map(async (tp) => {
|
||||
// 如果还没有获取过全部模型数据,则获取一次
|
||||
if (!models) {
|
||||
try {
|
||||
const res = await getModelList(tp === 'image2text' ? 'chat' : tp, { page: 1, pagesize: 100 });
|
||||
const options = (res?.items || []).map((m: any) => ({ label: m.name, value: m.id }));
|
||||
return [tp, options] as [string, { label: string; value: string }[]];
|
||||
} catch {
|
||||
return [tp, []] as [string, { label: string; value: string }[]];
|
||||
models = await getModelList({ page: 1, pagesize: 100 });
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch models:', error);
|
||||
models = { items: [] };
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// 从全部模型数据中过滤出需要的类型
|
||||
const typesToFetch = types.includes('llm') ? [...types, 'chat'] : types;
|
||||
const next: Record<string, { label: string; value: string }[]> = {};
|
||||
entries.forEach(([k, v]) => { next[k] = v; });
|
||||
|
||||
typesToFetch.forEach((tp) => {
|
||||
const targetType = tp === 'image2text' ? 'chat' : tp;
|
||||
const filteredModels = (models?.items || []).filter((m: any) => m.type === targetType);
|
||||
next[tp] = filteredModels.map((m: any) => ({ label: m.name, value: m.id }));
|
||||
});
|
||||
|
||||
setModelOptionsByType(next);
|
||||
};
|
||||
|
||||
@@ -157,7 +166,7 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
|
||||
console.log('Validation failed:', err)
|
||||
});
|
||||
}
|
||||
const handleChange = (value: string, tp: string) => {
|
||||
const handleChange = (_value: string, tp: string) => {
|
||||
// 只在编辑模式且类型为 embedding 时触发提示
|
||||
if (datasets?.id && tp.toLowerCase() === 'embedding') {
|
||||
const fieldKey = typeToFieldKey(tp);
|
||||
|
||||
Reference in New Issue
Block a user