🏗️ refactor: Replace model categories with vendor-based filtering and optimize data structure
- **Backend Changes:** - Refactor pricing API to return separate vendors array with ID-based model references - Remove redundant vendor_name/vendor_icon fields from pricing records, use vendor_id only - Add vendor_description to pricing response for frontend display - Maintain 1-minute cache protection for pricing endpoint security - **Frontend Data Flow:** - Update useModelPricingData hook to build vendorsMap from API response - Enhance model records with vendor info during data processing - Pass vendorsMap through component hierarchy for consistent vendor data access - **UI Component Replacements:** - Replace PricingCategories with PricingVendors component for vendor-based filtering - Replace PricingCategoryIntro with PricingVendorIntro in header section - Remove all model category related components and logic - **Header Improvements:** - Implement vendor intro with real backend data (name, icon, description) - Add text collapsible feature (2-line limit with expand/collapse functionality) - Support carousel animation for "All Vendors" view with vendor icon rotation - **Model Detail Modal Enhancements:** - Update ModelHeader to use real vendor icons via getLobeHubIcon() - Move tags from header to ModelBasicInfo content area to avoid SideSheet title width constraints - Display only custom tags from backend with stringToColor() for consistent styling - Use Space component with wrap property for proper tag layout - **Table View Optimizations:** - Integrate RenderUtils for description and tags columns - Implement renderLimitedItems for tags (max 3 visible, +x popover for overflow) - Use renderDescription for text truncation with tooltip support - **Filter Logic Updates:** - Vendor filter shows disabled options instead of hiding when no models match - Include "Unknown Vendor" category for models without vendor information - Remove all hardcoded vendor descriptions, use real backend data - **Code Quality:** - Fix import paths after component relocation - Remove unused model category utilities and hardcoded mappings - Ensure consistent vendor data usage across all pricing views - Maintain backward compatibility with existing pricing calculation logic This refactor provides a more scalable vendor-based architecture while eliminating data redundancy and improving user experience with real-time backend data integration.
This commit is contained in:
@@ -46,6 +46,7 @@ const ModelDetailSideSheet = ({
|
||||
displayPrice,
|
||||
showRatio,
|
||||
usableGroup,
|
||||
vendorsMap,
|
||||
t,
|
||||
}) => {
|
||||
const isMobile = useIsMobile();
|
||||
@@ -53,7 +54,7 @@ const ModelDetailSideSheet = ({
|
||||
return (
|
||||
<SideSheet
|
||||
placement="right"
|
||||
title={<ModelHeader modelData={modelData} t={t} />}
|
||||
title={<ModelHeader modelData={modelData} vendorsMap={vendorsMap} t={t} />}
|
||||
bodyStyle={{
|
||||
padding: '0',
|
||||
display: 'flex',
|
||||
@@ -80,7 +81,7 @@ const ModelDetailSideSheet = ({
|
||||
)}
|
||||
{modelData && (
|
||||
<>
|
||||
<ModelBasicInfo modelData={modelData} t={t} />
|
||||
<ModelBasicInfo modelData={modelData} vendorsMap={vendorsMap} t={t} />
|
||||
<ModelEndpoints modelData={modelData} t={t} />
|
||||
<ModelPricingTable
|
||||
modelData={modelData}
|
||||
|
||||
@@ -32,8 +32,6 @@ const PricingFilterModal = ({
|
||||
const handleResetFilters = () =>
|
||||
resetPricingFilters({
|
||||
handleChange: sidebarProps.handleChange,
|
||||
setActiveKey: sidebarProps.setActiveKey,
|
||||
availableCategories: sidebarProps.availableCategories,
|
||||
setShowWithRecharge: sidebarProps.setShowWithRecharge,
|
||||
setCurrency: sidebarProps.setCurrency,
|
||||
setShowRatio: sidebarProps.setShowRatio,
|
||||
@@ -41,6 +39,7 @@ const PricingFilterModal = ({
|
||||
setFilterGroup: sidebarProps.setFilterGroup,
|
||||
setFilterQuotaType: sidebarProps.setFilterQuotaType,
|
||||
setFilterEndpointType: sidebarProps.setFilterEndpointType,
|
||||
setFilterVendor: sidebarProps.setFilterVendor,
|
||||
setCurrentPage: sidebarProps.setCurrentPage,
|
||||
setTokenUnit: sidebarProps.setTokenUnit,
|
||||
});
|
||||
|
||||
@@ -19,10 +19,10 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
|
||||
import React from 'react';
|
||||
import PricingDisplaySettings from '../../filter/PricingDisplaySettings';
|
||||
import PricingCategories from '../../filter/PricingCategories';
|
||||
import PricingGroups from '../../filter/PricingGroups';
|
||||
import PricingQuotaTypes from '../../filter/PricingQuotaTypes';
|
||||
import PricingEndpointTypes from '../../filter/PricingEndpointTypes';
|
||||
import PricingVendors from '../../filter/PricingVendors';
|
||||
import { usePricingFilterCounts } from '../../../../../hooks/model-pricing/usePricingFilterCounts';
|
||||
|
||||
const FilterModalContent = ({ sidebarProps, t }) => {
|
||||
@@ -43,6 +43,8 @@ const FilterModalContent = ({ sidebarProps, t }) => {
|
||||
setFilterQuotaType,
|
||||
filterEndpointType,
|
||||
setFilterEndpointType,
|
||||
filterVendor,
|
||||
setFilterVendor,
|
||||
tokenUnit,
|
||||
setTokenUnit,
|
||||
loading,
|
||||
@@ -52,15 +54,14 @@ const FilterModalContent = ({ sidebarProps, t }) => {
|
||||
const {
|
||||
quotaTypeModels,
|
||||
endpointTypeModels,
|
||||
dynamicCategoryCounts,
|
||||
vendorModels,
|
||||
groupCountModels,
|
||||
} = usePricingFilterCounts({
|
||||
models: categoryProps.models,
|
||||
modelCategories: categoryProps.modelCategories,
|
||||
activeKey: categoryProps.activeKey,
|
||||
filterGroup,
|
||||
filterQuotaType,
|
||||
filterEndpointType,
|
||||
filterVendor,
|
||||
searchValue: sidebarProps.searchValue,
|
||||
});
|
||||
|
||||
@@ -81,10 +82,11 @@ const FilterModalContent = ({ sidebarProps, t }) => {
|
||||
t={t}
|
||||
/>
|
||||
|
||||
<PricingCategories
|
||||
{...categoryProps}
|
||||
categoryCounts={dynamicCategoryCounts}
|
||||
setActiveKey={setActiveKey}
|
||||
<PricingVendors
|
||||
filterVendor={filterVendor}
|
||||
setFilterVendor={setFilterVendor}
|
||||
models={vendorModels}
|
||||
allModels={categoryProps.models}
|
||||
loading={loading}
|
||||
t={t}
|
||||
/>
|
||||
|
||||
@@ -18,20 +18,43 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Card, Avatar, Typography } from '@douyinfe/semi-ui';
|
||||
import { Card, Avatar, Typography, Tag, Space } from '@douyinfe/semi-ui';
|
||||
import { IconInfoCircle } from '@douyinfe/semi-icons';
|
||||
import { stringToColor } from '../../../../../helpers';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const ModelBasicInfo = ({ modelData, t }) => {
|
||||
// 获取模型描述
|
||||
const ModelBasicInfo = ({ modelData, vendorsMap = {}, t }) => {
|
||||
// 获取模型描述(使用后端真实数据)
|
||||
const getModelDescription = () => {
|
||||
if (!modelData) return t('暂无模型描述');
|
||||
// 这里可以根据模型名称返回不同的描述
|
||||
if (modelData.model_name?.includes('gpt-4o-image')) {
|
||||
return t('逆向plus账号的可绘图的gpt-4o模型,由于OpenAI限制绘图数量,并非每次都能绘图成功,由于是逆向模型,只要请求成功,即使绘图失败也会扣费,请谨慎使用。推荐使用正式版的 gpt-image-1模型。');
|
||||
|
||||
// 优先使用后端提供的描述
|
||||
if (modelData.description) {
|
||||
return modelData.description;
|
||||
}
|
||||
return modelData.description || t('暂无模型描述');
|
||||
|
||||
// 如果没有描述但有供应商描述,显示供应商信息
|
||||
if (modelData.vendor_description) {
|
||||
return t('供应商信息:') + modelData.vendor_description;
|
||||
}
|
||||
|
||||
return t('暂无模型描述');
|
||||
};
|
||||
|
||||
// 获取模型标签
|
||||
const getModelTags = () => {
|
||||
const tags = [];
|
||||
|
||||
if (modelData?.tags) {
|
||||
const customTags = modelData.tags.split(',').filter(tag => tag.trim());
|
||||
customTags.forEach(tag => {
|
||||
const tagText = tag.trim();
|
||||
tags.push({ text: tagText, color: stringToColor(tagText) });
|
||||
});
|
||||
}
|
||||
|
||||
return tags;
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -46,7 +69,24 @@ const ModelBasicInfo = ({ modelData, t }) => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-gray-600">
|
||||
<p>{getModelDescription()}</p>
|
||||
<p className="mb-4">{getModelDescription()}</p>
|
||||
{getModelTags().length > 0 && (
|
||||
<div>
|
||||
<Text className="text-sm font-medium text-gray-700 mb-2 block">{t('模型标签')}</Text>
|
||||
<Space wrap>
|
||||
{getModelTags().map((tag, index) => (
|
||||
<Tag
|
||||
key={index}
|
||||
color={tag.color}
|
||||
shape="circle"
|
||||
size="small"
|
||||
>
|
||||
{tag.text}
|
||||
</Tag>
|
||||
))}
|
||||
</Space>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -18,8 +18,8 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Tag, Typography, Toast, Avatar } from '@douyinfe/semi-ui';
|
||||
import { getModelCategories } from '../../../../../helpers';
|
||||
import { Typography, Toast, Avatar } from '@douyinfe/semi-ui';
|
||||
import { getLobeHubIcon } from '../../../../../helpers';
|
||||
|
||||
const { Paragraph } = Typography;
|
||||
|
||||
@@ -28,52 +28,22 @@ const CARD_STYLES = {
|
||||
icon: "w-8 h-8 flex items-center justify-center",
|
||||
};
|
||||
|
||||
const ModelHeader = ({ modelData, t }) => {
|
||||
// 获取模型图标
|
||||
const getModelIcon = (modelName) => {
|
||||
// 如果没有模型名称,直接返回默认头像
|
||||
if (!modelName) {
|
||||
return (
|
||||
<div className={CARD_STYLES.container}>
|
||||
<Avatar
|
||||
size="large"
|
||||
style={{
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 16,
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold'
|
||||
}}
|
||||
>
|
||||
AI
|
||||
</Avatar>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const categories = getModelCategories(t);
|
||||
let icon = null;
|
||||
|
||||
// 遍历分类,找到匹配的模型图标
|
||||
for (const [key, category] of Object.entries(categories)) {
|
||||
if (key !== 'all' && category.filter({ model_name: modelName })) {
|
||||
icon = category.icon;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果找到了匹配的图标,返回包装后的图标
|
||||
if (icon) {
|
||||
const ModelHeader = ({ modelData, vendorsMap = {}, t }) => {
|
||||
// 获取模型图标(使用供应商图标)
|
||||
const getModelIcon = () => {
|
||||
// 优先使用供应商图标
|
||||
if (modelData?.vendor_icon) {
|
||||
return (
|
||||
<div className={CARD_STYLES.container}>
|
||||
<div className={CARD_STYLES.icon}>
|
||||
{React.cloneElement(icon, { size: 32 })}
|
||||
{getLobeHubIcon(modelData.vendor_icon, 32)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const avatarText = modelName?.slice(0, 2).toUpperCase() || 'AI';
|
||||
// 如果没有供应商图标,使用模型名称的前两个字符
|
||||
const avatarText = modelData?.model_name?.slice(0, 2).toUpperCase() || 'AI';
|
||||
return (
|
||||
<div className={CARD_STYLES.container}>
|
||||
<Avatar
|
||||
@@ -92,23 +62,12 @@ const ModelHeader = ({ modelData, t }) => {
|
||||
);
|
||||
};
|
||||
|
||||
// 获取模型标签
|
||||
const getModelTags = () => {
|
||||
const tags = [
|
||||
{ text: t('文本对话'), color: 'green' },
|
||||
{ text: t('图片生成'), color: 'blue' },
|
||||
{ text: t('图像分析'), color: 'cyan' }
|
||||
];
|
||||
|
||||
return tags;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
{getModelIcon(modelData?.model_name)}
|
||||
{getModelIcon()}
|
||||
<div className="ml-3 font-normal">
|
||||
<Paragraph
|
||||
className="!mb-1 !text-lg !font-medium"
|
||||
className="!mb-0 !text-lg !font-medium"
|
||||
copyable={{
|
||||
content: modelData?.model_name || '',
|
||||
onCopy: () => Toast.success({ content: t('已复制模型名称') })
|
||||
@@ -116,18 +75,6 @@ const ModelHeader = ({ modelData, t }) => {
|
||||
>
|
||||
<span className="truncate max-w-60 font-bold">{modelData?.model_name || t('未知模型')}</span>
|
||||
</Paragraph>
|
||||
<div className="inline-flex gap-2 mt-1">
|
||||
{getModelTags().map((tag, index) => (
|
||||
<Tag
|
||||
key={index}
|
||||
color={tag.color}
|
||||
shape="circle"
|
||||
size="small"
|
||||
>
|
||||
{tag.text}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user