🎛️ feat(web): add “Conflict Rates” filter & highlight in Model Settings Visual Editor (#1286)
Introduce the ability to quickly locate models with conflicting billing configurations. Key points • Added `hasConflict` flag to detect models that define both a fixed price (`ModelPrice`) and any ratio (`ModelRatio` or `CompletionRatio`). • Added “Show Only Conflict Rates” `Checkbox` to toolbar; filtering logic now supports keyword + conflict filtering. • Display a red `Tag` beside the model name when a conflict is detected for immediate visual feedback. • Kept `hasConflict` state in sync during add, update and delete operations. • Imported `Checkbox` and `Tag` from **@douyinfe/semi-ui**. • Minor UI tweaks (circle tag style, margin) for consistency. This enhancement helps administrators swiftly identify and resolve incompatible pricing rules, addressing the need discussed in issue #1286.
This commit is contained in:
@@ -1726,5 +1726,7 @@
|
|||||||
"放大编辑": "Expand editor",
|
"放大编辑": "Expand editor",
|
||||||
"编辑公告内容": "Edit announcement content",
|
"编辑公告内容": "Edit announcement content",
|
||||||
"自适应列表": "Adaptive list",
|
"自适应列表": "Adaptive list",
|
||||||
"紧凑列表": "Compact list"
|
"紧凑列表": "Compact list",
|
||||||
|
"仅显示矛盾倍率": "Only show conflicting ratios",
|
||||||
|
"矛盾": "Conflict"
|
||||||
}
|
}
|
||||||
@@ -8,7 +8,9 @@ import {
|
|||||||
Form,
|
Form,
|
||||||
Space,
|
Space,
|
||||||
RadioGroup,
|
RadioGroup,
|
||||||
Radio
|
Radio,
|
||||||
|
Checkbox,
|
||||||
|
Tag
|
||||||
} from '@douyinfe/semi-ui';
|
} from '@douyinfe/semi-ui';
|
||||||
import {
|
import {
|
||||||
IconDelete,
|
IconDelete,
|
||||||
@@ -30,6 +32,7 @@ export default function ModelSettingsVisualEditor(props) {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [pricingMode, setPricingMode] = useState('per-token'); // 'per-token' or 'per-request'
|
const [pricingMode, setPricingMode] = useState('per-token'); // 'per-token' or 'per-request'
|
||||||
const [pricingSubMode, setPricingSubMode] = useState('ratio'); // 'ratio' or 'token-price'
|
const [pricingSubMode, setPricingSubMode] = useState('ratio'); // 'ratio' or 'token-price'
|
||||||
|
const [conflictOnly, setConflictOnly] = useState(false);
|
||||||
const formRef = useRef(null);
|
const formRef = useRef(null);
|
||||||
const pageSize = 10;
|
const pageSize = 10;
|
||||||
const quotaPerUnit = getQuotaPerUnit();
|
const quotaPerUnit = getQuotaPerUnit();
|
||||||
@@ -47,13 +50,19 @@ export default function ModelSettingsVisualEditor(props) {
|
|||||||
...Object.keys(completionRatio),
|
...Object.keys(completionRatio),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const modelData = Array.from(modelNames).map((name) => ({
|
const modelData = Array.from(modelNames).map((name) => {
|
||||||
name,
|
const price = modelPrice[name] === undefined ? '' : modelPrice[name];
|
||||||
price: modelPrice[name] === undefined ? '' : modelPrice[name],
|
const ratio = modelRatio[name] === undefined ? '' : modelRatio[name];
|
||||||
ratio: modelRatio[name] === undefined ? '' : modelRatio[name],
|
const comp = completionRatio[name] === undefined ? '' : completionRatio[name];
|
||||||
completionRatio:
|
|
||||||
completionRatio[name] === undefined ? '' : completionRatio[name],
|
return {
|
||||||
}));
|
name,
|
||||||
|
price,
|
||||||
|
ratio,
|
||||||
|
completionRatio: comp,
|
||||||
|
hasConflict: price !== '' && (ratio !== '' || comp !== ''),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
setModels(modelData);
|
setModels(modelData);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -69,11 +78,13 @@ export default function ModelSettingsVisualEditor(props) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 在 return 语句之前,先处理过滤和分页逻辑
|
// 在 return 语句之前,先处理过滤和分页逻辑
|
||||||
const filteredModels = models.filter((model) =>
|
const filteredModels = models.filter((model) => {
|
||||||
searchText
|
const keywordMatch = searchText
|
||||||
? model.name.toLowerCase().includes(searchText.toLowerCase())
|
? model.name.toLowerCase().includes(searchText.toLowerCase())
|
||||||
: true,
|
: true;
|
||||||
);
|
const conflictMatch = conflictOnly ? model.hasConflict : true;
|
||||||
|
return keywordMatch && conflictMatch;
|
||||||
|
});
|
||||||
|
|
||||||
// 然后基于过滤后的数据计算分页数据
|
// 然后基于过滤后的数据计算分页数据
|
||||||
const pagedData = getPagedData(filteredModels, currentPage, pageSize);
|
const pagedData = getPagedData(filteredModels, currentPage, pageSize);
|
||||||
@@ -152,6 +163,16 @@ export default function ModelSettingsVisualEditor(props) {
|
|||||||
title: t('模型名称'),
|
title: t('模型名称'),
|
||||||
dataIndex: 'name',
|
dataIndex: 'name',
|
||||||
key: 'name',
|
key: 'name',
|
||||||
|
render: (text, record) => (
|
||||||
|
<span>
|
||||||
|
{text}
|
||||||
|
{record.hasConflict && (
|
||||||
|
<Tag color='red' shape='circle' className='ml-2'>
|
||||||
|
{t('矛盾')}
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t('模型固定价格'),
|
title: t('模型固定价格'),
|
||||||
@@ -219,9 +240,13 @@ export default function ModelSettingsVisualEditor(props) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setModels((prev) =>
|
setModels((prev) =>
|
||||||
prev.map((model) =>
|
prev.map((model) => {
|
||||||
model.name === name ? { ...model, [field]: value } : model,
|
if (model.name !== name) return model;
|
||||||
),
|
const updated = { ...model, [field]: value };
|
||||||
|
updated.hasConflict =
|
||||||
|
updated.price !== '' && (updated.ratio !== '' || updated.completionRatio !== '');
|
||||||
|
return updated;
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -296,16 +321,18 @@ export default function ModelSettingsVisualEditor(props) {
|
|||||||
if (existingModelIndex >= 0) {
|
if (existingModelIndex >= 0) {
|
||||||
// Update existing model
|
// Update existing model
|
||||||
setModels((prev) =>
|
setModels((prev) =>
|
||||||
prev.map((model, index) =>
|
prev.map((model, index) => {
|
||||||
index === existingModelIndex
|
if (index !== existingModelIndex) return model;
|
||||||
? {
|
const updated = {
|
||||||
name: values.name,
|
name: values.name,
|
||||||
price: values.price || '',
|
price: values.price || '',
|
||||||
ratio: values.ratio || '',
|
ratio: values.ratio || '',
|
||||||
completionRatio: values.completionRatio || '',
|
completionRatio: values.completionRatio || '',
|
||||||
}
|
};
|
||||||
: model,
|
updated.hasConflict =
|
||||||
),
|
updated.price !== '' && (updated.ratio !== '' || updated.completionRatio !== '');
|
||||||
|
return updated;
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
showSuccess(t('更新成功'));
|
showSuccess(t('更新成功'));
|
||||||
@@ -317,15 +344,17 @@ export default function ModelSettingsVisualEditor(props) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setModels((prev) => [
|
setModels((prev) => {
|
||||||
{
|
const newModel = {
|
||||||
name: values.name,
|
name: values.name,
|
||||||
price: values.price || '',
|
price: values.price || '',
|
||||||
ratio: values.ratio || '',
|
ratio: values.ratio || '',
|
||||||
completionRatio: values.completionRatio || '',
|
completionRatio: values.completionRatio || '',
|
||||||
},
|
};
|
||||||
...prev,
|
newModel.hasConflict =
|
||||||
]);
|
newModel.price !== '' && (newModel.ratio !== '' || newModel.completionRatio !== '');
|
||||||
|
return [newModel, ...prev];
|
||||||
|
});
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
showSuccess(t('添加成功'));
|
showSuccess(t('添加成功'));
|
||||||
}
|
}
|
||||||
@@ -427,6 +456,15 @@ export default function ModelSettingsVisualEditor(props) {
|
|||||||
}}
|
}}
|
||||||
style={{ width: 200 }}
|
style={{ width: 200 }}
|
||||||
/>
|
/>
|
||||||
|
<Checkbox
|
||||||
|
checked={conflictOnly}
|
||||||
|
onChange={(e) => {
|
||||||
|
setConflictOnly(e.target.checked);
|
||||||
|
setCurrentPage(1);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('仅显示矛盾倍率')}
|
||||||
|
</Checkbox>
|
||||||
</Space>
|
</Space>
|
||||||
<Table
|
<Table
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
|||||||
Reference in New Issue
Block a user