feat(knowledgeBase): Refactor document list API and improve polling logic

- Update getDocumentList API to accept kb_id as separate parameter instead of extracting from query object
- Fix parameter name from auto_question to auto_questions in parser config
- Add progress field initialization in document update params
- Improve polling logic to handle both auto-return and manual stay scenarios with proper loading state management
- Add console logging for debugging polling status and document processing
- Reduce polling interval from 5000ms to 3000ms for faster status updates
- Enhance cleanup logic with route change detection to prevent memory leaks
- Add record parameter to progress render function for better data access
- Refactor confirm dialog callbacks to properly manage loading state timing
- Ensure loading indicator displays correctly when user chooses to stay on page
This commit is contained in:
yujiangping
2025-12-22 10:10:07 +08:00
parent 0a9c01cf33
commit ad2f47029d
6 changed files with 164 additions and 54 deletions

View File

@@ -199,8 +199,8 @@ export const deleteFile = async (id: string) => {
}; };
// 获取文档列表 // 获取文档列表
export const getDocumentList = async (query: PathQuery) => { export const getDocumentList = async (kb_id:string, query: PathQuery) => {
const response = await request.get(`${apiPrefix}/documents/${query.kb_id}/documents`, query); const response = await request.get(`${apiPrefix}/documents/${kb_id}/documents`, query);
return response as KnowledgeBaseDocumentData[]; return response as KnowledgeBaseDocumentData[];
}; };
// 文档详情 // 文档详情

View File

