diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 79fed8f..e6ec03b 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -148,6 +148,14 @@ "pricing.status": "Status", "pricing.online": "Online", "pricing.unknown": "Unknown", + "pricing.catAll": "All", + "pricing.catChat": "Chat", + "pricing.catCompletion": "Completion", + "pricing.catEmbedding": "Embedding", + "pricing.catImage": "Image", + "pricing.catAudio": "Audio", + "pricing.catVideo": "Video", + "pricing.catRerank": "Rerank", "packages.title": "Packages & Plans", "packages.subtitle": "Choose a package that fits your usage. All plans include access to all available models.", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index 65dc040..2dec961 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -148,6 +148,14 @@ "pricing.status": "状态", "pricing.online": "在线", "pricing.unknown": "未知", + "pricing.catAll": "全部", + "pricing.catChat": "对话", + "pricing.catCompletion": "补全", + "pricing.catEmbedding": "向量", + "pricing.catImage": "图像", + "pricing.catAudio": "音频", + "pricing.catVideo": "视频", + "pricing.catRerank": "重排序", "packages.title": "套餐方案", "packages.subtitle": "选择适合您用量的套餐。所有方案均可访问全部可用模型。", diff --git a/src/pages/Pricing.jsx b/src/pages/Pricing.jsx index c983cf6..1634a01 100644 --- a/src/pages/Pricing.jsx +++ b/src/pages/Pricing.jsx @@ -1,11 +1,23 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { getSiteModels } from '../api'; +const CATEGORIES = [ + { value: '', icon: '🌐', labelKey: 'pricing.catAll' }, + { value: 'chat', icon: '💬', labelKey: 'pricing.catChat' }, + { value: 'completion', icon: '✍️', labelKey: 'pricing.catCompletion' }, + { value: 'embedding', icon: '📐', labelKey: 'pricing.catEmbedding' }, + { value: 'image', icon: '🖼️', labelKey: 'pricing.catImage' }, + { value: 'audio', icon: '🎵', labelKey: 'pricing.catAudio' }, + { value: 'video', icon: '🎬', labelKey: 'pricing.catVideo' }, + { value: 'rerank', icon: '🔀', labelKey: 'pricing.catRerank' }, +]; + export default function Pricing() { const { t } = useTranslation(); const [models, setModels] = useState([]); const [search, setSearch] = useState(''); + const [category, setCategory] = useState(''); const [loading, setLoading] = useState(true); useEffect(() => { @@ -19,10 +31,29 @@ export default function Pricing() { const enabledModels = models.filter((m) => m.enabled !== false); - const filtered = enabledModels.filter((m) => - !search || - (m.display_name || m.model_name || '').toLowerCase().includes(search.toLowerCase()) - ); + // Collect categories that actually have models + const availableCategories = useMemo(() => { + const cats = new Set(enabledModels.map((m) => m.category || 'chat')); + return CATEGORIES.filter((c) => c.value === '' || cats.has(c.value)); + }, [enabledModels]); + + const filtered = useMemo(() => { + let list = enabledModels; + // Category filter + if (category) { + list = list.filter((m) => (m.category || 'chat') === category); + } + // Search filter + if (search) { + const q = search.toLowerCase(); + list = list.filter((m) => + (m.display_name || m.model_name || '').toLowerCase().includes(q) + ); + } + // Sort by input_price ascending + list = [...list].sort((a, b) => (Number(a.input_price) || 0) - (Number(b.input_price) || 0)); + return list; + }, [enabledModels, category, search]); if (loading) { return ( @@ -41,6 +72,24 @@ export default function Pricing() {

+ {/* Category Filter */} +
+ {availableCategories.map((cat) => ( + + ))} +
+ {/* Search */}
@@ -59,7 +108,7 @@ export default function Pricing() { {filtered.length === 0 ? (
- {search ? t('pricing.noMatch') : t('pricing.noModels')} + {search || category ? t('pricing.noMatch') : t('pricing.noModels')}
) : (