import { useCallback, useEffect, useMemo, useState } from 'react' import { fetchSettings, getExportData, postImportData, postPassword, putSettings, } from './settingsApi' const MAX_AUTO_FETCH_FAILURES = 3 const DEFAULT_FORM = { 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: '' }, auto_delete: { sessions: false }, claude_mapping_text: '{\n "fast": "deepseek-chat",\n "slow": "deepseek-reasoner"\n}', model_aliases_text: '{}', } function parseJSONMap(raw, fieldName, t) { 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 } function fromServerForm(data) { return { 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 || '', }, auto_delete: { sessions: Boolean(data.auto_delete?.sessions || false), }, claude_mapping_text: JSON.stringify(data.claude_mapping || {}, null, 2), model_aliases_text: JSON.stringify(data.model_aliases || {}, null, 2), } } function toServerPayload(form) { return { 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() }, auto_delete: { sessions: Boolean(form.auto_delete?.sessions) }, } } export function useSettingsForm({ apiFetch, t, onMessage, onRefresh, onForceLogout, isVercel = false }) { 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(DEFAULT_FORM) const trackLoadFailure = useCallback(() => { setConsecutiveFailures((prev) => { const next = prev + 1 if (isVercel && next >= MAX_AUTO_FETCH_FAILURES) { setAutoFetchPaused(true) } return next }) }, [isVercel]) const loadSettings = useCallback(async ({ manual = false } = {}) => { if (isVercel && autoFetchPaused && !manual) { return } setLoading(true) try { const { res, data } = await fetchSettings(apiFetch, t) if (!res.ok) { const detail = data.detail || t('settings.loadFailed') setLastError(detail) onMessage('error', detail) trackLoadFailure() 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(fromServerForm(data)) } catch (e) { const detail = e?.message || t('settings.loadFailed') setLastError(detail) onMessage('error', detail) trackLoadFailure() // eslint-disable-next-line no-console console.error(e) } finally { setLoading(false) } }, [apiFetch, autoFetchPaused, isVercel, onMessage, t, trackLoadFailure]) useEffect(() => { loadSettings() }, [loadSettings]) const retryLoadSettings = useCallback(() => { setAutoFetchPaused(false) loadSettings({ manual: true }) }, [loadSettings]) const saveSettings = useCallback(async () => { let claudeMapping = {} let modelAliases = {} try { claudeMapping = parseJSONMap(form.claude_mapping_text, 'claude_mapping', t) modelAliases = parseJSONMap(form.model_aliases_text, 'model_aliases', t) } catch (e) { onMessage('error', e.message) return } const payload = { ...toServerPayload(form), claude_mapping: claudeMapping, model_aliases: modelAliases, } setSaving(true) try { const { res, data } = await putSettings(apiFetch, payload) 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) } }, [apiFetch, form, loadSettings, onMessage, onRefresh, t]) const updatePassword = useCallback(async () => { if (String(newPassword || '').trim().length < 4) { onMessage('error', t('settings.passwordTooShort')) return } setChangingPassword(true) try { const { res, data } = await postPassword(apiFetch, newPassword.trim()) 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) } }, [apiFetch, newPassword, onForceLogout, onMessage, t]) const loadExportData = useCallback(async () => { try { const { res, data } = await getExportData(apiFetch) 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')) } }, [apiFetch, onMessage, t]) const doImport = useCallback(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, data } = await postImportData(apiFetch, importMode, parsed) 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) } }, [apiFetch, importMode, importText, loadSettings, onMessage, onRefresh, t]) const syncHintVisible = useMemo( () => settingsMeta.env_backed || settingsMeta.needs_vercel_sync, [settingsMeta.env_backed, settingsMeta.needs_vercel_sync], ) return { form, setForm, loading, saving, changingPassword, importing, exportData, importMode, setImportMode, importText, setImportText, newPassword, setNewPassword, consecutiveFailures, autoFetchPaused, lastError, settingsMeta, syncHintVisible, retryLoadSettings, saveSettings, updatePassword, loadExportData, doImport, } }