@@ -111,15 +111,17 @@ const CreateDataset = () => {
// 从参数设置进入确认上传时的处理 // 从参数设置进入确认上传时的处理
if(current === 1 && nextStep === 2) { if(current === 1 && nextStep === 2) {
// debugger
// handlePreview(data[0],0) // handlePreview(data[0],0)
if(parameterSettings === 'customSettings'){ if(parameterSettings === 'customSettings' || processingMethod === 'qaExtract'){
rechunkFileIds.map((id) => { rechunkFileIds.map((id) => {
const params = { const params = {
progress: 0,
parser_config: { parser_config: {
layout_recognize:'DeepDOC', layout_recognize:'DeepDOC',
delimiter: delimiter, delimiter: delimiter,
chunk_token_num: blockSize, chunk_token_num: blockSize,
auto_question: processingMethod === 'directBlock' ? 0 : 1, auto_questions: processingMethod === 'directBlock' ? 0 : 1,
} }
} }
updateDocument(id, params) updateDocument(id, params)
@@ -144,7 +146,6 @@ const CreateDataset = () => {
}); });
return; return;
} }
// 显示确认弹框 // 显示确认弹框
confirm({ confirm({
@@ -153,12 +154,18 @@ const CreateDataset = () => {
okText: t('knowledgeBase.returnToList') || '返回列表页', okText: t('knowledgeBase.returnToList') || '返回列表页',
cancelText: t('knowledgeBase.stayOnPage') || '停留在此页', cancelText: t('knowledgeBase.stayOnPage') || '停留在此页',
onOk: () => { onOk: () => {
// 用户选择返回列表页 // 用户选择返回列表页 - 不显示 loading直接跳转
startProcessing(true); startProcessing(true);
}, },
onCancel: () => { onCancel: () => {
// 用户选择停留在当前页 // 用户选择停留在当前页 - 显示 loading 并开始轮询
startProcessing(false); console.log('用户选择停留,开始显示 loading');
setPollingLoading(true);
// 延迟一点时间让用户看到 loading 效果,然后开始处理
setTimeout(() => {
startProcessing(false);
}, 100);
}, },
}); });
}; };
@@ -170,15 +177,12 @@ const CreateDataset = () => {
parseDocument(id, {}); parseDocument(id, {});
}); });
// 开启 loading
setPollingLoading(true);
if (autoReturnToList) { if (autoReturnToList) {
// 用户选择立即返回,直接跳转 // 用户选择立即返回,直接跳转(不显示 loading
console.log('用户选择立即返回列表页'); console.log('用户选择立即返回列表页');
handleBack(); handleBack();
} else { } else {
// 用户选择停留,启动轮询查看进度 // 用户选择停留,启动轮询查看进度loading 已在 onCancel 中设置)
console.log('用户选择停留查看进度'); console.log('用户选择停留查看进度');
// 立即执行一次轮询(启用自动返回) // 立即执行一次轮询(启用自动返回)
@@ -187,7 +191,7 @@ const CreateDataset = () => {
// 然后每3秒执行一次启用自动返回 // 然后每3秒执行一次启用自动返回
pollingTimerRef.current = setInterval(() => { pollingTimerRef.current = setInterval(() => {
pollDocumentStatus(true); pollDocumentStatus(true);
}, 5000); }, 3000);
} }
}; };
const handleDelete = (record: AnyObject) => { const handleDelete = (record: AnyObject) => {
@@ -222,7 +226,7 @@ const CreateDataset = () => {
title: t('knowledgeBase.status'), title: t('knowledgeBase.status'),
dataIndex: 'progress', dataIndex: 'progress',
key: 'progress', key: 'progress',
render: (value: number) => { render: (value: number, record: any) => {
return ( return (
<span className="rb:text-xs rb:border rb:border-[#DFE4ED] rb:bg-[#FBFDFF] rb:rounded rb:items-center rb:text-[#212332] rb:py-1 rb:px-2"> <span className="rb:text-xs rb:border rb:border-[#DFE4ED] rb:bg-[#FBFDFF] rb:rounded rb:items-center rb:text-[#212332] rb:py-1 rb:px-2">
<span className="rb:inline-block rb:w-[5px] rb:h-[5px] rb:mr-2 rb:rounded-full" style={{ backgroundColor: value === 1 ? '#369F21' : '#FF8A4C' }}></span> <span className="rb:inline-block rb:w-[5px] rb:h-[5px] rb:mr-2 rb:rounded-full" style={{ backgroundColor: value === 1 ? '#369F21' : '#FF8A4C' }}></span>
@@ -280,44 +284,59 @@ const CreateDataset = () => {
// 轮询检查文档处理状态 // 轮询检查文档处理状态
// autoReturn: 是否在所有文档完成时自动返回列表页 // autoReturn: 是否在所有文档完成时自动返回列表页
const pollDocumentStatus = (autoReturn: boolean = false) => { const pollDocumentStatus = (autoReturn: boolean = false) => {
console.log('开始轮询文档状态,当前 pollingLoading:', pollingLoading);
if (!knowledgeBaseId || !parentId || rechunkFileIds.length === 0) { if (!knowledgeBaseId || !parentId || rechunkFileIds.length === 0) {
console.log('轮询条件不满足,退出');
return; return;
} }
// 刷新 Table 组件的数据(仅在 confirmUpload 步骤) // 获取文档列表检查是否全部完成,并刷新表格数据
if (current === 2) { getDocumentList(knowledgeBaseId, {
tableRef.current?.loadData();
}
// 同时获取文档列表检查是否全部完成
getDocumentList({
kb_id: knowledgeBaseId,
parent_id: parentId,
document_ids: rechunkFileIds.join(','), document_ids: rechunkFileIds.join(','),
}) })
.then((res: any) => { .then((res: any) => {
const documents = res.items || []; const documents = res.items || [];
setData(documents); setData(documents);
// 只在 confirmUpload 步骤刷新表格数据
if (current === 2) {
tableRef.current?.loadData();
}
console.log('documents', documents);
// 检查是否所有文档的 progress 都为 1 // 检查是否所有文档的 progress 都为 1
const allCompleted = documents.every((doc: KnowledgeBaseDocumentData) => doc.progress === 1); const allCompleted = documents.every((doc: KnowledgeBaseDocumentData) => doc.progress === 1);
console.log('轮询状态:', documents.map((d: KnowledgeBaseDocumentData) => ({ name: d.file_name, progress: d.progress }))); console.log('轮询状态:', allCompleted);
// 只有在 autoReturn 为 true 且所有文档完成时才自动返回 // 检查是否所有文档完成
if (allCompleted && autoReturn) { // debugger
// 所有文档处理完成,清除定时器和 loading if (allCompleted) {
// 清除定时器和 loading 状态
if (pollingTimerRef.current) { if (pollingTimerRef.current) {
clearInterval(pollingTimerRef.current); clearInterval(pollingTimerRef.current);
pollingTimerRef.current = null; pollingTimerRef.current = null;
} }
setPollingLoading(false);
// 延迟 2 秒后跳转,让用户看到完成状态 // 延迟清除 loading,让用户看到完成状态
console.log('所有文档处理完成2秒后返回列表页');
setTimeout(() => { setTimeout(() => {
handleBack(); setPollingLoading(false);
}, 2000); }, 1000);
// 只有在 autoReturn 为 true 时才自动返回
if (autoReturn) {
// 延迟 2 秒后跳转,让用户看到完成状态
console.log('所有文档处理完成2秒后返回列表页');
setTimeout(() => {
handleBack();
}, 2000);
} else {
console.log('所有文档处理完成,用户可手动操作');
}
} else {
// 如果还有文档在处理中,确保 loading 状态保持
console.log('还有文档在处理中,保持 loading 状态');
} }
}) })
.catch((error) => { .catch((error) => {
@@ -349,9 +368,7 @@ const CreateDataset = () => {
useEffect(() => { useEffect(() => {
if (initialFileIds.length > 0 && initialStepKey !== 'selectFile' && knowledgeBaseId && parentId) { if (initialFileIds.length > 0 && initialStepKey !== 'selectFile' && knowledgeBaseId && parentId) {
// 加载文档列表数据 // 加载文档列表数据
getDocumentList({ getDocumentList(knowledgeBaseId,{
kb_id: knowledgeBaseId,
parent_id: parentId,
document_ids: initialFileIds.join(','), document_ids: initialFileIds.join(','),
}) })
.then((res: any) => { .then((res: any) => {
@@ -364,7 +381,7 @@ const CreateDataset = () => {
} }
}, []); }, []);
// 清理函数:组件卸载时清除定时器 // 清理函数:组件卸载时清除定时器和 loading 状态
useEffect(() => { useEffect(() => {
return () => { return () => {
if (pollingTimerRef.current) { if (pollingTimerRef.current) {
@@ -375,6 +392,18 @@ const CreateDataset = () => {
}; };
}, []); }, []);
// 监听路由变化,确保在页面切换时清理状态
useEffect(() => {
return () => {
// 页面卸载时清理状态
if (pollingTimerRef.current) {
clearInterval(pollingTimerRef.current);
pollingTimerRef.current = null;
}
setPollingLoading(false);
};
}, [location.pathname]);
return ( return (
<> <>
{contextHolder} {contextHolder}
@@ -553,7 +582,7 @@ const CreateDataset = () => {
<Table <Table
ref={tableRef} ref={tableRef}
apiUrl={`/documents/${knowledgeBaseId}/documents`} apiUrl={`/documents/${knowledgeBaseId}/documents`}
apiParams={{ apiParams={{
document_ids: rechunkFileIds.join(','), document_ids: rechunkFileIds.join(','),
}} }}
columns={columns} columns={columns}

View File

@@ -4,7 +4,7 @@
* @Author: yujiangping * @Author: yujiangping
* @Date: 2025-11-15 16:13:47 * @Date: 2025-11-15 16:13:47
* @LastEditors: yujiangping * @LastEditors: yujiangping
* @LastEditTime: 2025-12-12 20:02:05 * @LastEditTime: 2025-12-19 20:19:59
*/ */
import { useEffect, useState, useRef, type FC } from 'react'; import { useEffect, useState, useRef, type FC } from 'react';
import { useNavigate, useParams, useLocation } from 'react-router-dom'; import { useNavigate, useParams, useLocation } from 'react-router-dom';
@@ -47,6 +47,7 @@ const DocumentDetails: FC = () => {
const [chunkLoading, setChunkLoading] = useState(false); const [chunkLoading, setChunkLoading] = useState(false);
const [keywords, setKeywords] = useState(''); const [keywords, setKeywords] = useState('');
const [fileUrl, setFileUrl] = useState(''); const [fileUrl, setFileUrl] = useState('');
const [parserMode, setParserMode] = useState(0);
const insertModalRef = useRef<InsertModalRef>(null); const insertModalRef = useRef<InsertModalRef>(null);
const isManualRefreshRef = useRef(false); const isManualRefreshRef = useRef(false);
@@ -127,6 +128,7 @@ const DocumentDetails: FC = () => {
setInfoItems(formatDocumentInfo(response)); setInfoItems(formatDocumentInfo(response));
const url = `${imagePath}/api/files/${response.file_id}` const url = `${imagePath}/api/files/${response.file_id}`
setFileUrl(url); setFileUrl(url);
setParserMode(response?.parser_config?.auto_questions || 0)
// ChunkList 会在 useEffect 中根据 document.progress 自动调用 // ChunkList 会在 useEffect 中根据 document.progress 自动调用
} catch (error) { } catch (error) {
console.error('获取文档详情失败:', error); console.error('获取文档详情失败:', error);
@@ -388,6 +390,7 @@ const DocumentDetails: FC = () => {
{t('knowledgeBase.chunkList') || '分块列表'} {t('knowledgeBase.chunkList') || '分块列表'}
</h2> </h2>
<RecallTestResult <RecallTestResult
data={chunkList} data={chunkList}
showEmpty={false} showEmpty={false}
hasMore={hasMore} hasMore={hasMore}
@@ -396,6 +399,7 @@ const DocumentDetails: FC = () => {
scrollableTarget="chunkScrollableDiv" scrollableTarget="chunkScrollableDiv"
editable={true} editable={true}
onItemClick={handleChunkClick} onItemClick={handleChunkClick}
parserMode={parserMode}
/> />
</div> </div>
</div> </div>

View File

@@ -22,6 +22,7 @@ const CreateFolderModal = forwardRef<CreateFolderModalRef,CreateFolderModalRefPr
}; };
const handleOpen = (folder?: FolderFormData | null) => { const handleOpen = (folder?: FolderFormData | null) => {
debugger
if (folder) { if (folder) {
setFolder(folder); setFolder(folder);
// 设置表单值 // 设置表单值

View File

@@ -24,6 +24,7 @@ interface RecallTestResultProps {
scrollableTarget?: string; scrollableTarget?: string;
editable?: boolean; // 是否可编辑 editable?: boolean; // 是否可编辑
onItemClick?: (item: RecallTestData, index: number) => void; // 点击项的回调 onItemClick?: (item: RecallTestData, index: number) => void; // 点击项的回调
parserMode?: number; // 解析模式1 表示 QA 格式
} }
const RecallTestResult = ({ const RecallTestResult = ({
@@ -35,9 +36,31 @@ const RecallTestResult = ({
scrollableTarget, scrollableTarget,
editable = false, editable = false,
onItemClick, onItemClick,
parserMode = 0,
}: RecallTestResultProps) => { }: RecallTestResultProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
// 解析 QA 格式内容
const parseQAContent = (content: string) => {
if (!content || parserMode !== 1) return null;
const qaRegex = /question:\s*(.*?)\s*answer:\s*(.*?)$/s;
const match = content.match(qaRegex);
if (match) {
const question = match[1]?.trim() || '';
const answer = match[2]?.trim() || '';
return { question, answer };
}
return null;
};
// 格式化 QA 内容为显示格式
const formatQAContent = (question: string, answer: string) => {
return `**问题:** ${question}\n\n**答案:** ${answer}`;
};
const handleItemClick = (e: React.MouseEvent, item: RecallTestData, index: number) => { const handleItemClick = (e: React.MouseEvent, item: RecallTestData, index: number) => {
// 检查点击的是否是图片或图片相关元素 // 检查点击的是否是图片或图片相关元素
const target = e.target as HTMLElement; const target = e.target as HTMLElement;
@@ -126,7 +149,14 @@ const RecallTestResult = ({
</div> </div>
<div className='rb:flex rb:text-left rb:px-4 rb:py-3 rb:bg-[#F0F3F8] rb:rounded-lg rb:mt-2'> <div className='rb:flex rb:text-left rb:px-4 rb:py-3 rb:bg-[#F0F3F8] 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:break-words rb:w-full'>
<RbMarkdown content={item.page_content} showHtmlComments={true} /> {(() => {
const qaContent = parseQAContent(item.page_content);
if (qaContent) {
const formattedContent = formatQAContent(qaContent.question, qaContent.answer);
return <RbMarkdown content={formattedContent} showHtmlComments={true} />;
}
return <RbMarkdown content={item.page_content} showHtmlComments={true} />;
})()}
</div> </div>
</div> </div>
{item.metadata?.file_created_at && ( {item.metadata?.file_created_at && (

View File

@@ -74,7 +74,8 @@ const KnowledgeBaseManagement: FC = () => {
const items: NonNullable<MenuProps['items']> = []; const items: NonNullable<MenuProps['items']> = [];
// 当权限为 share 时,不显示编辑按钮 // 当权限为 share 时,不显示编辑按钮
if (item.permission_id !== 'share') { const permissionId = (item.permission_id || '').toLowerCase();
if (permissionId !== 'share') {
items.push({ items.push({
key: '1', key: '1',
label: t('knowledgeBase.edit'), label: t('knowledgeBase.edit'),
@@ -131,7 +132,7 @@ const KnowledgeBaseManagement: FC = () => {
}; };
// 处理创建 // 处理创建
const handleCreate = (type?: string) => { const handleCreate = useCallback((type?: string) => {
// 如果在文件夹内,使用 folderPath 的最后一项作为 parent_id // 如果在文件夹内,使用 folderPath 的最后一项作为 parent_id
// 这样更可靠,因为 folderPath 是直接管理的状态 // 这样更可靠,因为 folderPath 是直接管理的状态
const currentParentId = folderPath.length > 0 const currentParentId = folderPath.length > 0
@@ -142,8 +143,17 @@ const KnowledgeBaseManagement: FC = () => {
parent_id: currentParentId as string, parent_id: currentParentId as string,
} as KnowledgeBaseListItem : null; } as KnowledgeBaseListItem : null;
console.log('handleCreate called:', {
type,
folderPath,
folderPathLength: folderPath.length,
queryParentId: query.parent_id,
currentParentId,
record
});
modalRef?.current?.handleOpen(record, type) modalRef?.current?.handleOpen(record, type)
} }, [folderPath, query.parent_id])
// 动态生成 createItems // 动态生成 createItems
const createItems: MenuProps['items'] = useMemo(() => { const createItems: MenuProps['items'] = useMemo(() => {
@@ -155,7 +165,7 @@ const KnowledgeBaseManagement: FC = () => {
handleCreate(type); handleCreate(type);
}, },
})); }));
}, [knowledgeBaseTypes, t]); }, [knowledgeBaseTypes, t, handleCreate]);
const typeToFieldKey = (type: string) => { const typeToFieldKey = (type: string) => {
const normalized = (type || '').toLowerCase(); const normalized = (type || '').toLowerCase();
switch (normalized) { switch (normalized) {
@@ -180,7 +190,7 @@ const KnowledgeBaseManagement: FC = () => {
key, key,
label: t(`knowledgeBase.${key}`), label: t(`knowledgeBase.${key}`),
children: key === 'permission_id' children: key === 'permission_id'
? (data[key] === 'Private' || data[key] === 'private' ? t('knowledgeBase.private') : t('knowledgeBase.share')) ? ((data[key] || '').toLowerCase() === 'private' ? t('knowledgeBase.private') : t('knowledgeBase.share'))
: String(data[key] || '-'), : String(data[key] || '-'),
})) }))
} }
@@ -283,7 +293,15 @@ const KnowledgeBaseManagement: FC = () => {
const fetchData = async (pageNum: number = 1, isLoadMore: boolean = false) => { const fetchData = async (pageNum: number = 1, isLoadMore: boolean = false) => {
if (!modelTypes.length) return; if (!modelTypes.length) return;
if (loading) return; if (loading) return;
console.log('fetchData called, pageNum:', pageNum, 'isLoadMore:', isLoadMore);
console.log('fetchData called:', {
pageNum,
isLoadMore,
currentQuery: query,
currentFolderPath: folderPath,
folderPathLastId: folderPath.length > 0 ? folderPath[folderPath.length - 1].id : 'none'
});
setLoading(true); setLoading(true);
try { try {
const params = { const params = {
@@ -293,6 +311,8 @@ const KnowledgeBaseManagement: FC = () => {
orderby:'created_at', orderby:'created_at',
desc:true, desc:true,
} }
console.log('API params:', params);
const res = await getKnowledgeBaseList(undefined, params); const res = await getKnowledgeBaseList(undefined, params);
const response = res as KnowledgeBaseListResponse & { items?: KnowledgeBaseListItem[] }; const response = res as KnowledgeBaseListResponse & { items?: KnowledgeBaseListItem[] };
console.log('API response:', response); console.log('API response:', response);
@@ -373,10 +393,21 @@ const KnowledgeBaseManagement: FC = () => {
}); });
}; };
// 处理跳转详情 // 处理跳转详情
const handleToDetail = (knowledgeBase: KnowledgeBaseListItem) => { const handleToDetail = useCallback((knowledgeBase: KnowledgeBaseListItem) => {
// 统一处理类型判断,忽略大小写
const itemType = (knowledgeBase.type || '').toLowerCase();
console.log('handleToDetail called with:', {
id: knowledgeBase.id,
name: knowledgeBase.name,
type: itemType,
currentFolderPath: folderPath,
currentQuery: query
});
// 如果是 Folder 类型,刷新当前页面,显示该文件夹下的知识库列表 // 如果是 Folder 类型,刷新当前页面,显示该文件夹下的知识库列表
if (knowledgeBase.type === 'Folder' || knowledgeBase.type === 'folder') { if (itemType === 'folder') {
// 添加到文件夹路径 // 计算新的文件夹路径
const newFolderPath = [ const newFolderPath = [
...folderPath, ...folderPath,
{ {
@@ -384,15 +415,33 @@ const KnowledgeBaseManagement: FC = () => {
name: knowledgeBase.name, name: knowledgeBase.name,
}, },
]; ];
setFolderPath(newFolderPath);
console.log('Folder clicked:', {
folderId: knowledgeBase.id,
folderName: knowledgeBase.name,
currentFolderPath: folderPath,
newFolderPath: newFolderPath
});
// 同步更新状态,保持与面包屑逻辑一致
setFolderPath(newFolderPath);
setQuery((prev) => ({ setQuery((prev) => ({
...prev, ...prev,
parent_id: knowledgeBase.id, parent_id: knowledgeBase.id,
})); }));
return; return;
} }
// 统一处理权限判断,忽略大小写
const permissionId = (knowledgeBase.permission_id || '').toLowerCase();
const isPrivate = permissionId === 'private';
// 根据权限类型跳转到不同的详情页 // 根据权限类型跳转到不同的详情页
const targetPath = isPrivate
? `/knowledge-base/${knowledgeBase.id}/private`
: `/knowledge-base/${knowledgeBase.id}/share`;
// 跳转时传递当前的文件夹路径信息 // 跳转时传递当前的文件夹路径信息
const navigationState = { const navigationState = {
fromKnowledgeBaseList: true, fromKnowledgeBaseList: true,
@@ -400,9 +449,6 @@ const KnowledgeBaseManagement: FC = () => {
parentId: query.parent_id, parentId: query.parent_id,
timestamp: Date.now(), // 添加时间戳确保每次跳转状态都不同 timestamp: Date.now(), // 添加时间戳确保每次跳转状态都不同
}; };
const targetPath = knowledgeBase.permission_id === 'Private' || knowledgeBase.permission_id === 'private'
? `/knowledge-base/${knowledgeBase.id}/private`
: `/knowledge-base/${knowledgeBase.id}/share`;
// 检查是否是相同路径跳转 // 检查是否是相同路径跳转
const currentPath = location.pathname; const currentPath = location.pathname;
@@ -417,7 +463,7 @@ const KnowledgeBaseManagement: FC = () => {
// 不同路径,正常跳转 // 不同路径,正常跳转
navigate(targetPath, { state: navigationState }); navigate(targetPath, { state: navigationState });
} }
} }, [folderPath, query, location.pathname, navigate])
// 更新面包屑 // 更新面包屑
useEffect(() => { useEffect(() => {
updateBreadcrumbs({ updateBreadcrumbs({