feat(frontend): field-level multi_entity toggle in extraction settings

- evaluation_points.ts: LLMFieldConfig type, LLMFieldType union,
  VLMField.multi_entity
- ExtractionSettings.tsx:
  - LLM fields render as colored buttons (green=multi_entity, gray=normal)
  - Click to toggle individual field multi_entity when switch is on
  - Toggle switch on: converts all string fields to {name, multi_entity:true}
  - Toggle switch off: converts back to plain strings
  - New fields default to multi_entity:true when switch is on

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-24 19:34:45 +08:00
parent 5d3cb2ba34
commit bc3e26c93e
2 changed files with 86 additions and 26 deletions
+68 -24
View File
@@ -2,7 +2,7 @@ import React, {
useState,
useEffect,
} from "react";
import type { EvaluationPoint } from "~/models/evaluation_points";
import type { EvaluationPoint, LLMFieldType } from "~/models/evaluation_points";
import { EVALUATION_OPTIONS, VLMFieldType } from "~/models/evaluation_points";
/**
@@ -35,6 +35,14 @@ import { EVALUATION_OPTIONS, VLMFieldType } from "~/models/evaluation_points";
* - '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<string, unknown>) => void;
initialData: EvaluationPoint;
@@ -196,10 +204,16 @@ export function ExtractionSettings({
const inputs = inputValue[type].split(/[,\s]+/).filter(Boolean);
if (type === 'llm') {
const newFields = [...fields.llm];
const newFields = [...fields.llm] as LLMFieldType[];
inputs.forEach(input => {
if (!newFields.includes(input)) {
newFields.push(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 });
@@ -263,6 +277,18 @@ export function ExtractionSettings({
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 getFieldInfo = (field: string | { name: string, type: string, template?: string }) => {
let fieldName, fieldType, typeName, badgeClass;
@@ -523,7 +549,7 @@ export function ExtractionSettings({
// 验证字段唯一性
const allFieldNames = [
...fields.llm,
...fields.llm.map(f => getLLMFieldName(f)),
...fields.vlm.map(f => typeof f === 'string' ? f : f.name),
...validRegexFields.map(f => f.field)
];
@@ -579,6 +605,19 @@ export function ExtractionSettings({
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);
};
@@ -595,7 +634,7 @@ export function ExtractionSettings({
<i className="ri-group-line text-lg mr-2 text-gray-600"></i>
<div>
<span className="font-medium text-gray-800"></span>
<span className="text-xs text-gray-500 ml-3">AI感知模式</span>
<span className="text-xs text-gray-500 ml-3">绿=</span>
</div>
</div>
@@ -677,25 +716,30 @@ export function ExtractionSettings({
<div className="form-tip mb-2 text-xs">
</div>
<div className="chips-container" id="fields-container">
{fields.llm.map((field, index) => (
<div className="chip" key={`llm-field-${index}`}>
{field}
<span
className="close-btn"
onClick={() => removeField("llm", index)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ")
removeField("llm", index);
}}
role="button"
tabIndex={0}
aria-label={`删除字段 ${field}`}
<div className="flex flex-wrap gap-2" id="fields-container">
{fields.llm.map((field, index) => {
const name = getLLMFieldName(field);
const isMulti = isLLMFieldMultiEntity(field);
return (
<button
type="button"
key={`llm-field-${index}`}
className={`ant-btn ${multiEntityEnabled && isMulti ? 'ant-btn-primary' : 'ant-btn-default tag-button'}`}
onClick={() => multiEntityEnabled && toggleLLMFieldMultiEntity(index)}
title={multiEntityEnabled ? (isMulti ? '点击关闭多实体展开' : '点击开启多实体展开') : name}
>
×
</span>
</div>
))}
{name}
<span
className="ml-1 cursor-pointer hover:text-red-500"
onClick={(e) => { e.stopPropagation(); removeField("llm", index); }}
role="button"
tabIndex={0}
>
×
</span>
</button>
);
})}
</div>
</div>