diff --git a/web/src/api/knowledgeBase.ts b/web/src/api/knowledgeBase.ts index 52384d06..6816beb9 100644 --- a/web/src/api/knowledgeBase.ts +++ b/web/src/api/knowledgeBase.ts @@ -154,6 +154,19 @@ export const uploadFile = async (data: FormData, options?: UploadFileOptions) => }); return response as UploadFileResponse; }; +// 上传 QA 文件 +export const uploadQaFile = async (data: FormData, options?: UploadFileOptions) => { + const { kb_id, parent_id, onUploadProgress, signal } = options || {}; + const params: Record = {}; + 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) => { @@ -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); 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) => { const response = await request.post(`${apiPrefix}/chunks/${kb_id}/${document_id}/chunk`, data); diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index e0247cd9..bbd431a3 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -709,6 +709,8 @@ export const en = { localFile: 'Local File', uploadFileTypes: 'Upload PDF, TXT, DOCX, IMAGE, MEDIA and other format files', webLink: 'Web Link', + csvFile: 'Tabular Dataset', + csvUploadFileTypes: 'Upload files in CSV format', 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', selectorTutorial: 'Selector Usage Tutorial', diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index 385a2ae7..78d8db7e 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -194,6 +194,8 @@ export const zh = { localFile: '本地文件', uploadFileTypes: '上传 PDF、 TXT、 DOCX、 IMAGE、 MEDIA 等格式的文件', webLink: '网页链接', + csvFile: '表格数据集', + csvUploadFileTypes: '上传 CSV 格式的文件', webLinkPlaceholder: '请输入', webLinkDesc: '仅支持静态链接。如果上传的数据显示为空,则该链接可能无法读取。每行一个,一次最多{{count}}个链接', selectorTutorial: '选择器使用教程', @@ -283,6 +285,7 @@ export const zh = { qaExtract: '问答对提取', default: '默认', customize: '自定义', + qaPrompt: 'QA 拆分引导词', defaultSettings: '使用系统默认的参数和规则', customSettings: '自定义设置数据处理规则', fileName: '文件名称', diff --git a/web/src/views/KnowledgeBase/[knowledgeBaseId]/CreateDataset.tsx b/web/src/views/KnowledgeBase/[knowledgeBaseId]/CreateDataset.tsx index 7d32796d..41375981 100644 --- a/web/src/views/KnowledgeBase/[knowledgeBaseId]/CreateDataset.tsx +++ b/web/src/views/KnowledgeBase/[knowledgeBaseId]/CreateDataset.tsx @@ -10,7 +10,7 @@ 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, createDocumentAndUpload } from '@/api/knowledgeBase'; +import { uploadFile, uploadQaFile, getDocumentList, parseDocument, updateDocument, deleteDocument, createDocumentAndUpload } from '@/api/knowledgeBase'; import exitIcon from '@/assets/images/knowledgeBase/exit.png'; 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 ParameterSettings = 'defaultSettings' | 'customSettings'; const stepKeys = ['selectFile', 'parameterSettings', 'dataPreview', 'confirmUpload'] as const; @@ -63,6 +63,8 @@ interface ContentFormData { title: string; content: string; } +const fileType = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'csv', 'md', 'htm', 'html', 'json', 'ppt', 'pptx', 'txt', 'png', 'jpg', 'mp3', 'mp4', 'mov', 'wav'] +const csvFileType = ['csv'] const CreateDataset = () => { const { t } = useTranslation(); const navigate = useNavigate(); @@ -91,11 +93,12 @@ const CreateDataset = () => { const pollingTimerRef = useRef | null>(null); const [delimiter, setDelimiter] = useState(undefined); const [blockSize, setBlockSize] = useState(130); + const [qaPrompt, setQaPrompt] = useState() + console.log('qaPrompt', qaPrompt) const [processingMethod, setProcessingMethod] = useState('directBlock'); const [parameterSettings, setParameterSettings] = useState('defaultSettings'); const [pdfEnhancementEnabled, setPdfEnhancementEnabled] = useState(true); const [pdfEnhancementMethod, setPdfEnhancementMethod] = useState('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( () => [ { title: t('knowledgeBase.selectFile') }, @@ -112,8 +115,11 @@ const CreateDataset = () => { const handleNext = async () => { // Temporarily hide step 3: adjust step index (0->1->2 corresponds to select file->parameter settings->confirm upload) let nextStep = current + 1; + if (current === 0 && source === 'csv') { + return + } - if(nextStep === 1 && source === 'local') { + if((nextStep === 1 && source === 'local') || (nextStep === 2 && source === 'csv')) { // Check if files have been uploaded if (rechunkFileIds.length === 0) { // If no files, prompt user to upload first @@ -159,6 +165,7 @@ const CreateDataset = () => { delimiter: delimiter, chunk_token_num: blockSize, auto_questions: processingMethod === 'directBlock' ? 0 : 1, + qa_prompt: qaPrompt } } updateDocument(id, params) @@ -378,40 +385,67 @@ const CreateDataset = () => { formData.append('parent_id', parentId); } - 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; - }); - } + if (source === 'csv') { + uploadQaFile(formData, { + kb_id: knowledgeBaseId, + parent_id: parentId, + signal: abortController.signal, }) - .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); - }); + .then((res: UploadFileResponse) => { + // Upload successful, remove AbortController + abortControllersRef.current.delete(fileUid); + + onSuccess?.(res, new XMLHttpRequest()); + messageApi.success(t('knowledgeBase.uploadSuccess')) + handleBack() + }) + .catch((error) => { + // Remove AbortController + abortControllersRef.current.delete(fileUid); + + // If user actively cancelled, don't show error message + if (error.name === 'AbortError' || error.code === 'ERR_CANCELED') { + console.log('Upload cancelled:', (file as File).name); + return; + } + onError?.(error as Error); + }); + } else { + uploadFile(formData, { + kb_id: knowledgeBaseId, + parent_id: parentId, + signal: abortController.signal, + onUploadProgress: (event) => { + if (!event.total) return; + const percent = Math.round((event.loaded / event.total) * 100); + onProgress?.({ percent }, file); + }, + }) + .then((res: UploadFileResponse) => { + // Upload successful, remove AbortController + abortControllersRef.current.delete(fileUid); + + onSuccess?.(res, new XMLHttpRequest()); + if (res?.id) { + setRechunkFileIds((prev) => { + if (prev.includes(res.id)) return prev; + const next = [...prev, res.id]; + return next; + }); + } + }) + .catch((error) => { + // Remove AbortController + abortControllersRef.current.delete(fileUid); + + // If user actively cancelled, don't show error message + if (error.name === 'AbortError' || error.code === 'ERR_CANCELED') { + console.log('Upload cancelled:', (file as File).name); + return; + } + onError?.(error as Error); + }); + } }; @@ -557,21 +591,21 @@ const CreateDataset = () => { exit {t('common.exit')} -
+ {source !== 'csv' &&
-
+
}
{current === 0 && (
- {source && source === 'local' && ( + {source && (source === 'local' || source === 'csv') && ( { console.log('File list changed:', fileList); @@ -765,18 +799,23 @@ const CreateDataset = () => { - {parameterSettings === 'customSettings' && ( + {parameterSettings === 'customSettings' && (<>
-
-
- {t('knowledgeBase.delimiter')} -
- +
+
+ {t('knowledgeBase.delimiter')} +
+
- - )} +
+
+ {t('knowledgeBase.qaPrompt')} +
+ setQaPrompt(e.target.value)} /> +
+ )}
)} @@ -853,7 +892,7 @@ const CreateDataset = () => { {t('common.previous') || 'Prev'} )} - + }
diff --git a/web/src/views/KnowledgeBase/[knowledgeBaseId]/DocumentDetails.tsx b/web/src/views/KnowledgeBase/[knowledgeBaseId]/DocumentDetails.tsx index 3a720598..137efdd5 100644 --- a/web/src/views/KnowledgeBase/[knowledgeBaseId]/DocumentDetails.tsx +++ b/web/src/views/KnowledgeBase/[knowledgeBaseId]/DocumentDetails.tsx @@ -11,7 +11,7 @@ import { useNavigate, useParams, useLocation, useSearchParams } from 'react-rout import { useTranslation } from 'react-i18next'; import { useBreadcrumbManager, type BreadcrumbPath } from '@/hooks/useBreadcrumbManager'; 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 { formatDateTime } from '@/utils/format'; import InfoPanel, { type InfoItem } from '../components/InfoPanel'; @@ -20,7 +20,6 @@ import SearchInput from '@/components/SearchInput'; import DocumentPreview from '@/components/DocumentPreview'; import InsertModal, { type InsertModalRef } from '../components/InsertModal'; import exitIcon from '@/assets/images/knowledgeBase/exit.png'; -const imagePath = 'https://devapi.mem.redbearai.com' import copy from 'copy-to-clipboard' const DocumentDetails: FC = () => { const { t } = useTranslation(); @@ -228,6 +227,11 @@ const DocumentDetails: FC = () => { } }; + const refreshChunks = () => { + let nextPage = 1; + setPage(nextPage); + ChunkList(nextPage); + } const loadMoreChunks = () => { const nextPage = page + 1; setPage(nextPage); @@ -363,8 +367,8 @@ const DocumentDetails: FC = () => { fileName={document?.file_name} fileExt={document?.file_ext} height="calc(100% - 40px)" - mode="google" - showModeSwitch={true} + // mode="google" + // showModeSwitch={true} /> )} @@ -425,7 +429,7 @@ const DocumentDetails: FC = () => { {t('knowledgeBase.chunkList') || '分块列表'} { @@ -86,7 +90,7 @@ const CreateDatasetModal = forwardRef{items[1].title} {items[1].description} + + + + {items[2].title} + {items[2].description} + diff --git a/web/src/views/KnowledgeBase/components/RecallTestResult.tsx b/web/src/views/KnowledgeBase/components/RecallTestResult.tsx index 365356e2..f2a16368 100644 --- a/web/src/views/KnowledgeBase/components/RecallTestResult.tsx +++ b/web/src/views/KnowledgeBase/components/RecallTestResult.tsx @@ -7,20 +7,22 @@ * @LastEditTime: 2025-12-22 13:47:53 */ 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 type { RecallTestData } from '@/views/KnowledgeBase/types'; import { NoData } from './noData'; import { formatDateTime } from '@/utils/format'; import InfiniteScroll from 'react-infinite-scroll-component'; import RbMarkdown from '@/components/Markdown'; -import { useMemo } from 'react'; +import { useMemo, type MouseEvent } from 'react'; +import { deleteDocumentChunk } from '@/api/knowledgeBase' interface RecallTestResultProps { data: RecallTestData[]; showEmpty?: boolean; hasMore?: boolean; loadMore?: () => void; + refresh?: () => void; loading?: boolean; scrollableTarget?: string; editable?: boolean; // Whether editable @@ -34,6 +36,7 @@ const RecallTestResult = ({ showEmpty = true, hasMore = false, loadMore, + refresh, loading = false, scrollableTarget, editable = false, @@ -42,6 +45,7 @@ const RecallTestResult = ({ handleCopy, }: RecallTestResultProps) => { const { t } = useTranslation(); + const { modal, message } = App.useApp() console.log('chunk data', data) // Parse QA format content @@ -133,6 +137,24 @@ const RecallTestResult = ({ 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 if (loading && data.length === 0) { @@ -186,17 +208,21 @@ const RecallTestResult = ({ {scorePercentage.toFixed(1)}% {t('knowledgeBase.similarity')} )} -
+
{item.metadata?.file_name || '-'} - + chunk_{item.metadata?.sort_id || index} +
handleDelete(e, item)} + >
-
+
{(() => { const qaContent = parseQAContent(item.page_content); if (qaContent) { @@ -239,7 +265,7 @@ const RecallTestResult = ({
{t('knowledgeBase.recallResult')} - + ({data.length} results)
@@ -262,7 +288,7 @@ const RecallTestResult = ({
{t('knowledgeBase.recallResult')} - + ({data.length} results)
diff --git a/web/src/views/KnowledgeBase/index.tsx b/web/src/views/KnowledgeBase/index.tsx index 3110ff63..21d256fe 100644 --- a/web/src/views/KnowledgeBase/index.tsx +++ b/web/src/views/KnowledgeBase/index.tsx @@ -16,6 +16,7 @@ import RbCard from '@/components/RbCard/Card' import SearchInput from '@/components/SearchInput' import Empty from '@/components/Empty' import { getKnowledgeBaseList, getModelList, getModelTypeList, deleteKnowledgeBase, getKnowledgeBaseTypeList } from '@/api/knowledgeBase' +import copy from 'copy-to-clipboard' import InfiniteScroll from 'react-infinite-scroll-component'; @@ -527,6 +528,10 @@ const KnowledgeBaseManagement: FC = () => { fetchData(1, false); } }, [modelTypes, query.parent_id, query.keywords, query.orderby, query.desc]) + const handleCopy = (value: string) => { + copy(value) + messageApi.success(t('common.copySuccess')) + } return ( <> @@ -595,6 +600,13 @@ const KnowledgeBaseManagement: FC = () => {
+
handleCopy(item.id)}> +
ID:
+ + {item.id} + + +
{item.descriptionItems?.map((description: Record) => (
void;