fix: 知识库标注框问题

This commit is contained in:
dolphin
2025-09-16 19:09:10 +08:00
parent 89449ee26a
commit 9a9c6e2914
7 changed files with 185 additions and 176 deletions
@@ -295,7 +295,7 @@ const useFileProcessingRules = (initialStrategies, resultFiles, kid, splitRule)
separator: splitRule?.separator || ['\\n\\n', '\\n'],
separatorRule: splitRule?.separator_rule || ['after', 'after'],
chunkSize: splitRule?.chunk_size?.toString() || "1000",
chunkOverlap: splitRule?.chunk_overlap?.toString() || "100",
chunkOverlap: splitRule?.chunk_overlap?.toString() || "10",
retainImages: splitRule?.retain_images ?? true,
enableFormula: splitRule?.enable_formula ?? true,
forceOcr: splitRule?.force_ocr ?? true,
@@ -4,7 +4,7 @@ import { Dialog, DialogContent, DialogHeader } from '@/components/bs-ui/dialog';
import { SearchInput } from '@/components/bs-ui/input';
import AutoPagination from '@/components/bs-ui/pagination/autoPagination';
import ShadTooltip from "@/components/ShadTooltipComponent";
import { delChunkApi, getFilePathApi, getKnowledgeChunkApi, getFileBboxApi, readFileByLibDatabase, updateChunkApi } from '@/controllers/API';
import { delChunkApi, getFileBboxApi, getFilePathApi, getKnowledgeChunkApi, readFileByLibDatabase, updateChunkApi } from '@/controllers/API';
import { captureAndAlertRequestErrorHoc } from '@/controllers/request';
import { useTable } from '@/util/hook';
import { truncateString } from "@/util/utils";
@@ -60,7 +60,13 @@ export default function Paragraphs({ fileId, onBack }) {
const [previewUrl, setPreviewUrl] = useState()
const [hasChunkBboxes, setHasChunkBboxes] = useState(false);
const latestFileUrlRef = useRef('');
const latestPreviewUrlRef = useRef('');
const latestPreviewUrlRef = useRef('');
const [selectedChunkIndex, setSelectedBbox] = useKnowledgeStore((state) => [state.selectedChunkIndex, state.setSelectedBbox]);
useEffect(() => {
// 切换chunk清空选中的高亮标注bbox
setSelectedBbox([])
}, [selectedChunkIndex])
// 表格配置(完全保留原始逻辑)
const tableConfig = useMemo(() => ({
@@ -89,19 +95,19 @@ const latestPreviewUrlRef = useRef('');
let chunkBboxes = [];
try {
const firstChunk = response.data?.[0];
if (firstChunk?.metadata?.bbox) {
console.log(JSON.parse(firstChunk?.metadata?.bbox).chunk_bboxes,6666666666666);
// 先判断bbox是否为空字符串
if (typeof firstChunk.metadata.bbox === 'string' && JSON.parse(firstChunk?.metadata?.bbox).chunk_bboxes === '') {
console.log('bbox为空字符串');
chunkBboxes = [];
} else {
// 解析JSON
const bboxObj = JSON.parse(firstChunk.metadata.bbox);
chunkBboxes = bboxObj.chunk_bboxes || [];
if (firstChunk?.metadata?.bbox) {
console.log(JSON.parse(firstChunk?.metadata?.bbox).chunk_bboxes, 6666666666666);
// 先判断bbox是否为空字符串
if (typeof firstChunk.metadata.bbox === 'string' && JSON.parse(firstChunk?.metadata?.bbox).chunk_bboxes === '') {
console.log('bbox为空字符串');
chunkBboxes = [];
} else {
// 解析JSON
const bboxObj = JSON.parse(firstChunk.metadata.bbox);
chunkBboxes = bboxObj.chunk_bboxes || [];
}
}
}
} catch (e) {
console.error('解析 chunk_bboxes 失败:', e);
chunkBboxes = [];
@@ -115,95 +121,95 @@ const latestPreviewUrlRef = useRef('');
return response;
}
);
const fetchFileUrl = useCallback(async (fileId) => {
console.log('获取文件URL:', fileId);
if (!fileId) return '';
const fetchFileUrl = useCallback(async (fileId) => {
console.log('获取文件URL:', fileId);
if (!fileId) return '';
try {
setIsFetchingUrl(true);
const res = await getFilePathApi(fileId);
const pares = await getFileBboxApi(fileId);
setPartitions(pares || []);
try {
setIsFetchingUrl(true);
const res = await getFilePathApi(fileId);
const pares = await getFileBboxApi(fileId);
setPartitions(pares || []);
// 获取当前选中的文件信息
const currentFile = rawFiles.find(f => String(f.id) === String(fileId));
let finalUrl = '';
let finalPreviewUrl = '';
// 获取当前选中的文件信息
const currentFile = rawFiles.find(f => String(f.id) === String(fileId));
let finalUrl = '';
let finalPreviewUrl = '';
// 检查是否有有效的preview_url和original_url
const hasPreviewUrl = typeof res.preview_url === 'string' && res.preview_url.trim() !== '';
const hasOriginalUrl = typeof res.original_url === 'string' && res.original_url.trim() !== '';
// 检查是否有有效的preview_url和original_url
const hasPreviewUrl = typeof res.preview_url === 'string' && res.preview_url.trim() !== '';
const hasOriginalUrl = typeof res.original_url === 'string' && res.original_url.trim() !== '';
if (currentFile) {
console.log(currentFile,3);
// 判断是否为UNS或LOCAL类型
const isUnsOrLocal = currentFile.parse_type === "uns" || currentFile.parse_type === "local";
console.log(isUnsOrLocal,currentFile,4444444);
if (isUnsOrLocal) {
// UNS或LOCAL类型:根据bbox是否有效选择URL
const isBboxesValid = hasChunkBboxes;
const isBboxesEmpty = !hasChunkBboxes || chunkBboxes.length === 0;
if (!isBboxesEmpty && hasPreviewUrl) {
// 有有效bbox且有preview_url → 使用preview_url
console.log(1111);
finalUrl = res.preview_url.trim();
finalPreviewUrl = res.preview_url.trim();
console.log('UNS/LOCAL类型(有有效bbox):使用preview_url');
} else {
// 无有效bbox(为空数组/字符串)或无preview_url → 强制使用original_url
console.log(2222);
finalUrl = hasOriginalUrl ? res.original_url.trim() : '';
finalPreviewUrl = finalUrl;
console.log('UNS/LOCAL类型(无有效bbox或无preview_url):使用original_url');
if (currentFile) {
console.log(currentFile, 3);
// 判断是否为UNS或LOCAL类型
const isUnsOrLocal = currentFile.parse_type === "uns" || currentFile.parse_type === "local";
console.log(isUnsOrLocal, currentFile, 4444444);
if (isUnsOrLocal) {
// UNS或LOCAL类型:根据bbox是否有效选择URL
const isBboxesValid = hasChunkBboxes;
const isBboxesEmpty = !hasChunkBboxes || chunkBboxes.length === 0;
if (!isBboxesEmpty && hasPreviewUrl) {
// 有有效bbox且有preview_url → 使用preview_url
console.log(1111);
finalUrl = res.preview_url.trim();
finalPreviewUrl = res.preview_url.trim();
console.log('UNS/LOCAL类型(有有效bbox):使用preview_url');
} else {
// 无有效bbox(为空数组/字符串)或无preview_url → 强制使用original_url
console.log(2222);
finalUrl = hasOriginalUrl ? res.original_url.trim() : '';
finalPreviewUrl = finalUrl;
console.log('UNS/LOCAL类型(无有效bbox或无preview_url):使用original_url');
}
} else {
// 其他类型:优先使用preview_url,无则使用original_url
if (hasPreviewUrl) {
// 有preview_url → 优先使用
finalUrl = res.preview_url.trim();
finalPreviewUrl = res.preview_url.trim();
console.log('其他类型:使用preview_url');
} else {
// 无preview_url → 使用original_url或备选URL
finalUrl = hasOriginalUrl ? res.original_url.trim() : '';
finalPreviewUrl = finalUrl;
console.log('其他类型(无preview_url):使用original_url');
}
}
} else {
// 如果没有找到当前文件,使用默认策略
finalUrl = hasPreviewUrl ? res.preview_url.trim() : (hasOriginalUrl ? res.original_url.trim() : '');
finalPreviewUrl = finalUrl;
console.log('未找到文件信息,使用默认URL策略');
}
if (finalUrl) {
finalUrl = decodeURIComponent(finalUrl);
finalPreviewUrl = decodeURIComponent(finalPreviewUrl);
// 同时更新状态和ref(ref会同步生效)
setFileUrl(finalUrl);
setPreviewUrl(finalPreviewUrl);
return finalUrl;
} else {
setFileUrl('');
setPreviewUrl('');
return '';
}
} catch (err) {
console.error('获取文件URL失败:', err);
setFileUrl('');
setPreviewUrl('');
setPartitions([]);
return '';
} finally {
setIsFetchingUrl(false);
}
} else {
// 其他类型:优先使用preview_url,无则使用original_url
if (hasPreviewUrl) {
// 有preview_url → 优先使用
finalUrl = res.preview_url.trim();
finalPreviewUrl = res.preview_url.trim();
console.log('其他类型:使用preview_url');
} else {
// 无preview_url → 使用original_url或备选URL
finalUrl = hasOriginalUrl ? res.original_url.trim() : '';
finalPreviewUrl = finalUrl;
console.log('其他类型(无preview_url):使用original_url');
}
}
} else {
// 如果没有找到当前文件,使用默认策略
finalUrl = hasPreviewUrl ? res.preview_url.trim() : (hasOriginalUrl ? res.original_url.trim() : '');
finalPreviewUrl = finalUrl;
console.log('未找到文件信息,使用默认URL策略');
}
if (finalUrl) {
finalUrl = decodeURIComponent(finalUrl);
finalPreviewUrl = decodeURIComponent(finalPreviewUrl);
// 同时更新状态和ref(ref会同步生效)
setFileUrl(finalUrl);
setPreviewUrl(finalPreviewUrl);
return finalUrl;
} else {
setFileUrl('');
setPreviewUrl('');
return '';
}
} catch (err) {
console.error('获取文件URL失败:', err);
setFileUrl('');
setPreviewUrl('');
setPartitions([]);
return '';
} finally {
setIsFetchingUrl(false);
}
}, [rawFiles, hasChunkBboxes]);
}, [rawFiles, hasChunkBboxes]);
@@ -286,7 +292,7 @@ const fetchFileUrl = useCallback(async (fileId) => {
if (filterData) filterData({ file_ids: [newFileId] });
await fetchFileUrl(newFileId);
await reload();
setChunkSwitchTrigger(prev => prev + 1);
setChunkSwitchTrigger(prev => prev + 1);
} catch (err) {
console.error('文件切换失败:', err);
setSelectError(err.message || '文件切换失败');
@@ -347,7 +353,8 @@ const fetchFileUrl = useCallback(async (fileId) => {
// 处理分段修改(完全保留原始逻辑)
const handleChunkChange = useCallback((chunkIndex, text) => {
const bbox = { chunk_bboxes: selectedBbox };
const bboxStr = JSON.stringify(bbox);
// selectedBbox空数组时,使用safeChunks的bbox
const bboxStr = selectedBbox.length ? JSON.stringify(bbox) : safeChunks[chunkIndex].bbox;
captureAndAlertRequestErrorHoc(updateChunkApi({
knowledge_id: Number(id),
@@ -392,7 +399,7 @@ const fetchFileUrl = useCallback(async (fileId) => {
chunkIndex: item?.metadata?.chunk_index || index,
bbox: item?.metadata?.bbox
}));
}, [datalist, selectedFileId,chunkSwitchTrigger]);
}, [datalist, selectedFileId, chunkSwitchTrigger]);
// 打开元数据弹窗(完全保留原始逻辑)
const handleMetadataClick = useCallback(() => {
@@ -407,8 +414,8 @@ const fetchFileUrl = useCallback(async (fileId) => {
// 调整分段策略(完全保留原始逻辑)
const handleAdjustSegmentation = useCallback(() => {
console.log(currentFile, fileUrl, previewUrl, 789);
const currentFileUrl = latestFileUrlRef.current;
const currentPreviewUrl = latestPreviewUrlRef.current;
const currentFileUrl = latestFileUrlRef.current;
const currentPreviewUrl = latestPreviewUrlRef.current;
navigate(`/filelib/adjust/${id}`, {
state: {
@@ -430,46 +437,46 @@ const fetchFileUrl = useCallback(async (fileId) => {
}, [id, selectedFileId, currentFile, navigate]);
// 解析切分策略描述(完全保留原始逻辑)
const splitRuleDesc = useCallback((file) => {
if (!file.split_rule) return '';
const suffix = file.file_name?.split('.').pop()?.toUpperCase() || '';
try {
const rule = JSON.parse(file.split_rule);
const { excel_rule } = rule;
// 处理Excel文件规则
if (excel_rule && ['XLSX', 'XLS', 'CSV'].includes(suffix)) {
return `每 ${excel_rule.slice_length} 行作为一个分段`;
const splitRuleDesc = useCallback((file) => {
if (!file.split_rule) return '';
const suffix = file.file_name?.split('.').pop()?.toUpperCase() || '';
try {
const rule = JSON.parse(file.split_rule);
const { excel_rule } = rule;
// 处理Excel文件规则
if (excel_rule && ['XLSX', 'XLS', 'CSV'].includes(suffix)) {
return `每 ${excel_rule.slice_length} 行作为一个分段`;
}
// 处理分隔符规则
const { separator, separator_rule } = rule;
if (separator && separator_rule && separator.length === separator_rule.length) {
const displayItems = separator.map((sep, index) => {
// 核心修复:将实际换行符转换为可见的 \n 字符串
const displaySep = sep
.replace(/\n/g, '\\n') // 替换换行符
.replace(/\r/g, '\\r') // 替换回车符(可选)
.replace(/\t/g, '\\t'); // 替换制表符(可选)
// 根据规则添加切割符号
const prefix = separator_rule[index] === 'before' ? '✂️' : '';
const suffix = separator_rule[index] === 'after' ? '✂️' : '';
return `${prefix}${displaySep}${suffix}`;
});
return displayItems.join(', ');
}
} catch (e) {
console.error('解析切分策略失败:', e);
}
// 处理分隔符规则
const { separator, separator_rule } = rule;
if (separator && separator_rule && separator.length === separator_rule.length) {
const displayItems = separator.map((sep, index) => {
// 核心修复:将实际换行符转换为可见的 \n 字符串
const displaySep = sep
.replace(/\n/g, '\\n') // 替换换行符
.replace(/\r/g, '\\r') // 替换回车符(可选)
.replace(/\t/g, '\\t'); // 替换制表符(可选)
// 根据规则添加切割符号
const prefix = separator_rule[index] === 'before' ? '✂️' : '';
const suffix = separator_rule[index] === 'after' ? '✂️' : '';
return `${prefix}${displaySep}${suffix}`;
});
return displayItems.join(', ');
}
} catch (e) {
console.error('解析切分策略失败:', e);
}
// 解析失败时的兜底处理
return file.split_rule
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/\t/g, '\\t');
}, []);
// 解析失败时的兜底处理
return file.split_rule
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/\t/g, '\\t');
}, []);
// 删除分段(完全保留原始逻辑)
@@ -544,20 +551,17 @@ const fetchFileUrl = useCallback(async (fileId) => {
const contentLayoutClass = useMemo(() => {
const isSingleVisible = isPreviewVisible !== isParagraphVisible;
if (isSingleVisible) {
return "flex justify-center bg-background-main";
return "flex justify-center bg-background-main min-h-0";
}
if (isPreviewVisible && isParagraphVisible && !isExcelFile) {
return "flex bg-background-main";
}
return "flex bg-background-main";
return "flex bg-background-main min-h-0";
}, [isPreviewVisible, isParagraphVisible, isExcelFile]);
useEffect(() => {
latestFileUrlRef.current = fileUrl;
latestPreviewUrlRef.current = previewUrl;
}, [fileUrl, previewUrl]);
useEffect(() => {
latestFileUrlRef.current = fileUrl;
latestPreviewUrlRef.current = previewUrl;
}, [fileUrl, previewUrl]);
// 渲染部分(完全保留原始样式,无任何修改)
return (
<div className="relative">
<div className="relative flex flex-col h-[calc(100vh-64px)]">
{/* 顶部导航栏 */}
<div className="flex justify-between items-center px-4 pt-4 pb-4">
<div className="min-w-72 max-w-[440px] flex items-center gap-2">
@@ -709,12 +713,13 @@ useEffect(() => {
{/* 分段组件 */}
{isParagraphVisible ? (
<div className={isPreviewVisible ? "w-1/2" : " w-full max-w-3xl"}>
<div className="flex justify-center items-center relative mb-2 text-sm gap-2 p-2 pt-0 ">
<div className="flex justify-center items-center relative text-sm gap-2 p-2 pt-0 ">
<PreviewParagraph
key={`preview-${selectedFileId}-${chunkSwitchTrigger}`}
key={`preview-${selectedFileId}-${chunkSwitchTrigger}`}
fileId={selectedFileId}
previewCount={datalist.length}
edit={isEditable}
className="h-[calc(100vh-206px)]"
fileSuffix={currentFile?.suffix || ''}
loading={loading}
chunks={safeChunks}
@@ -751,7 +756,7 @@ useEffect(() => {
<DialogHeader>
<h3 className="text-lg font-semibold">{t('文档元数据')}</h3>
</DialogHeader>
{console.log(metadataDialog.file,67678)}
{console.log(metadataDialog.file, 67678)}
<div className="grid gap-4 py-4 max-h-[60vh] overflow-y-auto pr-2">
<div className="space-y-2">
{[
@@ -28,8 +28,8 @@ export default function PreviewFile({
setChunks: any;
edit?: boolean;
}) {
console.log(urlState,file,rawFiles,previewUrl,67);
console.log(urlState, file, rawFiles, previewUrl, 67);
const { t } = useTranslation('knowledge')
const MemoizedFileView = React.memo(FileView);
const selectedChunkIndex = useKnowledgeStore((state) => state.selectedChunkIndex);
@@ -67,6 +67,7 @@ export default function PreviewFile({
// 4. 初始化标签数据(完全对齐ParagraphEdit的initData逻辑)
useEffect(() => {
if (selectedChunkIndex === -1) return
setLabelChange(false);
// 仅对非uns类型的非PDF文件清空标签
if (suffix !== 'pdf' && !isUnsType) {
@@ -232,7 +233,7 @@ export default function PreviewFile({
case 'md': return <TxtFileViewer markdown filePath={url} />;
case 'html': return <TxtFileViewer html filePath={url} />;
case 'doc':
case 'docx': return <DocxPreview filePath={previewUrl||url} />;
case 'docx': return <DocxPreview filePath={previewUrl || url} />;
case 'png':
case 'jpg':
case 'jpeg':
@@ -226,7 +226,7 @@ const EditMarkdown = ({ data, active, fileSuffix, onClick, onDel, onChange, onPo
}
// 分段结果列表
export default function PreviewParagraph({ fileId, previewCount, edit, fileSuffix, loading, chunks, onDel, onChange }) {
export default function PreviewParagraph({ fileId, previewCount, edit, fileSuffix, loading, chunks, className, onDel, onChange }) {
const containerRef = useRef(null);
const [visibleItems, setVisibleItems] = useState(10); // 初始加载数量
const loadingRef = useRef(false);
@@ -269,13 +269,13 @@ export default function PreviewParagraph({ fileId, previewCount, edit, fileSuffi
return () => container.removeEventListener('scroll', handleScroll);
}, [chunks.length]);
return <div className=" pt-3 pb-10 relative w-full">
return <div className="pt-3 relative w-full">
{loading && (
<div className="absolute left-0 top-0 z-10 flex h-full w-full items-center justify-center bg-[rgba(255,255,255,0.6)] dark:bg-blur-shared">
<LoadingIcon />
</div>
)}
<div ref={containerRef} className={`${edit ? 'h-[calc(100vh-206px)]' : 'h-[calc(100vh-284px)] mt-9'} overflow-y-auto`}
<div ref={containerRef} className={`${className} overflow-y-auto`}
style={{ scrollbarWidth: 'thin' }}
>
<div className="space-y-6">
@@ -189,6 +189,7 @@ export default function PreviewResult({ showPreview, previewCount, rules, step,
fileSuffix={currentFile?.suffix}
previewCount={previewCount}
edit={step === 3}
className="h-[calc(100vh-284px)]"
loading={loading}
chunks={chunks}
onDel={handleDelete}
@@ -60,10 +60,10 @@ export default function FilesUpload() {
// 步骤3:原文对比回调,控制下一步按钮禁用状态
const handlePreviewResult = (isSuccess) => {
if (currentStep === 3) {
console.log("更新按钮状态:", { isSuccess, currentStep });
setIsNextDisabled(!isSuccess);
}
if (currentStep === 3) {
console.log("更新按钮状态:", { isSuccess, currentStep });
setIsNextDisabled(!isSuccess);
}
};
// 下一步:按当前步骤跳转
@@ -183,9 +183,6 @@ export default function FilesUpload() {
repeatCallBackRef.current();
}));
};
useEffect(() => {
console.log("父组件文件数据:", resultFiles);
}, [resultFiles]);
return (
<div className="relative h-full flex flex-col">
{/* 顶部返回栏 */}
@@ -195,7 +192,7 @@ useEffect(() => {
variant="outline"
size="icon"
className="bg-[#fff] size-8"
onClick={() => navigate(-1)}
onClick={() => navigate(-1)}
>
<ChevronLeft />
</Button>
@@ -213,7 +210,7 @@ useEffect(() => {
{/* 步骤内容区域(正常模式专属步骤) */}
<div className="flex flex-1 overflow-hidden px-4">
<div className="w-full overflow-y-auto">
<div className="h-full py-4">
<div className="h-full">
{/* 步骤1:文件上传(正常模式独有) */}
{currentStep === 1 && (
<FileUploadStep1
@@ -242,7 +239,7 @@ useEffect(() => {
<PreviewResult
rules={segmentRules.rules}
resultFiles={resultFiles}
handlePreviewResult={handlePreviewResult }
handlePreviewResult={handlePreviewResult}
onPrev={handleBack}
onNext={() => {
setCurrentStep(4);
@@ -22,7 +22,7 @@ export default function AppChatDetail() {
const loading = false;
const title = t('log.detailedSession');
const { loadAssistantState, destroy } = useAssistantStore()
const { assistantState, loadAssistantState, destroy } = useAssistantStore()
const { loadHistoryMsg, loadMoreHistoryMsg, changeChatId, clearMsgs } = useMessageStore()
const {
loadHistoryMsg: loadFlowHistoryMsg,
@@ -49,7 +49,7 @@ export default function AppChatDetail() {
}
}, [])
const logo = useAppAvatar(cid, type)
const logo = useAppAvatar(cid, type, assistantState)
return <div>
{loading && <div className="absolute w-full h-full top-0 left-0 flex justify-center items-center z-10 bg-[rgba(255,255,255,0.6)] dark:bg-blur-shared">
@@ -81,7 +81,7 @@ export default function AppChatDetail() {
};
const useAppAvatar = (cid, flowType) => {
const useAppAvatar = (cid, flowType, assistantState) => {
const [logo, setLogo] = useState('')
const [title, setTitle] = useState('')
const loadAppDetail = async () => {
@@ -89,9 +89,14 @@ const useAppAvatar = (cid, flowType) => {
setLogo(lostflow.flow_logo)
setTitle(lostflow.flow_name)
}
useEffect(() => {
loadAppDetail()
if (flowType !== AppNumType.ASSISTANT) {
loadAppDetail()
}
}, [])
return <AppAvator id={title} flowType={flowType} url={logo} className="size-5 min-w-5"></AppAvator>
return flowType === AppNumType.ASSISTANT ?
<AppAvator id={assistantState.name} flowType={flowType} url={assistantState.logo} className="size-5 min-w-5"></AppAvator> :
<AppAvator id={title} flowType={flowType} url={logo} className="size-5 min-w-5"></AppAvator>
}