fix model admin

This commit is contained in:
songtianlun 2025-08-28 00:06:19 +08:00
parent 1d6b1ae0e1
commit be53af4638

View File

@ -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>
) )
} }