fix model admin
This commit is contained in:
parent
1d6b1ae0e1
commit
be53af4638
@ -1,9 +1,8 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { useTranslations } from 'next-intl'
|
|
||||||
import { Button } from '@/components/ui/button'
|
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 { LoadingSpinner } from '@/components/ui/loading-spinner'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import {
|
import {
|
||||||
@ -16,12 +15,13 @@ import {
|
|||||||
Trash2,
|
Trash2,
|
||||||
Eye,
|
Eye,
|
||||||
EyeOff,
|
EyeOff,
|
||||||
AlertCircle
|
Search,
|
||||||
|
CheckCircle2,
|
||||||
|
Globe
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
|
|
||||||
interface Model {
|
interface Model {
|
||||||
id: string
|
id: string
|
||||||
subscriptionPlanId: string
|
|
||||||
modelId: string
|
modelId: string
|
||||||
name: string
|
name: string
|
||||||
provider: string
|
provider: string
|
||||||
@ -36,17 +36,6 @@ interface Model {
|
|||||||
isActive: boolean
|
isActive: boolean
|
||||||
createdAt: string
|
createdAt: string
|
||||||
updatedAt: string
|
updatedAt: string
|
||||||
subscriptionPlan: {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
displayName: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SubscriptionPlan {
|
|
||||||
id: string
|
|
||||||
displayName: string
|
|
||||||
name: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AvailableModel {
|
interface AvailableModel {
|
||||||
@ -64,17 +53,17 @@ interface AvailableModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function AdminModelsPage() {
|
export default function AdminModelsPage() {
|
||||||
const t = useTranslations('admin')
|
|
||||||
const [models, setModels] = useState<Model[]>([])
|
const [models, setModels] = useState<Model[]>([])
|
||||||
const [plans, setPlans] = useState<SubscriptionPlan[]>([])
|
|
||||||
const [availableModels, setAvailableModels] = useState<AvailableModel[]>([])
|
const [availableModels, setAvailableModels] = useState<AvailableModel[]>([])
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
const [isFetching, setIsFetching] = useState(false)
|
const [isFetching, setIsFetching] = useState(false)
|
||||||
const [isAdding, setIsAdding] = useState(false)
|
const [isAdding, setIsAdding] = useState(false)
|
||||||
const [selectedPlan, setSelectedPlan] = useState<string>('')
|
|
||||||
const [selectedModels, setSelectedModels] = useState<string[]>([])
|
const [selectedModels, setSelectedModels] = useState<string[]>([])
|
||||||
const [showAvailableModels, setShowAvailableModels] = useState(false)
|
const [showAvailableModels, setShowAvailableModels] = useState(false)
|
||||||
const [selectedServiceProvider, setSelectedServiceProvider] = useState<'openrouter' | 'replicate' | 'uniapi' | 'fal'>('openrouter')
|
const [selectedServiceProvider, setSelectedServiceProvider] = useState<'openrouter' | 'replicate' | 'uniapi' | 'fal'>('openrouter')
|
||||||
|
const [searchTerm, setSearchTerm] = useState('')
|
||||||
|
const [filterProvider, setFilterProvider] = useState('')
|
||||||
|
const [filterOutputType, setFilterOutputType] = useState('')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadInitialData()
|
loadInitialData()
|
||||||
@ -84,33 +73,19 @@ export default function AdminModelsPage() {
|
|||||||
try {
|
try {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
|
|
||||||
const [modelsRes, plansRes] = await Promise.all([
|
const response = await fetch('/api/admin/models')
|
||||||
fetch('/api/admin/models'),
|
if (response.ok) {
|
||||||
fetch('/api/admin/subscription-plans')
|
const data = await response.json()
|
||||||
])
|
setModels(data.models || [])
|
||||||
|
|
||||||
if (modelsRes.ok) {
|
|
||||||
const modelsData = await modelsRes.json()
|
|
||||||
setModels(modelsData.models || [])
|
|
||||||
}
|
|
||||||
|
|
||||||
if (plansRes.ok) {
|
|
||||||
const plansData = await plansRes.json()
|
|
||||||
setPlans(plansData.plans || [])
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading data:', error)
|
console.error('Error loading models:', error)
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchModelsFromProvider = async () => {
|
const fetchModelsFromProvider = async () => {
|
||||||
if (!selectedPlan) {
|
|
||||||
alert(t('noPlanSelected'))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setIsFetching(true)
|
setIsFetching(true)
|
||||||
|
|
||||||
@ -121,7 +96,6 @@ export default function AdminModelsPage() {
|
|||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
action: 'sync',
|
action: 'sync',
|
||||||
planId: selectedPlan,
|
|
||||||
serviceProvider: selectedServiceProvider
|
serviceProvider: selectedServiceProvider
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -143,9 +117,9 @@ export default function AdminModelsPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const addSelectedModelsToplan = async () => {
|
const addSelectedModels = async () => {
|
||||||
if (!selectedPlan || selectedModels.length === 0) {
|
if (selectedModels.length === 0) {
|
||||||
alert(selectedModels.length === 0 ? t('noModelsSelected') : t('noPlanSelected'))
|
alert('Please select models to add')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,7 +137,6 @@ export default function AdminModelsPage() {
|
|||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
action: 'add',
|
action: 'add',
|
||||||
planId: selectedPlan,
|
|
||||||
selectedModels: selectedModelData
|
selectedModels: selectedModelData
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -173,14 +146,14 @@ export default function AdminModelsPage() {
|
|||||||
await loadInitialData() // 重新加载数据
|
await loadInitialData() // 重新加载数据
|
||||||
setShowAvailableModels(false)
|
setShowAvailableModels(false)
|
||||||
setSelectedModels([])
|
setSelectedModels([])
|
||||||
alert(`${result.models?.length || 0} ${t('modelsAdded')}`)
|
alert(`Successfully added ${result.models?.length || 0} models`)
|
||||||
} else {
|
} else {
|
||||||
const error = await response.json()
|
const error = await response.json()
|
||||||
alert(`Error: ${error.error}`)
|
alert(`Error: ${error.error}`)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error adding models:', error)
|
console.error('Error adding models:', error)
|
||||||
alert('Failed to add models to plan')
|
alert('Failed to add models')
|
||||||
} finally {
|
} finally {
|
||||||
setIsAdding(false)
|
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<string, number>),
|
||||||
|
byServiceProvider: serviceProviders.reduce((acc, sp) => {
|
||||||
|
acc[sp] = models.filter(m => m.serviceProvider === sp).length
|
||||||
|
return acc
|
||||||
|
}, {} as Record<string, number>)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
@ -250,7 +252,7 @@ export default function AdminModelsPage() {
|
|||||||
<div className="flex items-center justify-center min-h-screen">
|
<div className="flex items-center justify-center min-h-screen">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<LoadingSpinner size="lg" />
|
<LoadingSpinner size="lg" />
|
||||||
<p className="mt-4 text-muted-foreground">{t('loadingModels')}</p>
|
<p className="mt-4 text-muted-foreground">Loading AI models...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@ -260,106 +262,125 @@ export default function AdminModelsPage() {
|
|||||||
<div className="container mx-auto px-4 py-8 max-w-7xl">
|
<div className="container mx-auto px-4 py-8 max-w-7xl">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<h1 className="text-3xl font-bold text-foreground mb-2">{t('modelsConfig')}</h1>
|
<h1 className="text-3xl font-bold text-foreground mb-2">AI Models Management</h1>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
{t('modelsConfigDesc')}
|
Manage AI models across all subscription plans. All models are globally available with different cost multipliers.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Add Models Section */}
|
{/* Stats Cards */}
|
||||||
<Card className="p-6 mb-8">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||||
<div className="flex items-center justify-between mb-6">
|
<Card>
|
||||||
<div>
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
<h2 className="text-xl font-semibold mb-2 flex items-center">
|
<CardTitle className="text-sm font-medium">Total Models</CardTitle>
|
||||||
<Plus className="h-5 w-5 mr-2" />
|
<Database className="h-4 w-4 text-muted-foreground" />
|
||||||
{t('addModelsTo')} {selectedPlan && plans.find(p => p.id === selectedPlan)?.displayName}
|
</CardHeader>
|
||||||
</h2>
|
<CardContent>
|
||||||
<p className="text-muted-foreground">
|
<div className="text-2xl font-bold">{stats.total}</div>
|
||||||
{t('syncFromOpenRouterDesc')}
|
<p className="text-xs text-muted-foreground">All configured models</p>
|
||||||
</p>
|
</CardContent>
|
||||||
</div>
|
</Card>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
<Card>
|
||||||
{/* Plan Selection */}
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Active Models</CardTitle>
|
||||||
|
<CheckCircle2 className="h-4 w-4 text-green-600" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold text-green-600">{stats.active}</div>
|
||||||
|
<p className="text-xs text-muted-foreground">Currently available</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Service Providers</CardTitle>
|
||||||
|
<Globe className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{serviceProviders.length}</div>
|
||||||
|
<p className="text-xs text-muted-foreground">Connected services</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Model Providers</CardTitle>
|
||||||
|
<Cpu className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{allProviders.length}</div>
|
||||||
|
<p className="text-xs text-muted-foreground">Different AI providers</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Add Models Section */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Plus className="h-5 w-5" />
|
||||||
|
Add New Models
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Sync and add new AI models from supported service providers. All models will be available globally.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
{/* Service Provider Selection */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium mb-2">{t('selectPlan')}</label>
|
<label className="block text-sm font-medium mb-2">Service Provider</label>
|
||||||
<select
|
<select
|
||||||
value={selectedPlan}
|
value={selectedServiceProvider}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setSelectedPlan(e.target.value)
|
setSelectedServiceProvider(e.target.value as 'openrouter' | 'replicate' | 'uniapi' | 'fal')
|
||||||
setShowAvailableModels(false)
|
setShowAvailableModels(false)
|
||||||
setSelectedModels([])
|
setSelectedModels([])
|
||||||
}}
|
}}
|
||||||
className="w-full md:w-64 bg-background border border-input rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-ring"
|
className="w-full md:w-80 bg-background border border-input rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-ring"
|
||||||
>
|
>
|
||||||
<option value="">{t('selectPlanPlaceholder')}</option>
|
<option value="openrouter">OpenRouter (Text & Chat Models)</option>
|
||||||
{plans.map(plan => (
|
<option value="replicate">Replicate (Image/Video/Audio Models)</option>
|
||||||
<option key={plan.id} value={plan.id}>
|
<option value="uniapi">UniAPI (Multi-modal Models)</option>
|
||||||
{plan.displayName}
|
<option value="fal">Fal.ai (AI Generation Models)</option>
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Service Provider Selection */}
|
|
||||||
{selectedPlan && (
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium mb-2">Service Provider</label>
|
|
||||||
<select
|
|
||||||
value={selectedServiceProvider}
|
|
||||||
onChange={(e) => {
|
|
||||||
setSelectedServiceProvider(e.target.value as 'openrouter' | 'replicate' | 'uniapi' | 'fal')
|
|
||||||
setShowAvailableModels(false)
|
|
||||||
setSelectedModels([])
|
|
||||||
}}
|
|
||||||
className="w-full md:w-64 bg-background border border-input rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-ring"
|
|
||||||
>
|
|
||||||
<option value="openrouter">OpenRouter (Text Models)</option>
|
|
||||||
<option value="replicate">Replicate (Image/Video/Audio Models)</option>
|
|
||||||
<option value="uniapi">UniAPI (Multi-modal Models)</option>
|
|
||||||
<option value="fal">Fal.ai (AI Generation Models)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Fetch Models Button */}
|
{/* Fetch Models Button */}
|
||||||
{selectedPlan && (
|
<div className="flex items-center space-x-4">
|
||||||
<div className="flex items-center space-x-4">
|
<Button
|
||||||
<Button
|
onClick={fetchModelsFromProvider}
|
||||||
onClick={fetchModelsFromProvider}
|
disabled={isFetching}
|
||||||
disabled={isFetching}
|
className="flex items-center space-x-2"
|
||||||
className="flex items-center space-x-2"
|
>
|
||||||
>
|
<RefreshCw className={`h-4 w-4 ${isFetching ? 'animate-spin' : ''}`} />
|
||||||
<RefreshCw className={`h-4 w-4 ${isFetching ? 'animate-spin' : ''}`} />
|
<span>{isFetching ? `Syncing ${selectedServiceProvider} models...` : `Fetch ${selectedServiceProvider} models`}</span>
|
||||||
<span>{isFetching ? `Syncing ${selectedServiceProvider} models...` : `Fetch ${selectedServiceProvider} models`}</span>
|
</Button>
|
||||||
</Button>
|
|
||||||
|
{showAvailableModels && (
|
||||||
{showAvailableModels && (
|
<div className="text-sm text-muted-foreground">
|
||||||
<div className="text-sm text-muted-foreground">
|
{availableModels.length} models available • {selectedModels.length} selected
|
||||||
{availableModels.length} models available • {selectedModels.length} selected
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Available Models Selection */}
|
{/* Available Models Selection */}
|
||||||
{showAvailableModels && availableModels.length > 0 && (
|
{showAvailableModels && availableModels.length > 0 && (
|
||||||
<div className="border border-border rounded-lg p-4">
|
<div className="border border-border rounded-lg p-4">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<h3 className="font-medium">{t('availableModels')} ({availableModels.length})</h3>
|
<h3 className="font-medium">Available Models ({availableModels.length})</h3>
|
||||||
<Button
|
<Button
|
||||||
onClick={addSelectedModelsToplan}
|
onClick={addSelectedModels}
|
||||||
disabled={selectedModels.length === 0 || isAdding}
|
disabled={selectedModels.length === 0 || isAdding}
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
{isAdding ? (
|
{isAdding ? (
|
||||||
<>
|
<>
|
||||||
<LoadingSpinner size="sm" />
|
<LoadingSpinner size="sm" />
|
||||||
<span className="ml-2">{t('addingModels')}</span>
|
<span className="ml-2">Adding models...</span>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
`${t('addSelectedModels')} (${selectedModels.length})`
|
`Add Selected (${selectedModels.length})`
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@ -367,26 +388,24 @@ export default function AdminModelsPage() {
|
|||||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4 max-h-96 overflow-y-auto">
|
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4 max-h-96 overflow-y-auto">
|
||||||
{availableModels.map(model => {
|
{availableModels.map(model => {
|
||||||
const isSelected = selectedModels.includes(model.modelId)
|
const isSelected = selectedModels.includes(model.modelId)
|
||||||
const isAlreadyAdded = models.some(m =>
|
const isAlreadyAdded = models.some(m => m.modelId === model.modelId)
|
||||||
m.modelId === model.modelId && m.subscriptionPlanId === selectedPlan
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={model.modelId}
|
key={model.modelId}
|
||||||
className={`border rounded-lg p-4 cursor-pointer transition-colors ${
|
className={`border rounded-lg p-4 cursor-pointer transition-all hover:shadow-sm ${
|
||||||
isAlreadyAdded
|
isAlreadyAdded
|
||||||
? 'border-yellow-300 bg-yellow-50 dark:bg-yellow-900/20'
|
? 'border-yellow-300 bg-yellow-50 dark:bg-yellow-900/20'
|
||||||
: isSelected
|
: isSelected
|
||||||
? 'border-primary bg-primary/5'
|
? 'border-primary bg-primary/5 shadow-sm'
|
||||||
: 'border-border hover:bg-accent'
|
: 'border-border hover:bg-accent'
|
||||||
}`}
|
}`}
|
||||||
onClick={() => !isAlreadyAdded && toggleModelSelection(model.modelId)}
|
onClick={() => !isAlreadyAdded && toggleModelSelection(model.modelId)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-start justify-between">
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<h4 className="font-medium text-base truncate mb-2">{model.name}</h4>
|
<h4 className="font-medium text-sm truncate mb-2">{model.name || model.modelId}</h4>
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
<Badge variant="secondary" className="text-xs">
|
<Badge variant="secondary" className="text-xs">
|
||||||
{model.provider}
|
{model.provider}
|
||||||
@ -394,9 +413,6 @@ export default function AdminModelsPage() {
|
|||||||
<Badge variant="outline" className="text-xs">
|
<Badge variant="outline" className="text-xs">
|
||||||
{model.outputType}
|
{model.outputType}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge variant="outline" className="text-xs">
|
|
||||||
{model.serviceProvider}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2 text-xs text-muted-foreground">
|
<div className="flex items-center space-x-2 text-xs text-muted-foreground">
|
||||||
@ -414,8 +430,8 @@ export default function AdminModelsPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{isAlreadyAdded && (
|
{isAlreadyAdded && (
|
||||||
<div className="flex items-center mt-1 text-xs text-yellow-600 dark:text-yellow-400">
|
<div className="flex items-center mt-2 text-xs text-yellow-600 dark:text-yellow-400">
|
||||||
<AlertCircle className="h-3 w-3 mr-1" />
|
<CheckCircle2 className="h-3 w-3 mr-1" />
|
||||||
Already added
|
Already added
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -436,125 +452,166 @@ export default function AdminModelsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|
||||||
{/* Current Models by Plan */}
|
{/* Current Models List */}
|
||||||
<div className="space-y-6">
|
<Card>
|
||||||
{plans.map(plan => {
|
<CardHeader>
|
||||||
const planModels = getModelsByPlan(plan.id)
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Database className="h-5 w-5" />
|
||||||
|
Configured Models ({filteredModels.length})
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
All AI models available across subscription plans with different cost multipliers
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
return (
|
{/* Search and Filter Controls */}
|
||||||
<Card key={plan.id} className="p-6">
|
<div className="flex flex-col sm:flex-row gap-4 mt-4">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="relative flex-1">
|
||||||
<div>
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" />
|
||||||
<h2 className="text-xl font-semibold text-foreground">
|
<input
|
||||||
{plan.displayName}
|
type="text"
|
||||||
</h2>
|
placeholder="Search models, providers, or IDs..."
|
||||||
<p className="text-sm text-muted-foreground">
|
value={searchTerm}
|
||||||
{planModels.length} {t('planModelsCount')}
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
</p>
|
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"
|
||||||
</div>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<select
|
||||||
|
value={filterProvider}
|
||||||
|
onChange={(e) => setFilterProvider(e.target.value)}
|
||||||
|
className="bg-background border border-input rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-ring"
|
||||||
|
>
|
||||||
|
<option value="">All Providers</option>
|
||||||
|
{allProviders.map(provider => (
|
||||||
|
<option key={provider} value={provider}>{provider}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
|
||||||
{planModels.length === 0 ? (
|
<select
|
||||||
<div className="text-center py-8">
|
value={filterOutputType}
|
||||||
<Database className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
|
onChange={(e) => setFilterOutputType(e.target.value)}
|
||||||
<p className="text-muted-foreground mb-2">{t('noModelsConfigured')}</p>
|
className="bg-background border border-input rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-ring"
|
||||||
<p className="text-xs text-muted-foreground">
|
>
|
||||||
Select this plan above and fetch models to get started
|
<option value="">All Types</option>
|
||||||
</p>
|
{allOutputTypes.map(type => (
|
||||||
</div>
|
<option key={type} value={type}>{type}</option>
|
||||||
) : (
|
))}
|
||||||
<div className="space-y-3">
|
</select>
|
||||||
{planModels.map(model => (
|
</div>
|
||||||
<div
|
</div>
|
||||||
key={model.id}
|
</CardHeader>
|
||||||
className="border border-border rounded-lg p-4"
|
|
||||||
>
|
<CardContent>
|
||||||
<div className="flex items-center justify-between">
|
{filteredModels.length === 0 ? (
|
||||||
<div className="flex-1">
|
<div className="text-center py-12">
|
||||||
<div className="flex items-center space-x-3 mb-2">
|
<Database className="h-16 w-16 text-muted-foreground mx-auto mb-4" />
|
||||||
<h3 className="font-medium text-foreground">{model.name}</h3>
|
<h3 className="text-lg font-semibold mb-2">No models found</h3>
|
||||||
<Badge variant="secondary" className="text-xs">
|
<p className="text-muted-foreground mb-4">
|
||||||
{model.provider}
|
{searchTerm || filterProvider || filterOutputType
|
||||||
</Badge>
|
? 'Try adjusting your search or filter criteria'
|
||||||
<Badge variant="outline" className="text-xs">
|
: 'Start by adding some models from the section above'
|
||||||
{model.outputType}
|
}
|
||||||
</Badge>
|
</p>
|
||||||
<Badge variant="outline" className="text-xs">
|
</div>
|
||||||
{model.serviceProvider}
|
) : (
|
||||||
</Badge>
|
<div className="space-y-3">
|
||||||
<Badge
|
{filteredModels.map(model => (
|
||||||
variant={model.isActive ? "default" : "outline"}
|
<div
|
||||||
className="text-xs"
|
key={model.id}
|
||||||
>
|
className="border border-border rounded-lg p-4 hover:shadow-sm transition-shadow"
|
||||||
{model.isActive ? t('modelEnabled') : t('modelDisabled')}
|
>
|
||||||
</Badge>
|
<div className="flex items-center justify-between">
|
||||||
</div>
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center space-x-3 mb-2">
|
||||||
<div className="flex items-center space-x-4 text-sm text-muted-foreground">
|
<h3 className="font-semibold text-foreground">{model.name}</h3>
|
||||||
<span className="flex items-center">
|
<Badge variant="secondary" className="text-xs">
|
||||||
<Cpu className="h-3 w-3 mr-1" />
|
{model.provider}
|
||||||
{model.modelId}
|
</Badge>
|
||||||
</span>
|
<Badge variant="outline" className="text-xs">
|
||||||
{model.maxTokens && (
|
{model.outputType}
|
||||||
<span className="flex items-center">
|
</Badge>
|
||||||
<Zap className="h-3 w-3 mr-1" />
|
<Badge variant="outline" className="text-xs">
|
||||||
{model.maxTokens.toLocaleString()} tokens
|
{model.serviceProvider}
|
||||||
</span>
|
</Badge>
|
||||||
)}
|
<Badge
|
||||||
{model.inputCostPer1k && (
|
variant={model.isActive ? "default" : "outline"}
|
||||||
<span className="flex items-center">
|
className={`text-xs ${model.isActive ? 'bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-400' : ''}`}
|
||||||
<DollarSign className="h-3 w-3 mr-1" />
|
>
|
||||||
${model.inputCostPer1k.toFixed(4)}/1K in
|
{model.isActive ? 'Active' : 'Inactive'}
|
||||||
</span>
|
</Badge>
|
||||||
)}
|
|
||||||
{model.outputCostPer1k && (
|
|
||||||
<span className="flex items-center">
|
|
||||||
<DollarSign className="h-3 w-3 mr-1" />
|
|
||||||
${model.outputCostPer1k.toFixed(4)}/1K out
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{model.description && (
|
|
||||||
<p className="text-sm text-muted-foreground mt-2 line-clamp-2">
|
|
||||||
{model.description}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => toggleModelStatus(model.id, model.isActive)}
|
|
||||||
>
|
|
||||||
{model.isActive ? (
|
|
||||||
<EyeOff className="h-4 w-4" />
|
|
||||||
) : (
|
|
||||||
<Eye className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => removeModel(model.id)}
|
|
||||||
className="text-red-600 hover:text-red-700"
|
|
||||||
>
|
|
||||||
<Trash2 className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-4 text-sm text-muted-foreground">
|
||||||
|
<span className="flex items-center">
|
||||||
|
<Cpu className="h-3 w-3 mr-1" />
|
||||||
|
{model.modelId}
|
||||||
|
</span>
|
||||||
|
{model.maxTokens && (
|
||||||
|
<span className="flex items-center">
|
||||||
|
<Zap className="h-3 w-3 mr-1" />
|
||||||
|
{model.maxTokens.toLocaleString()} tokens
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{model.inputCostPer1k && (
|
||||||
|
<span className="flex items-center">
|
||||||
|
<DollarSign className="h-3 w-3 mr-1" />
|
||||||
|
${model.inputCostPer1k.toFixed(6)}/1K in
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{model.outputCostPer1k && (
|
||||||
|
<span className="flex items-center">
|
||||||
|
<DollarSign className="h-3 w-3 mr-1" />
|
||||||
|
${model.outputCostPer1k.toFixed(6)}/1K out
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{model.description && (
|
||||||
|
<p className="text-sm text-muted-foreground mt-2 line-clamp-2">
|
||||||
|
{model.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
|
||||||
|
<div className="flex items-center space-x-2 ml-4">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => toggleModelStatus(model.id, model.isActive)}
|
||||||
|
title={model.isActive ? 'Disable model' : 'Enable model'}
|
||||||
|
>
|
||||||
|
{model.isActive ? (
|
||||||
|
<EyeOff className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<Eye className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => removeModel(model.id)}
|
||||||
|
className="text-red-600 hover:text-red-700 hover:border-red-200"
|
||||||
|
title="Remove model"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
))}
|
||||||
</Card>
|
</div>
|
||||||
)
|
)}
|
||||||
})}
|
</CardContent>
|
||||||
</div>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user