fix: bug修复

This commit is contained in:
lmr
2025-09-17 16:19:37 +08:00
parent f090087053
commit b32ac6e855
9 changed files with 183 additions and 70 deletions
@@ -199,9 +199,17 @@ export default function AdjustFilesUpload() {
submittingRef.current = true;
setIsSubmitting(true);
/**
* 将 UI 可见的换行转义("\\n")还原为真实换行("\n")
*/
const normalizeSeparators = (arr) =>
(arr || []).map((s) =>
typeof s === 'string' ? s.replace(/\\n/g, '\n') : s
);
const apiConfig = {
knowledge_id: Number(_config.rules.knowledgeId || initFileData.knowledgeId || knowledgeId),
separator: _config.rules.separator,
separator: normalizeSeparators(_config.rules.separator),
separator_rule: _config.rules.separatorRule,
chunk_size: _config.rules.chunkSize,
chunk_overlap: _config.rules.chunkOverlap,
@@ -216,6 +224,8 @@ export default function AdjustFilesUpload() {
captureAndAlertRequestErrorHoc(
rebUploadFile(apiConfig)
.then(res => {
console.log(apiConfig,2323);
// 1. 修复重复文件判断(单个对象处理)
const _repeatFiles = res.status === 3 ? [res] : [];
if (_repeatFiles.length) {
@@ -539,8 +539,8 @@ export default function KnowledgeFile() {
}}
>
<div className="flex items-center gap-2">
<div className="flex items-center justify-center size-12 bg-primary text-white rounded-[4px] w-[40px] h-[40px]">
<BookCopy />
<div className="flex items-center justify-center size-12 bg-primary/60 text-white rounded-[4px] w-[40px] h-[40px]">
<BookCopy className="size-5"/>
</div>
<div>
<div className="truncate max-w-[500px] w-[264px] text-[14px] font-medium pt-2 flex items-center gap-2">
@@ -13,12 +13,12 @@ import { getKnowledgeModelConfig, getModelListApi } from "@/controllers/API/fine
import { captureAndAlertRequestErrorHoc } from "@/controllers/request";
import { useTable } from "@/util/hook";
import { t } from "i18next";
import { Ellipsis, Trash2 } from "lucide-react";
import { Ellipsis, MessageSquare, MessageSquareIcon, MessagesSquare, Trash2 } from "lucide-react";
import { useContext, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { Link, useNavigate } from "react-router-dom";
import index from "../BuildPage/bench/LingSiWork";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu";
import { Select, SelectContent, SelectItem, SelectTrigger } from "@/components/bs-ui/select";
import { QuestionTooltip, TooltipContent, TooltipTrigger } from "@/components/bs-ui/tooltip";
function CreateModal({ datalist, open, setOpen, onLoadEnd }) {
@@ -167,6 +167,8 @@ export default function KnowledgeQa(params) {
const navigate = useNavigate()
const [copyLoadingId, setCopyLoadingId] = useState<string | null>(null);
const [openMenus, setOpenMenus] = useState<Record<string, boolean>>({});
const [selectOpenId, setSelectOpenId] = useState<string | null>(null);
const [modalKey, setModalKey] = useState(0);
const { page, pageSize, data: datalist, total, loading, setPage, search, reload } = useTable({}, (param) => {
return readFileLibDatabase({ ...param, name: param.keyword, type: 1 })
@@ -257,11 +259,9 @@ export default function KnowledgeQa(params) {
{/* 名称+描述单元格:恢复原有气泡结构,确保蓝色生效 */}
<TableCell className="font-medium max-w-[280px]">
<div className="flex items-center gap-2">
<img
src={__APP_ENV__.BASE_URL + "/assets/qa-logo.svg"}
alt="知识库图标"
className="w-[50px] h-[50px] mt-1 flex-shrink-0"
/>
<div className="flex items-center justify-center size-[40px] min-w-[40px] bg-[rgb(255,237,160)] text-white rounded-[4px] ">
<MessagesSquare className="size-5 text-black"/>
</div>
<div className="min-w-0 overflow-visible">
{/* 知识库名称(不变) */}
@@ -298,42 +298,52 @@ export default function KnowledgeQa(params) {
{/* 操作列:修复「按钮移入行不高亮」 */}
<TableCell className="text-right">
{/* 关键:hover:bg-transparent覆盖行的hover:bg-gray-50 */}
<div className="flex items-center justify-end gap-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
className="size-10 px-2 bg-transparent border-none shadow-none hover:bg-gray-300 flex items-center justify-center duration-200 relative"
onClick={(e) => e.stopPropagation()}
>
<Ellipsis size={24} color="#a69ba2" strokeWidth={1.75} />
</button>
</DropdownMenuTrigger>
<div className="flex items-center justify-end gap-2">
<Select
key={`${el.id}-${modalKey}`}
open={selectOpenId === el.id}
onOpenChange={(isOpen) => {
setSelectOpenId(isOpen ? el.id : null);
}}
onValueChange={(selectedValue) => {
setSelectOpenId(null);
console.log("Selected value:", selectedValue, "for qa:", el.id);
<DropdownMenuContent
align="end"
className="rounded-md shadow-lg py-1 border-gray-200 z-[100] border border-transparent"
style={{ backgroundColor: 'white', opacity: 1 }}
onInteractOutside={() => setOpenMenus({})}
>
<DropdownMenuItem
className={`flex items-center gap-2 px-4 py-2 ${(el.copiable || user.role === 'admin')
? 'hover:bg-gray-100 cursor-pointer'
: 'text-gray-400 cursor-not-allowed'
}`}
onClick={(e) => {
e.stopPropagation();
switch (selectedValue) {
case 'delete':
if (el.copiable || user.role === 'admin') {
handleDelete(el.id);
}
}}
break;
}
}}
>
<SelectTrigger
showIcon={false}
onClick={(e) => {
e.stopPropagation();
}}
className="size-10 px-2 bg-transparent border-none shadow-none hover:bg-gray-300 flex items-center justify-center duration-200 relative"
>
<Ellipsis size={24} color="#a69ba2" strokeWidth={1.75} />
</SelectTrigger>
<SelectContent
onClick={(e) => {
e.stopPropagation();
}}
className="z-50"
>
<SelectItem
value="delete"
disabled={!(el.copiable || user.role === 'admin')}
>
<Trash2 className="w-4 h-4" />
{t('delete')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<div className="flex gap-2 items-center">
<Trash2 className="w-4 h-4" />
{t('delete')}
</div>
</SelectItem>
</SelectContent>
</Select>
</div>
</TableCell>
</TableRow>
@@ -28,6 +28,7 @@ interface IProps {
onNext: (step: number, config?: any) => void
onPrev: () => void
isAdjustMode?: boolean
kId?: string | number
}
const enum DisplayModeType {
OnlyTables = 'table',
@@ -86,26 +87,50 @@ const FileUploadStep2 = forwardRef(({ step, resultFiles, isSubmitting, onNext, o
useEffect(() => {
applyRuleRef.current = applyRule;
}, [applyRule]);
// 起始行不能大于结束行校验
// 起始行与切分参数校验
const vildateCell = () => {
// 表头行区间校验
if (applyEachCell
? rules.fileList.some(file => file.excelRule.append_header && Number(file.excelRule.header_start_row) > Number(file.excelRule.header_end_row))
: cellGeneralConfig.append_header && Number(cellGeneralConfig.header_start_row) > Number(cellGeneralConfig.header_end_row)) {
return toast({
toast({
variant: 'warning',
description: '最小行不能大于最大行'
})
return true
}
// 切分长度与重叠长度校验
const chunkSizeNum = Number((rules as any)?.chunkSize ?? (rules as any)?.chunk_size ?? 0)
const chunkOverlapNum = Number((rules as any)?.chunkOverlap ?? (rules as any)?.chunk_overlap ?? 0)
if (!Number.isNaN(chunkSizeNum) && !Number.isNaN(chunkOverlapNum) && chunkOverlapNum > chunkSizeNum) {
toast({
variant: 'warning',
description: '重叠区长度不能大于预期切分长度'
})
return true
}
return false
}
const { toast } = useToast()
const internalHandleNext = () => {
console.log(step, displayStep,'previewCount11');
// 全局校验:无论在哪个分支,都先校验参数
if (vildateCell()) return;
// 规则校验:过滤后若为空或存在空regex,阻止继续
const hasEmptyCustomRule = (strategies || []).some(s => String(s?.regex ?? '') === '');
if (hasEmptyCustomRule) {
toast({ variant: 'warning', description: '自定义规则不能为空' });
return;
}
if (!rules.separator || rules.separator.length === 0) {
toast({ variant: 'warning', description: '请至少添加一个分割规则' });
return;
}
const nextStep = step + 1;
if (step === 2 || displayStep === 2) {
if (vildateCell()) return;
const config = {
applyEachCell,
cellGeneralConfig,
@@ -153,6 +178,15 @@ useEffect(() => {
}, [applyEachCell, cellGeneralConfig, rules]);
const handlePreview = () => {
if (vildateCell()) return
const hasEmptyCustomRule = (strategies || []).some(s => String(s?.regex ?? '') === '');
if (hasEmptyCustomRule) {
toast({ variant: 'warning', description: '自定义规则不能为空' });
return;
}
if (!rules.separator || rules.separator.length === 0) {
toast({ variant: 'warning', description: '请至少添加一个分割规则' });
return;
}
setShowPreview(true)
setPreviewLoading(true);
setPreviewCount(c => c + 1) // 刷新分段
@@ -197,7 +231,7 @@ useEffect(() => {
rules={rules}
setRules={setRules}
strategies={strategies}
setStrategies={setStrategies}
setStrategies={(next) => setStrategies(next)}
showPreview={showPreview}
/>
</TabsContent>
@@ -231,7 +265,7 @@ useEffect(() => {
{/* 原文预览 & 分段预览:预览时占50% */}
{
(showPreview || step === 3) ? (
(showPreview || (step === 3&&!isAdjustMode)) ? (
<div className={cn(
"relative",
// 预览时占50%宽度
@@ -272,7 +306,7 @@ useEffect(() => {
<Button
className="h-8"
// disabled={strategies.length === 0}
disabled={previewFailed || isSubmitting}
disabled={previewFailed || isSubmitting||strategies.length === 0}
onClick={internalHandleNext}
>
{isSubmitting ? (
@@ -290,17 +324,34 @@ export default FileUploadStep2;
const useFileProcessingRules = (initialStrategies, resultFiles, kid, splitRule) => {
// 统一解析 splitRule:可能是字符串或对象
const parsedSplitRule = useMemo(() => {
if (!splitRule) return {} as any;
if (typeof splitRule === 'string') {
try {
const obj = JSON.parse(splitRule);
return obj && typeof obj === 'object' ? obj : {};
} catch (e) {
console.error('splitRule 解析失败:', e);
return {} as any;
}
}
return splitRule || {};
}, [splitRule]);
const [rules, setRules] = useState({
knowledgeId: kid,
fileList: [],
separator: splitRule?.separator || ['\\n\\n', '\\n'],
separatorRule: splitRule?.separator_rule || ['after', 'after'],
chunkSize: splitRule?.chunk_size?.toString() || "1000",
chunkOverlap: splitRule?.chunk_overlap?.toString() || "0",
retainImages: splitRule?.retain_images ?? true,
enableFormula: splitRule?.enable_formula ?? true,
forceOcr: splitRule?.force_ocr ?? true,
pageHeaderFooter: splitRule?.filter_page_header_footer ?? true
// 统一显示:将真实换行转为可见的转义形式
separator: (parsedSplitRule?.separator || ['\\n\\n', '\\n']).map((s) =>
typeof s === 'string' ? s.replace(/\n/g, '\\n') : s
),
separatorRule: parsedSplitRule?.separator_rule || ['after', 'after'],
chunkSize: parsedSplitRule?.chunk_size?.toString() || "1000",
chunkOverlap: parsedSplitRule?.chunk_overlap?.toString() || "0",
retainImages: parsedSplitRule?.retain_images ?? true,
enableFormula: parsedSplitRule?.enable_formula ?? true,
forceOcr: parsedSplitRule?.force_ocr ?? true,
pageHeaderFooter: parsedSplitRule?.filter_page_header_footer ?? true
});
const [applyEachCell, setApplyEachCell] = useState(false);
@@ -316,8 +367,12 @@ const useFileProcessingRules = (initialStrategies, resultFiles, kid, splitRule)
// 根据正则表达式生成策略描述
const getStrategyRuleDescription = (regex) => {
const ruleMap = {
// 转义形式
'\\n\\n': '双换行后切分,用于分隔段落',
'\\n': '单换行后切分,用于分隔普通换行',
// 真实换行字符形式
'\n\n': '双换行后切分,用于分隔段落',
'\n': '单换行后切分,用于分隔普通换行',
'第.{1,3}章': '"第X章"前切分,切分章节等',
'第.{1,3}条': '"第X条"前切分,切分条目等',
'。': '中文句号后切分,中文断句',
@@ -326,21 +381,24 @@ const useFileProcessingRules = (initialStrategies, resultFiles, kid, splitRule)
return ruleMap[regex] || `自定义规则: ${regex}`;
};
const [strategies, setStrategies] = useState(() => {
if (splitRule?.separator && splitRule?.separator_rule) {
// 从 splitRule 初始化策略
return splitRule.separator.map((regex, index) => ({
if (parsedSplitRule?.separator && parsedSplitRule?.separator_rule) {
// 从 parsedSplitRule 初始化策略(将真实换行标准化为可见的转义形式)
return parsedSplitRule.separator.map((regex, index) => ({
id: `strategy-${index}`,
regex,
position: splitRule.separator_rule[index] || 'after',
regex: typeof regex === 'string' ? regex.replace(/\n/g, '\\n') : regex,
position: parsedSplitRule.separator_rule[index] || 'after',
rule: getStrategyRuleDescription(regex) // 根据正则生成描述
}));
}
return initialStrategies;
});
// Update rules when strategies change
// Update rules when strategies change(不过滤只含换行的规则)
useEffect(() => {
const [separator, separatorRule] = strategies.reduce(([_separator, _separatorRule], strategy) => {
const { regex, position } = strategy;
const cleaned = (strategies || []).filter(s => String(s?.regex ?? '') !== '');
const [separator, separatorRule] = cleaned.reduce(([_separator, _separatorRule], strategy) => {
// 统一显示:确保换行以可见转义“\\n”存在,避免 UI 为空
const regex = String(strategy.regex).replace(/\n/g, '\\n');
const position = strategy.position || 'after';
return [[..._separator, regex], [..._separatorRule, position]];
}, [[], []]);
@@ -21,7 +21,7 @@ export default function PreviewFile({
edit = false,
previewUrl
}: {
urlState: { load: false; url: '' };
urlState: { load: boolean; url: string };
file: any;
partitions: Partition;
chunks: any;
@@ -203,7 +203,7 @@ export default function PreviewFile({
const previewFileUrl = targetFile.url;
// 强制uns类型的Excel文件使用pdf预览
const renderType = fileParseType === 'etl4lm' && ['ppt', 'pptx'].includes(suffix)
const renderType = (fileParseType === 'etl4lm'||fileParseType === 'un_etl4lm') && ['ppt', 'pptx'].includes(suffix)
? 'pdf'
: suffix;
// 加载状态处理
@@ -24,6 +24,7 @@ interface IProps {
cellGeneralConfig: any;
handlePreviewResult: (isSuccess: boolean) => void;
kId?: string | number;
showPreview?: boolean;
}
export type Partition = {
[key in string]: { text: string, type: string, part_id: string }
@@ -57,7 +58,7 @@ console.log(showPreview, previewCount, rules, step, applyEachCell, cellGeneralCo
}
return _currentFile
}, [selectId, rules])
const [fileViewUrl, setFileViewUrl] = useState({ load: true, url: '' }) // 当前选择文件预览url
const [fileViewUrl, setFileViewUrl] = useState<{ load: boolean; url: string }>({ load: true, url: '' }) // 当前选择文件预览url
const [loading, setLoading] = useState(false)
const prevPreviewCountMapRef = useRef({})
@@ -82,6 +83,11 @@ console.log(showPreview, previewCount, rules, step, applyEachCell, cellGeneralCo
preview_url = currentFile?.filePath
}
// 将 UI 可见的 "\\n" 还原为真实 "\n"
const normalizeSeparators = (arr) => (arr || []).map((s) =>
typeof s === 'string' ? s.replace(/\\n/g, '\n') : s
);
captureAndAlertRequestErrorHoc(previewFileSplitApi({
// 缓存(修改规则后需要清空缓存, 切换文件使用缓存)
// previewCount变更时为重新预览分段操作,不使用缓存
@@ -93,7 +99,7 @@ console.log(showPreview, previewCount, rules, step, applyEachCell, cellGeneralCo
? currentFile.excelRule
: { ...cellGeneralConfig }
}],
separator,
separator: normalizeSeparators(separator),
separator_rule: separatorRule,
chunk_size: chunkSize,
chunk_overlap: chunkOverlap,
@@ -32,6 +32,17 @@ const getStrategyId = (regexStr, position) => {
return `strategy-${Math.abs(hash)}`;
};
interface RuleFileProps {
rules: any;
setRules: (updater: any) => void;
strategies?: any[];
setStrategies?: (newStrategies: any[]) => void;
originalSplitRule?: any;
setOriginalSplitRule?: (updater: any) => void;
isAdjustMode?: boolean;
showPreview?: boolean;
}
export default function RuleFile({
rules,
setRules,
@@ -41,7 +52,7 @@ export default function RuleFile({
setOriginalSplitRule = () => {},
isAdjustMode = false,
showPreview =false
}) {
}: RuleFileProps) {
console.log(showPreview,199);
const { appConfig } = useContext(locationContext);
@@ -93,6 +93,16 @@ const ItemForm = ({ data, setData }) => {
interface RuleTableProps {
rules: any;
setRules: (updater: any) => void;
applyEachCell: boolean;
setApplyEachCell: (checked: boolean) => void;
cellGeneralConfig: any;
setCellGeneralConfig: (updater: any) => void;
showPreview?: boolean;
}
export default function RuleTable({
rules,
setRules,
@@ -101,7 +111,7 @@ export default function RuleTable({
cellGeneralConfig,
setCellGeneralConfig,
showPreview,
}) {
}: RuleTableProps) {
const { t } = useTranslation('knowledge')
console.log('rules.fileList :>> ', rules.fileList);
@@ -111,10 +111,18 @@ export default function FilesUpload() {
submittingRef.current = true;
setIsSubmitting(true);
/**
* 将 UI 可见的换行转义("\\n")还原为真实换行("\n")
*/
const normalizeSeparators = (arr) =>
(arr || []).map((s) =>
typeof s === 'string' ? s.replace(/\\n/g, '\n') : s
);
// 正常模式API参数格式
const apiConfig = {
knowledge_id: Number(_config.rules.knowledgeId || knowledgeId),
separator: _config.rules.separator,
separator: normalizeSeparators(_config.rules.separator),
separator_rule: _config.rules.separatorRule,
chunk_size: _config.rules.chunkSize,
chunk_overlap: _config.rules.chunkOverlap,