Просмотр исходного кода

🎛️ 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.
t0ng7u 8 месяцев назад
Родитель
Сommit
5367015a31
2 измененных файлов с 71 добавлено и 31 удалено
  1. 3 1
      web/src/i18n/locales/en.json
  2. 68 30
      web/src/pages/Setting/Ratio/ModelSettingsVisualEditor.js

+ 3 - 1
web/src/i18n/locales/en.json

@@ -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"
 }
 }

+ 68 - 30
web/src/pages/Setting/Ratio/ModelSettingsVisualEditor.js

@@ -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) => ({
-        name,
-        price: modelPrice[name] === undefined ? '' : modelPrice[name],
-        ratio: modelRatio[name] === undefined ? '' : modelRatio[name],
-        completionRatio:
-          completionRatio[name] === undefined ? '' : completionRatio[name],
-      }));
+      const modelData = Array.from(modelNames).map((name) => {
+        const price = modelPrice[name] === undefined ? '' : modelPrice[name];
+        const ratio = modelRatio[name] === undefined ? '' : modelRatio[name];
+        const comp = 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) =>
-    searchText
+  const filteredModels = models.filter((model) => {
+    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) =>
-        model.name === name ? { ...model, [field]: value } : model,
-      ),
+      prev.map((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) =>
-          index === existingModelIndex
-            ? {
-              name: values.name,
-              price: values.price || '',
-              ratio: values.ratio || '',
-              completionRatio: values.completionRatio || '',
-            }
-            : model,
-        ),
+        prev.map((model, index) => {
+          if (index !== existingModelIndex) return model;
+          const updated = {
+            name: values.name,
+            price: values.price || '',
+            ratio: values.ratio || '',
+            completionRatio: values.completionRatio || '',
+          };
+          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}