import { useCallback, useEffect, useMemo, useState } from 'react' import { AlertTriangle, Download, Lock, Save, Upload } from 'lucide-react' import { useI18n } from '../i18n' const MAX_AUTO_FETCH_FAILURES = 3 export default function Settings({ onRefresh, onMessage, authFetch, onForceLogout, isVercel = false }) { const { t } = useI18n() const apiFetch = authFetch || fetch const [loading, setLoading] = useState(false) const [saving, setSaving] = useState(false) const [changingPassword, setChangingPassword] = useState(false) const [importing, setImporting] = useState(false) const [exportData, setExportData] = useState(null) const [importMode, setImportMode] = useState('merge') const [importText, setImportText] = useState('') const [newPassword, setNewPassword] = useState('') const [consecutiveFailures, setConsecutiveFailures] = useState(0) const [autoFetchPaused, setAutoFetchPaused] = useState(false) const [lastError, setLastError] = useState('') const [settingsMeta, setSettingsMeta] = useState({ default_password_warning: false, env_backed: false, needs_vercel_sync: false }) const [form, setForm] = useState({ admin: { jwt_expire_hours: 24 }, runtime: { account_max_inflight: 2, account_max_queue: 10, global_max_inflight: 10 }, toolcall: { mode: 'feature_match', early_emit_confidence: 'high' }, responses: { store_ttl_seconds: 900 }, embeddings: { provider: '' }, claude_mapping_text: '{\n "fast": "deepseek-chat",\n "slow": "deepseek-reasoner"\n}', model_aliases_text: '{}', }) const parseJSONMap = (raw, fieldName) => { const text = String(raw || '').trim() if (!text) { return {} } let parsed try { parsed = JSON.parse(text) } catch (_e) { throw new Error(t('settings.invalidJsonField', { field: fieldName })) } if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { throw new Error(t('settings.invalidJsonField', { field: fieldName })) } return parsed } const parseJSONResponse = useCallback(async (res) => { const contentType = String(res.headers.get('content-type') || '').toLowerCase() if (!contentType.includes('application/json')) { throw new Error(t('settings.nonJsonResponse', { status: res.status })) } return res.json() }, [t]) const loadSettings = useCallback(async ({ manual = false } = {}) => { if (isVercel && autoFetchPaused && !manual) { return } setLoading(true) try { const res = await apiFetch('/admin/settings') const data = await parseJSONResponse(res) if (!res.ok) { const detail = data.detail || t('settings.loadFailed') setLastError(detail) onMessage('error', detail) setConsecutiveFailures((prev) => { const next = prev + 1 if (isVercel && next >= MAX_AUTO_FETCH_FAILURES) { setAutoFetchPaused(true) } return next }) return } setConsecutiveFailures(0) setAutoFetchPaused(false) setLastError('') setSettingsMeta({ default_password_warning: Boolean(data.admin?.default_password_warning), env_backed: Boolean(data.env_backed), needs_vercel_sync: Boolean(data.needs_vercel_sync), }) setForm({ admin: { jwt_expire_hours: Number(data.admin?.jwt_expire_hours || 24) }, runtime: { account_max_inflight: Number(data.runtime?.account_max_inflight || 2), account_max_queue: Number(data.runtime?.account_max_queue || 10), global_max_inflight: Number(data.runtime?.global_max_inflight || 10), }, toolcall: { mode: data.toolcall?.mode || 'feature_match', early_emit_confidence: data.toolcall?.early_emit_confidence || 'high', }, responses: { store_ttl_seconds: Number(data.responses?.store_ttl_seconds || 900), }, embeddings: { provider: data.embeddings?.provider || '', }, claude_mapping_text: JSON.stringify(data.claude_mapping || {}, null, 2), model_aliases_text: JSON.stringify(data.model_aliases || {}, null, 2), }) } catch (e) { const detail = e?.message || t('settings.loadFailed') setLastError(detail) onMessage('error', detail) setConsecutiveFailures((prev) => { const next = prev + 1 if (isVercel && next >= MAX_AUTO_FETCH_FAILURES) { setAutoFetchPaused(true) } return next }) // eslint-disable-next-line no-console console.error(e) } finally { setLoading(false) } }, [apiFetch, autoFetchPaused, isVercel, onMessage, parseJSONResponse, t]) useEffect(() => { loadSettings() }, [loadSettings]) const retryLoadSettings = () => { setAutoFetchPaused(false) loadSettings({ manual: true }) } const saveSettings = async () => { let claudeMapping = {} let modelAliases = {} try { claudeMapping = parseJSONMap(form.claude_mapping_text, 'claude_mapping') modelAliases = parseJSONMap(form.model_aliases_text, 'model_aliases') } catch (e) { onMessage('error', e.message) return } const payload = { admin: { jwt_expire_hours: Number(form.admin.jwt_expire_hours) }, runtime: { account_max_inflight: Number(form.runtime.account_max_inflight), account_max_queue: Number(form.runtime.account_max_queue), global_max_inflight: Number(form.runtime.global_max_inflight), }, toolcall: { mode: String(form.toolcall.mode || '').trim(), early_emit_confidence: String(form.toolcall.early_emit_confidence || '').trim(), }, responses: { store_ttl_seconds: Number(form.responses.store_ttl_seconds) }, embeddings: { provider: String(form.embeddings.provider || '').trim() }, claude_mapping: claudeMapping, model_aliases: modelAliases, } setSaving(true) try { const res = await apiFetch('/admin/settings', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }) const data = await res.json() if (!res.ok) { onMessage('error', data.detail || t('settings.saveFailed')) return } onMessage('success', t('settings.saveSuccess')) if (typeof onRefresh === 'function') { onRefresh() } await loadSettings() } catch (e) { onMessage('error', t('settings.saveFailed')) // eslint-disable-next-line no-console console.error(e) } finally { setSaving(false) } } const updatePassword = async () => { if (String(newPassword || '').trim().length < 4) { onMessage('error', t('settings.passwordTooShort')) return } setChangingPassword(true) try { const res = await apiFetch('/admin/settings/password', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ new_password: newPassword.trim() }), }) const data = await res.json() if (!res.ok) { onMessage('error', data.detail || t('settings.passwordUpdateFailed')) return } onMessage('success', t('settings.passwordUpdated')) setNewPassword('') if (typeof onForceLogout === 'function') { onForceLogout() } } catch (e) { onMessage('error', t('settings.passwordUpdateFailed')) } finally { setChangingPassword(false) } } const loadExportData = async () => { try { const res = await apiFetch('/admin/config/export') const data = await res.json() if (!res.ok) { onMessage('error', data.detail || t('settings.exportFailed')) return } setExportData(data) onMessage('success', t('settings.exportLoaded')) } catch (e) { onMessage('error', t('settings.exportFailed')) } } const doImport = async () => { if (!String(importText || '').trim()) { onMessage('error', t('settings.importEmpty')) return } let parsed try { parsed = JSON.parse(importText) } catch (_e) { onMessage('error', t('settings.importInvalidJson')) return } setImporting(true) try { const res = await apiFetch(`/admin/config/import?mode=${encodeURIComponent(importMode)}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ config: parsed, mode: importMode }), }) const data = await res.json() if (!res.ok) { onMessage('error', data.detail || t('settings.importFailed')) return } onMessage('success', t('settings.importSuccess', { mode: importMode })) if (typeof onRefresh === 'function') { onRefresh() } await loadSettings() } catch (e) { onMessage('error', t('settings.importFailed')) } finally { setImporting(false) } } const syncHintVisible = useMemo(() => settingsMeta.env_backed || settingsMeta.needs_vercel_sync, [settingsMeta.env_backed, settingsMeta.needs_vercel_sync]) return (