'use client'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import toast from 'react-hot-toast'; import { DotsHorizontalIcon, PlusIcon } from '@/components/icons/Icons'; import { providerSupportsCustomModel, resolveProviderModels, type TtsModelDefinition, type TtsProviderId, type TtsProviderType } from '@/lib/shared/tts-provider-catalog'; import { defaultBaseUrlForProviderType, defaultModelForProviderType, resolveTtsProviderModelPolicy } from '@/lib/shared/tts-provider-policy'; import { Badge, Field, Section, ToggleRow, Select, Button, IconButton, Input, Textarea, MenuItemsSurface, MenuActionItem, MenuRoot, MenuTrigger, MenuTransition, } from '@/components/ui'; type ProviderType = TtsProviderId; interface AdminProviderMasked { id: string; slug: string; displayName: string; providerType: ProviderType; baseUrl: string | null; apiKeyMask: string; defaultModel: string | null; defaultInstructions: string | null; enabled: boolean; createdAt: number; updatedAt: number; } const PROVIDER_TYPE_OPTIONS: { value: ProviderType; label: string }[] = [ { value: 'custom-openai', label: 'Custom OpenAI-like' }, { value: 'openai', label: 'OpenAI' }, { value: 'deepinfra', label: 'Deepinfra' }, { value: 'replicate', label: 'Replicate' }, ]; interface FormState { slug: string; displayName: string; providerType: ProviderType; baseUrl: string; apiKey: string; defaultModel: string; defaultInstructions: string; enabled: boolean; } const providerDefaultModel = defaultModelForProviderType; const ADMIN_PROVIDERS_QUERY_KEY = ['admin-providers'] as const; const ADMIN_SETTINGS_QUERY_KEY = ['admin-settings'] as const; const ADMIN_DEFAULT_PROVIDER_QUERY_KEY = ['admin-settings', 'default-provider-slug'] as const; async function fetchDefaultProviderSlug(): Promise { const res = await fetch('/api/admin/settings'); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = (await res.json()) as { values?: { defaultTtsProvider?: unknown } }; return typeof data.values?.defaultTtsProvider === 'string' ? data.values.defaultTtsProvider : ''; } async function patchDefaultProviderSlug(slug: string): Promise { const res = await fetch('/api/admin/settings', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ updates: { defaultTtsProvider: slug } }), }); if (res.status === 200 || res.status === 204) return; if (res.status === 207) { // Multi-status: the request succeeded for some keys but failed for others. // Only treat it as success when the defaultTtsProvider field itself was // accepted (no matching entry in the errors array). const payload = (await res.json().catch(() => ({}))) as { errors?: Array<{ key?: string; message?: string }>; }; const failure = payload.errors?.find((entry) => entry?.key === 'defaultTtsProvider'); if (failure) { throw new Error(failure.message || 'Failed to update default provider'); } return; } throw new Error(`HTTP ${res.status}`); } async function patchProviderEnabled(input: { id: string; enabled: boolean }): Promise { const res = await fetch(`/api/admin/providers/${input.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled: input.enabled }), }); if (!res.ok) { const data = (await res.json().catch(() => ({}))) as { error?: string }; throw new Error(data.error || `HTTP ${res.status}`); } } function createEmptyForm(): FormState { return { slug: '', displayName: '', providerType: 'custom-openai', baseUrl: '', apiKey: '', defaultModel: providerDefaultModel('custom-openai'), defaultInstructions: '', enabled: true, }; } function truncateModelLabel(value: string, maxLength = 56): string { if (value.length <= maxLength) return value; return `${value.slice(0, maxLength - 3)}...`; } async function fetchAdminProviders(): Promise { const res = await fetch('/api/admin/providers'); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = (await res.json()) as { providers: AdminProviderMasked[] }; return data.providers; } async function upsertAdminProvider(input: { editingId: string | null; form: FormState; }): Promise { const isNew = input.editingId === '__new'; const body = { slug: input.form.slug.trim(), displayName: input.form.displayName.trim(), providerType: input.form.providerType, baseUrl: input.form.baseUrl.trim() || null, ...(input.form.apiKey.length > 0 ? { apiKey: input.form.apiKey } : {}), defaultModel: input.form.defaultModel.trim() || null, defaultInstructions: input.form.defaultInstructions.trim() || null, enabled: input.form.enabled, }; const url = isNew ? '/api/admin/providers' : `/api/admin/providers/${input.editingId}`; const res = await fetch(url, { method: isNew ? 'POST' : 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); if (!res.ok) { const data = (await res.json().catch(() => ({}))) as { error?: string }; throw new Error(data.error || `HTTP ${res.status}`); } } async function deleteAdminProvider(id: string): Promise { const res = await fetch(`/api/admin/providers/${id}`, { method: 'DELETE' }); if (!res.ok) { const data = (await res.json().catch(() => ({}))) as { error?: string }; throw new Error(data.error || `HTTP ${res.status}`); } } export function AdminProvidersPanel() { const queryClient = useQueryClient(); const [editingId, setEditingId] = useState(null); const [form, setForm] = useState(() => createEmptyForm()); const [customModelInput, setCustomModelInput] = useState(''); const { data: providers = [], isPending: isLoading, error } = useQuery({ queryKey: ADMIN_PROVIDERS_QUERY_KEY, queryFn: fetchAdminProviders, }); const { data: defaultProviderSlug = '', error: defaultProviderError, } = useQuery({ queryKey: ADMIN_DEFAULT_PROVIDER_QUERY_KEY, queryFn: fetchDefaultProviderSlug, }); useEffect(() => { if (!error) return; console.error('[AdminProvidersPanel] load failed:', error); toast.error('Failed to load admin providers'); }, [error]); useEffect(() => { if (!defaultProviderError) return; console.error('[AdminProvidersPanel] default provider load failed:', defaultProviderError); toast.error('Failed to load default provider'); }, [defaultProviderError]); const saveMutation = useMutation({ mutationFn: upsertAdminProvider, onSuccess: async (_data, variables) => { toast.success(variables.editingId === '__new' ? 'Provider created' : 'Provider updated'); cancelEdit(); await Promise.all([ queryClient.invalidateQueries({ queryKey: ADMIN_PROVIDERS_QUERY_KEY }), queryClient.invalidateQueries({ queryKey: ADMIN_SETTINGS_QUERY_KEY }), queryClient.invalidateQueries({ queryKey: ADMIN_DEFAULT_PROVIDER_QUERY_KEY }), ]); }, onError: (mutationError) => { console.error('[AdminProvidersPanel] save failed:', mutationError); toast.error((mutationError as Error).message || 'Save failed'); }, }); const deleteMutation = useMutation({ mutationFn: deleteAdminProvider, onSuccess: async () => { toast.success('Provider deleted'); await Promise.all([ queryClient.invalidateQueries({ queryKey: ADMIN_PROVIDERS_QUERY_KEY }), queryClient.invalidateQueries({ queryKey: ADMIN_SETTINGS_QUERY_KEY }), queryClient.invalidateQueries({ queryKey: ADMIN_DEFAULT_PROVIDER_QUERY_KEY }), ]); }, onError: (mutationError) => { console.error('[AdminProvidersPanel] delete failed:', mutationError); toast.error((mutationError as Error).message || 'Delete failed'); }, }); const toggleEnabledMutation = useMutation({ mutationFn: patchProviderEnabled, onSuccess: async (_data, vars) => { toast.success(vars.enabled ? 'Provider enabled' : 'Provider disabled'); await Promise.all([ queryClient.invalidateQueries({ queryKey: ADMIN_PROVIDERS_QUERY_KEY }), queryClient.invalidateQueries({ queryKey: ADMIN_SETTINGS_QUERY_KEY }), queryClient.invalidateQueries({ queryKey: ADMIN_DEFAULT_PROVIDER_QUERY_KEY }), ]); }, onError: (mutationError) => { console.error('[AdminProvidersPanel] toggle enabled failed:', mutationError); toast.error((mutationError as Error).message || 'Update failed'); }, }); const setDefaultMutation = useMutation({ mutationFn: patchDefaultProviderSlug, onSuccess: async () => { toast.success('Default provider updated'); await Promise.all([ queryClient.invalidateQueries({ queryKey: ADMIN_SETTINGS_QUERY_KEY }), queryClient.invalidateQueries({ queryKey: ADMIN_DEFAULT_PROVIDER_QUERY_KEY }), ]); }, onError: (mutationError) => { console.error('[AdminProvidersPanel] set default failed:', mutationError); toast.error((mutationError as Error).message || 'Failed to set default'); }, }); const startCreate = () => { setForm(createEmptyForm()); setCustomModelInput(''); setEditingId('__new'); }; const startEdit = (provider: AdminProviderMasked) => { setForm({ slug: provider.slug, displayName: provider.displayName, providerType: provider.providerType, baseUrl: provider.baseUrl ?? '', apiKey: '', defaultModel: provider.defaultModel ?? providerDefaultModel(provider.providerType), defaultInstructions: provider.defaultInstructions ?? '', enabled: provider.enabled, }); setCustomModelInput(''); setEditingId(provider.id); }; function cancelEdit() { setEditingId(null); setForm(createEmptyForm()); setCustomModelInput(''); } const submit = () => { if (saveMutation.isPending) return; saveMutation.mutate({ editingId, form }); }; const remove = (id: string) => { if (!confirm('Delete this shared provider? Users selecting it will lose access until they switch.')) return; if (deleteMutation.isPending) return; deleteMutation.mutate(id); }; const toggleEnabled = (provider: AdminProviderMasked) => { if (toggleEnabledMutation.isPending) return; toggleEnabledMutation.mutate({ id: provider.id, enabled: !provider.enabled }); }; const setDefault = (slug: string) => { if (setDefaultMutation.isPending) return; setDefaultMutation.mutate(slug); }; const isEditingExisting = editingId !== null && editingId !== '__new'; const editingProvider = isEditingExisting ? providers.find((p) => p.id === editingId) : undefined; const submitDisabled = saveMutation.isPending || !form.slug.trim() || !form.displayName.trim(); const selectedProviderType = PROVIDER_TYPE_OPTIONS.find((opt) => opt.value === form.providerType) ?? PROVIDER_TYPE_OPTIONS[0]; const modelDefinitions: TtsModelDefinition[] = useMemo( () => resolveProviderModels(form.providerType, { apiKey: form.apiKey, }), [form.providerType, form.apiKey], ); const supportsCustomModel = providerSupportsCustomModel(form.providerType); const modelIsPreset = modelDefinitions.some((model) => model.id === form.defaultModel); const selectedModelId = modelIsPreset ? form.defaultModel : supportsCustomModel ? 'custom' : modelDefinitions[0]?.id ?? ''; const selectedModelDefinition = modelDefinitions.find((model) => model.id === selectedModelId); const modelSupportsInstructions = useCallback((model: string, providerType: TtsProviderType = form.providerType) => resolveTtsProviderModelPolicy({ providerRef: form.slug, providerType, model, }).supportsInstructions, [form.slug, form.providerType]); const baseUrlPlaceholder = form.providerType === 'custom-openai' ? 'https://your-tts-host/v1' : defaultBaseUrlForProviderType(form.providerType); const shouldShowBaseUrl = form.providerType === 'custom-openai'; const shouldShowInstructions = modelSupportsInstructions(form.defaultModel); useEffect(() => { if (!supportsCustomModel) { if (customModelInput) setCustomModelInput(''); return; } if (!modelIsPreset && form.defaultModel && customModelInput !== form.defaultModel) { setCustomModelInput(form.defaultModel); } if (modelIsPreset && customModelInput) { setCustomModelInput(''); } }, [supportsCustomModel, modelIsPreset, form.defaultModel, customModelInput]); useEffect(() => { if (modelSupportsInstructions(form.defaultModel)) return; if (!form.defaultInstructions) return; setForm((prev) => ({ ...prev, defaultInstructions: '' })); }, [form.defaultModel, form.defaultInstructions, form.providerType, form.slug, modelSupportsInstructions]); return (
Shared {!editingId ? ( ) : null} } > {editingId && (

{isEditingExisting ? `Edit "${editingProvider?.slug}"` : 'New provider'}

{isEditingExisting && ( slug cannot be changed after create )}
setForm({ ...form, slug: e.target.value })} placeholder="kokoro-prod" disabled={isEditingExisting} /> setForm({ ...form, displayName: e.target.value })} placeholder="Kokoro (production)" /> model.id} renderValue={(model) => model.name} renderOption={(model, { selected }) => ( {model.name} {model.id.includes(':') ? ( {model.id.slice(model.id.indexOf(':'))} ) : null} )} placeholder="Select model" chevronClassName="h-4 w-4 text-muted" onChange={(model) => { if (model.id === 'custom') { const nextModel = customModelInput.trim(); setForm({ ...form, defaultModel: nextModel, defaultInstructions: modelSupportsInstructions(nextModel) ? form.defaultInstructions : '', }); return; } setForm({ ...form, defaultModel: model.id, defaultInstructions: modelSupportsInstructions(model.id) ? form.defaultInstructions : '', }); setCustomModelInput(''); }} /> {supportsCustomModel && selectedModelId === 'custom' && ( { const nextModel = e.target.value; setCustomModelInput(nextModel); setForm({ ...form, defaultModel: nextModel, defaultInstructions: modelSupportsInstructions(nextModel) ? form.defaultInstructions : '', }); }} placeholder="Enter custom model id" /> )}
{shouldShowInstructions && (