'use client'; import { Fragment, useEffect, useMemo, useState } from 'react'; import { Button, Listbox, ListboxButton, ListboxOption, ListboxOptions, Transition } from '@headlessui/react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import toast from 'react-hot-toast'; import { ChevronUpDownIcon, CheckIcon } from '@/components/icons/Icons'; import { Badge, Section, ToggleRow, btnPrimary, btnSecondary, listboxButtonClass, listboxOptionClass, listboxOptionsClass, } from '@/components/formPrimitives'; import { type TtsProviderId } from '@/lib/shared/tts-provider-catalog'; import { useSharedProviders, type SharedProviderEntry } from '@/hooks/useSharedProviders'; type RuntimeConfigSource = 'env-seed' | 'admin' | 'default'; interface SettingsResponse { values: Record; sources: Record; } interface ProviderOption { id: string; name: string; providerType: TtsProviderId; } const ADMIN_SETTINGS_QUERY_KEY = ['admin-settings'] as const; async function fetchAdminSettings(): Promise { const res = await fetch('/api/admin/settings'); if (!res.ok) throw new Error(`HTTP ${res.status}`); return (await res.json()) as SettingsResponse; } async function patchAdminSettings(payload: { updates?: Record; reset?: string[] }): Promise { const res = await fetch('/api/admin/settings', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); if (!res.ok && res.status !== 207) throw new Error(`HTTP ${res.status}`); } export function AdminFeaturesPanel() { const queryClient = useQueryClient(); const { data, error } = useQuery({ queryKey: ADMIN_SETTINGS_QUERY_KEY, queryFn: fetchAdminSettings, }); const [draft, setDraft] = useState>({}); const [dirty, setDirty] = useState>(new Set()); const { providers: sharedProviders } = useSharedProviders(); useEffect(() => { if (!data) return; setDraft({ ...data.values }); setDirty(new Set()); }, [data]); useEffect(() => { if (!error) return; console.error('[AdminFeaturesPanel] load failed:', error); toast.error('Failed to load site settings'); }, [error]); const resetMutation = useMutation({ mutationFn: async (key: string) => { await patchAdminSettings({ reset: [key] }); }, onSuccess: async () => { toast.success('Reset to env default'); await queryClient.invalidateQueries({ queryKey: ADMIN_SETTINGS_QUERY_KEY }); }, onError: (mutationError) => { console.error(mutationError); toast.error('Reset failed'); }, }); const saveMutation = useMutation({ mutationFn: async (updates: Record) => { await patchAdminSettings({ updates }); }, onSuccess: async () => { toast.success('Settings saved'); await queryClient.invalidateQueries({ queryKey: ADMIN_SETTINGS_QUERY_KEY }); }, onError: (mutationError) => { console.error(mutationError); toast.error('Save failed'); }, }); const saving = resetMutation.isPending || saveMutation.isPending; const updateDraft = (key: string, value: unknown) => { setDraft((d) => ({ ...d, [key]: value })); setDirty((s) => { const next = new Set(s); next.add(key); return next; }); }; const resetField = (key: string) => { if (saving) return; resetMutation.mutate(key); }; const saveAll = () => { if (saving || dirty.size === 0) return; const updates: Record = {}; for (const key of dirty) updates[key] = draft[key]; saveMutation.mutate(updates); }; const discardAll = () => { if (!data) return; setDraft({ ...data.values }); setDirty(new Set()); }; const providerOptions = useMemo(() => { return sharedProviders.map((entry) => ({ id: entry.slug, name: `${entry.displayName} (shared)`, providerType: entry.providerType, })); }, [sharedProviders]); const currentProviderId = typeof draft.defaultTtsProvider === 'string' ? draft.defaultTtsProvider : ''; const currentSharedEntry: SharedProviderEntry | undefined = sharedProviders.find( (p) => p.slug === currentProviderId, ); const fallbackShared = providerOptions[0]; const effectiveSelectedProvider = currentSharedEntry ? { id: currentSharedEntry.slug, name: `${currentSharedEntry.displayName} (shared)`, providerType: currentSharedEntry.providerType, } as ProviderOption : fallbackShared; const selectedProviderOption = effectiveSelectedProvider; const handleProviderChange = (opt: ProviderOption) => { updateDraft('defaultTtsProvider', opt.id); }; const renderSource = (key: string) => { const source = data?.sources?.[key] ?? 'default'; const isDirty = dirty.has(key); return ( resetField(key)} saving={saving} /> ); }; if (!data) { return ( ); } return (
Defaults} >

Default TTS provider

Starting provider for new users.

{renderSource('defaultTtsProvider')}
{providerOptions.length > 0 ? ( {selectedProviderOption?.name ?? 'Select provider'} {providerOptions.map((opt) => ( listboxOptionClass(active)} > {({ selected }) => ( <> {opt.name} {selected && ( )} )} ))} ) : (
No shared providers yet. Add one first.
)}
{ if (!checked) { const ok = confirm( 'Turning this off allows user-supplied API keys to flow through this server. Continue?', ); if (!ok) return; } updateDraft('restrictUserApiKeys', checked); }} right={renderSource('restrictUserApiKeys')} variant="flat" /> updateDraft('enableTtsProvidersTab', checked)} right={renderSource('enableTtsProvidersTab')} variant="flat" /> updateDraft('showAllProviderModels', checked)} right={renderSource('showAllProviderModels')} variant="flat" />
Feature Flags} > updateDraft('enableWordHighlight', checked)} right={renderSource('enableWordHighlight')} variant="flat" /> updateDraft('enableAudiobookExport', checked)} right={renderSource('enableAudiobookExport')} variant="flat" /> updateDraft('enableDocxConversion', checked)} right={renderSource('enableDocxConversion')} variant="flat" /> updateDraft('enableDestructiveDeleteActions', checked)} right={renderSource('enableDestructiveDeleteActions')} variant="flat" />

{dirty.size > 0 ? `${dirty.size} unsaved change${dirty.size === 1 ? '' : 's'}` : 'No unsaved changes'}

); } function AdminFeaturesSkeleton() { return (
} >
} >
); } function SourceBadge({ source, dirty, canReset, onReset, saving, }: { source: RuntimeConfigSource; dirty: boolean; canReset: boolean; onReset: () => void; saving: boolean; }) { return (
{canReset && !dirty && ( )} {dirty ? ( Modified ) : source === 'env-seed' ? ( from env ) : source === 'admin' ? ( admin ) : ( default )}
); }