feat(web): knowledge base
This commit is contained in:
@@ -154,6 +154,19 @@ export const uploadFile = async (data: FormData, options?: UploadFileOptions) =>
|
|||||||
});
|
});
|
||||||
return response as UploadFileResponse;
|
return response as UploadFileResponse;
|
||||||
};
|
};
|
||||||
|
// 上传 QA 文件
|
||||||
|
export const uploadQaFile = async (data: FormData, options?: UploadFileOptions) => {
|
||||||
|
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(`/chunks/${kb_id}/import_qa`, data, {
|
||||||
|
params,
|
||||||
|
onUploadProgress,
|
||||||
|
signal,
|
||||||
|
});
|
||||||
|
return response as UploadFileResponse;
|
||||||
|
};
|
||||||
|
|
||||||
// 下载文件
|
// 下载文件
|
||||||
export const downloadFile = async (fileId: string, fileName?: string) => {
|
export const downloadFile = async (fileId: string, fileName?: string) => {
|
||||||
@@ -293,7 +306,10 @@ export const updateDocumentChunk = async (kb_id:string, document_id:string, doc_
|
|||||||
const response = await request.put(`${apiPrefix}/chunks/${kb_id}/${document_id}/${doc_id}`, data);
|
const response = await request.put(`${apiPrefix}/chunks/${kb_id}/${document_id}/${doc_id}`, data);
|
||||||
return response as any;
|
return response as any;
|
||||||
};
|
};
|
||||||
|
export const deleteDocumentChunk = async (kb_id: string, document_id: string, doc_id: string) => {
|
||||||
|
const response = await request.delete(`${apiPrefix}/chunks/${kb_id}/${document_id}/${doc_id}`);
|
||||||
|
return response as any;
|
||||||
|
};
|
||||||
// 文档块儿创建
|
// 文档块儿创建
|
||||||
export const createDocumentChunk = async (kb_id:string, document_id:string, data: any) => {
|
export const createDocumentChunk = async (kb_id:string, document_id:string, data: any) => {
|
||||||
const response = await request.post(`${apiPrefix}/chunks/${kb_id}/${document_id}/chunk`, data);
|
const response = await request.post(`${apiPrefix}/chunks/${kb_id}/${document_id}/chunk`, data);
|
||||||
|
|||||||
@@ -709,6 +709,8 @@ export const en = {
|
|||||||
localFile: 'Local File',
|
localFile: 'Local File',
|
||||||
uploadFileTypes: 'Upload PDF, TXT, DOCX, IMAGE, MEDIA and other format files',
|
uploadFileTypes: 'Upload PDF, TXT, DOCX, IMAGE, MEDIA and other format files',
|
||||||
webLink: 'Web Link',
|
webLink: 'Web Link',
|
||||||
|
csvFile: 'Tabular Dataset',
|
||||||
|
csvUploadFileTypes: 'Upload files in CSV format',
|
||||||
webLinkPlaceholder:'Please enter',
|
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',
|
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',
|
||||||
selectorTutorial: 'Selector Usage Tutorial',
|
selectorTutorial: 'Selector Usage Tutorial',
|
||||||
|
|||||||
@@ -194,6 +194,8 @@ export const zh = {
|
|||||||
localFile: '本地文件',
|
localFile: '本地文件',
|
||||||
uploadFileTypes: '上传 PDF、 TXT、 DOCX、 IMAGE、 MEDIA 等格式的文件',
|
uploadFileTypes: '上传 PDF、 TXT、 DOCX、 IMAGE、 MEDIA 等格式的文件',
|
||||||
webLink: '网页链接',
|
webLink: '网页链接',
|
||||||
|
csvFile: '表格数据集',
|
||||||
|
csvUploadFileTypes: '上传 CSV 格式的文件',
|
||||||
webLinkPlaceholder: '请输入',
|
webLinkPlaceholder: '请输入',
|
||||||
webLinkDesc: '仅支持静态链接。如果上传的数据显示为空,则该链接可能无法读取。每行一个,一次最多{{count}}个链接',
|
webLinkDesc: '仅支持静态链接。如果上传的数据显示为空,则该链接可能无法读取。每行一个,一次最多{{count}}个链接',
|
||||||
selectorTutorial: '选择器使用教程',
|
selectorTutorial: '选择器使用教程',
|
||||||
@@ -283,6 +285,7 @@ export const zh = {
|
|||||||
qaExtract: '问答对提取',
|
qaExtract: '问答对提取',
|
||||||
default: '默认',
|
default: '默认',
|
||||||
customize: '自定义',
|
customize: '自定义',
|
||||||
|
qaPrompt: 'QA 拆分引导词',
|
||||||
defaultSettings: '使用系统默认的参数和规则',
|
defaultSettings: '使用系统默认的参数和规则',
|
||||||
customSettings: '自定义设置数据处理规则',
|
customSettings: '自定义设置数据处理规则',
|
||||||
fileName: '文件名称',
|
fileName: '文件名称',
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import type { ColumnsType } from 'antd/es/table';
|
|||||||
import type { UploadFile } from 'antd';
|
import type { UploadFile } from 'antd';
|
||||||
import UploadFiles from '@/components/Upload/UploadFiles';
|
import UploadFiles from '@/components/Upload/UploadFiles';
|
||||||
import type { UploadRequestOption } from 'rc-upload/lib/interface';
|
import type { UploadRequestOption } from 'rc-upload/lib/interface';
|
||||||
import { uploadFile, getDocumentList, parseDocument, updateDocument, deleteDocument, createDocumentAndUpload } from '@/api/knowledgeBase';
|
import { uploadFile, uploadQaFile, getDocumentList, parseDocument, updateDocument, deleteDocument, createDocumentAndUpload } from '@/api/knowledgeBase';
|
||||||
import exitIcon from '@/assets/images/knowledgeBase/exit.png';
|
import exitIcon from '@/assets/images/knowledgeBase/exit.png';
|
||||||
|
|
||||||
import SliderInput from '@/components/SliderInput';
|
import SliderInput from '@/components/SliderInput';
|
||||||
@@ -38,7 +38,7 @@ const { TextArea } = Input;
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
type SourceType = 'local' | 'link' | 'text';
|
type SourceType = 'local' | 'link' | 'text' | 'csv';
|
||||||
type ProcessingMethod = 'directBlock' | 'qaExtract';
|
type ProcessingMethod = 'directBlock' | 'qaExtract';
|
||||||
type ParameterSettings = 'defaultSettings' | 'customSettings';
|
type ParameterSettings = 'defaultSettings' | 'customSettings';
|
||||||
const stepKeys = ['selectFile', 'parameterSettings', 'dataPreview', 'confirmUpload'] as const;
|
const stepKeys = ['selectFile', 'parameterSettings', 'dataPreview', 'confirmUpload'] as const;
|
||||||
@@ -63,6 +63,8 @@ interface ContentFormData {
|
|||||||
title: string;
|
title: string;
|
||||||
content: 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 CreateDataset = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -91,11 +93,12 @@ const CreateDataset = () => {
|
|||||||
const pollingTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
const pollingTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||||
const [delimiter, setDelimiter] = useState<string | undefined>(undefined);
|
const [delimiter, setDelimiter] = useState<string | undefined>(undefined);
|
||||||
const [blockSize, setBlockSize] = useState<number>(130);
|
const [blockSize, setBlockSize] = useState<number>(130);
|
||||||
|
const [qaPrompt, setQaPrompt] = useState<string | undefined>()
|
||||||
|
console.log('qaPrompt', qaPrompt)
|
||||||
const [processingMethod, setProcessingMethod] = useState<ProcessingMethod>('directBlock');
|
const [processingMethod, setProcessingMethod] = useState<ProcessingMethod>('directBlock');
|
||||||
const [parameterSettings, setParameterSettings] = useState<ParameterSettings>('defaultSettings');
|
const [parameterSettings, setParameterSettings] = useState<ParameterSettings>('defaultSettings');
|
||||||
const [pdfEnhancementEnabled, setPdfEnhancementEnabled] = useState<boolean>(true);
|
const [pdfEnhancementEnabled, setPdfEnhancementEnabled] = useState<boolean>(true);
|
||||||
const [pdfEnhancementMethod, setPdfEnhancementMethod] = useState<string>('mineru');
|
const [pdfEnhancementMethod, setPdfEnhancementMethod] = useState<string>('mineru');
|
||||||
const fileType = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'csv', 'md', 'htm', 'html', 'json', 'ppt', 'pptx', 'txt','png','jpg','mp3','mp4','mov','wav']
|
|
||||||
const steps = useMemo(
|
const steps = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{ title: t('knowledgeBase.selectFile') },
|
{ title: t('knowledgeBase.selectFile') },
|
||||||
@@ -112,8 +115,11 @@ const CreateDataset = () => {
|
|||||||
const handleNext = async () => {
|
const handleNext = async () => {
|
||||||
// Temporarily hide step 3: adjust step index (0->1->2 corresponds to select file->parameter settings->confirm upload)
|
// Temporarily hide step 3: adjust step index (0->1->2 corresponds to select file->parameter settings->confirm upload)
|
||||||
let nextStep = current + 1;
|
let nextStep = current + 1;
|
||||||
|
if (current === 0 && source === 'csv') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if(nextStep === 1 && source === 'local') {
|
if((nextStep === 1 && source === 'local') || (nextStep === 2 && source === 'csv')) {
|
||||||
// Check if files have been uploaded
|
// Check if files have been uploaded
|
||||||
if (rechunkFileIds.length === 0) {
|
if (rechunkFileIds.length === 0) {
|
||||||
// If no files, prompt user to upload first
|
// If no files, prompt user to upload first
|
||||||
@@ -159,6 +165,7 @@ const CreateDataset = () => {
|
|||||||
delimiter: delimiter,
|
delimiter: delimiter,
|
||||||
chunk_token_num: blockSize,
|
chunk_token_num: blockSize,
|
||||||
auto_questions: processingMethod === 'directBlock' ? 0 : 1,
|
auto_questions: processingMethod === 'directBlock' ? 0 : 1,
|
||||||
|
qa_prompt: qaPrompt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
updateDocument(id, params)
|
updateDocument(id, params)
|
||||||
@@ -378,40 +385,67 @@ const CreateDataset = () => {
|
|||||||
formData.append('parent_id', parentId);
|
formData.append('parent_id', parentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
uploadFile(formData, {
|
if (source === 'csv') {
|
||||||
kb_id: knowledgeBaseId,
|
uploadQaFile(formData, {
|
||||||
parent_id: parentId,
|
kb_id: knowledgeBaseId,
|
||||||
signal: abortController.signal,
|
parent_id: parentId,
|
||||||
onUploadProgress: (event) => {
|
signal: abortController.signal,
|
||||||
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) => {
|
.then((res: UploadFileResponse) => {
|
||||||
// Remove AbortController
|
// Upload successful, remove AbortController
|
||||||
abortControllersRef.current.delete(fileUid);
|
abortControllersRef.current.delete(fileUid);
|
||||||
|
|
||||||
// If user actively cancelled, don't show error message
|
onSuccess?.(res, new XMLHttpRequest());
|
||||||
if (error.name === 'AbortError' || error.code === 'ERR_CANCELED') {
|
messageApi.success(t('knowledgeBase.uploadSuccess'))
|
||||||
console.log('Upload cancelled:', (file as File).name);
|
handleBack()
|
||||||
return;
|
})
|
||||||
}
|
.catch((error) => {
|
||||||
onError?.(error as 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -557,21 +591,21 @@ const CreateDataset = () => {
|
|||||||
<img src={exitIcon} alt='exit' className='rb:w-4 rb:h-4' />
|
<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>
|
<span className='rb:text-gray-500 rb:text-sm'>{t('common.exit')}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className='rb:px-24 rb:py-5 rb:bg-white rb:rounded-xl'>
|
{source !== 'csv' && <div className='rb:px-24 rb:py-5 rb:bg-white rb:rounded-xl'>
|
||||||
<Steps current={current} items={steps} className="custom-steps" />
|
<Steps current={current} items={steps} className="custom-steps" />
|
||||||
</div>
|
</div> }
|
||||||
<div className='rb:bg-white rb:rounded-xl rb:flex-1 rb:mt-3'>
|
<div className='rb:bg-white rb:rounded-xl rb:flex-1 rb:mt-3'>
|
||||||
|
|
||||||
{current === 0 && (
|
{current === 0 && (
|
||||||
<div className='rb:flex rb:w-full rb:p-6'>
|
<div className='rb:flex rb:w-full rb:p-6'>
|
||||||
{source && source === 'local' && (
|
{source && (source === 'local' || source === 'csv') && (
|
||||||
<UploadFiles
|
<UploadFiles
|
||||||
ref={uploadRef}
|
ref={uploadRef}
|
||||||
isCanDrag={true}
|
isCanDrag={true}
|
||||||
fileSize={100}
|
fileSize={100}
|
||||||
multiple={true}
|
multiple={source !== 'csv'}
|
||||||
maxCount={99}
|
maxCount={source === 'csv' ? 1 : 99}
|
||||||
fileType={fileType}
|
fileType={source === 'csv' ? csvFileType : fileType}
|
||||||
customRequest={handleUpload}
|
customRequest={handleUpload}
|
||||||
onChange={(fileList) => {
|
onChange={(fileList) => {
|
||||||
console.log('File list changed:', fileList);
|
console.log('File list changed:', fileList);
|
||||||
@@ -765,18 +799,23 @@ const CreateDataset = () => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
</Radio>
|
</Radio>
|
||||||
</Radio.Group>
|
</Radio.Group>
|
||||||
{parameterSettings === 'customSettings' && (
|
{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 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>
|
||||||
<div className='rb:w-full rb:text-[#5B6167] rb:leading-5 rb:mb-2'>
|
<div className='rb:w-full rb:text-[#5B6167] rb:leading-5 rb:mb-2'>
|
||||||
{t('knowledgeBase.delimiter')}
|
{t('knowledgeBase.delimiter')}
|
||||||
</div>
|
</div>
|
||||||
<DelimiterSelector value={delimiter} onChange={setDelimiter} />
|
<DelimiterSelector value={delimiter} onChange={setDelimiter} />
|
||||||
</div>
|
</div>
|
||||||
<SliderInput label={t('knowledgeBase.suggestedBlockSize')} max={1024} min={1} step={1} value={blockSize} onChange={handleChange} />
|
<SliderInput label={t('knowledgeBase.suggestedBlockSize')} max={1024} min={1} step={1} value={blockSize} onChange={handleChange} />
|
||||||
</div>
|
</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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -853,7 +892,7 @@ const CreateDataset = () => {
|
|||||||
{t('common.previous') || 'Prev'}
|
{t('common.previous') || 'Prev'}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button
|
{source !== 'csv' && <Button
|
||||||
type='primary'
|
type='primary'
|
||||||
onClick={current === 2 ? handleStartUpload : handleNext}
|
onClick={current === 2 ? handleStartUpload : handleNext}
|
||||||
disabled={
|
disabled={
|
||||||
@@ -863,7 +902,7 @@ const CreateDataset = () => {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{current === 2 ? t('knowledgeBase.startUploading') || 'Start Upload' : t('common.next') || 'Next'}
|
{current === 2 ? t('knowledgeBase.startUploading') || 'Start Upload' : t('common.next') || 'Next'}
|
||||||
</Button>
|
</Button>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { useNavigate, useParams, useLocation, useSearchParams } from 'react-rout
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useBreadcrumbManager, type BreadcrumbPath } from '@/hooks/useBreadcrumbManager';
|
import { useBreadcrumbManager, type BreadcrumbPath } from '@/hooks/useBreadcrumbManager';
|
||||||
import { Button, Spin, message, Switch, App } from 'antd';
|
import { Button, Spin, message, Switch, App } from 'antd';
|
||||||
import { getDocumentDetail, getDocumentChunkList, downloadFile, updateDocument, updateDocumentChunk, createDocumentChunk, getFileUrl } from '@/api/knowledgeBase';
|
import { getDocumentDetail, getDocumentChunkList, downloadFile, updateDocument, updateDocumentChunk, createDocumentChunk } from '@/api/knowledgeBase';
|
||||||
import type { KnowledgeBaseDocumentData, RecallTestData } from '@/views/KnowledgeBase/types';
|
import type { KnowledgeBaseDocumentData, RecallTestData } from '@/views/KnowledgeBase/types';
|
||||||
import { formatDateTime } from '@/utils/format';
|
import { formatDateTime } from '@/utils/format';
|
||||||
import InfoPanel, { type InfoItem } from '../components/InfoPanel';
|
import InfoPanel, { type InfoItem } from '../components/InfoPanel';
|
||||||
@@ -20,7 +20,6 @@ import SearchInput from '@/components/SearchInput';
|
|||||||
import DocumentPreview from '@/components/DocumentPreview';
|
import DocumentPreview from '@/components/DocumentPreview';
|
||||||
import InsertModal, { type InsertModalRef } from '../components/InsertModal';
|
import InsertModal, { type InsertModalRef } from '../components/InsertModal';
|
||||||
import exitIcon from '@/assets/images/knowledgeBase/exit.png';
|
import exitIcon from '@/assets/images/knowledgeBase/exit.png';
|
||||||
const imagePath = 'https://devapi.mem.redbearai.com'
|
|
||||||
import copy from 'copy-to-clipboard'
|
import copy from 'copy-to-clipboard'
|
||||||
const DocumentDetails: FC = () => {
|
const DocumentDetails: FC = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -228,6 +227,11 @@ const DocumentDetails: FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const refreshChunks = () => {
|
||||||
|
let nextPage = 1;
|
||||||
|
setPage(nextPage);
|
||||||
|
ChunkList(nextPage);
|
||||||
|
}
|
||||||
const loadMoreChunks = () => {
|
const loadMoreChunks = () => {
|
||||||
const nextPage = page + 1;
|
const nextPage = page + 1;
|
||||||
setPage(nextPage);
|
setPage(nextPage);
|
||||||
@@ -363,8 +367,8 @@ const DocumentDetails: FC = () => {
|
|||||||
fileName={document?.file_name}
|
fileName={document?.file_name}
|
||||||
fileExt={document?.file_ext}
|
fileExt={document?.file_ext}
|
||||||
height="calc(100% - 40px)"
|
height="calc(100% - 40px)"
|
||||||
mode="google"
|
// mode="google"
|
||||||
showModeSwitch={true}
|
// showModeSwitch={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -425,7 +429,7 @@ const DocumentDetails: FC = () => {
|
|||||||
{t('knowledgeBase.chunkList') || '分块列表'}
|
{t('knowledgeBase.chunkList') || '分块列表'}
|
||||||
</h2>
|
</h2>
|
||||||
<RecallTestResult
|
<RecallTestResult
|
||||||
|
refresh={refreshChunks}
|
||||||
data={chunkList}
|
data={chunkList}
|
||||||
showEmpty={false}
|
showEmpty={false}
|
||||||
hasMore={hasMore}
|
hasMore={hasMore}
|
||||||
|
|||||||
@@ -55,6 +55,10 @@ const CreateDatasetModal = forwardRef<CreateDatasetModalRef,CreateDatasetModalRe
|
|||||||
title: t('knowledgeBase.customText'),
|
title: t('knowledgeBase.customText'),
|
||||||
description: t('knowledgeBase.manuallyInputText')
|
description: t('knowledgeBase.manuallyInputText')
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: t('knowledgeBase.csvFile'),
|
||||||
|
description: t('knowledgeBase.csvUploadFileTypes')
|
||||||
|
},
|
||||||
]
|
]
|
||||||
// 封装取消方法,添加关闭弹窗逻辑
|
// 封装取消方法,添加关闭弹窗逻辑
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
@@ -86,7 +90,7 @@ const CreateDatasetModal = forwardRef<CreateDatasetModalRef,CreateDatasetModalRe
|
|||||||
// description: selected.description,
|
// description: selected.description,
|
||||||
// });
|
// });
|
||||||
// 跳转到创建数据集页面并携带来源参数
|
// 跳转到创建数据集页面并携带来源参数
|
||||||
const source = value === 0 ? 'local' : value === 1 ? 'link' : 'text';
|
const source = value === 3 ? 'csv' : value === 0 ? 'local' : value === 1 ? 'link' : 'text';
|
||||||
if (knowledgeBaseId) {
|
if (knowledgeBaseId) {
|
||||||
navigate(`/knowledge-base/${knowledgeBaseId}/create-dataset`,{
|
navigate(`/knowledge-base/${knowledgeBaseId}/create-dataset`,{
|
||||||
state: {
|
state: {
|
||||||
@@ -139,6 +143,12 @@ 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-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>
|
<span className='rb:text-xs rb:text-gray-500'>{items[1].description}</span>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
</Radio>
|
||||||
|
<Radio value={3} style={getActiveRadioStyle(value === 3)} 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>
|
||||||
|
</Flex>
|
||||||
</Radio>
|
</Radio>
|
||||||
</Radio.Group>
|
</Radio.Group>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,20 +7,22 @@
|
|||||||
* @LastEditTime: 2025-12-22 13:47:53
|
* @LastEditTime: 2025-12-22 13:47:53
|
||||||
*/
|
*/
|
||||||
import { FileOutlined, FieldTimeOutlined, EditOutlined } from '@ant-design/icons';
|
import { FileOutlined, FieldTimeOutlined, EditOutlined } from '@ant-design/icons';
|
||||||
import { Skeleton, Flex, Space } from 'antd';
|
import { Skeleton, Flex, Space, App } from 'antd';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import type { RecallTestData } from '@/views/KnowledgeBase/types';
|
import type { RecallTestData } from '@/views/KnowledgeBase/types';
|
||||||
import { NoData } from './noData';
|
import { NoData } from './noData';
|
||||||
import { formatDateTime } from '@/utils/format';
|
import { formatDateTime } from '@/utils/format';
|
||||||
import InfiniteScroll from 'react-infinite-scroll-component';
|
import InfiniteScroll from 'react-infinite-scroll-component';
|
||||||
import RbMarkdown from '@/components/Markdown';
|
import RbMarkdown from '@/components/Markdown';
|
||||||
import { useMemo } from 'react';
|
import { useMemo, type MouseEvent } from 'react';
|
||||||
|
import { deleteDocumentChunk } from '@/api/knowledgeBase'
|
||||||
|
|
||||||
interface RecallTestResultProps {
|
interface RecallTestResultProps {
|
||||||
data: RecallTestData[];
|
data: RecallTestData[];
|
||||||
showEmpty?: boolean;
|
showEmpty?: boolean;
|
||||||
hasMore?: boolean;
|
hasMore?: boolean;
|
||||||
loadMore?: () => void;
|
loadMore?: () => void;
|
||||||
|
refresh?: () => void;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
scrollableTarget?: string;
|
scrollableTarget?: string;
|
||||||
editable?: boolean; // Whether editable
|
editable?: boolean; // Whether editable
|
||||||
@@ -34,6 +36,7 @@ const RecallTestResult = ({
|
|||||||
showEmpty = true,
|
showEmpty = true,
|
||||||
hasMore = false,
|
hasMore = false,
|
||||||
loadMore,
|
loadMore,
|
||||||
|
refresh,
|
||||||
loading = false,
|
loading = false,
|
||||||
scrollableTarget,
|
scrollableTarget,
|
||||||
editable = false,
|
editable = false,
|
||||||
@@ -42,6 +45,7 @@ const RecallTestResult = ({
|
|||||||
handleCopy,
|
handleCopy,
|
||||||
}: RecallTestResultProps) => {
|
}: RecallTestResultProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { modal, message } = App.useApp()
|
||||||
console.log('chunk data', data)
|
console.log('chunk data', data)
|
||||||
|
|
||||||
// Parse QA format content
|
// Parse QA format content
|
||||||
@@ -133,6 +137,24 @@ const RecallTestResult = ({
|
|||||||
return 'rb:text-[#FF5D34]';
|
return 'rb:text-[#FF5D34]';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const handleDelete = (e: MouseEvent, item: RecallTestData) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
modal.confirm({
|
||||||
|
title: t('common.confirmDeleteDesc', { name: `chunk_${item.metadata?.sort_id}` }),
|
||||||
|
okText: t('common.delete'),
|
||||||
|
cancelText: t('common.cancel'),
|
||||||
|
okType: 'danger',
|
||||||
|
onOk: () => {
|
||||||
|
deleteDocumentChunk(item.metadata.knowledge_id, item.metadata.document_id, item.metadata.doc_id)
|
||||||
|
.then(() => {
|
||||||
|
message.success(t('common.deleteSuccess'));
|
||||||
|
refresh?.()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
console.log('RecallTestData', item)
|
||||||
|
}
|
||||||
|
|
||||||
// Show skeleton when initial loading
|
// Show skeleton when initial loading
|
||||||
if (loading && data.length === 0) {
|
if (loading && data.length === 0) {
|
||||||
@@ -186,17 +208,21 @@ const RecallTestResult = ({
|
|||||||
{scorePercentage.toFixed(1)}% {t('knowledgeBase.similarity')}
|
{scorePercentage.toFixed(1)}% {t('knowledgeBase.similarity')}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<div className={`rb:flex rb:mt-2 rb:flex rb:items-end rb:justify-end rb:gap-4 ${!showScore ? 'rb:w-full' : ''}`}>
|
<div className={`rb:flex rb:mt-2 rb:items-end rb:justify-end rb:gap-4 ${!showScore ? 'rb:w-full' : ''}`}>
|
||||||
<span className='rb:text-gray-800'>
|
<span className='rb:text-gray-800'>
|
||||||
<FileOutlined /> {item.metadata?.file_name || '-'}
|
<FileOutlined /> {item.metadata?.file_name || '-'}
|
||||||
</span>
|
</span>
|
||||||
<span className='rb:text-gray-500 rb:text-xs rb:bg-[#DFDFDF] rb:px-1 rb:py-[2px] rb:rounded'>
|
<span className='rb:text-gray-500 rb:text-xs rb:bg-[#DFDFDF] rb:px-1 rb:py-0.5 rb:rounded'>
|
||||||
chunk_{item.metadata?.sort_id || index}
|
chunk_{item.metadata?.sort_id || index}
|
||||||
</span>
|
</span>
|
||||||
|
<div
|
||||||
|
className="rb:size-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/common/delete.svg')] rb:hover:bg-[url('@/assets/images/common/delete_hover.svg')]"
|
||||||
|
onClick={(e) => handleDelete(e, item)}
|
||||||
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='rb:flex rb:text-left rb:px-4 rb:py-3 rb:bg-white rb:rounded-lg rb:mt-2'>
|
<div className='rb:flex rb:text-left rb:px-4 rb:py-3 rb:bg-white rb:rounded-lg rb:mt-2'>
|
||||||
<div className='rb:text-gray-800 rb:text-sm rb:whitespace-pre-wrap rb:break-words rb:w-full'>
|
<div className='rb:text-gray-800 rb:text-sm rb:whitespace-pre-wrap rb:wrap-break-word rb:w-full'>
|
||||||
{(() => {
|
{(() => {
|
||||||
const qaContent = parseQAContent(item.page_content);
|
const qaContent = parseQAContent(item.page_content);
|
||||||
if (qaContent) {
|
if (qaContent) {
|
||||||
@@ -239,7 +265,7 @@ const RecallTestResult = ({
|
|||||||
<div className='rb:flex rb:h-full rb:flex-col'>
|
<div className='rb:flex rb:h-full rb:flex-col'>
|
||||||
<div className='rb:flex rb:items-center rb:justify-start rb:gap-2'>
|
<div className='rb:flex rb:items-center rb:justify-start rb:gap-2'>
|
||||||
<span className='rb:text-lg rb:font-medium'>{t('knowledgeBase.recallResult')}</span>
|
<span className='rb:text-lg rb:font-medium'>{t('knowledgeBase.recallResult')}</span>
|
||||||
<span className='rb:text-gray-500 rb:text-xs rb:pt-[2px]'>
|
<span className='rb:text-gray-500 rb:text-xs rb:pt-0.5'>
|
||||||
(<span className='rb:text-[#155EEF]'>{data.length}</span> results)
|
(<span className='rb:text-[#155EEF]'>{data.length}</span> results)
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -262,7 +288,7 @@ const RecallTestResult = ({
|
|||||||
<div className='rb:flex rb:flex-col'>
|
<div className='rb:flex rb:flex-col'>
|
||||||
<div className='rb:flex rb:items-center rb:justify-start rb:gap-2'>
|
<div className='rb:flex rb:items-center rb:justify-start rb:gap-2'>
|
||||||
<span className='rb:text-lg rb:font-medium'>{t('knowledgeBase.recallResult')}</span>
|
<span className='rb:text-lg rb:font-medium'>{t('knowledgeBase.recallResult')}</span>
|
||||||
<span className='rb:text-gray-500 rb:text-xs rb:pt-[2px]'>
|
<span className='rb:text-gray-500 rb:text-xs rb:pt-0.5'>
|
||||||
(<span className='rb:text-[#155EEF]'>{data.length}</span> results)
|
(<span className='rb:text-[#155EEF]'>{data.length}</span> results)
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import RbCard from '@/components/RbCard/Card'
|
|||||||
import SearchInput from '@/components/SearchInput'
|
import SearchInput from '@/components/SearchInput'
|
||||||
import Empty from '@/components/Empty'
|
import Empty from '@/components/Empty'
|
||||||
import { getKnowledgeBaseList, getModelList, getModelTypeList, deleteKnowledgeBase, getKnowledgeBaseTypeList } from '@/api/knowledgeBase'
|
import { getKnowledgeBaseList, getModelList, getModelTypeList, deleteKnowledgeBase, getKnowledgeBaseTypeList } from '@/api/knowledgeBase'
|
||||||
|
import copy from 'copy-to-clipboard'
|
||||||
|
|
||||||
import InfiniteScroll from 'react-infinite-scroll-component';
|
import InfiniteScroll from 'react-infinite-scroll-component';
|
||||||
|
|
||||||
@@ -527,6 +528,10 @@ const KnowledgeBaseManagement: FC = () => {
|
|||||||
fetchData(1, false);
|
fetchData(1, false);
|
||||||
}
|
}
|
||||||
}, [modelTypes, query.parent_id, query.keywords, query.orderby, query.desc])
|
}, [modelTypes, query.parent_id, query.keywords, query.orderby, query.desc])
|
||||||
|
const handleCopy = (value: string) => {
|
||||||
|
copy(value)
|
||||||
|
messageApi.success(t('common.copySuccess'))
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -595,6 +600,13 @@ const KnowledgeBaseManagement: FC = () => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<Flex vertical gap={4} className='rb:min-h-15 rb:py-2.5! rb:px-3! rb:bg-[#F6F6F6] rb:rounded-lg rb:mb-3'>
|
<Flex vertical gap={4} className='rb:min-h-15 rb:py-2.5! rb:px-3! rb:bg-[#F6F6F6] rb:rounded-lg rb:mb-3'>
|
||||||
|
<div className="rb:cursor-pointer rb:mb-3 rb:w-full" onClick={() => handleCopy(item.id)}>
|
||||||
|
<div className="rb:text-gray-800 rb:font-medium">ID:</div>
|
||||||
|
<Flex align="center" className="rb:text-[#5B6167]">
|
||||||
|
{item.id}
|
||||||
|
<span className="rb:ml-1 rb:inline-block rb:size-4 rb:bg-cover rb:bg-[url('@/assets/images/common/copy_dark.svg')]"></span>
|
||||||
|
</Flex>
|
||||||
|
</div>
|
||||||
{item.descriptionItems?.map((description: Record<string, unknown>) => (
|
{item.descriptionItems?.map((description: Record<string, unknown>) => (
|
||||||
<div
|
<div
|
||||||
key={description.key as string}
|
key={description.key as string}
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ export interface ParserConfig {
|
|||||||
auto_keywords?: number; // 自动关键词
|
auto_keywords?: number; // 自动关键词
|
||||||
auto_questions?: number; // 自动问题
|
auto_questions?: number; // 自动问题
|
||||||
html4excel?: boolean; // 是否为Excel文件
|
html4excel?: boolean; // 是否为Excel文件
|
||||||
graphrag: GraphragConfig; // 知识图谱生成
|
graphrag?: GraphragConfig; // 知识图谱生成
|
||||||
|
|
||||||
// Web 类型特有字段
|
// Web 类型特有字段
|
||||||
entry_url?: string; // 入口网址
|
entry_url?: string; // 入口网址
|
||||||
@@ -135,6 +135,7 @@ export interface KnowledgeBaseDocumentData { // 知识库文档数据
|
|||||||
status?: number; // 状态 1 可检索 0 不可检索
|
status?: number; // 状态 1 可检索 0 不可检索
|
||||||
created_at?: string; // 创建时间
|
created_at?: string; // 创建时间
|
||||||
updated_at?: string; // 更新时间
|
updated_at?: string; // 更新时间
|
||||||
|
qa_prompt?: string; // 提示词
|
||||||
}
|
}
|
||||||
export interface DocumentModalRef {
|
export interface DocumentModalRef {
|
||||||
handleOpen: (file?: KnowledgeBaseDocumentData | null) => void;
|
handleOpen: (file?: KnowledgeBaseDocumentData | null) => void;
|
||||||
|
|||||||
Reference in New Issue
Block a user