From be53af4638997a53bbec66d6d75e4178f9a3f650 Mon Sep 17 00:00:00 2001 From: songtianlun Date: Thu, 28 Aug 2025 00:06:19 +0800 Subject: [PATCH] fix model admin --- src/app/admin/models/page.tsx | 539 +++++++++++++++++++--------------- 1 file changed, 298 insertions(+), 241 deletions(-) diff --git a/src/app/admin/models/page.tsx b/src/app/admin/models/page.tsx index cc08a36..78766b8 100644 --- a/src/app/admin/models/page.tsx +++ b/src/app/admin/models/page.tsx @@ -1,9 +1,8 @@ 'use client' import { useState, useEffect } from 'react' -import { useTranslations } from 'next-intl' import { Button } from '@/components/ui/button' -import { Card } from '@/components/ui/card' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { LoadingSpinner } from '@/components/ui/loading-spinner' import { Badge } from '@/components/ui/badge' import { @@ -16,12 +15,13 @@ import { Trash2, Eye, EyeOff, - AlertCircle + Search, + CheckCircle2, + Globe } from 'lucide-react' interface Model { id: string - subscriptionPlanId: string modelId: string name: string provider: string @@ -36,17 +36,6 @@ interface Model { isActive: boolean createdAt: string updatedAt: string - subscriptionPlan: { - id: string - name: string - displayName: string - } -} - -interface SubscriptionPlan { - id: string - displayName: string - name: string } interface AvailableModel { @@ -64,17 +53,17 @@ interface AvailableModel { } export default function AdminModelsPage() { - const t = useTranslations('admin') const [models, setModels] = useState([]) - const [plans, setPlans] = useState([]) const [availableModels, setAvailableModels] = useState([]) const [isLoading, setIsLoading] = useState(true) const [isFetching, setIsFetching] = useState(false) const [isAdding, setIsAdding] = useState(false) - const [selectedPlan, setSelectedPlan] = useState('') const [selectedModels, setSelectedModels] = useState([]) const [showAvailableModels, setShowAvailableModels] = useState(false) const [selectedServiceProvider, setSelectedServiceProvider] = useState<'openrouter' | 'replicate' | 'uniapi' | 'fal'>('openrouter') + const [searchTerm, setSearchTerm] = useState('') + const [filterProvider, setFilterProvider] = useState('') + const [filterOutputType, setFilterOutputType] = useState('') useEffect(() => { loadInitialData() @@ -84,33 +73,19 @@ export default function AdminModelsPage() { try { setIsLoading(true) - const [modelsRes, plansRes] = await Promise.all([ - fetch('/api/admin/models'), - fetch('/api/admin/subscription-plans') - ]) - - if (modelsRes.ok) { - const modelsData = await modelsRes.json() - setModels(modelsData.models || []) - } - - if (plansRes.ok) { - const plansData = await plansRes.json() - setPlans(plansData.plans || []) + const response = await fetch('/api/admin/models') + if (response.ok) { + const data = await response.json() + setModels(data.models || []) } } catch (error) { - console.error('Error loading data:', error) + console.error('Error loading models:', error) } finally { setIsLoading(false) } } const fetchModelsFromProvider = async () => { - if (!selectedPlan) { - alert(t('noPlanSelected')) - return - } - try { setIsFetching(true) @@ -121,7 +96,6 @@ export default function AdminModelsPage() { }, body: JSON.stringify({ action: 'sync', - planId: selectedPlan, serviceProvider: selectedServiceProvider }) }) @@ -143,9 +117,9 @@ export default function AdminModelsPage() { } } - const addSelectedModelsToplan = async () => { - if (!selectedPlan || selectedModels.length === 0) { - alert(selectedModels.length === 0 ? t('noModelsSelected') : t('noPlanSelected')) + const addSelectedModels = async () => { + if (selectedModels.length === 0) { + alert('Please select models to add') return } @@ -163,7 +137,6 @@ export default function AdminModelsPage() { }, body: JSON.stringify({ action: 'add', - planId: selectedPlan, selectedModels: selectedModelData }) }) @@ -173,14 +146,14 @@ export default function AdminModelsPage() { await loadInitialData() // 重新加载数据 setShowAvailableModels(false) setSelectedModels([]) - alert(`${result.models?.length || 0} ${t('modelsAdded')}`) + alert(`Successfully added ${result.models?.length || 0} models`) } else { const error = await response.json() alert(`Error: ${error.error}`) } } catch (error) { console.error('Error adding models:', error) - alert('Failed to add models to plan') + alert('Failed to add models') } finally { setIsAdding(false) } @@ -241,8 +214,37 @@ export default function AdminModelsPage() { ) } - const getModelsByPlan = (planId: string) => { - return models.filter(model => model.subscriptionPlanId === planId) + // 过滤和搜索逻辑 + const filteredModels = models.filter(model => { + const matchesSearch = searchTerm === '' || + model.name.toLowerCase().includes(searchTerm.toLowerCase()) || + model.modelId.toLowerCase().includes(searchTerm.toLowerCase()) || + model.provider.toLowerCase().includes(searchTerm.toLowerCase()) + + const matchesProvider = filterProvider === '' || model.provider === filterProvider + const matchesOutputType = filterOutputType === '' || model.outputType === filterOutputType + + return matchesSearch && matchesProvider && matchesOutputType + }) + + // 获取所有可用的提供商和输出类型用于过滤器 + const allProviders = [...new Set(models.map(m => m.provider))].sort() + const allOutputTypes = [...new Set(models.map(m => m.outputType))].sort() + const serviceProviders = [...new Set(models.map(m => m.serviceProvider))].sort() + + // 统计信息 + const stats = { + total: models.length, + active: models.filter(m => m.isActive).length, + inactive: models.filter(m => !m.isActive).length, + byProvider: allProviders.reduce((acc, provider) => { + acc[provider] = models.filter(m => m.provider === provider).length + return acc + }, {} as Record), + byServiceProvider: serviceProviders.reduce((acc, sp) => { + acc[sp] = models.filter(m => m.serviceProvider === sp).length + return acc + }, {} as Record) } if (isLoading) { @@ -250,7 +252,7 @@ export default function AdminModelsPage() {
-

{t('loadingModels')}

+

Loading AI models...

) @@ -260,106 +262,125 @@ export default function AdminModelsPage() {
{/* Header */}
-

{t('modelsConfig')}

+

AI Models Management

- {t('modelsConfigDesc')} + Manage AI models across all subscription plans. All models are globally available with different cost multipliers.

- {/* Add Models Section */} - -
-
-

- - {t('addModelsTo')} {selectedPlan && plans.find(p => p.id === selectedPlan)?.displayName} -

-

- {t('syncFromOpenRouterDesc')} -

-
-
+ {/* Stats Cards */} +
+ + + Total Models + + + +
{stats.total}
+

All configured models

+
+
-
- {/* Plan Selection */} + + + Active Models + + + +
{stats.active}
+

Currently available

+
+
+ + + + Service Providers + + + +
{serviceProviders.length}
+

Connected services

+
+
+ + + + Model Providers + + + +
{allProviders.length}
+

Different AI providers

+
+
+
+ + {/* Add Models Section */} + + + + + Add New Models + + + Sync and add new AI models from supported service providers. All models will be available globally. + + + + {/* Service Provider Selection */}
- +
- {/* Service Provider Selection */} - {selectedPlan && ( -
- - -
- )} - {/* Fetch Models Button */} - {selectedPlan && ( -
- - - {showAvailableModels && ( -
- {availableModels.length} models available • {selectedModels.length} selected -
- )} -
- )} +
+ + + {showAvailableModels && ( +
+ {availableModels.length} models available • {selectedModels.length} selected +
+ )} +
{/* Available Models Selection */} {showAvailableModels && availableModels.length > 0 && (
-

{t('availableModels')} ({availableModels.length})

+

Available Models ({availableModels.length})

@@ -367,26 +388,24 @@ export default function AdminModelsPage() {
{availableModels.map(model => { const isSelected = selectedModels.includes(model.modelId) - const isAlreadyAdded = models.some(m => - m.modelId === model.modelId && m.subscriptionPlanId === selectedPlan - ) + const isAlreadyAdded = models.some(m => m.modelId === model.modelId) return (
!isAlreadyAdded && toggleModelSelection(model.modelId)} > -
+
-

{model.name}

+

{model.name || model.modelId}

{model.provider} @@ -394,9 +413,6 @@ export default function AdminModelsPage() { {model.outputType} - - {model.serviceProvider} -
@@ -414,8 +430,8 @@ export default function AdminModelsPage() { )}
{isAlreadyAdded && ( -
- +
+ Already added
)} @@ -436,125 +452,166 @@ export default function AdminModelsPage() {
)} -
+ + - {/* Current Models by Plan */} -
- {plans.map(plan => { - const planModels = getModelsByPlan(plan.id) + {/* Current Models List */} + + +
+
+ + + Configured Models ({filteredModels.length}) + + + All AI models available across subscription plans with different cost multipliers + +
+
- return ( - -
-
-

- {plan.displayName} -

-

- {planModels.length} {t('planModelsCount')} -

-
-
+ {/* Search and Filter Controls */} +
+
+ + setSearchTerm(e.target.value)} + className="w-full pl-10 pr-4 py-2 bg-background border border-input rounded-md focus:outline-none focus:ring-2 focus:ring-ring" + /> +
+ +
+ - {planModels.length === 0 ? ( -
- -

{t('noModelsConfigured')}

-

- Select this plan above and fetch models to get started -

-
- ) : ( -
- {planModels.map(model => ( -
-
-
-
-

{model.name}

- - {model.provider} - - - {model.outputType} - - - {model.serviceProvider} - - - {model.isActive ? t('modelEnabled') : t('modelDisabled')} - -
- -
- - - {model.modelId} - - {model.maxTokens && ( - - - {model.maxTokens.toLocaleString()} tokens - - )} - {model.inputCostPer1k && ( - - - ${model.inputCostPer1k.toFixed(4)}/1K in - - )} - {model.outputCostPer1k && ( - - - ${model.outputCostPer1k.toFixed(4)}/1K out - - )} -
- - {model.description && ( -

- {model.description} -

- )} -
- -
- - -
+ +
+
+ + + + {filteredModels.length === 0 ? ( +
+ +

No models found

+

+ {searchTerm || filterProvider || filterOutputType + ? 'Try adjusting your search or filter criteria' + : 'Start by adding some models from the section above' + } +

+
+ ) : ( +
+ {filteredModels.map(model => ( +
+
+
+
+

{model.name}

+ + {model.provider} + + + {model.outputType} + + + {model.serviceProvider} + + + {model.isActive ? 'Active' : 'Inactive'} +
+ +
+ + + {model.modelId} + + {model.maxTokens && ( + + + {model.maxTokens.toLocaleString()} tokens + + )} + {model.inputCostPer1k && ( + + + ${model.inputCostPer1k.toFixed(6)}/1K in + + )} + {model.outputCostPer1k && ( + + + ${model.outputCostPer1k.toFixed(6)}/1K out + + )} +
+ + {model.description && ( +

+ {model.description} +

+ )}
- ))} + +
+ + +
+
- )} - - ) - })} -
+ ))} +
+ )} + +
) } \ No newline at end of file