Merge #64 into develop_web from feature/20251219_yjp

feat(knowledgeBase): add media dataset support and improve file handling

* feature/20251219_yjp: (1 commits)
  feat(knowledgeBase): add media dataset support and improve file handling

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/64
This commit is contained in:
vrhs@163.com
2025-12-25 20:18:24 +08:00
5 changed files with 115 additions and 35 deletions

View File

@@ -505,6 +505,7 @@ export const en = {
createImport: 'Create/Import', createImport: 'Create/Import',
textDataSet: 'Text Dataset', textDataSet: 'Text Dataset',
imageDataSet: 'Image Dataset', imageDataSet: 'Image Dataset',
mediaDataSet: 'Media Dataset',
blankDataset: 'Blank Dataset', blankDataset: 'Blank Dataset',
customTextDataset: 'Custom Text Dataset', customTextDataset: 'Custom Text Dataset',
text: 'Text', text: 'Text',
@@ -581,6 +582,7 @@ export const en = {
datasetName: 'Dataset Name', datasetName: 'Dataset Name',
pleaseEnterDatasetName: 'Please enter dataset name', pleaseEnterDatasetName: 'Please enter dataset name',
uploadImages: 'Upload Images', uploadImages: 'Upload Images',
uploadMedia: 'Upload Media files',
pleaseUploadImages: 'Please upload images', pleaseUploadImages: 'Please upload images',
embedding_id: 'Embedding', embedding_id: 'Embedding',
llm_id: 'LLM', llm_id: 'LLM',

View File

@@ -128,6 +128,7 @@ export const zh = {
createImport: '新建/导入', createImport: '新建/导入',
textDataSet: '文本数据集', textDataSet: '文本数据集',
imageDataSet: '图片数据集', imageDataSet: '图片数据集',
mediaDataSet: '媒体数据集',
blankDataset: '空白数据集', blankDataset: '空白数据集',
emptyDataSet: '空白数据集', emptyDataSet: '空白数据集',
customTextDataset: '自定义文本数据集', customTextDataset: '自定义文本数据集',
@@ -204,6 +205,7 @@ export const zh = {
datasetName: '数据集名称', datasetName: '数据集名称',
pleaseEnterDatasetName: '请输入数据集名称', pleaseEnterDatasetName: '请输入数据集名称',
uploadImages: '上传图片', uploadImages: '上传图片',
uploadMedia: '上传媒体文件',
pleaseUploadImages: '请上传图片', pleaseUploadImages: '请上传图片',
embedding_id: '嵌入模型', embedding_id: '嵌入模型',
llm_id: '大语言模型', llm_id: '大语言模型',

View File

@@ -53,8 +53,8 @@ interface CreateDatasetLocationState {
knowledgeBaseId?: string; knowledgeBaseId?: string;
parentId?: string; parentId?: string;
startStep?: StepKey; startStep?: StepKey;
fileId?: string; fileId?: string | string[];
fileIds?: string[]; fileIds?: string | string[];
} }
const CreateDataset = () => { const CreateDataset = () => {
@@ -67,7 +67,11 @@ const CreateDataset = () => {
const knowledgeBaseId = locationState.knowledgeBaseId || routeKnowledgeBaseId; const knowledgeBaseId = locationState.knowledgeBaseId || routeKnowledgeBaseId;
const parentId = locationState.parentId; const parentId = locationState.parentId;
const initialStepKey = locationState.startStep ?? 'selectFile'; const initialStepKey = locationState.startStep ?? 'selectFile';
const initialFileIds = locationState.fileIds ?? (locationState.fileId ? [locationState.fileId] : []); 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 [current, setCurrent] = useState<number>(stepIndexMap[initialStepKey]);
const tableRef = useRef<TableRef>(null); const tableRef = useRef<TableRef>(null);

View File

@@ -13,6 +13,7 @@ import folderIcon from '@/assets/images/knowledgeBase/folder.png';
import textIcon from '@/assets/images/knowledgeBase/text.png'; import textIcon from '@/assets/images/knowledgeBase/text.png';
import editIcon from '@/assets/images/knowledgeBase/edit.png'; import editIcon from '@/assets/images/knowledgeBase/edit.png';
import blankIcon from '@/assets/images/knowledgeBase/blankDocument.png'; import blankIcon from '@/assets/images/knowledgeBase/blankDocument.png';
import imageIcon from '@/assets/images/knowledgeBase/image.png'
import { getKnowledgeBaseDetail, deleteDocument, downloadFile, updateKnowledgeBase } from '@/api/knowledgeBase'; import { getKnowledgeBaseDetail, deleteDocument, downloadFile, updateKnowledgeBase } from '@/api/knowledgeBase';
import { import {
type CreateModalRef, type CreateModalRef,
@@ -327,15 +328,15 @@ const Private: FC = () => {
// handleCreate('folder'); // 传入 type: 'folder' // handleCreate('folder'); // 传入 type: 'folder'
}, },
}, },
// 暂时未实现 {
// { key: '3',
// key: '3', icon: <img src={imageIcon} alt="image" style={{ width: 16, height: 16 }} />,
// icon: <img src={imageIcon} alt="image" style={{ width: 16, height: 16 }} />, label: t('knowledgeBase.mediaDataSet'),
// label: t('knowledgeBase.imageDataSet'), onClick: () => {
// onClick: () => { createImageDataset?.current?.handleOpen(knowledgeBaseId || '', parentId || '')
// createImageDataset?.current?.handleOpen(knowledgeBaseId || '', parentId || '') },
// }, },
// }, // 暂时未实现
// { // {
// key: '4', // key: '4',
// icon: <img src={blankIcon} alt="blank" style={{ width: 16, height: 16 }} />, // icon: <img src={blankIcon} alt="blank" style={{ width: 16, height: 16 }} />,

View File

@@ -1,5 +1,7 @@
import { forwardRef, useImperativeHandle, useState, useRef } from 'react'; import { forwardRef, useImperativeHandle, useState, useRef } from 'react';
import { Form, Input } from 'antd'; import { useNavigate } from 'react-router-dom';
import { Form, Input, message } from 'antd';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import type { UploadFile } from 'antd'; import type { UploadFile } from 'antd';
import type { CreateSetModalRef, CreateSetMoealRefProps, UploadFileResponse } from '@/views/KnowledgeBase/types'; import type { CreateSetModalRef, CreateSetMoealRefProps, UploadFileResponse } from '@/views/KnowledgeBase/types';
@@ -16,12 +18,16 @@ interface ImageDatasetFormData {
const CreateImageDataset = forwardRef<CreateSetModalRef, CreateSetMoealRefProps>( const CreateImageDataset = forwardRef<CreateSetModalRef, CreateSetMoealRefProps>(
({ refreshTable }, ref) => { ({ refreshTable }, ref) => {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate();
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const [messageApi, contextHolder] = message.useMessage();
const [form] = Form.useForm<ImageDatasetFormData>(); const [form] = Form.useForm<ImageDatasetFormData>();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [kbId, setKbId] = useState<string>(''); const [kbId, setKbId] = useState<string>('');
const [parentId, setParentId] = useState<string>(''); const [parentId, setParentId] = useState<string>('');
const uploadRef = useRef<{ fileList: UploadFile[]; clearFiles: () => void }>(null); const uploadRef = useRef<{ fileList: UploadFile[]; clearFiles: () => void }>(null);
// const fileIds = [];
const handleClose = () => { const handleClose = () => {
form.resetFields(); form.resetFields();
@@ -50,22 +56,23 @@ const CreateImageDataset = forwardRef<CreateSetModalRef, CreateSetMoealRefProps>
if (fileList.length === 0) { if (fileList.length === 0) {
throw new Error(t('knowledgeBase.pleaseUploadImages')); throw new Error(t('knowledgeBase.pleaseUploadImages'));
} }
const ids = fileList.map((file) => file.response?.id);
// 上传所有图片 handleChunking(kbId, parentId, ids)
const uploadPromises = fileList.map(async (file) => { // // 上传所有图片
if (file.originFileObj) { // const uploadPromises = fileList.map(async (file) => {
const formData = new FormData(); // if (file.originFileObj) {
formData.append('file', file.originFileObj); // const formData = new FormData();
// formData.append('file', file.originFileObj);
return uploadFile(formData, { // return uploadFile(formData, {
kb_id: kbId, // kb_id: kbId,
parent_id: parentId, // parent_id: parentId,
}); // });
} // }
return null; // return null;
}); // });
await Promise.all(uploadPromises); // await Promise.all(uploadPromises);
if (refreshTable) { if (refreshTable) {
await refreshTable(); await refreshTable();
@@ -78,13 +85,73 @@ const CreateImageDataset = forwardRef<CreateSetModalRef, CreateSetMoealRefProps>
setLoading(false); setLoading(false);
} }
}; };
const handleChunking = (kb_id: string, parent_id: string, file_id: Array<string>) => {
if (!kb_id) return;
const targetFileId = file_id
navigate(`/knowledge-base/${kb_id}/create-dataset`, {
state: {
source: 'local',
knowledgeBaseId: kb_id,
parentId: parent_id ?? kb_id,
startStep: 'parameterSettings',
fileId: targetFileId,
},
});
}
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
handleOpen, handleOpen,
})); }));
// 检查媒体文件时长的辅助函数
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('无法读取媒体文件'));
};
media.src = url;
});
};
// 上传文件 // 上传文件
const handleUpload = (options: UploadRequestOption) => { const handleUpload = async (options: UploadRequestOption) => {
const { file, onSuccess, onError, onProgress, filename = 'file' } = options; 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(); const formData = new FormData();
formData.append(filename, file as File); formData.append(filename, file as File);
@@ -108,6 +175,7 @@ const CreateImageDataset = forwardRef<CreateSetModalRef, CreateSetMoealRefProps>
onSuccess?.(res, new XMLHttpRequest()); onSuccess?.(res, new XMLHttpRequest());
if (res?.id) { if (res?.id) {
// 上传成功 // 上传成功
// fileIds.push(res.id)
} }
}) })
.catch((error) => { .catch((error) => {
@@ -115,6 +183,8 @@ const CreateImageDataset = forwardRef<CreateSetModalRef, CreateSetMoealRefProps>
}); });
}; };
return ( return (
<>
{contextHolder}
<RbModal <RbModal
title={`${t('knowledgeBase.createA')} ${t('knowledgeBase.imageDataSet')}`} title={`${t('knowledgeBase.createA')} ${t('knowledgeBase.imageDataSet')}`}
open={visible} open={visible}
@@ -124,27 +194,28 @@ const CreateImageDataset = forwardRef<CreateSetModalRef, CreateSetMoealRefProps>
confirmLoading={loading} confirmLoading={loading}
> >
<Form form={form} layout="vertical"> <Form form={form} layout="vertical">
<Form.Item {/* <Form.Item
name="name" name="name"
label={t('knowledgeBase.datasetName')} label={t('knowledgeBase.datasetName')}
rules={[{ required: true, message: t('knowledgeBase.pleaseEnterDatasetName') }]} rules={[{ required: true, message: t('knowledgeBase.pleaseEnterDatasetName') }]}
> >
<Input placeholder={t('knowledgeBase.pleaseEnterDatasetName')} /> <Input placeholder={t('knowledgeBase.pleaseEnterDatasetName')} />
</Form.Item> </Form.Item> */}
<Form.Item label={t('knowledgeBase.uploadImages')}> <Form.Item label={t('knowledgeBase.uploadMedia')}>
<UploadFiles <UploadFiles
ref={uploadRef}
isCanDrag={true} isCanDrag={true}
fileSize={50} fileSize={100}
multiple={true} multiple={true}
maxCount={99} maxCount={99}
fileType={['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']} fileType={['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'mp3', 'mp4', 'mov', 'wav']}
customRequest={handleUpload} customRequest={handleUpload}
/> />
</Form.Item> </Form.Item>
</Form> </Form>
</RbModal> </RbModal>
); </>);
} }
); );