From f99a1f05d460500cbf6a84ab1cfd42eed9e3e5bb Mon Sep 17 00:00:00 2001 From: awen Date: Thu, 10 Apr 2025 02:06:56 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9A=82=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../rules/new/ExtractionSettings.tsx | 1530 +++++------------ app/components/rules/new/ReviewSettings.tsx | 219 ++- app/components/rules/new/SimpleCodeEditor.tsx | 60 +- app/routes/rules.new.tsx | 244 ++- 4 files changed, 759 insertions(+), 1294 deletions(-) diff --git a/app/components/rules/new/ExtractionSettings.tsx b/app/components/rules/new/ExtractionSettings.tsx index bc064c7..350fa6b 100644 --- a/app/components/rules/new/ExtractionSettings.tsx +++ b/app/components/rules/new/ExtractionSettings.tsx @@ -1,42 +1,9 @@ import React, { useState, - KeyboardEvent, - FormEvent, - useContext, useEffect, - useCallback, - useRef, } from "react"; -import { RuleContext } from "~/contexts/RuleContext"; -import type { VLMFieldType } from "~/models/evaluation_points"; - -// 定义通知函数的类型 -type NotifyFn = (data: Record) => void; - -// 添加防抖工具函数,使用简单的函数类型 -const debounce = (fn: NotifyFn, ms = 300): NotifyFn => { - let timeoutId: ReturnType; - return function (data: Record): void { - clearTimeout(timeoutId); - timeoutId = setTimeout(() => fn(data), ms); - }; -}; - -// 添加类型守卫函数 -const isVlmField = (field: string | VlmField): field is VlmField => { - return typeof field !== "string" && "name" in field && "type" in field; -}; - -// 安全处理字段名提取 - 使用不同的函数名避免冲突 -const safeProcessFieldName = (field: string | VlmField): string => { - if (isVlmField(field)) { - return field.name; // 如果是VlmField类型,直接返回name属性 - } else { - // 如果是字符串,假设格式为 name_type,尝试分割 - const parts = field.split("_"); - return parts[0]; // 返回字段名部分 - } -}; +import type { EvaluationPoint } from "~/models/evaluation_points"; +import { EVALUATION_OPTIONS, VLMFieldType } from "~/models/evaluation_points"; /** * ExtractionSettings 组件 @@ -68,1097 +35,465 @@ const safeProcessFieldName = (field: string | VlmField): string => { * - 'or': 规则内任一条件满足即可 */ -interface RegexField { - field: string; - pattern: string; -} - -interface VlmField { - name: string; - type: VLMFieldType | string; -} - -interface PromptTemplate { - id: number; - template_name: string; - template_type: string; - template_content: string; -} - interface ExtractionSettingsProps { - onChange?: (data: Record) => void; - initialData?: { - llm?: { - fields?: string[]; - prompt_setting?: { - type?: string; - template?: string; - }; - }; - vlm?: { - fields?: VlmField[] | string[]; - prompt_setting?: { - type?: string; - template?: string; - }; - }; - regex?: { - fields?: RegexField[]; - }; - }; + onChange: (data: Record) => void; + initialData: EvaluationPoint; promptTypeOptions?: Array<{ value: string; label: string }>; vlmFieldTypeOptions?: Array<{ value: string; label: string }>; } -// 更新类型定义 -type FormDataType = { - fields: { - llm: string[]; - vlm: (string | VlmField)[]; - }; - regexFields: RegexField[]; - promptType: { - llm: string; - vlm: string; - }; - promptContent: { - llm: string; - vlm: string; - }; - selectedTemplate: { - llm: string; - vlm: string; - }; -}; - export function ExtractionSettings({ onChange, initialData, - promptTypeOptions = [], - vlmFieldTypeOptions = [], }: ExtractionSettingsProps) { - const ruleContext = useContext(RuleContext); // 核心数据状态 - const [formData, setFormData] = useState({ + const [formData, setFormData] = useState({ // 字段配置 - fields: { - llm: initialData?.llm?.fields ?? [], - vlm: initialData?.vlm?.fields - ? // 处理两种可能的vlm字段格式 - Array.isArray(initialData.vlm.fields) && - initialData.vlm.fields.length > 0 - ? typeof initialData.vlm.fields[0] === "string" - ? initialData.vlm.fields as string[] - : (initialData.vlm.fields as VlmField[]).map((field) => - field.type && field.type !== "default" - ? `${field.name}_${field.type}` - : field.name - ) - : [] - : [], - }, - // 正则字段配置 - regexFields: (initialData?.regex?.fields && initialData.regex.fields.length > 0) - ? [...initialData.regex.fields] - : [{ field: "", pattern: "" }], - // 提示词配置 - promptType: { - llm: initialData?.llm?.prompt_setting?.type ?? "system", - vlm: initialData?.vlm?.prompt_setting?.type ?? "system", - }, - promptContent: { - llm: initialData?.llm?.prompt_setting?.template ?? "", - vlm: initialData?.vlm?.prompt_setting?.template ?? "", - }, - selectedTemplate: { - llm: "", - vlm: "", + extraction_config: { + llm: initialData?.extraction_config?.llm ?? { + fields: [], + prompt_setting: { + type: "system", + template: "", + }, + }, + vlm: initialData?.extraction_config?.vlm ?? { + fields: [], + prompt_setting: { + type: "system", + template: "", + }, + }, + regex: initialData?.extraction_config?.regex ?? { + fields: [], + }, }, }); - // 为了简化访问,解构出常用字段 - const { fields, regexFields, promptType, promptContent, selectedTemplate } = - formData; + // 当前选中的标签页 + const [currentTab, setCurrentTab] = useState("llm"); + // 字段输入值 + const [inputValue, setInputValue] = useState({ + llm: '', + vlm: '' + }); + // 字段列表 + const [fields, setFields] = useState({ + llm: initialData?.extraction_config?.llm?.fields || [], + vlm: initialData?.extraction_config?.vlm?.fields || [] + }); + // VLM字段类型 + const [selectedVlmFieldType, setSelectedVlmFieldType] = useState('default'); + // 提示词类型 + const [promptType, setPromptType] = useState({ + llm: initialData?.extraction_config?.llm?.prompt_setting?.type || 'system', + vlm: initialData?.extraction_config?.vlm?.prompt_setting?.type || 'system' + }); + // 提示词模板 + const [selectedTemplate, setSelectedTemplate] = useState({ + llm: '', + vlm: '' + }); + // 提示词内容 + const [promptContent, setPromptContent] = useState({ + llm: initialData?.extraction_config?.llm?.prompt_setting?.template || '', + vlm: initialData?.extraction_config?.vlm?.prompt_setting?.template || '' + }); + // 正则表达式字段 + const [regexFields, setRegexFields] = useState( + initialData?.extraction_config?.regex?.fields || [] + ); + // 状态消息 + const [statusMessage, setStatusMessage] = useState<{id: string, message: string} | null>(null); + // 是否有未保存更改 + const [hasPendingChanges, setHasPendingChanges] = useState(false); + // 更新状态 + const [updateStatus, setUpdateStatus] = useState<{success: boolean, message: string} | null>(null); - // UI状态和临时状态 - const [currentTab, setCurrentTab] = useState("llm"); - const [inputValue, setInputValue] = useState({ llm: "", vlm: "" }); - const [selectedFieldType, setSelectedFieldType] = useState("default"); - const [fieldEditStatus, setFieldEditStatus] = useState<{ - [id: string]: boolean; - }>({}); - const [statusMessage, setStatusMessage] = useState<{ - id: string; - message: string; - } | null>(null); - const [updateStatus, setUpdateStatus] = useState<{ - success: boolean; - message: string; - } | null>(null); - const [hasPendingChanges, setHasPendingChanges] = useState(false); - - // 引用值 - const lastUpdateTimeRef = useRef(0); - const lastEventFieldsRef = useRef([]); - const ignoreEmptyFieldsRef = useRef(false); - const debouncedNotifyParentRef = useRef(null); - const activeFieldRef = useRef(null); - const fieldEditResetTimeoutRef = useRef(null); - - // 加载初始数据 - useEffect(() => { - if (initialData) { - const newFields: { llm: string[], vlm: (string | VlmField)[] } = { - llm: initialData.llm?.fields ?? [], - vlm: [], - }; - - // 处理vlm字段 - if (initialData.vlm?.fields) { - // 处理两种可能的格式:字符串数组或对象数组 - if (Array.isArray(initialData.vlm.fields)) { - if (initialData.vlm.fields.length > 0) { - if (typeof initialData.vlm.fields[0] === "string") { - // 如果是字符串数组,直接使用 - newFields.vlm = initialData.vlm.fields as string[]; - } else { - // 如果是对象数组,转换为字符串数组 (name_type 格式) - newFields.vlm = (initialData.vlm.fields as VlmField[]).map( - (field) => - field.type && field.type !== "default" - ? `${field.name}_${field.type}` - : field.name - ); - } - } - } - } - - // 安全地设置表单数据 - setFormData((prev) => ({ - ...prev, - fields: { - ...prev.fields, - llm: newFields.llm, - vlm: newFields.vlm, - }, - promptType: { - llm: initialData.llm?.prompt_setting?.type ?? prev.promptType.llm, - vlm: initialData.vlm?.prompt_setting?.type ?? prev.promptType.vlm, - }, - promptContent: { - llm: initialData.llm?.prompt_setting?.template ?? prev.promptContent.llm, - vlm: initialData.vlm?.prompt_setting?.template ?? prev.promptContent.vlm, - }, - regexFields: initialData.regex?.fields && initialData.regex.fields.length > 0 - ? [...initialData.regex.fields] - : prev.regexFields - })); - } - }, [initialData]); // 只依赖 initialData,避免 ruleContext 导致频繁触发 + const handleTabChange = (tab: string) => { + setCurrentTab(tab); + }; // 自动保存字段变更状态 // 这个效果确保添加字段后自动保存到组件状态,但不自动提交更新 useEffect(() => { - // 仅标记有未保存的更改,不立即触发onChange setHasPendingChanges(true); + }, [fields, regexFields, promptContent]) - // 这些字段变化不会立即反映到父组件,只在点击更新按钮时才会提交 - }, [formData.fields, formData.regexFields]); - - // 独立处理父组件传过来的初始数据 - useEffect(() => { - if (!initialData && ruleContext?.extractionFields?.length > 0) { - setFormData((prev) => ({ - ...prev, - fields: { - ...prev.fields, - [currentTab]: [...ruleContext.extractionFields], - }, - })); - } - }, [ruleContext?.extractionFields, currentTab, initialData]); // 依赖具体属性而非整个 ruleContext - - // 获取所有字段(使用 useCallback 稳定函数引用) - const getAllFields = useCallback(() => { - // 1. 收集大模型抽取字段 - const llm_fields = fields.llm || []; - - // 2. 收集多模态抽取字段(去掉类型后缀) - const vlm_fields = (fields.vlm || []).map(safeProcessFieldName); - - // 3. 收集正则抽取字段(仅保留有效字段) - const regex_fields = regexFields - .filter((field) => field.field && field.field.trim() !== "") - .map((field) => field.field.trim()); - - // 4. 合并所有字段并确保唯一性(使用Set去重) - // 这样即使用户在不同标签页添加了同名字段,最终也只会保留一个 - return [...new Set([...llm_fields, ...vlm_fields, ...regex_fields])]; - }, [fields, regexFields]); - - // 检查字段名是否存在 - const isFieldNameExists = useCallback( - (fieldName: string, excludeId?: string): boolean => { - if (!fieldName || !fieldName.trim()) return false; - - const fieldNameTrimmed = fieldName.trim(); - const fieldNameLower = fieldNameTrimmed.toLowerCase(); - - // 获取所有字段(不包括regexFields,这部分单独处理) - const llm_fields = fields.llm || []; - const vlm_fields = (fields.vlm || []).map(safeProcessFieldName); - - // 检查是否在其他类型字段中存在 - if ( - llm_fields.some((f) => f.toLowerCase() === fieldNameLower) || - vlm_fields.some((f) => f.toLowerCase() === fieldNameLower) - ) { - return true; - } - - // 检查是否在其他正则字段中存在(排除当前正在编辑的字段) - const otherRegexFields = regexFields - .filter((f) => !excludeId || f.field !== excludeId) - .map((f) => (f.field ? f.field.trim() : "")); - - return otherRegexFields.some((f) => f.toLowerCase() === fieldNameLower); - }, - [fields, regexFields] - ); - - // 验证并更新字段的函数 - const validateAndUpdateFields = useCallback(() => { - try { - // 收集所有三种类型的字段,无论当前在哪个标签页 - // 验证正则字段,只需要字段名有值即可,不要求正则表达式必须有值 - const validRegexFields = regexFields.filter( - (field) => field.field && field.field.trim() !== "" - ); - - // 检查字段名称是否重复 - const fieldNames = new Map(); - let hasDuplicates = false; - const duplicateFields: string[] = []; - - // 收集所有字段名 - 不受当前标签页影响,始终收集所有类型的字段 - const allFieldNamesList = [ - ...fields.llm, - ...fields.vlm.map((f) => safeProcessFieldName(f)), - ...validRegexFields.map((f) => f.field.trim()), - ].filter((name) => name); // 过滤空值 - - allFieldNamesList.forEach((name) => { - const lowercaseName = name.toLowerCase(); - fieldNames.set(lowercaseName, (fieldNames.get(lowercaseName) || 0) + 1); - if (fieldNames.get(lowercaseName)! > 1) { - hasDuplicates = true; - duplicateFields.push(name); - } - }); - - if (hasDuplicates) { - setUpdateStatus({ - success: false, - message: `发现重复字段: ${[...new Set(duplicateFields)].join( - ", " - )},请修正后再更新`, - }); - return false; - } - - // 更新有效的字段列表 - 确保获取所有三种类型的字段 - const allFields = getAllFields(); - - // 创建摘要信息,显示所有类型的字段数量 - const llmCount = fields.llm.length; - const vlmCount = fields.vlm.length; - const regexCount = validRegexFields.length; - const totalCount = allFields.length; - - // 更新ruleContext - if (ruleContext?.updateFields) { - ruleContext.updateFields(allFields); - } - - // 触发父组件的onChange回调 - 始终传递所有三种类型的字段数据 - if (onChange) { - onChange({ - fields: { - llm: fields.llm, - vlm: fields.vlm, - }, - regexFields: validRegexFields, - allFields, - pendingUpdate: false, // 标记已完成更新 - }); - } - - // 不再使用自定义事件,统一通过Context共享数据 - - // 更新上次发送的字段列表和时间 - lastEventFieldsRef.current = [...allFields]; - lastUpdateTimeRef.current = Date.now(); - - // 清除待更新状态 - setHasPendingChanges(false); - - // 生成更详细的成功消息,列出每种类型的字段数量 - setUpdateStatus({ - success: true, - message: `已成功更新${totalCount}个字段(大模型字段: ${llmCount},多模态字段: ${vlmCount},正则字段: ${regexCount})`, - }); - - // 3秒后清除更新状态 - setTimeout(() => { - setUpdateStatus(null); - }, 3000); - - return true; - } catch (error) { - console.error("更新字段时出错:", error); - setUpdateStatus({ - success: false, - message: `更新失败: ${ - error instanceof Error ? error.message : "未知错误" - }`, - }); - return false; - } - }, [fields, regexFields, getAllFields, ruleContext, onChange]); - - // 初始化防抖函数 - useEffect(() => { - if (onChange) { - debouncedNotifyParentRef.current = debounce( - (data: Record) => { - onChange(data); - }, - 500 - ); // 500ms的防抖延迟 - } - - return () => { - // 组件卸载时清理 - debouncedNotifyParentRef.current = null; - }; - }, [onChange]); - - // 通知父组件的包装函数,使用防抖 - const notifyParent = useCallback( - (data: Record, immediate = false) => { - if (!onChange) return; - - if (immediate) { - // 对于需要立即响应的操作,直接调用onChange - onChange(data); - } else if (debouncedNotifyParentRef.current) { - // 对于可以延迟处理的操作,使用防抖函数 - debouncedNotifyParentRef.current(data); - } - }, - [onChange] - ); - - // 修改addField函数,使用防抖通知 - const addField = (type: "llm" | "vlm") => { - const value = inputValue[type].trim(); - if (!value) return; - - const newFields = { ...fields }; - - if (type === "llm") { - // 大模型抽取支持一次性添加多个字段 - const fieldsToAdd = value - .split(/[\s、,]+/) - .map((f) => f.trim()) - .filter((f) => f && !isFieldNameExists(f)); - if (fieldsToAdd.length === 0) { - alert("所有字段名已存在,请确保字段名称唯一"); - return; - } - newFields[type] = [...fields[type], ...fieldsToAdd]; - } else { - // 多模态抽取需要添加字段类型后缀 - if (isFieldNameExists(value)) { - alert(`字段名 "${value}" 已存在,请确保字段名称唯一`); - return; - } - newFields[type] = [...fields[type], `${value}_${selectedFieldType}`]; - } - - setFormData((prev) => ({ - ...prev, - fields: newFields, - })); - setInputValue((prev) => ({ ...prev, [type]: "" })); - - // 标记有未保存的更改 - setHasPendingChanges(true); - - // 添加字段后通知父组件,使用防抖 - notifyParent({ - fields: newFields, - pendingUpdate: true, - allFields: getFieldsWithNewAddition( - newFields, - type === "llm" ? value : `${value}_${selectedFieldType}` - ), + // 处理字段输入变化 + const handleFieldInputChange = ( + e: React.ChangeEvent, + type: 'llm' | 'vlm' + ) => { + setInputValue({ + ...inputValue, + [type]: e.target.value }); }; - // 新增辅助函数,计算包含新添加字段的完整字段列表 - const getFieldsWithNewAddition = ( - fieldsObj: { llm: string[]; vlm: (string | VlmField)[] }, - newField: string - ) => { - // 收集大模型抽取字段 - const llm_fields = fieldsObj.llm || []; + // 处理添加字段 + const addField = (type: 'llm' | 'vlm') => { + if (!inputValue[type]) return; - // 收集多模态抽取字段(去掉类型后缀) - const vlm_fields = (fieldsObj.vlm || []).map(safeProcessFieldName); + // 处理多个字段输入 + const inputs = inputValue[type].split(/[,,\s]+/).filter(Boolean); - // 收集正则抽取字段(仅保留有效字段) - const regex_fields = regexFields - .filter((field) => field.field && field.field.trim() !== "") - .map((field) => field.field.trim()); - - // 添加新字段(处理新字段格式) - const newFieldName = newField.split("_")[0]; - - // 合并所有字段并确保唯一性(使用Set去重) - return [ - ...new Set([...llm_fields, ...vlm_fields, ...regex_fields, newFieldName]), - ]; - }; - - // 修改removeField函数,使用防抖通知 - const removeField = (type: "llm" | "vlm", index: number) => { - const newFields = { ...fields }; - - if (type === "llm") { - const tempFields = [...fields.llm]; - const removedField = tempFields[index]; - tempFields.splice(index, 1); - newFields.llm = tempFields; - - setFormData((prev) => ({ - ...prev, - fields: { - ...prev.fields, - llm: tempFields, - }, - })); - - // 删除字段后通知父组件,使用防抖 - notifyParent({ - fields: newFields, - pendingUpdate: true, - allFields: getFieldsWithRemoval(newFields, removedField), - }); - } else { - // vlm类型 - const tempFields = [...fields.vlm]; - const removedField = tempFields[index]; - tempFields.splice(index, 1); - newFields.vlm = tempFields; - - setFormData((prev) => ({ - ...prev, - fields: { - ...prev.fields, - vlm: tempFields, - }, - })); - - // 删除字段后通知父组件,使用防抖 - notifyParent({ - fields: newFields, - pendingUpdate: true, - allFields: getFieldsWithRemoval(newFields, removedField), - }); - } - - // 标记有未保存的更改 - setHasPendingChanges(true); - }; - - // 新增辅助函数,计算移除字段后的完整字段列表 - const getFieldsWithRemoval = ( - fieldsObj: { llm: string[]; vlm: (string | VlmField)[] }, - removedField: string | VlmField - ) => { - // 收集大模型抽取字段 - const llm_fields = fieldsObj.llm || []; - - // 收集多模态抽取字段(去掉类型后缀) - const vlm_fields = (fieldsObj.vlm || []).map(safeProcessFieldName); - - // 收集正则抽取字段(仅保留有效字段) - const regex_fields = regexFields - .filter((field) => field.field && field.field.trim() !== "") - .map((field) => field.field.trim()); - - // 移除字段处理 - const fieldToRemove = safeProcessFieldName(removedField); - - // 合并所有字段并确保唯一性(使用Set去重) - const allFields = [ - ...new Set([...llm_fields, ...vlm_fields, ...regex_fields]), - ]; - return allFields.filter((field) => field !== fieldToRemove); - }; - - // 修改addRegexFieldRow函数,使用防抖通知 - const addRegexFieldRow = () => { - // 使用时间戳和随机数生成唯一ID - const newId = `regex_${Date.now()}_${Math.floor(Math.random() * 100000)}`; - - // 设置标记表示正在添加新字段,临时忽略空字段检查 - ignoreEmptyFieldsRef.current = true; - // 记录正在添加的字段ID - activeFieldRef.current = newId; - // 标记新字段为编辑状态 - setFieldEditStatus((prev) => ({ ...prev, [newId]: true })); - - // 添加空字段但不会立即触发验证和更新 - setFormData((prev) => ({ - ...prev, - regexFields: [...prev.regexFields, { field: "", pattern: "" }], - })); - - // 手动触发一次onChange,确保父组件知道我们添加了新字段 - // 但不触发完整的字段验证和更新,此处立即通知,不使用防抖 - notifyParent( - { - regexFields: [...regexFields, { field: "", pattern: "" }], - pendingUpdate: true, // 标记有待更新的内容 - }, - true - ); - }; - - const removeRegexFieldRow = (id: string) => { - if (regexFields.length <= 1) return; - - // 先保存更新前的状态,以便通知父组件 - const updatedRegexFields = regexFields.filter( - (field) => field.field !== id - ); - - setFormData((prev) => ({ - ...prev, - regexFields: updatedRegexFields, - })); - - // 标记有未保存的更改 - setHasPendingChanges(true); - - // 删除字段后通知父组件,使用立即通知模式确保立即删除 - notifyParent( - { - regexFields: updatedRegexFields, - pendingUpdate: true, - allFields: getAllFields().filter((field) => { - // 找到被删除的字段名 - const deletedField = regexFields.find((f) => f.field === id); - return deletedField ? field !== deletedField.field.trim() : true; - }), - }, - true - ); // 使用立即通知,确保字段立即删除 - }; - - // 修改updateRegexField函数,使用防抖通知 - const updateRegexField = ( - id: string, - key: "field" | "pattern", - value: string - ) => { - // 标记此字段为正在编辑状态 - setFieldEditStatus((prev) => ({ ...prev, [id]: true })); - // 记录当前活动字段ID - activeFieldRef.current = id; - - setFormData((prev) => ({ - ...prev, - regexFields: regexFields.map((field) => - field.field === id ? { ...field, [key]: value } : field - ), - })); - - // 标记有未保存的更改 - setHasPendingChanges(true); - - // 如果状态消息是关于这个字段的,且该字段有内容了,则清除状态消息 - if (statusMessage?.id === id && value.trim() !== "") { - setTimeout(() => { - setStatusMessage(null); - }, 1500); - } - - // 如果用户正在输入,重置编辑状态计时器 - if (fieldEditResetTimeoutRef.current) { - clearTimeout(fieldEditResetTimeoutRef.current); - } - - // 设置一个更长的超时时间,给用户充分的编辑时间 - fieldEditResetTimeoutRef.current = setTimeout(() => { - // 检查字段是否已填写完成 - const currentField = regexFields.find((f) => f.field === id); - if (currentField) { - // 只有当字段名有值时,才考虑将字段标记为完成状态 - if (currentField.field && currentField.field.trim() !== "") { - // 即使正则为空,也不要自动删除字段,只是更新编辑状态 - setFieldEditStatus((prev) => ({ ...prev, [id]: false })); - if (activeFieldRef.current === id) { - activeFieldRef.current = null; - } - - // 如果正则为空,提示用户填写,但不删除字段 - if (!currentField.pattern || currentField.pattern.trim() === "") { - setStatusMessage({ - id, - message: "正则表达式为空,此字段会保留但不会执行抽取。", - }); - - // 5秒后自动隐藏提示 - setTimeout(() => { - setStatusMessage((current) => - current?.id === id ? null : current - ); - }, 5000); - } + if (type === 'llm') { + const newFields = [...fields.llm]; + inputs.forEach(input => { + if (!newFields.includes(input)) { + newFields.push(input); } - } - }, 3600000); // 设置为1小时,确保用户有足够时间完成编辑 - - // 每次字段更新都触发onChange,确保父组件知道字段状态变化,使用防抖 - notifyParent({ - regexFields: regexFields.map((field) => - field.field === id ? { ...field, [key]: value } : field - ), - pendingUpdate: true, - }); - }; - - // 修改handleRegexFieldBlur函数,使用防抖通知 - const handleRegexFieldBlur = (id: string, key: "field" | "pattern") => { - // 如果用户从正则表达式字段离开并且字段名和正则都已填写,则标记字段编辑完成 - const field = regexFields.find((f) => f.field === id); - if (!field) return; - - if (key === "field") { - // 如果字段名为空,不进行任何操作,保留字段 - if (!field.field || field.field.trim() === "") { - return; - } - - // 检查重复字段 - if (isFieldNameExists(field.field, id)) { - alert(`字段名 "${field.field.trim()}" 已存在,请确保字段名称唯一`); - setFormData((prev) => ({ - ...prev, - regexFields: prev.regexFields.map((f) => - f.field === id ? { ...f, field: "" } : f - ), - })); - - // 通知父组件字段已更新,此处立即通知,不使用防抖 - notifyParent( - { - regexFields: regexFields.map((f) => - f.field === id ? { ...f, field: "" } : f - ), - pendingUpdate: true, - }, - true + }); + setFields({ ...fields, llm: newFields }); + } else { + const newFields = [...fields.vlm]; + inputs.forEach(input => { + const exists = newFields.some(field => + typeof field === 'string' + ? field === input + : field.name === input ); - } else if (field.field.trim() !== "") { - // 如果字段名不为空且不重复,通知父组件字段已更新,使用防抖 - notifyParent({ - regexFields: regexFields, - pendingUpdate: true, - }); - } - } else if (key === "pattern") { - // 如果字段名和正则都已填写,标记为完成状态 - if (field.field && field.field.trim() !== "") { - // 即使正则为空,也不要自动删除字段 - setTimeout(() => { - // 只有当正则不为空时,才显示完成提示 - if (field.pattern && field.pattern.trim() !== "") { - setFieldEditStatus((prev) => ({ ...prev, [id]: false })); - if (activeFieldRef.current === id) { - activeFieldRef.current = null; - } - - // 显示完成提示 - setStatusMessage({ - id, - message: "字段配置完成", - }); - - // 2秒后自动隐藏提示 - setTimeout(() => { - setStatusMessage((current) => - current?.id === id ? null : current - ); - }, 2000); - } else { - // 正则为空时,显示提示但不删除字段 - setStatusMessage({ - id, - message: "未设置正则表达式,此字段会保留但不会执行抽取。", - }); - - // 5秒后自动隐藏提示 - setTimeout(() => { - setStatusMessage((current) => - current?.id === id ? null : current - ); - }, 5000); - } - - // 不管正则是否为空,都通知父组件字段已更新,使用防抖 - notifyParent({ - regexFields: regexFields, - pendingUpdate: true, + + if (!exists) { + newFields.push({ + name: input, + type: selectedVlmFieldType as VLMFieldType }); - }, 200); - } - } - }; - - const applyRegexTemplate = (regex: string) => { - const lastField = regexFields[regexFields.length - 1]; - if (lastField) { - updateRegexField(lastField.field, "pattern", regex); - } - }; - - const getFieldInfo = (field: string) => { - const [fieldName, fieldType = "default"] = field.split("_"); - const typeName = - { - default: "默认", - seal: "印章", - "cross-seal": "骑缝章", - handwriting: "手写体", - print: "印刷体", - english: "英文", - number: "数字", - currency: "货币", - }[fieldType] || "默认"; - const badgeClass = - { - default: "bg-blue-100 text-blue-800", - seal: "bg-red-100 text-red-800", - "cross-seal": "bg-red-100 text-red-800", - handwriting: "bg-yellow-100 text-yellow-800", - print: "bg-purple-100 text-purple-800", - english: "bg-indigo-100 text-indigo-800", - number: "bg-gray-100 text-gray-800", - currency: "bg-green-100 text-green-800", - }[fieldType] || "bg-blue-100 text-blue-800"; - return { fieldName, fieldType, typeName, badgeClass }; - }; - - const handleTemplateChange = ( - e: FormEvent, - type: "llm" | "vlm" - ) => { - const value = e.currentTarget.value; - setFormData((prev) => ({ - ...prev, - selectedTemplate: { ...prev.selectedTemplate, [type]: value }, - })); - - if (value) { - const templateData = getPromptTemplateById(Number(value)); - if (templateData) { - let content = templateData.template_content; - if (content.includes("{fieldsList}") && fields[type].length > 0) { - const fieldListStr = - type === "llm" - ? fields[type] - .map((field, idx) => `${idx + 1}. ${field}`) - .join("\n") - : fields[type] - .map((field, idx) => { - const { fieldName, typeName } = getFieldInfo( - typeof field === "string" - ? field - : `${field.name}_${field.type}` - ); - return `${idx + 1}. ${fieldName} (${typeName})`; - }) - .join("\n"); - content = content.replace("{fieldsList}", fieldListStr); } - setFormData((prev) => ({ - ...prev, - promptContent: { ...prev.promptContent, [type]: content }, - })); - // 标记有未保存的更改,但不触发onChange - setHasPendingChanges(true); - } - } else { - setFormData((prev) => ({ - ...prev, - promptContent: { ...prev.promptContent, [type]: "" }, - })); - // 标记有未保存的更改,但不触发onChange - setHasPendingChanges(true); + }); + setFields({ ...fields, vlm: newFields }); } - }; - const handlePromptContentChange = ( - e: FormEvent, - type: "llm" | "vlm" - ) => { - const value = e.currentTarget.value; - setFormData((prev) => ({ - ...prev, - promptContent: { ...prev.promptContent, [type]: value }, - })); - // 标记有未保存的更改,但不触发onChange + // 清空输入框 + setInputValue({ + ...inputValue, + [type]: '' + }); + setHasPendingChanges(true); }; - const applyVariableToPrompt = (variable: string, type: "llm" | "vlm") => { - const textarea = document.getElementById( - type === "llm" ? "llm-prompt-content" : "multimodal-prompt-content" - ) as HTMLTextAreaElement; - if (textarea) { - const start = textarea.selectionStart; - const end = textarea.selectionEnd; - const text = textarea.value; - const newText = - text.substring(0, start) + `{${variable}}` + text.substring(end); - setFormData((prev) => ({ - ...prev, - promptContent: { ...prev.promptContent, [type]: newText }, - })); - setTimeout(() => { - textarea.focus(); - textarea.setSelectionRange( - start + variable.length + 2, - start + variable.length + 2 - ); - }, 0); - // 标记有未保存的更改,但不触发onChange - setHasPendingChanges(true); - } - }; - - const getPromptTemplateById = (id: number): PromptTemplate | null => { - const templates: Record = { - 1: { - id: 1, - template_name: "行政处罚-抽取通用模板", - template_type: "Extraction", - template_content: `你是一个专业的文档信息抽取助手。请从以下{docType}文档中抽取关键信息:\n{fieldsList}\n请将结果以JSON格式输出,包含以上字段。如果某个字段在文档中未找到,则该字段的值设为null。`, - }, - 4: { - id: 4, - template_name: "采购合同-乙方资质抽取", - template_type: "Extraction", - template_content: `你是一个专业的合同信息抽取助手。请从以下{docType}中抽取乙方的资质信息:\n需要抽取的信息包括:\n{fieldsList}\n{companyName}要求所有供应商必须提供完整的资质信息。请将结果以JSON格式输出,包含以上字段。`, - }, - 5: { - id: 5, - template_name: "合同-关键条款抽取", - template_type: "Extraction", - template_content: `请作为{industry}行业的专业合同审核员,从提供的{docType}中提取以下关键条款信息:\n{fieldsList}\n文档ID: {documentId}\n审核日期: {date}\n请以JSON格式输出结果,对于未明确指定的条款需标记为"未明确约定"。`, - }, - 6: { - id: 6, - template_name: "烟草许可证-信息抽取", - template_type: "Extraction", - template_content: `请从下列烟草专卖许可证文件中抽取以下关键信息:\n{fieldsList}\n这些信息将用于{companyName}内部数据库更新。请确保许可证编号和有效期格式准确无误。`, - }, - 7: { - id: 7, - template_name: "多模态-印章识别模板", - template_type: "Multimodal", - template_content: `请识别并提取文档中的所有印章信息,包括:\n{fieldsList}\n文档类型: {docType}\n页面范围: {pageRange}\n请注意区分公章、法人章和合同专用章,并分析印章的清晰度和完整性。`, - }, - 8: { - id: 8, - template_name: "多模态-表格抽取模板", - template_type: "Multimodal", - template_content: `请从文档中的表格提取以下信息:\n{fieldsList}\n文档类型: {docType}\n表格可能跨页,请确保完整提取所有内容。表格中的数值需保留原始精度。`, - }, - 9: { - id: 9, - template_name: "多模态-手写内容识别模板", - template_type: "Multimodal", - template_content: `请识别文档中的手写内容,特别关注:\n{fieldsList}\n文档类型: {docType}\n内容类型: {contentType}\n对于难以辨认的手写内容,请标注为"[难以辨认]"并尽可能给出可能的解读。`, - }, - }; - return templates[id] || null; - }; - - // 修复缺失的handleKeyDown函数 - const handleKeyDown = ( - e: KeyboardEvent, - type: "llm" | "vlm" - ) => { - if (e.key === "Enter") { + // 处理键盘事件 + const handleKeyDown = (e: React.KeyboardEvent, type: 'llm' | 'vlm') => { + if (e.key === 'Enter') { e.preventDefault(); addField(type); } }; - // 修改handleUpdateFields函数,使用立即通知模式 - const handleUpdateFields = () => { - // 只有在点击更新按钮时,才执行验证和向父组件提交数据 - if (validateAndUpdateFields()) { - // 当更新成功时,才传递字段数据到父组件 - // 保留所有有字段名的正则字段,包括那些正则表达式为空的字段 - const validRegexFields = regexFields.filter( - (field) => field.field && field.field.trim() !== "" - ); - - if (onChange) { - // 更新按钮点击时使用立即通知,不使用防抖 - notifyParent( - { - fields: { - llm: fields.llm, - vlm: fields.vlm, - }, - regexFields: validRegexFields, - allFields: getAllFields(), - pendingUpdate: false, // 标记已完成更新 - - // 同时提交提示词设置 - promptType, - promptContent, - promptSettings: { - llm: { - type: promptType.llm, - content: promptContent.llm, - template: selectedTemplate.llm, - }, - vlm: { - type: promptType.vlm, - content: promptContent.vlm, - template: selectedTemplate.vlm, - }, - }, - }, - true - ); - - // 更新完成后,取消所有编辑状态 - setFieldEditStatus({}); - // 清除活动字段引用 - activeFieldRef.current = null; - // 重置待更新状态 - setHasPendingChanges(false); - - // 显示成功提示,包含字段数量统计 - setUpdateStatus({ - success: true, - message: `更新成功!共更新字段 ${getAllFields().length} 个 (大模型: ${ - fields.llm.length - }, 多模态: ${fields.vlm.length}, 正则: ${validRegexFields.length})`, - }); - - // 5秒后自动隐藏成功提示 - setTimeout(() => { - setUpdateStatus(null); - }, 5000); - } + // 处理删除字段 + const removeField = (type: 'llm' | 'vlm', index: number) => { + if (type === 'llm') { + const newFields = [...fields.llm]; + newFields.splice(index, 1); + setFields({ ...fields, llm: newFields }); + } else { + const newFields = [...fields.vlm]; + newFields.splice(index, 1); + setFields({ ...fields, vlm: newFields }); } + + setHasPendingChanges(true); }; - const handleTabChange = (tab: string) => { - setCurrentTab(tab); - - // 不触发父组件的onChange回调,只记录当前标签页,使界面切换 - // onChange?.({ extractionMethod: tab }); + // 获取VLM字段信息 + const getFieldInfo = (fieldString: string) => { + const parts = fieldString.split('_'); + const fieldName = parts[0]; + const fieldType = parts.length > 1 ? parts[1] : 'default'; + + let typeName, badgeClass; + switch (fieldType) { + case 'currency': + typeName = '货币'; + badgeClass = 'bg-green-100 text-green-800'; + break; + case 'print': + typeName = '打印'; + badgeClass = 'bg-blue-100 text-blue-800'; + break; + case 'seal': + typeName = '印章'; + badgeClass = 'bg-red-100 text-red-800'; + break; + case 'cross-seal': + typeName = '骑缝章'; + badgeClass = 'bg-orange-100 text-orange-800'; + break; + case 'english': + typeName = '英文'; + badgeClass = 'bg-purple-100 text-purple-800'; + break; + case 'number': + typeName = '数字'; + badgeClass = 'bg-yellow-100 text-yellow-800'; + break; + case 'handwriting': + typeName = '手写'; + badgeClass = 'bg-pink-100 text-pink-800'; + break; + default: + typeName = '默认'; + badgeClass = 'bg-gray-100 text-gray-800'; + } + + return { fieldName, fieldType, typeName, badgeClass }; }; - const handleFieldInputChange = ( - e: FormEvent, - type: "llm" | "vlm" + // 渲染提示词类型选择 + const renderPromptTypeSelect = (value: string, type: 'llm' | 'vlm') => { + return ( + + ); + }; + + // 处理提示词类型变化 + const handlePromptTypeChange = ( + e: React.ChangeEvent, + type: 'llm' | 'vlm' ) => { - setInputValue({ ...inputValue, [type]: e.currentTarget.value }); + setPromptType({ + ...promptType, + [type]: e.target.value + }); + + setHasPendingChanges(true); }; - const handleFieldTypeChange = (e: FormEvent) => { - setSelectedFieldType(e.currentTarget.value); + // 处理提示词模板变化 + const handleTemplateChange = ( + e: React.ChangeEvent, + type: 'llm' | 'vlm' + ) => { + const templateId = e.target.value; + setSelectedTemplate({ + ...selectedTemplate, + [type]: templateId + }); + + // 这里可以根据模板ID获取模板内容 + const templateContent = getTemplateContent(templateId); + if (templateContent) { + setPromptContent({ + ...promptContent, + [type]: templateContent + }); + } + + setHasPendingChanges(true); }; - // 在渲染选择模态字段类型的下拉列表时使用vlmFieldTypeOptions + // 获取模板内容 + const getTemplateContent = (templateId: string) => { + // 模拟模板内容,实际应从API获取或配置中读取 + const templates: Record = { + '1': '请从以下文档中提取关键信息,以JSON格式返回以下字段:${fieldsList}', + '4': '请从以下采购合同中提取乙方资质信息,包括:${fieldsList}', + '5': '请从以下合同中提取关键条款,包括:${fieldsList}', + '6': '请从以下烟草许可证中提取信息:${fieldsList}', + '7': '请识别文档中的印章信息,提取以下字段:${fieldsList}', + '8': '请从文档表格中提取以下信息:${fieldsList}', + '9': '请识别文档中的手写内容,提取以下字段:${fieldsList}' + }; + + return templates[templateId] || ''; + }; + + // 处理提示词内容变化 + const handlePromptContentChange = ( + e: React.ChangeEvent, + type: 'llm' | 'vlm' + ) => { + setPromptContent({ + ...promptContent, + [type]: e.target.value + }); + + setHasPendingChanges(true); + }; + + // 将变量应用到提示词 + const applyVariableToPrompt = (variable: string, type: 'llm' | 'vlm') => { + const variableText = `\${${variable}}`; + setPromptContent({ + ...promptContent, + [type]: promptContent[type] + variableText + }); + + setHasPendingChanges(true); + }; + + // 渲染VLM字段类型选择 const renderVlmFieldTypeSelect = () => { return ( ); }; - // 在渲染提示词类型的选择器时使用promptTypeOptions - const renderPromptTypeSelect = ( - type: string, - promptTypeKey: "llm" | "vlm" - ) => { - return ( - + // 添加正则字段行 + const addRegexFieldRow = () => { + const newField = { + field: '', + pattern: '' + }; + + setRegexFields([...regexFields, newField]); + setHasPendingChanges(true); + }; + + // 移除正则字段行 + const removeRegexFieldRow = (index: number) => { + const newFields = [...regexFields]; + newFields.splice(index, 1); + setRegexFields(newFields); + + setHasPendingChanges(true); + }; + + // 更新正则字段 + const updateRegexField = (index: number, property: 'field' | 'pattern', value: string) => { + const newFields = [...regexFields]; + newFields[index] = { + ...newFields[index], + [property]: value + }; + + setRegexFields(newFields); + setHasPendingChanges(true); + }; + + // 处理正则字段失去焦点 + const handleRegexFieldBlur = (index: number, property: 'field' | 'pattern') => { + // 显示暂时状态消息 + if (property === 'pattern' && regexFields[index].pattern) { + setStatusMessage({ + id: `field-${index}`, + message: '正则表达式已更新' + }); + + // 3秒后清除消息 + setTimeout(() => { + setStatusMessage(null); + }, 3000); + } + }; + + // 应用正则模板 + const applyRegexTemplate = (regex: string) => { + // 如果有字段,应用到最后一个字段 + if (regexFields.length > 0) { + const lastIndex = regexFields.length - 1; + updateRegexField(lastIndex, 'pattern', regex); + } + + setHasPendingChanges(true); + }; + + // 处理更新全部字段 + const handleUpdateFields = () => { + // 过滤掉没有字段名的正则字段 + const validRegexFields = regexFields.filter(field => field.field.trim() !== ''); + + // 收集所有字段数据 + const updatedFormData = { + ...formData, + extraction_config: { + llm: { + fields: fields.llm, + prompt_setting: { + type: promptType.llm, + template: promptType.llm === 'custom' ? promptContent.llm : '' + } + }, + vlm: { + fields: fields.vlm, + prompt_setting: { + type: promptType.vlm, + template: promptType.vlm === 'custom' ? promptContent.vlm : '' + } + }, + regex: { + fields: validRegexFields + } + } + }; + + // 验证字段唯一性 + const allFieldNames = [ + ...fields.llm, + ...fields.vlm.map(f => typeof f === 'string' ? f : f.name), + ...validRegexFields.map(f => f.field) + ]; + + const duplicates = allFieldNames.filter((item, index) => + allFieldNames.indexOf(item) !== index ); + + if (duplicates.length > 0) { + setUpdateStatus({ + success: false, + message: `发现重复字段名:${duplicates.join(', ')},请修改后再提交` + }); + return; + } + + // 更新数据 + setFormData(updatedFormData); + + // 调用父组件的onChange回调 + if (onChange) { + onChange(updatedFormData); + + // 同时通过RuleContext上下文更新字段列表,确保评查设置组件能立即使用 + if (typeof window !== 'undefined') { + // 使用setTimeout确保在React更新周期之外执行 + setTimeout(() => { + // 触发一个自定义事件,通知RuleContext更新 + const event = new CustomEvent('extraction-fields-updated', { + detail: { fields: allFieldNames } + }); + window.dispatchEvent(event); + }, 0); + } + + // 显示成功消息 + setUpdateStatus({ + success: true, + message: `更新成功! 共更新 ${fields.llm.length} 个大模型字段, ${fields.vlm.length} 个多模态字段, ${validRegexFields.length} 个正则字段` + }); + + // 重置更改状态 + setHasPendingChanges(false); + + // 3秒后清除状态消息 + setTimeout(() => { + setUpdateStatus(null); + }, 3000); + } }; return ( @@ -1215,6 +550,7 @@ export function ExtractionSettings({ value={inputValue.llm} onChange={(e) => handleFieldInputChange(e, "llm")} onKeyDown={(e) => handleKeyDown(e, "llm")} + autoComplete="off" /> diff --git a/app/components/rules/new/ReviewSettings.tsx b/app/components/rules/new/ReviewSettings.tsx index 1e99dd8..f277db9 100644 --- a/app/components/rules/new/ReviewSettings.tsx +++ b/app/components/rules/new/ReviewSettings.tsx @@ -1,14 +1,7 @@ import React, { useState, useEffect, useContext, useCallback, useRef } from 'react'; import { SimpleCodeEditor } from './SimpleCodeEditor'; import { RuleContext } from '~/contexts/RuleContext'; -import { processFieldNames, areArraysDifferent, getArrayDifference, debounce } from '~/utils'; -import type { - Rule as ModelRule, - RuleType as ModelRuleType, - LogicType, - SuggestionMessageType, - PostActionType -} from '~/models/evaluation_points'; +import { processFieldNames, areArraysDifferent, getArrayDifference } from '~/utils'; interface RuleType { id: string; @@ -60,15 +53,6 @@ interface ReviewSettingsProps { export function ReviewSettings({ onChange, initialData, - ruleTypeOptions = [], - logicTypeOptions = [], - logicOperatorOptions = [], - compareMethodOptions = [], - formatTypeOptions = [], - comparisonOperatorOptions = [], - matchTypeOptions = [], - suggestionMessageTypeOptions = [], - postActionOptions = [] }: ReviewSettingsProps) { const [rules, setRules] = useState([ { id: '1', type: '', config: {} } @@ -227,7 +211,7 @@ export function ReviewSettings({ // 只在字段列表实际发生变化时更新 if (areArraysDifferent(uniqueFields, availableFields)) { // 检查删除和新增的字段 - const { added, removed } = getArrayDifference(uniqueFields, availableFields); + const { removed } = getArrayDifference(uniqueFields, availableFields); // 处理删除的字段 if (removed.length > 0) { @@ -467,67 +451,73 @@ export function ReviewSettings({ const handleRuleTypeChange = (id: string, type: string) => { const newRules = rules.map(rule => { if (rule.id === id) { + // 查找原始规则以获取现有配置 + const originalRule = rules.find(r => r.id === id); + const originalConfig = originalRule ? originalRule.config : {}; + // 为新类型初始化配置 let initialConfig: Record = {}; // 如果类型没变,保留原配置 if (type === rule.type) { - initialConfig = { ...rule.config }; + initialConfig = { ...originalConfig }; } else { // 根据类型设置初始配置 switch(type) { case 'exists': initialConfig = { - fields: [], // 使用fields替代selectedFields - logic: 'and', // 使用logic替代existsLogic + fields: Array.isArray(originalConfig.fields) ? originalConfig.fields : [], + logic: originalConfig.logic || originalConfig.logicRelation || 'and', availableFields: availableFields }; break; case 'consistency': initialConfig = { - pairs: [], - logic: 'and', // 使用logic替代logicRelation + pairs: Array.isArray(originalConfig.pairs) ? originalConfig.pairs : [], + logic: originalConfig.logic || originalConfig.logicRelation || 'and', availableFields: availableFields }; break; case 'format': initialConfig = { - field: '', // 用于内部运算 - checkField: '', // 用于UI展示 - formatType: '', - formatParams: '', + field: originalConfig.field || '', + checkField: originalConfig.checkField || originalConfig.field || '', + formatType: originalConfig.formatType || '', + formatParams: originalConfig.formatParams || originalConfig.parameters || '', availableFields: availableFields }; break; case 'logic': initialConfig = { - conditions: [], - logic: 'and', // 使用logic替代logicRelation + conditions: Array.isArray(originalConfig.conditions) ? originalConfig.conditions : [], + logic: originalConfig.logic || originalConfig.logicRelation || 'and', availableFields: availableFields }; break; case 'regex': initialConfig = { - field: '', // 用于内部运算 - checkField: '', // 用于UI展示 - pattern: '', - regexPattern: '', // 用于UI展示 - matchType: 'match', // 默认为必须匹配 + field: originalConfig.field || '', + checkField: originalConfig.checkField || originalConfig.field || '', + pattern: originalConfig.pattern || '', + regexPattern: originalConfig.regexPattern || originalConfig.pattern || '', + matchType: originalConfig.matchType || 'match', availableFields: availableFields }; break; case 'ai': initialConfig = { - model: 'qwen14b', - temperature: 0.1, - prompt: '', + model: originalConfig.model || 'qwen14b', + temperature: typeof originalConfig.temperature === 'number' ? originalConfig.temperature : 0.1, + prompt: originalConfig.prompt || `请判断以下{字段}内容是否符合规范要求,仅回答"符合"或"不符合",并简要说明理由。 + +{字段内容}`, availableFields: availableFields }; break; case 'code': initialConfig = { - language: 'javascript', - code: '', + language: originalConfig.language || 'javascript', + code: originalConfig.code || '', availableFields: availableFields }; break; @@ -595,7 +585,10 @@ export function ReviewSettings({ // 渲染字段标签,确保已选择的字段即使在新的字段列表中不存在也会显示 const renderFieldTags = (ruleId: string, config: Record) => { // 获取规则的当前已选字段 - const selectedFields = Array.isArray(config.selectedFields) ? config.selectedFields as string[] : []; + // 修复:对于exists类型规则,应该使用fields而不是selectedFields + const selectedFields = Array.isArray(config.fields) ? + config.fields as string[] : + (Array.isArray(config.selectedFields) ? config.selectedFields as string[] : []); // 优先使用配置中存储的可用字段,如果没有则使用当前可用字段 const fieldsToRender = Array.isArray(config.availableFields) ? @@ -960,8 +953,15 @@ export function ReviewSettings({ name={`logicRelation_${id}`} className="form-radio" value="and" - checked={!config.logicRelation || config.logicRelation === 'and'} - onChange={(e) => handleRuleConfigChange(id, { logicRelation: e.target.value })} + checked={!config.logic && !config.logicRelation ? true : (config.logic === 'and' || config.logicRelation === 'and')} + onChange={(e) => { + handleRuleConfigChange(id, { + logicRelation: e.target.value, + logic: e.target.value + }); + // 触发配置更新 + generateEvaluationConfig(); + }} /> AND(所有条件都满足) @@ -971,8 +971,15 @@ export function ReviewSettings({ name={`logicRelation_${id}`} className="form-radio" value="or" - checked={config.logicRelation === 'or'} - onChange={(e) => handleRuleConfigChange(id, { logicRelation: e.target.value })} + checked={config.logic === 'or' || config.logicRelation === 'or'} + onChange={(e) => { + handleRuleConfigChange(id, { + logicRelation: e.target.value, + logic: e.target.value + }); + // 触发配置更新 + generateEvaluationConfig(); + }} /> OR(任一条件满足) @@ -1188,9 +1195,12 @@ export function ReviewSettings({ name={`logicRelation_${id}`} className="form-radio" value="and" - checked={!config.logicRelation || config.logicRelation === 'and'} + checked={!config.logic && !config.logicRelation ? true : (config.logic === 'and' || config.logicRelation === 'and')} onChange={(e) => { - handleRuleConfigChange(id, { logicRelation: e.target.value }); + handleRuleConfigChange(id, { + logicRelation: e.target.value, + logic: e.target.value + }); // 触发配置更新 generateEvaluationConfig(); }} @@ -1203,9 +1213,12 @@ export function ReviewSettings({ name={`logicRelation_${id}`} className="form-radio" value="or" - checked={config.logicRelation === 'or'} + checked={config.logic === 'or' || config.logicRelation === 'or'} onChange={(e) => { - handleRuleConfigChange(id, { logicRelation: e.target.value }); + handleRuleConfigChange(id, { + logicRelation: e.target.value, + logic: e.target.value + }); // 触发配置更新 generateEvaluationConfig(); }} @@ -1302,7 +1315,12 @@ export function ReviewSettings({