'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[] providerModels: Record }> { 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 [model, setModel] = useState('') const [customModel, setCustomModel] = useState('') const [isCustomModel, setIsCustomModel] = useState(false) // Dynamic models fetched directly via user's API Key const [liveModels, setLiveModels] = useState([]) const [isFetchingLiveModels, setIsFetchingLiveModels] = useState(false) const { data, isLoading, error } = useQuery({ queryKey: ['user', 'api-keys'], queryFn: fetchByokKeys, }) const providerModels = data?.providerModels ?? {} const handleProviderChange = (p: string) => { setProvider(p) setApiKey('') setLiveModels([]) setIsCustomModel(false) setModel('') setCustomModel('') } // Triggered dynamically to fetch models when user enters/pastes their API key const fetchLiveModels = async (p: string, key: string) => { if (!p || !key || key.length < 8) return; setIsFetchingLiveModels(true) try { const query = new URLSearchParams({ provider: p, key }) const res = await fetch(`/api/user/api-keys/live-models?${query.toString()}`) if (res.ok) { const body = await res.json() if (body.success && Array.isArray(body.models)) { setLiveModels(body.models) if (body.models.length > 0) { setModel(body.models[0]) setIsCustomModel(false) } else { setIsCustomModel(true) } return; } } } catch (err) { console.error('[fetchLiveModels] Failed:', err) } finally { setIsFetchingLiveModels(false) } // Fallback if request fails const fallbackList = providerModels[p] || [] setLiveModels(fallbackList) if (fallbackList.length > 0) { setModel(fallbackList[0]) setIsCustomModel(false) } else { setIsCustomModel(true) } } const invalidate = useCallback(() => { queryClient.invalidateQueries({ queryKey: ['user', 'api-keys'] }) queryClient.invalidateQueries({ queryKey: ['usage', 'current'] }) }, [queryClient]) const saveMutation = useMutation({ mutationFn: async () => { const resolvedModel = isCustomModel ? customModel : model const res = await fetch('/api/user/api-keys', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ provider, apiKey, alias: alias || undefined, model: resolvedModel || 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('') setModel('') setCustomModel('') setLiveModels([]) setIsCustomModel(false) 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.loading')}
) } if (error) { return (

{t('byokSettings.loadError')}

) } return (

{t('byokSettings.title')} {hasActiveByok && ( {t('byokSettings.badgeActive')} )}

{t('byokSettings.description')}

{tierBlocked ? (

{t('byokSettings.tierRequired')}

) : (
setAlias(e.target.value)} placeholder={t('byokSettings.aliasPlaceholder')} disabled={saveMutation.isPending} />
{/* Model Selection Row */} {provider && (
{isFetchingLiveModels ? (
Récupération de vos modèles disponibles...
) : liveModels && liveModels.length > 0 ? ( ) : (
Entrez votre clé API ci-dessous pour charger vos modèles.
)}
{(isCustomModel || (!isFetchingLiveModels && !(liveModels && liveModels.length > 0))) && (
{ if (isCustomModel) { setCustomModel(e.target.value) } else { setModel(e.target.value) } }} placeholder="ex. deepseek-reasoner, minimax-abab6.5" disabled={saveMutation.isPending} />
)}
)}
{ const val = e.target.value setApiKey(val) fetchLiveModels(provider, val) }} placeholder={t('byokSettings.apiKeyPlaceholder')} disabled={saveMutation.isPending} />
)} {keys.length > 0 && (
    {keys.map((key) => (
  • {providerLabel(t, key.provider)}
    {key.alias ? (

    {key.alias}

    ) : null} {key.model ? (

    Modèle : {key.model}

    ) : null}
  • ))}
)} {!tierBlocked && keys.length === 0 && (

{t('byokSettings.empty')}

)}
) }