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 (

共 {catalog.summary.sourceCount} 个源,按别名合并为 {catalog.summary.modelCount} 个模型,当前显示 {filteredModels.length} 个

setQuery(event.target.value)} placeholder="模型名称、能力或厂商" />
{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) => ( ))} {denyGroups.map((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'; }