From 59a76b3970b3ed6d23c2a7a16f52f4618daba38d Mon Sep 17 00:00:00 2001 From: t0ng7u Date: Thu, 24 Jul 2025 03:25:57 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20Add=20endpoint=20type=20fil?= =?UTF-8?q?ter=20to=20model=20pricing=20system?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create PricingEndpointTypes.jsx component for endpoint type filtering - Add filterEndpointType state management in useModelPricingData hook - Integrate endpoint type filtering logic in filteredModels computation - Update PricingSidebar.jsx to include endpoint type filter component - Update PricingFilterModal.jsx to support endpoint type filtering on mobile - Extend resetPricingFilters utility function to include endpoint type reset - Support filtering models by endpoint types (OpenAI, Anthropic, Gemini, etc.) - Display model count for each endpoint type with localized labels - Ensure filter state resets to first page when endpoint type changes This enhancement allows users to filter models by their supported endpoint types, providing more granular control over model selection in the pricing interface. --- .../filter/PricingEndpointTypes.jsx | 92 +++++++++++++++++++ .../model-pricing/layout/PricingSidebar.jsx | 12 +++ .../modal/PricingFilterModal.jsx | 12 +++ web/src/helpers/utils.js | 6 ++ .../model-pricing/useModelPricingData.js | 15 ++- 5 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 web/src/components/table/model-pricing/filter/PricingEndpointTypes.jsx diff --git a/web/src/components/table/model-pricing/filter/PricingEndpointTypes.jsx b/web/src/components/table/model-pricing/filter/PricingEndpointTypes.jsx new file mode 100644 index 00000000..d9f22d95 --- /dev/null +++ b/web/src/components/table/model-pricing/filter/PricingEndpointTypes.jsx @@ -0,0 +1,92 @@ +/* +Copyright (C) 2025 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ + +import React from 'react'; +import SelectableButtonGroup from '../../../common/ui/SelectableButtonGroup'; + +/** + * 端点类型筛选组件 + * @param {string|'all'} filterEndpointType 当前值 + * @param {Function} setFilterEndpointType setter + * @param {Array} models 模型列表 + * @param {boolean} loading 是否加载中 + * @param {Function} t i18n + */ +const PricingEndpointTypes = ({ filterEndpointType, setFilterEndpointType, models = [], loading = false, t }) => { + // 获取所有可用的端点类型 + const getAllEndpointTypes = () => { + const endpointTypes = new Set(); + models.forEach(model => { + if (model.supported_endpoint_types && Array.isArray(model.supported_endpoint_types)) { + model.supported_endpoint_types.forEach(endpoint => { + endpointTypes.add(endpoint); + }); + } + }); + return Array.from(endpointTypes).sort(); + }; + + // 计算每个端点类型的模型数量 + const getEndpointTypeCount = (endpointType) => { + if (endpointType === 'all') { + return models.length; + } + return models.filter(model => + model.supported_endpoint_types && + model.supported_endpoint_types.includes(endpointType) + ).length; + }; + + // 端点类型显示名称映射 + const getEndpointTypeLabel = (endpointType) => { + const labelMap = { + 'openai': 'OpenAI', + 'openai-response': 'OpenAI Response', + 'anthropic': 'Anthropic', + 'gemini': 'Gemini', + 'jina-rerank': 'Jina Rerank', + 'image-generation': t('图像生成'), + }; + return labelMap[endpointType] || endpointType; + }; + + const availableEndpointTypes = getAllEndpointTypes(); + + const items = [ + { value: 'all', label: t('全部端点'), tagCount: getEndpointTypeCount('all') }, + ...availableEndpointTypes.map(endpointType => ({ + value: endpointType, + label: getEndpointTypeLabel(endpointType), + tagCount: getEndpointTypeCount(endpointType) + })) + ]; + + return ( + + ); +}; + +export default PricingEndpointTypes; \ No newline at end of file diff --git a/web/src/components/table/model-pricing/layout/PricingSidebar.jsx b/web/src/components/table/model-pricing/layout/PricingSidebar.jsx index 8b4ccfd8..f503e246 100644 --- a/web/src/components/table/model-pricing/layout/PricingSidebar.jsx +++ b/web/src/components/table/model-pricing/layout/PricingSidebar.jsx @@ -22,6 +22,7 @@ import { Button } from '@douyinfe/semi-ui'; import PricingCategories from '../filter/PricingCategories'; import PricingGroups from '../filter/PricingGroups'; import PricingQuotaTypes from '../filter/PricingQuotaTypes'; +import PricingEndpointTypes from '../filter/PricingEndpointTypes'; import PricingDisplaySettings from '../filter/PricingDisplaySettings'; import { resetPricingFilters } from '../../../../helpers/utils'; @@ -40,6 +41,8 @@ const PricingSidebar = ({ setFilterGroup, filterQuotaType, setFilterQuotaType, + filterEndpointType, + setFilterEndpointType, currentPage, setCurrentPage, loading, @@ -58,6 +61,7 @@ const PricingSidebar = ({ setViewMode, setFilterGroup, setFilterQuotaType, + setFilterEndpointType, setCurrentPage, }); @@ -114,6 +118,14 @@ const PricingSidebar = ({ loading={loading} t={t} /> + + ); }; diff --git a/web/src/components/table/model-pricing/modal/PricingFilterModal.jsx b/web/src/components/table/model-pricing/modal/PricingFilterModal.jsx index 3d0601b8..ff8459d4 100644 --- a/web/src/components/table/model-pricing/modal/PricingFilterModal.jsx +++ b/web/src/components/table/model-pricing/modal/PricingFilterModal.jsx @@ -22,6 +22,7 @@ import { Modal, Button } from '@douyinfe/semi-ui'; import PricingCategories from '../filter/PricingCategories'; import PricingGroups from '../filter/PricingGroups'; import PricingQuotaTypes from '../filter/PricingQuotaTypes'; +import PricingEndpointTypes from '../filter/PricingEndpointTypes'; import PricingDisplaySettings from '../filter/PricingDisplaySettings'; import { resetPricingFilters } from '../../../../helpers/utils'; @@ -46,6 +47,8 @@ const PricingFilterModal = ({ setFilterGroup, filterQuotaType, setFilterQuotaType, + filterEndpointType, + setFilterEndpointType, currentPage, setCurrentPage, loading, @@ -63,6 +66,7 @@ const PricingFilterModal = ({ setViewMode, setFilterGroup, setFilterQuotaType, + setFilterEndpointType, setCurrentPage, }); @@ -133,6 +137,14 @@ const PricingFilterModal = ({ loading={loading} t={t} /> + + ); diff --git a/web/src/helpers/utils.js b/web/src/helpers/utils.js index 22b4fbc6..9972fb3a 100644 --- a/web/src/helpers/utils.js +++ b/web/src/helpers/utils.js @@ -682,6 +682,7 @@ export const resetPricingFilters = ({ setViewMode, setFilterGroup, setFilterQuotaType, + setFilterEndpointType, setCurrentPage, }) => { // 重置搜索 @@ -728,6 +729,11 @@ export const resetPricingFilters = ({ setFilterQuotaType('all'); } + // 重置端点类型筛选 + if (typeof setFilterEndpointType === 'function') { + setFilterEndpointType('all'); + } + // 重置当前页面 if (typeof setCurrentPage === 'function') { setCurrentPage(1); diff --git a/web/src/hooks/model-pricing/useModelPricingData.js b/web/src/hooks/model-pricing/useModelPricingData.js index c32ddf84..6d750b87 100644 --- a/web/src/hooks/model-pricing/useModelPricingData.js +++ b/web/src/hooks/model-pricing/useModelPricingData.js @@ -35,6 +35,7 @@ export const useModelPricingData = () => { const [filterGroup, setFilterGroup] = useState('all'); // 用于 Table 的可用分组筛选,“all” 表示不过滤 const [filterQuotaType, setFilterQuotaType] = useState('all'); // 计费类型筛选: 'all' | 0 | 1 const [activeKey, setActiveKey] = useState('all'); + const [filterEndpointType, setFilterEndpointType] = useState('all'); // 端点类型筛选: 'all' | string const [pageSize, setPageSize] = useState(10); const [currentPage, setCurrentPage] = useState(1); const [currency, setCurrency] = useState('USD'); @@ -93,6 +94,14 @@ export const useModelPricingData = () => { result = result.filter(model => model.quota_type === filterQuotaType); } + // 端点类型筛选 + if (filterEndpointType !== 'all') { + result = result.filter(model => + model.supported_endpoint_types && + model.supported_endpoint_types.includes(filterEndpointType) + ); + } + // 搜索筛选 if (searchValue.length > 0) { const searchTerm = searchValue.toLowerCase(); @@ -102,7 +111,7 @@ export const useModelPricingData = () => { } return result; - }, [activeKey, models, searchValue, filterGroup, filterQuotaType]); + }, [activeKey, models, searchValue, filterGroup, filterQuotaType, filterEndpointType]); const rowSelection = useMemo( () => ({ @@ -216,7 +225,7 @@ export const useModelPricingData = () => { // 当筛选条件变化时重置到第一页 useEffect(() => { setCurrentPage(1); - }, [activeKey, filterGroup, filterQuotaType, searchValue]); + }, [activeKey, filterGroup, filterQuotaType, filterEndpointType, searchValue]); return { // 状态 @@ -234,6 +243,8 @@ export const useModelPricingData = () => { setFilterGroup, filterQuotaType, setFilterQuotaType, + filterEndpointType, + setFilterEndpointType, activeKey, setActiveKey, pageSize,