'use client' import { useCallback, useState } from 'react' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { KeyRound, Loader2, Shield, Trash2 } from 'lucide-react' import { toast } from 'sonner' import { useLanguage } from '@/lib/i18n' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { cn } from '@/lib/utils' type PublicKey = { provider: string alias: string model: string | null isActive: boolean lastUsedAt: string | null } async function fetchByokKeys(): Promise<{ keys: PublicKey[] allowedProviders: string[] }> { const res = await fetch('/api/user/api-keys') if (!res.ok) { const body = await res.json().catch(() => ({})) throw new Error(body.message || body.error || 'Failed to load keys') } return res.json() } function providerLabel(t: (key: string) => string, provider: string): string { const key = `byokSettings.providers.${provider}` const translated = t(key) return translated === key ? provider : translated } export function ByokSettingsPanel() { const { t } = useLanguage() const queryClient = useQueryClient() const [provider, setProvider] = useState('') const [apiKey, setApiKey] = useState('') const [alias, setAlias] = useState('') const { data, isLoading, error } = useQuery({ queryKey: ['user', 'api-keys'], queryFn: fetchByokKeys, }) const invalidate = useCallback(() => { queryClient.invalidateQueries({ queryKey: ['user', 'api-keys'] }) queryClient.invalidateQueries({ queryKey: ['usage', 'current'] }) }, [queryClient]) const saveMutation = useMutation({ mutationFn: async () => { const res = await fetch('/api/user/api-keys', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ provider, apiKey, alias: alias || undefined }), }) const body = await res.json().catch(() => ({})) if (!res.ok) { throw new Error(body.message || body.error || 'Save failed') } return body }, onSuccess: () => { toast.success(t('byokSettings.saved')) setApiKey('') setAlias('') setProvider('') invalidate() }, onError: (err: Error) => { toast.error(err.message || t('byokSettings.error')) }, }) const toggleMutation = useMutation({ mutationFn: async ({ provider: p, isActive, }: { provider: string isActive: boolean }) => { const res = await fetch(`/api/user/api-keys/${encodeURIComponent(p)}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ isActive }), }) if (!res.ok) throw new Error('Update failed') }, onSuccess: invalidate, onError: () => toast.error(t('byokSettings.error')), }) const deleteMutation = useMutation({ mutationFn: async (p: string) => { const res = await fetch(`/api/user/api-keys/${encodeURIComponent(p)}`, { method: 'DELETE', }) if (!res.ok) throw new Error('Delete failed') }, onSuccess: () => { toast.success(t('byokSettings.deleted')) invalidate() }, onError: () => toast.error(t('byokSettings.error')), }) const allowed = data?.allowedProviders ?? [] const keys = data?.keys ?? [] const hasActiveByok = keys.some((k) => k.isActive) const tierBlocked = !isLoading && allowed.length === 0 if (isLoading) { return (
{t('byokSettings.loadError')}
) } return ({t('byokSettings.description')}
{t('byokSettings.tierRequired')}
) : ({key.alias}
) : null}{t('byokSettings.empty')}
)}