import React, { useState, useEffect, } from "react"; import type { EvaluationPoint, LLMFieldType } from "~/models/evaluation_points"; import { EVALUATION_OPTIONS, VLMFieldType } from "~/models/evaluation_points"; /** * ExtractionSettings 组件 * * 功能: * - 提供三种抽取设置方式:大模型抽取、多模态抽取和正则抽取 * - 允许在三个标签页中添加不同类型的字段 * - 统一的更新机制,确保点击"更新全部字段"按钮时,所有三种类型的字段都会被收集和更新 * * 优化后的交互逻辑: * 1. 用户可以在三个标签页之间切换,在每个标签页中添加对应类型的字段 * 2. 添加字段后,会自动标记为"有未保存更改"状态 * 3. 无论当前在哪个标签页,点击底部的"更新全部字段"按钮都会收集所有三种类型的字段 * 4. 更新成功后会显示详细的字段数量统计信息,包括每种类型的字段数 * 5. 系统会自动检查字段名重复,确保所有字段名唯一 * * 注意: * - 仅当点击"更新全部字段"按钮后,字段才会真正提交给父组件和规则上下文 * - 用户必须手动点击更新按钮,才能在评查设置中使用这些字段 * * 类型定义: * - LogicType: 'and' | 'or' | 'custom' - 用于评查配置中多个规则的组合逻辑 * - 'and': 所有规则都必须满足 * - 'or': 任一规则满足即可 * - 'custom': 自定义逻辑表达式,如 "(规则1 AND 规则2) OR 规则3" * * - LogicOperator: 'and' | 'or' - 用于单个规则内的条件组合 * - 'and': 规则内所有条件都必须满足 * - 'or': 规则内任一条件满足即可 */ /** 获取 LLM 字段名称 */ const getLLMFieldName = (field: LLMFieldType): string => typeof field === 'string' ? field : field.name; /** 获取 LLM 字段的 multi_entity 状态 */ const isLLMFieldMultiEntity = (field: LLMFieldType): boolean => typeof field === 'string' ? false : !!field.multi_entity; interface ExtractionSettingsProps { onChange: (data: Record) => void; initialData: EvaluationPoint; promptTypeOptions?: Array<{ value: string; label: string }>; vlmFieldTypeOptions?: Array<{ value: string; label: string }>; } export function ExtractionSettings({ onChange, initialData, vlmFieldTypeOptions = EVALUATION_OPTIONS.vlmFieldTypeOptions, }: ExtractionSettingsProps) { // 多实体抽取开关状态 const [multiEntityEnabled, setMultiEntityEnabled] = useState( initialData?.extraction_config?.multi_entity?.enabled ?? false ); // 核心数据状态 const [formData, setFormData] = useState({ // 字段配置 extraction_config: { multi_entity: initialData?.extraction_config?.multi_entity ?? { enabled: false, expand_mode: 'awareness', }, llm: initialData?.extraction_config?.llm ?? { fields: [], prompt_setting: { type: "llm_default_prompt", template: "", }, }, vlm: initialData?.extraction_config?.vlm ?? { fields: [], prompt_setting: { type: "vlm_default_prompt", template: "", }, }, regex: initialData?.extraction_config?.regex ?? { fields: [], }, }, }); // 当前选中的标签页 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(() => { // 使用传入的选项中的第一个作为默认值,如果没有则使用 vlm_default_prompt return vlmFieldTypeOptions.length > 0 ? vlmFieldTypeOptions[0].value : 'vlm_default_prompt'; }); // 自定义字段的提示词模板 const [customVlmPrompt, setCustomVlmPrompt] = useState('请识别文档中的印章信息,提取以下字段'); // 提示词类型 const [promptType, setPromptType] = useState({ llm: initialData?.extraction_config?.llm?.prompt_setting?.type || 'llm_default_prompt', vlm: initialData?.extraction_config?.vlm?.prompt_setting?.type || 'vlm_default_prompt' }); // 提示词模板 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); const handleTabChange = (tab: string) => { setCurrentTab(tab); }; // 当 vlmFieldTypeOptions 加载完成时,更新默认选中的类型 useEffect(() => { if (vlmFieldTypeOptions.length > 0 && !vlmFieldTypeOptions.find(opt => opt.value === selectedVlmFieldType)) { // 如果当前选中的类型不在新的选项列表中,选择第一个选项 setSelectedVlmFieldType(vlmFieldTypeOptions[0].value); } }, [vlmFieldTypeOptions, selectedVlmFieldType]); // 初始化自定义字段的提示词 useEffect(() => { // 在编辑模式下,如果有自定义类型的字段,加载其 template const vlmFields = initialData?.extraction_config?.vlm?.fields || []; const customField = vlmFields.find( (f: string | { name: string; type: string; template?: string }) => typeof f === 'object' && f.type === 'custom' && f.template ); if (customField && typeof customField === 'object' && customField.template) { setCustomVlmPrompt(customField.template); } }, [initialData]); // 自动保存字段变更状态 // 这个效果确保添加字段后自动保存到组件状态,但不自动提交更新 useEffect(() => { // 初始加载时不设置hasPendingChanges为true // 仅当用户进行了实际修改后才标记为有变更 const initialLlmFields = initialData?.extraction_config?.llm?.fields || []; const initialVlmFields = initialData?.extraction_config?.vlm?.fields || []; const initialRegexFields = initialData?.extraction_config?.regex?.fields || []; const initialLlmPrompt = initialData?.extraction_config?.llm?.prompt_setting?.template || ''; const initialVlmPrompt = initialData?.extraction_config?.vlm?.prompt_setting?.template || ''; // 检查是否有实际变化 const hasLlmFieldsChanged = JSON.stringify(fields.llm) !== JSON.stringify(initialLlmFields); const hasVlmFieldsChanged = JSON.stringify(fields.vlm) !== JSON.stringify(initialVlmFields); const hasRegexFieldsChanged = JSON.stringify(regexFields) !== JSON.stringify(initialRegexFields); const hasPromptContentChanged = promptContent.llm !== initialLlmPrompt || promptContent.vlm !== initialVlmPrompt; // 只有实际发生变化时才设置为true if (hasLlmFieldsChanged || hasVlmFieldsChanged || hasRegexFieldsChanged || hasPromptContentChanged) { setHasPendingChanges(true); } }, [fields, regexFields, promptContent, initialData]) // 处理字段输入变化 const handleFieldInputChange = ( e: React.ChangeEvent, type: 'llm' | 'vlm' ) => { setInputValue({ ...inputValue, [type]: e.target.value }); }; // 处理添加字段 const addField = (type: 'llm' | 'vlm') => { if (!inputValue[type]) return; // 处理多个字段输入 const inputs = inputValue[type].split(/[,,\s]+/).filter(Boolean); if (type === 'llm') { const newFields = [...fields.llm] as LLMFieldType[]; inputs.forEach(input => { const exists = newFields.some(f => getLLMFieldName(f) === input); if (!exists) { if (multiEntityEnabled) { // 多实体模式:新字段默认 multi_entity=true newFields.push({ name: input, multi_entity: true }); } else { newFields.push(input); } } }); 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 ); if (!exists) { // 如果是自定义类型,添加 template 字段 if (selectedVlmFieldType === 'custom') { newFields.push({ name: input, type: selectedVlmFieldType as VLMFieldType, template: customVlmPrompt, multi_entity: false, }); } else { newFields.push({ name: input, type: selectedVlmFieldType as VLMFieldType, multi_entity: false, }); } } }); setFields({ ...fields, vlm: newFields }); } // 清空输入框 setInputValue({ ...inputValue, [type]: '' }); setHasPendingChanges(true); }; // 处理键盘事件 const handleKeyDown = (e: React.KeyboardEvent, type: 'llm' | 'vlm') => { if (e.key === 'Enter') { e.preventDefault(); addField(type); } }; // 处理删除字段 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); }; // 切换 LLM 字段的多实体状态 const toggleLLMFieldMultiEntity = (index: number) => { if (!multiEntityEnabled) return; // 多实体未开启时不允许切换 const newFields = [...fields.llm] as LLMFieldType[]; const field = newFields[index]; const name = getLLMFieldName(field); const currentMulti = isLLMFieldMultiEntity(field); newFields[index] = { name, multi_entity: !currentMulti }; setFields({ ...fields, llm: newFields }); setHasPendingChanges(true); }; // 切换 VLM 字段的多实体状态 const toggleVLMFieldMultiEntity = (index: number) => { if (!multiEntityEnabled) return; // 多实体未开启时不允许切换 const newFields = [...fields.vlm]; const field = newFields[index]; if (typeof field === 'object') { newFields[index] = { ...field, multi_entity: !field.multi_entity }; setFields({ ...fields, vlm: newFields }); setHasPendingChanges(true); } }; // 获取VLM字段信息 const getFieldInfo = (field: string | { name: string, type: string, template?: string }) => { let fieldName, fieldType, typeName, badgeClass; if (typeof field === 'string') { const parts = field.split('_'); fieldName = parts[0]; fieldType = parts.length > 1 ? parts[1] : 'vlm_default_prompt'; } else { fieldName = field.name; fieldType = field.type; } // 首先尝试从 vlmFieldTypeOptions 中查找对应的标签 const optionItem = vlmFieldTypeOptions.find(opt => opt.value === fieldType); if (optionItem) { typeName = optionItem.label; // 根据不同类型设置不同的颜色 switch (fieldType) { case 'vlm_default_prompt': badgeClass = 'bg-gray-100 text-gray-800'; break; case 'custom': badgeClass = 'bg-indigo-100 text-indigo-800'; break; default: // 对于从数据库获取的类型,使用统一的蓝色系 badgeClass = 'bg-blue-100 text-blue-800'; } } else { // 如果找不到,使用默认值 typeName = '未知类型'; badgeClass = 'bg-gray-100 text-gray-800'; } return { fieldName, fieldType, typeName, badgeClass }; }; // 渲染提示词类型选择 const renderPromptTypeSelect = (value: string, type: 'llm' | 'vlm') => { return ( ); }; // 处理提示词类型变化 const handlePromptTypeChange = ( e: React.ChangeEvent, type: 'llm' | 'vlm' ) => { setPromptType({ ...promptType, [type]: e.target.value }); setHasPendingChanges(true); }; // 处理提示词模板变化 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); }; // 获取模板内容 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 ( ); }; // 添加正则字段行 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() !== ''); // 更新所有自定义类型字段的 template const updatedVlmFields = fields.vlm.map(field => { if (typeof field === 'object' && field.type === 'custom') { return { ...field, template: customVlmPrompt }; } return field; }); // 收集所有字段数据 const updatedFormData = { ...formData, extraction_config: { multi_entity: { enabled: multiEntityEnabled, expand_mode: 'awareness' as const }, llm: { fields: fields.llm, prompt_setting: { type: promptType.llm || 'llm_default_prompt', template: promptType.llm === 'custom' ? promptContent.llm : '' } }, vlm: { fields: updatedVlmFields, prompt_setting: { type: promptType.vlm || 'vlm_default_prompt', template: promptType.vlm === 'custom' ? promptContent.vlm : '' } }, regex: { fields: validRegexFields } } }; // 验证字段唯一性 const allFieldNames = [ ...fields.llm.map(f => getLLMFieldName(f)), ...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); } }; // 处理多实体抽取开关变化 const handleMultiEntityToggle = () => { const newValue = !multiEntityEnabled; setMultiEntityEnabled(newValue); if (newValue) { // 开启:将所有字符串字段转为 dict(默认 multi_entity=true) const converted = fields.llm.map(f => typeof f === 'string' ? { name: f, multi_entity: true } : f ); setFields({ ...fields, llm: converted }); } else { // 关闭:将所有字段转回字符串 const simplified = fields.llm.map(f => getLLMFieldName(f)); setFields({ ...fields, llm: simplified }); } setHasPendingChanges(true); }; return (

抽取设置

{/* 多实体抽取开关 */}
多实体抽取 启用后,点击字段可切换是否按实体展开抽取(绿色=展开)
handleFieldInputChange(e, "llm")} onKeyDown={(e) => handleKeyDown(e, "llm")} autoComplete="off" />
支持一次输入多个字段,用逗号、空格或顿号分隔
{fields.llm.map((field, index) => { const name = getLLMFieldName(field); const isMulti = isLLMFieldMultiEntity(field); return ( ); })}
{renderPromptTypeSelect(promptType.llm, "llm")}
系统将根据评查点类型和抽取目标自动生成适合的提示词,您无需额外配置。

