diff --git a/app/components/rules/new/ExtractionSettings.tsx b/app/components/rules/new/ExtractionSettings.tsx index 464b8ea..d433f1c 100644 --- a/app/components/rules/new/ExtractionSettings.tsx +++ b/app/components/rules/new/ExtractionSettings.tsx @@ -2,6 +2,18 @@ import { useState, KeyboardEvent, FormEvent, useContext, useEffect, useCallback, import { RuleContext } from '~/contexts/RuleContext'; import { processFieldName } from '~/utils'; +// 定义通知函数的类型 +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); + }; +}; + /** * ExtractionSettings 组件 * @@ -23,9 +35,13 @@ import { processFieldName } from '~/utils'; */ interface RegexField { - id: string; - fieldName: string; - regex: string; + field: string; + pattern: string; +} + +interface VlmField { + name: string; + type: string; } interface PromptTemplate { @@ -38,21 +54,21 @@ interface PromptTemplate { interface ExtractionSettingsProps { onChange?: (data: Record) => void; initialData?: { - llm_ocr?: { + llm?: { fields?: string[]; prompt_setting?: { type?: string; template?: string; }; }; - llm_vl?: { - fields?: string[]; + vlm?: { + fields?: VlmField[] | string[]; prompt_setting?: { type?: string; template?: string; }; }; - ocr_regex?: { + regex?: { fields?: RegexField[]; }; }; @@ -63,21 +79,23 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings const lastUpdateTimeRef = useRef(0); // 添加一个ref来记录上次更新时间 const lastEventFieldsRef = useRef([]); const ignoreEmptyFieldsRef = useRef(false); + // 添加对防抖通知函数的引用,使用具体的函数类型 + const debouncedNotifyParentRef = useRef(null); - const [currentTab, setCurrentTab] = useState('llm_ocr'); - const [fields, setFields] = useState<{ [key: string]: string[] }>({ - llm_ocr: [], + const [currentTab, setCurrentTab] = useState('llm'); + const [fields, setFields] = useState<{ [key: string]: Array }>({ llm: [], + vlm: [], }); const [inputValue, setInputValue] = useState({ - llm_ocr: '', llm: '', + vlm: '', }); const [selectedFieldType, setSelectedFieldType] = useState('default'); - const [regexFields, setRegexFields] = useState([{ id: '1', fieldName: '', regex: '' }]); - const [promptType, setPromptType] = useState({ llm_ocr: 'system', llm: 'system' }); - const [promptContent, setPromptContent] = useState({ llm_ocr: '', llm: '' }); - const [selectedTemplate, setSelectedTemplate] = useState({ llm_ocr: '', llm: '' }); + const [regexFields, setRegexFields] = useState([{ field: '', pattern: '' }]); + const [promptType, setPromptType] = useState({ llm: 'system', vlm: 'system' }); + const [promptContent, setPromptContent] = useState({ llm: '', vlm: '' }); + const [selectedTemplate, setSelectedTemplate] = useState({ llm: '', vlm: '' }); // 添加状态记录每个字段的编辑状态 const [fieldEditStatus, setFieldEditStatus] = useState<{[id: string]: boolean}>({}); @@ -95,27 +113,42 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings // 加载初始数据 useEffect(() => { if (initialData) { - const newFields = { - llm_ocr: initialData.llm_ocr?.fields || [], - llm: initialData.llm_vl?.fields || [], + const newFields: { [key: string]: Array } = { + 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 + ); + } + } + } + } + setFields(newFields); setPromptType({ - llm_ocr: initialData.llm_ocr?.prompt_setting?.type || 'system', - llm: initialData.llm_vl?.prompt_setting?.type || 'system', + llm: initialData.llm?.prompt_setting?.type || 'system', + vlm: initialData.vlm?.prompt_setting?.type || 'system', }); setPromptContent({ - llm_ocr: initialData.llm_ocr?.prompt_setting?.template || '', - llm: initialData.llm_vl?.prompt_setting?.template || '', + llm: initialData.llm?.prompt_setting?.template || '', + vlm: initialData.vlm?.prompt_setting?.template || '', }); - if (initialData.ocr_regex?.fields?.length) { - setRegexFields( - initialData.ocr_regex.fields.map((field: RegexField, index: number) => ({ - id: String(index + 1), - fieldName: field.fieldName || '', - regex: field.regex || '', - })) - ); + if (initialData.regex?.fields?.length) { + setRegexFields(initialData.regex.fields); } } }, [initialData]); // 只依赖 initialData,避免 ruleContext 导致频繁触发 @@ -142,22 +175,22 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings // 获取所有字段(使用 useCallback 稳定函数引用) const getAllFields = useCallback(() => { // 1. 收集大模型抽取字段 - const llm_ocr_fields = fields.llm_ocr || []; + const llm_fields = fields.llm || []; // 2. 收集多模态抽取字段(去掉类型后缀) - const llm_fields = (fields.llm || []).map((field) => { + const vlm_fields = (fields.vlm || []).map((field) => { // 从字段名_类型格式中提取字段名部分 return field.split('_')[0]; }); // 3. 收集正则抽取字段(仅保留有效字段) const regex_fields = regexFields - .filter((field) => field.fieldName && field.fieldName.trim() !== '') - .map((field) => field.fieldName.trim()); + .filter((field) => field.field && field.field.trim() !== '') + .map((field) => field.field.trim()); // 4. 合并所有字段并确保唯一性(使用Set去重) // 这样即使用户在不同标签页添加了同名字段,最终也只会保留一个 - return [...new Set([...llm_ocr_fields, ...llm_fields, ...regex_fields])]; + return [...new Set([...llm_fields, ...vlm_fields, ...regex_fields])]; }, [fields, regexFields]); // 检查字段名是否存在 @@ -169,19 +202,19 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings const fieldNameLower = fieldNameTrimmed.toLowerCase(); // 获取所有字段(不包括regexFields,这部分单独处理) - const llm_ocr_fields = fields.llm_ocr || []; - const llm_fields = (fields.llm || []).map(processFieldName); + const llm_fields = fields.llm || []; + const vlm_fields = (fields.vlm || []).map(processFieldName); // 检查是否在其他类型字段中存在 - if (llm_ocr_fields.some(f => f.toLowerCase() === fieldNameLower) || - llm_fields.some(f => f.toLowerCase() === fieldNameLower)) { + if (llm_fields.some(f => f.toLowerCase() === fieldNameLower) || + vlm_fields.some(f => f.toLowerCase() === fieldNameLower)) { return true; } // 检查是否在其他正则字段中存在(排除当前正在编辑的字段) const otherRegexFields = regexFields - .filter((f) => !excludeId || f.id !== excludeId) - .map((f) => f.fieldName ? f.fieldName.trim() : ''); + .filter((f) => !excludeId || f.field !== excludeId) + .map((f) => f.field ? f.field.trim() : ''); return otherRegexFields.some(f => f.toLowerCase() === fieldNameLower); }, @@ -193,7 +226,7 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings try { // 收集所有三种类型的字段,无论当前在哪个标签页 // 验证正则字段,只需要字段名有值即可,不要求正则表达式必须有值 - const validRegexFields = regexFields.filter(field => field.fieldName && field.fieldName.trim() !== ''); + const validRegexFields = regexFields.filter(field => field.field && field.field.trim() !== ''); // 检查字段名称是否重复 const fieldNames = new Map(); @@ -202,9 +235,9 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings // 收集所有字段名 - 不受当前标签页影响,始终收集所有类型的字段 const allFieldNamesList = [ - ...fields.llm_ocr, - ...fields.llm.map(f => processFieldName(f)), - ...validRegexFields.map(f => f.fieldName.trim()) + ...fields.llm, + ...fields.vlm.map(f => processFieldName(f)), + ...validRegexFields.map(f => f.field.trim()) ].filter(name => name); // 过滤空值 allFieldNamesList.forEach(name => { @@ -228,8 +261,8 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings const allFields = getAllFields(); // 创建摘要信息,显示所有类型的字段数量 - const llmOcrCount = fields.llm_ocr.length; - const llmVlCount = fields.llm.length; + const llmCount = fields.llm.length; + const vlmCount = fields.vlm.length; const regexCount = validRegexFields.length; const totalCount = allFields.length; @@ -242,8 +275,8 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings if (onChange) { onChange({ fields: { - llm_ocr: fields.llm_ocr, - llm: fields.llm + llm: fields.llm, + vlm: fields.vlm }, regexFields: validRegexFields, allFields, @@ -263,7 +296,7 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings // 生成更详细的成功消息,列出每种类型的字段数量 setUpdateStatus({ success: true, - message: `已成功更新${totalCount}个字段(大模型字段: ${llmOcrCount},多模态字段: ${llmVlCount},正则字段: ${regexCount})` + message: `已成功更新${totalCount}个字段(大模型字段: ${llmCount},多模态字段: ${vlmCount},正则字段: ${regexCount})` }); // 3秒后清除更新状态 @@ -282,62 +315,42 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings } }, [fields, regexFields, getAllFields, ruleContext, onChange]); - // 处理更新字段按钮点击 - const handleUpdateFields = () => { - // 只有在点击更新按钮时,才执行验证和向父组件提交数据 - if (validateAndUpdateFields()) { - // 当更新成功时,才传递字段数据到父组件 - const validRegexFields = regexFields.filter(field => field.fieldName && field.fieldName.trim() !== ''); - if (onChange) { - onChange({ - fields: { - llm_ocr: fields.llm_ocr, - llm: fields.llm - }, - regexFields: validRegexFields, - allFields: getAllFields(), - pendingUpdate: false, // 标记已完成更新 - - // 同时提交提示词设置 - promptType, - promptContent, - promptSettings: { - llm_ocr: { - type: promptType.llm_ocr, - content: promptContent.llm_ocr, - template: selectedTemplate.llm_ocr - }, - llm: { - type: promptType.llm, - content: promptContent.llm, - template: selectedTemplate.llm - } - } - }); - } + // 初始化防抖函数 + useEffect(() => { + if (onChange) { + debouncedNotifyParentRef.current = debounce((data: Record) => { + onChange(data); + }, 500); // 500ms的防抖延迟 } - }; - - const handleTabChange = (tab: string) => { - setCurrentTab(tab); - // 不触发父组件的onChange回调,只记录当前标签页,使界面切换 - // onChange?.({ extractionMethod: tab }); - }; + return () => { + // 组件卸载时清理 + debouncedNotifyParentRef.current = null; + }; + }, [onChange]); - const handleFieldInputChange = (e: FormEvent, type: 'llm_ocr' | 'llm') => { - setInputValue({ ...inputValue, [type]: e.currentTarget.value }); - }; + // 通知父组件的包装函数,使用防抖 + const notifyParent = useCallback((data: Record, immediate = false) => { + if (!onChange) return; + + if (immediate) { + // 对于需要立即响应的操作,直接调用onChange + onChange(data); + } else if (debouncedNotifyParentRef.current) { + // 对于可以延迟处理的操作,使用防抖函数 + debouncedNotifyParentRef.current(data); + } + }, [onChange]); - const handleFieldTypeChange = (e: FormEvent) => { - setSelectedFieldType(e.currentTarget.value); - }; - - const addField = (type: 'llm_ocr' | 'llm') => { + // 修改addField函数,使用防抖通知 + const addField = (type: 'llm' | 'vlm') => { const value = inputValue[type].trim(); if (!value) return; - if (type === 'llm_ocr') { + const newFields = { ...fields }; + + if (type === 'llm') { + // 大模型抽取支持一次性添加多个字段 const fieldsToAdd = value .split(/[\s、,]+/) .map((f) => f.trim()) @@ -346,39 +359,100 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings alert('所有字段名已存在,请确保字段名称唯一'); return; } - setFields((prev) => ({ ...prev, [type]: [...prev[type], ...fieldsToAdd] })); + newFields[type] = [...fields[type], ...fieldsToAdd]; } else { + // 多模态抽取需要添加字段类型后缀 if (isFieldNameExists(value)) { alert(`字段名 "${value}" 已存在,请确保字段名称唯一`); return; } - setFields((prev) => ({ ...prev, [type]: [...prev[type], `${value}_${selectedFieldType}`] })); - setSelectedFieldType('default'); + newFields[type] = [...fields[type], `${value}_${selectedFieldType}`]; } + + setFields(newFields); setInputValue((prev) => ({ ...prev, [type]: '' })); // 标记有未保存的更改 setHasPendingChanges(true); - }; - - const handleKeyDown = (e: KeyboardEvent, type: 'llm_ocr' | 'llm') => { - if (e.key === 'Enter') { - e.preventDefault(); - addField(type); - } - }; - - const removeField = (type: 'llm_ocr' | 'llm', index: number) => { - setFields((prev) => { - const newFields = [...prev[type]]; - newFields.splice(index, 1); - return { ...prev, [type]: newFields }; + + // 添加字段后通知父组件,使用防抖 + notifyParent({ + fields: newFields, + pendingUpdate: true, + allFields: getFieldsWithNewAddition(newFields, type === 'llm' ? value : `${value}_${selectedFieldType}`) }); + }; + + // 新增辅助函数,计算包含新添加字段的完整字段列表 + const getFieldsWithNewAddition = (fieldsObj: {[key: string]: string[]}, newField: string) => { + // 收集大模型抽取字段 + const llm_fields = fieldsObj.llm || []; + + // 收集多模态抽取字段(去掉类型后缀) + const vlm_fields = (fieldsObj.vlm || []).map((field) => { + // 从字段名_类型格式中提取字段名部分 + return field.split('_')[0]; + }); + + // 收集正则抽取字段(仅保留有效字段) + 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 }; + const tempFields = [...fields[type]]; + // 保存被删除的字段,以便从allFields中移除 + const removedField = tempFields[index]; + tempFields.splice(index, 1); + newFields[type] = tempFields; + + setFields(newFields); // 标记有未保存的更改 setHasPendingChanges(true); + + // 删除字段后通知父组件,使用防抖 + notifyParent({ + fields: newFields, + pendingUpdate: true, + allFields: getFieldsWithRemoval(newFields, type === 'llm' ? removedField : removedField.split('_')[0]) + }); }; + // 新增辅助函数,计算移除字段后的完整字段列表 + const getFieldsWithRemoval = (fieldsObj: {[key: string]: string[]}, removedField: string) => { + // 收集大模型抽取字段 + const llm_fields = fieldsObj.llm || []; + + // 收集多模态抽取字段(去掉类型后缀) + const vlm_fields = (fieldsObj.vlm || []).map((field) => { + // 从字段名_类型格式中提取字段名部分 + return field.split('_')[0]; + }); + + // 收集正则抽取字段(仅保留有效字段) + const regex_fields = regexFields + .filter((field) => field.field && field.field.trim() !== '') + .map((field) => field.field.trim()); + + // 移除字段处理:如果是多模态字段,提取字段名部分 + const fieldToRemove = removedField.split('_')[0]; + + // 合并所有字段并确保唯一性(使用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)}`; @@ -392,7 +466,7 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings // 添加空字段但不会立即触发验证和更新 setRegexFields(prev => { - const newFields = [...prev, { id: newId, fieldName: '', regex: '' }]; + const newFields = [...prev, { field: '', pattern: '' }]; // 延迟聚焦到新添加的字段 setTimeout(() => { @@ -401,32 +475,56 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings input.focus(); } - // 5秒后重置标记,恢复正常字段检查 + // 设置一个非常长的超时时间,确保字段不会因为空值而被自动删除 + // 用户需要手动点击更新按钮才会处理这些字段 setTimeout(() => { ignoreEmptyFieldsRef.current = false; - }, 5000); // 设置为5秒,给用户足够的时间填写 + }, 3600000); // 设置为1小时,基本上确保用户有足够的时间完成编辑 }, 50); return newFields; }); + + // 手动触发一次onChange,确保父组件知道我们添加了新字段 + // 但不触发完整的字段验证和更新,此处立即通知,不使用防抖 + notifyParent({ + regexFields: [...regexFields, { field: '', pattern: '' }], + pendingUpdate: true // 标记有待更新的内容 + }, true); }; const removeRegexFieldRow = (id: string) => { if (regexFields.length <= 1) return; - setRegexFields((prev) => prev.filter((field) => field.id !== id)); + + // 先保存更新前的状态,以便通知父组件 + const updatedRegexFields = regexFields.filter((field) => field.field !== id); + + setRegexFields(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); // 使用立即通知,确保字段立即删除 }; - const updateRegexField = (id: string, key: 'fieldName' | 'regex', value: string) => { + // 修改updateRegexField函数,使用防抖通知 + const updateRegexField = (id: string, key: 'field' | 'pattern', value: string) => { // 标记此字段为正在编辑状态 setFieldEditStatus(prev => ({ ...prev, [id]: true })); // 记录当前活动字段ID activeFieldRef.current = id; setRegexFields((prev) => - prev.map((field) => (field.id === id ? { ...field, [key]: value } : field)) + prev.map((field) => (field.field === id ? { ...field, [key]: value } : field)) ); // 标记有未保存的更改 @@ -447,18 +545,18 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings // 设置一个更长的超时时间,给用户充分的编辑时间 fieldEditResetTimeoutRef.current = setTimeout(() => { // 检查字段是否已填写完成 - const currentField = regexFields.find(f => f.id === id); + const currentField = regexFields.find(f => f.field === id); if (currentField) { // 只有当字段名有值时,才考虑将字段标记为完成状态 - if (currentField.fieldName && currentField.fieldName.trim() !== '') { + if (currentField.field && currentField.field.trim() !== '') { // 即使正则为空,也不要自动删除字段,只是更新编辑状态 setFieldEditStatus(prev => ({ ...prev, [id]: false })); if (activeFieldRef.current === id) { activeFieldRef.current = null; } - // 如果正则为空,提示用户填写 - if (!currentField.regex || currentField.regex.trim() === '') { + // 如果正则为空,提示用户填写,但不删除字段 + if (!currentField.pattern || currentField.pattern.trim() === '') { setStatusMessage({ id, message: "正则表达式为空,此字段会保留但不会执行抽取。" @@ -471,34 +569,57 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings } } } - }, 180000); // 给用户3分钟的编辑时间,大幅延长以避免过早丢失编辑状态 + }, 3600000); // 设置为1小时,确保用户有足够时间完成编辑 + + // 每次字段更新都触发onChange,确保父组件知道字段状态变化,使用防抖 + notifyParent({ + regexFields: regexFields.map(field => + field.field === id ? { ...field, [key]: value } : field + ), + pendingUpdate: true + }); }; - const handleRegexFieldBlur = (id: string, key: 'fieldName' | 'regex') => { + // 修改handleRegexFieldBlur函数,使用防抖通知 + const handleRegexFieldBlur = (id: string, key: 'field' | 'pattern') => { // 如果用户从正则表达式字段离开并且字段名和正则都已填写,则标记字段编辑完成 - const field = regexFields.find((f) => f.id === id); + const field = regexFields.find((f) => f.field === id); if (!field) return; - if (key === 'fieldName') { + if (key === 'field') { // 如果字段名为空,不进行任何操作,保留字段 - if (!field.fieldName || field.fieldName.trim() === '') { + if (!field.field || field.field.trim() === '') { return; } // 检查重复字段 - if (isFieldNameExists(field.fieldName, id)) { - alert(`字段名 "${field.fieldName.trim()}" 已存在,请确保字段名称唯一`); + if (isFieldNameExists(field.field, id)) { + alert(`字段名 "${field.field.trim()}" 已存在,请确保字段名称唯一`); setRegexFields((prev) => - prev.map((f) => (f.id === id ? { ...f, fieldName: '' } : f)) + prev.map((f) => (f.field === id ? { ...f, field: '' } : f)) ); + + // 通知父组件字段已更新,此处立即通知,不使用防抖 + notifyParent({ + regexFields: regexFields.map(f => + f.field === id ? { ...f, field: '' } : f + ), + pendingUpdate: true + }, true); + } else if (field.field.trim() !== '') { + // 如果字段名不为空且不重复,通知父组件字段已更新,使用防抖 + notifyParent({ + regexFields, + pendingUpdate: true + }); } - } else if (key === 'regex') { + } else if (key === 'pattern') { // 如果字段名和正则都已填写,标记为完成状态 - if (field.fieldName && field.fieldName.trim() !== '') { + if (field.field && field.field.trim() !== '') { // 即使正则为空,也不要自动删除字段 setTimeout(() => { - // 正则有内容,标记为完成状态 - if (field.regex && field.regex.trim() !== '') { + // 只有当正则不为空时,才显示完成提示 + if (field.pattern && field.pattern.trim() !== '') { setFieldEditStatus(prev => ({ ...prev, [id]: false })); if (activeFieldRef.current === id) { activeFieldRef.current = null; @@ -514,7 +635,24 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings 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, + pendingUpdate: true + }); }, 200); } } @@ -523,7 +661,7 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings const applyRegexTemplate = (regex: string) => { const lastField = regexFields[regexFields.length - 1]; if (lastField) { - updateRegexField(lastField.id, 'regex', regex); + updateRegexField(lastField.field, 'pattern', regex); } }; @@ -554,14 +692,14 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings return { fieldName, fieldType, typeName, badgeClass }; }; - const handlePromptTypeChange = (e: FormEvent, type: 'llm_ocr' | 'llm') => { + const handlePromptTypeChange = (e: FormEvent, type: 'llm' | 'vlm') => { const value = e.currentTarget.value; setPromptType((prev) => ({ ...prev, [type]: value })); // 标记有未保存的更改,但不触发onChange setHasPendingChanges(true); }; - const handleTemplateChange = (e: FormEvent, type: 'llm_ocr' | 'llm') => { + const handleTemplateChange = (e: FormEvent, type: 'llm' | 'vlm') => { const value = e.currentTarget.value; setSelectedTemplate((prev) => ({ ...prev, [type]: value })); @@ -571,7 +709,7 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings let content = templateData.template_content; if (content.includes('{fieldsList}') && fields[type].length > 0) { const fieldListStr = - type === 'llm_ocr' + type === 'llm' ? fields[type].map((field, idx) => `${idx + 1}. ${field}`).join('\n') : fields[type] .map((field, idx) => { @@ -592,16 +730,16 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings } }; - const handlePromptContentChange = (e: FormEvent, type: 'llm_ocr' | 'llm') => { + const handlePromptContentChange = (e: FormEvent, type: 'llm' | 'vlm') => { const value = e.currentTarget.value; setPromptContent((prev) => ({ ...prev, [type]: value })); // 标记有未保存的更改,但不触发onChange setHasPendingChanges(true); }; - const applyVariableToPrompt = (variable: string, type: 'llm_ocr' | 'llm') => { + const applyVariableToPrompt = (variable: string, type: 'llm' | 'vlm') => { const textarea = document.getElementById( - type === 'llm_ocr' ? 'llm-prompt-content' : 'multimodal-prompt-content' + type === 'llm' ? 'llm-prompt-content' : 'multimodal-prompt-content' ) as HTMLTextAreaElement; if (textarea) { const start = textarea.selectionStart; @@ -666,6 +804,86 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings return templates[id] || null; }; + // 修复缺失的handleKeyDown函数 + const handleKeyDown = (e: 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 handleTabChange = (tab: string) => { + setCurrentTab(tab); + + // 不触发父组件的onChange回调,只记录当前标签页,使界面切换 + // onChange?.({ extractionMethod: tab }); + }; + + const handleFieldInputChange = (e: FormEvent, type: 'llm' | 'vlm') => { + setInputValue({ ...inputValue, [type]: e.currentTarget.value }); + }; + + const handleFieldTypeChange = (e: FormEvent) => { + setSelectedFieldType(e.currentTarget.value); + }; + return (
@@ -675,22 +893,22 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings
-
+
-