import { useEffect, useMemo, useState } from 'react';
import { Boxes, Check, Search, X } from 'lucide-react';
import type { ModelCatalogFilterOption, ModelCatalogItem, ModelCatalogPermission } from '@easyai-ai-gateway/contracts';
import type { ConsoleData } from '../app-state';
import { Card, CardContent, Input } from '../components/ui';
export function ModelsPage(props: { data: ConsoleData }) {
const catalog = props.data.modelCatalog;
const [query, setQuery] = useState('');
const [provider, setProvider] = useState('all');
const [capability, setCapability] = useState('all');
const providerOptions = catalog.filters.providers.length ? catalog.filters.providers : [{ value: 'all', label: '全部', count: catalog.items.length }];
const capabilityOptions = catalog.filters.capabilities.length ? catalog.filters.capabilities : [{ value: 'all', label: '全部', count: catalog.items.length }];
useEffect(() => {
if (!providerOptions.some((item) => item.value === provider)) setProvider('all');
}, [provider, providerOptions]);
useEffect(() => {
if (!capabilityOptions.some((item) => item.value === capability)) setCapability('all');
}, [capability, capabilityOptions]);
const filteredModels = useMemo(() => {
const normalizedQuery = query.trim().toLowerCase();
return catalog.items.filter((model) => {
const matchedProvider = provider === 'all' || model.providerKeys.includes(provider);
const matchedCapability = capability === 'all' || modelMatchesCapability(model, capability);
const matchedQuery = !normalizedQuery || [
model.alias,
model.displayName,
model.modelName,
model.description,
...model.providers.map((item) => item.name),
...model.capabilityTags,
]
.filter(Boolean)
.join(' ')
.toLowerCase()
.includes(normalizedQuery);
return matchedProvider && matchedCapability && matchedQuery;
});
}, [capability, catalog.items, provider, query]);
return (
{filteredModels.map((model) => (
))}
{!filteredModels.length && (
没有匹配的模型
)}
);
}
function FilterGroup(props: {
items: ModelCatalogFilterOption[];
title: string;
value: string;
onChange: (value: string) => void;
}) {
return (
{props.title}
{props.items.map((item) => (
))}
);
}
function FilterIcon(props: { item: ModelCatalogFilterOption }) {
if (props.item.iconPath) {
return (
);
}
if (props.item.value === 'all') return null;
return {providerInitials(props.item.label)};
}
function ModelCard(props: { model: ModelCatalogItem }) {
const description = props.model.description || '暂无模型描述';
const pricing = modelCardPricing(props.model.pricing);
return (
{props.model.displayName || props.model.alias}
{description}
{props.model.capabilityTags.map((tag) => {tag})}
- 源
- {props.model.source.label}
- 折扣率
- {props.model.discount.label}
- 限流
- {props.model.rateLimits.label}
- {pricing.label}
- {pricing.lines.join(';')}
);
}
function modelCardPricing(pricing: ModelCatalogItem['pricing']) {
const hasFiveSecondBasis = pricing.lines.some((line) => line.includes('5秒基准'));
if (!hasFiveSecondBasis) {
return {
label: '模型定价',
lines: pricing.lines,
title: pricing.title,
};
}
const lines = pricing.lines.map(stripFiveSecondBasis).filter(Boolean);
const title = stripFiveSecondBasis(pricing.title || pricing.lines.join(';'));
return {
label: '模型定价(每5秒)',
lines: lines.length ? lines : pricing.lines,
title: title || pricing.title,
};
}
function stripFiveSecondBasis(value: string) {
return value
.replace(/(5秒基准)/g, '')
.replace(/\s*\/\s*5秒基准/g, '')
.trim();
}
function PermissionValue(props: { permission: ModelCatalogPermission }) {
const allowGroups = props.permission.allowGroups ?? [];
const denyGroups = props.permission.denyGroups ?? [];
if (!allowGroups.length && !denyGroups.length) {
return {props.permission.label};
}
return (
{allowGroups.map((group) => (
{group}
))}
{denyGroups.map((group) => (
{group}
))}
);
}
function ModelIcon(props: { iconPath?: string; label: string }) {
if (props.iconPath) {
return (
);
}
return {providerInitials(props.label)}
;
}
function modelMatchesCapability(model: ModelCatalogItem, capability: string) {
if (model.modelType.some((type) => modelTypeMatchesCapability(type, capability))) return true;
if (capability === 'tools') return model.capabilityTags.includes('工具调用');
if (capability === 'omni') return model.capabilityTags.includes('全模态');
return false;
}
function modelTypeMatchesCapability(value: string, capability: string) {
const type = value.trim().toLowerCase();
if (capability === 'chat') return type === 'text_generate' || type === 'chat' || type === 'responses' || type.includes('text');
if (capability === 'image') return type.includes('image') && !type.includes('video');
if (capability === 'video') return type.includes('video');
if (capability === 'audio') return type.includes('audio') || type.includes('speech');
if (capability === 'embedding') return type === 'text_embedding' || type === 'embedding';
if (capability === 'tools') return type === 'tools_call';
if (capability === 'omni') return type === 'omni' || type === 'omni_video';
return type === capability;
}
function providerInitials(label: string) {
return label
.split(/\s+/)
.map((part) => part[0])
.join('')
.slice(0, 2)
.toUpperCase() || 'AI';
}