Merge #77 into develop_web from feature/20251219_yjp

feat(knowledgeBase): enhance file upload and dataset creation with abort support and improved UX

* feature/20251219_yjp: (1 commits)
  feat(knowledgeBase): enhance file upload and dataset creation with abort support and improved UX

Signed-off-by: vrhs@163.com <accounts_660b6454a0eb398d3f8d2c76@mail.teambition.com>
Merged-by: vrhs@163.com <accounts_660b6454a0eb398d3f8d2c76@mail.teambition.com>

CR-link: https://codeup.aliyun.com/redbearai/python/redbear-mem-open/change/77
This commit is contained in:
vrhs@163.com
2025-12-29 19:14:36 +08:00
9 changed files with 311 additions and 107 deletions

View File

@@ -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;
};

View File

@@ -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>

View File

@@ -296,7 +296,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',
@@ -473,7 +475,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',
@@ -481,6 +483,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',

View File

@@ -94,7 +94,7 @@ export const zh = {
operation: '操作',
selectDataSource: '选择来源',
localFile: '本地文件',
uploadFileTypes: '上传 PDF、TXT、DOCX 等格式的文件',
uploadFileTypes: '上传 PDF、TXT、DOCX, IMAGE, MEDIA 等格式的文件',
webLink: '网页链接',
webLinkPlaceholder: '请输入',
webLinkDesc: '仅支持静态链接。如果上传的数据显示为空,则该链接可能无法读取。每行一个,一次最多{{count}}个链接',
@@ -102,6 +102,7 @@ export const zh = {
readStaticWebPage: '读取静态网页内容作为数据集',
customText: '自定义文本',
customContent: '自定义内容',
createContentError: '创建自定义文件失败',
manuallyInputText: '手动输入一段文本作为数据集',
openKnowledgeBase: '打开知识库',
searchPlaceholder: '搜索',
@@ -796,7 +797,9 @@ export const zh = {
add: '添加',
addOption: '添加选项',
viewDetail: '查看详情',
confirmRemoveFile: '确定要移除此文件吗?',
deleteSuccess: '删除成功',
deleteFailed: '删除失败',
foldUp: '收起',
expanded: '展开',
clickUploadIcon: '点击上传图标',

View File

@@ -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'}

View File

@@ -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',

View File

@@ -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>

View File

@@ -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>

View File

@@ -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);