支持的变量 (点击变量将其添加到提示词中):

{[ "docType", "fieldsList", "companyName", "documentId", "date", "industry", "ocrText", ].map((variable) => ( ))}
handleFieldInputChange(e, "vlm")} onKeyDown={(e) => handleKeyDown(e, "vlm")} autoComplete="off" /> {renderVlmFieldTypeSelect()}
请为每个字段选择适当的抽取类型,有助于提高识别准确率
{fields.vlm.map((field, index) => { const { fieldName, fieldType, typeName, badgeClass } = getFieldInfo(field); const isMulti = typeof field === 'object' && field.multi_entity === true; return (
toggleVLMFieldMultiEntity(index)} role={multiEntityEnabled ? 'button' : undefined} title={multiEntityEnabled ? (isMulti ? '点击关闭多实体展开' : '点击开启多实体展开') : fieldName} > {fieldName} {typeName} { e.stopPropagation(); removeField("vlm", index); }} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") removeField("vlm", index); }} role="button" tabIndex={0} aria-label={`删除字段 ${fieldName}`} style={multiEntityEnabled && isMulti ? { color: 'rgba(255,255,255,0.8)' } : undefined} > ×
); })}
{/* 只有当选择了自定义类型时才显示提示词设置 */} {selectedVlmFieldType === 'custom' && (

支持的变量 (点击变量将其添加到提示词中):

{[ "docType", "fieldsList", "companyName", "documentId", "date", "industry", "contentType", "pageRange", "colorMode", "ocrText", ].map((variable) => ( ))}
)}
{regexFields.map((field, index) => (
updateRegexField( index, "field", e.target.value ) } onBlur={() => handleRegexFieldBlur(index, "field") } autoComplete="off" />
updateRegexField( index, "pattern", e.target.value ) } onBlur={() => handleRegexFieldBlur(index, "pattern") } autoComplete="off" /> {statusMessage && statusMessage.id === `field-${index}` && (
{statusMessage.message}
)} {!field.pattern && (
未设置正则表达式,此字段将保留但不会执行抽取
)}
))}
{/* 🔑 只有在添加字段后或本来就有字段时才显示常用正则模板 */} {regexFields.length > 0 && (
{[ { label: "日期格式:yyyy-mm-dd", regex: "\\d{4}[-/年](0?[1-9]|1[0-2])[-/月](0?[1-9]|[12][0-9]|3[01])[日]?", }, { label: "合同编号格式", regex: "[A-Z]{2,5}-\\d{4,10}" }, { label: "金额格式", regex: "(人民币|RMB)?\\s?(\\d{1,3}(,\\d{3})*(\\.\\d{2})?)\\s?[万元]?", }, { label: "座机号码格式", regex: "\\d{3}-\\d{8}|\\d{4}-\\d{7,8}", }, { label: "手机号码格式", regex: "1[3-9]\\d{9}" }, ].map(({ label, regex }) => (
applyRegexTemplate(regex)} role="button" tabIndex={0} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") applyRegexTemplate(regex); }} > {label}
))}
)}
{/* 在所有标签页外部添加统一的更新按钮和状态显示,这样在任何标签页都可见 */}
{hasPendingChanges && !updateStatus && (
您有未更新的字段变更,请点击下方的“更新全部字段”按钮提交所有标签页的字段变更
)} {!hasPendingChanges && !updateStatus && (
提示:请在完成所有标签页的字段编辑后,点击此按钮更新所有字段
)}
{updateStatus && (
{updateStatus.message}
)}
); }