import { useCallback, useEffect, useState } from 'react' import { ChevronDown, ChevronUp, Plus, Save, Trash2 } from 'lucide-react' import { fetchModelProfiles, putModelProfiles } from './settingsApi' const FALLBACK_BASE_MODELS = [ 'deepseek-v4-flash', 'deepseek-v4-flash-search', 'deepseek-v4-flash-nothinking', 'deepseek-v4-flash-search-nothinking', ] function emptyProfile() { return { id: '', base: FALLBACK_BASE_MODELS[0], name: '', remark: '', prompt: '' } } function normalizeItems(data) { const items = Array.isArray(data?.items) ? data.items : [] return items.map((item) => ({ id: String(item?.id || ''), base: String(item?.base || FALLBACK_BASE_MODELS[0]), name: String(item?.name || ''), remark: String(item?.remark || ''), prompt: String(item?.prompt || ''), })) } export default function ModelProfilesSection({ t, apiFetch, onMessage }) { const [loading, setLoading] = useState(false) const [saving, setSaving] = useState(false) const [items, setItems] = useState([]) const [baseModels, setBaseModels] = useState(FALLBACK_BASE_MODELS) const [maxCount, setMaxCount] = useState(50) const [maxPromptChars, setMaxPromptChars] = useState(20000) const [expandedRows, setExpandedRows] = useState({}) const [error, setError] = useState('') const loadProfiles = useCallback(async () => { setLoading(true) try { const { res, data } = await fetchModelProfiles(apiFetch) if (!res.ok) { setError(data?.detail || t('settings.modelProfilesLoadFailed')) return } setItems(normalizeItems(data)) if (Array.isArray(data?.base_models) && data.base_models.length > 0) { setBaseModels(data.base_models) } if (Number(data?.max_count) > 0) { setMaxCount(Number(data.max_count)) } if (Number(data?.max_prompt_chars) > 0) { setMaxPromptChars(Number(data.max_prompt_chars)) } setError('') } catch (_e) { setError(t('settings.modelProfilesLoadFailed')) } finally { setLoading(false) } }, [apiFetch, t]) useEffect(() => { loadProfiles() }, [loadProfiles]) const updateItem = useCallback((index, patch) => { setItems((prev) => prev.map((item, i) => (i === index ? { ...item, ...patch } : item))) }, []) const removeItem = useCallback((index) => { setItems((prev) => prev.filter((_item, i) => i !== index)) setExpandedRows({}) }, []) const addItem = useCallback(() => { setItems((prev) => (prev.length >= maxCount ? prev : [...prev, emptyProfile()])) }, [maxCount]) const toggleRow = useCallback((index) => { setExpandedRows((prev) => ({ ...prev, [index]: !prev[index] })) }, []) const validate = useCallback(() => { const seen = new Set() for (let i = 0; i < items.length; i += 1) { const item = items[i] const id = String(item.id || '').trim() if (!id) { return t('settings.modelProfilesIdRequired', { index: i + 1 }) } if (!/^[A-Za-z0-9._-]{1,64}$/.test(id)) { return t('settings.modelProfilesIdInvalid', { id }) } const key = id.toLowerCase() if (seen.has(key)) { return t('settings.modelProfilesIdDuplicate', { id }) } seen.add(key) if (!String(item.base || '').trim()) { return t('settings.modelProfilesBaseRequired', { id }) } if (String(item.prompt || '').length > maxPromptChars) { return t('settings.modelProfilesPromptTooLong', { id, count: maxPromptChars }) } } return '' }, [items, maxPromptChars, t]) const save = useCallback(async () => { const message = validate() if (message) { setError(message) onMessage?.('error', message) return } setSaving(true) try { const payload = { items: items.map((item) => ({ id: String(item.id || '').trim(), base: String(item.base || '').trim(), name: String(item.name || '').trim(), remark: String(item.remark || '').trim(), prompt: String(item.prompt || ''), })), } const { res, data } = await putModelProfiles(apiFetch, payload) if (!res.ok) { const detail = data?.detail || t('settings.modelProfilesSaveFailed') setError(detail) onMessage?.('error', detail) return } setError('') onMessage?.('success', t('settings.modelProfilesSaveSuccess', { count: items.length })) await loadProfiles() } catch (_e) { setError(t('settings.modelProfilesSaveFailed')) onMessage?.('error', t('settings.modelProfilesSaveFailed')) } finally { setSaving(false) } }, [apiFetch, items, loadProfiles, maxPromptChars, onMessage, t, validate]) return (
{t('settings.modelProfilesDesc')}
{error}
} {loading &&{t('actions.loading')}
} {!loading && items.length === 0 && ({t('settings.modelProfilesEmpty')}
)}