Download webui/src/features/settings/ModelProfilesSection.jsx from a3216/ds2api: direct link, hf CLI and curl.
- Browser
- Download file 13.1 kB
-
https://huggingface.co/spaces/a3216/ds2api/resolve/fcdd4aa00c4b7afbcde9155c5ec0a9c5bceeeeb4/webui/src/features/settings/ModelProfilesSection.jsx
- Command line
-
hf download hf://spaces/a3216/ds2api@fcdd4aa00c4b7afbcde9155c5ec0a9c5bceeeeb4/webui/src/features/settings/ModelProfilesSection.jsx
-
curl -L -o ModelProfilesSection.jsx https://huggingface.co/spaces/a3216/ds2api/resolve/fcdd4aa00c4b7afbcde9155c5ec0a9c5bceeeeb4/webui/src/features/settings/ModelProfilesSection.jsx
13.1 kB
| 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 ( | |
| <div className="bg-card border border-border rounded-xl p-5 space-y-4"> | |
| <div className="space-y-1"> | |
| <div className="flex items-center justify-between gap-3"> | |
| <h3 className="font-semibold">{t('settings.modelProfilesTitle')}</h3> | |
| <span className="text-xs text-muted-foreground"> | |
| {t('settings.modelProfilesCount', { count: items.length })} | |
| </span> | |
| </div> | |
| <p className="text-xs text-muted-foreground">{t('settings.modelProfilesDesc')}</p> | |
| </div> | |
| {error && <p className="text-xs text-destructive">{error}</p>} | |
| {loading && <p className="text-xs text-muted-foreground">{t('actions.loading')}</p>} | |
| {!loading && items.length === 0 && ( | |
| <p className="text-xs text-muted-foreground">{t('settings.modelProfilesEmpty')}</p> | |
| )} | |
| <div className="space-y-3"> | |
| {items.map((item, index) => { | |
| const expanded = Boolean(expandedRows[index]) | |
| return ( | |
| <div key={`${index}-${item.id}`} className="rounded-lg border border-border bg-background/60 p-4 space-y-3"> | |
| <div className="grid grid-cols-1 md:grid-cols-4 gap-3"> | |
| <label className="text-xs space-y-1"> | |
| <span className="text-muted-foreground block">{t('settings.modelProfilesId')}</span> | |
| <input | |
| type="text" | |
| value={item.id} | |
| onChange={(e) => updateItem(index, { id: e.target.value })} | |
| placeholder={t('settings.modelProfilesIdPlaceholder')} | |
| className="w-full bg-background border border-border rounded-lg px-3 py-2 text-sm" | |
| /> | |
| </label> | |
| <label className="text-xs space-y-1"> | |
| <span className="text-muted-foreground block">{t('settings.modelProfilesBase')}</span> | |
| <select | |
| value={item.base} | |
| onChange={(e) => updateItem(index, { base: e.target.value })} | |
| className="w-full bg-background border border-border rounded-lg px-3 py-2 text-sm" | |
| > | |
| {baseModels.map((model) => ( | |
| <option key={model} value={model}>{model}</option> | |
| ))} | |
| </select> | |
| </label> | |
| <label className="text-xs space-y-1"> | |
| <span className="text-muted-foreground block">{t('settings.modelProfilesName')}</span> | |
| <input | |
| type="text" | |
| value={item.name} | |
| onChange={(e) => updateItem(index, { name: e.target.value })} | |
| placeholder={t('settings.modelProfilesNamePlaceholder')} | |
| className="w-full bg-background border border-border rounded-lg px-3 py-2 text-sm" | |
| /> | |
| </label> | |
| <label className="text-xs space-y-1"> | |
| <span className="text-muted-foreground block">{t('settings.modelProfilesRemark')}</span> | |
| <input | |
| type="text" | |
| value={item.remark} | |
| onChange={(e) => updateItem(index, { remark: e.target.value })} | |
| className="w-full bg-background border border-border rounded-lg px-3 py-2 text-sm" | |
| /> | |
| </label> | |
| </div> | |
| <div className="flex items-center justify-between gap-2"> | |
| <button | |
| type="button" | |
| onClick={() => toggleRow(index)} | |
| className="text-xs text-muted-foreground hover:text-foreground flex items-center gap-1" | |
| > | |
| {expanded ? <ChevronUp className="w-3.5 h-3.5" /> : <ChevronDown className="w-3.5 h-3.5" />} | |
| {expanded ? t('settings.skillsCollapse') : t('settings.modelProfilesPrompt')} | |
| </button> | |
| <button | |
| type="button" | |
| onClick={() => removeItem(index)} | |
| className="text-xs text-destructive hover:opacity-80 flex items-center gap-1" | |
| > | |
| <Trash2 className="w-3.5 h-3.5" /> | |
| {t('settings.modelProfilesRemove')} | |
| </button> | |
| </div> | |
| {expanded && ( | |
| <div className="space-y-1"> | |
| <textarea | |
| rows={8} | |
| value={item.prompt} | |
| onChange={(e) => updateItem(index, { prompt: e.target.value })} | |
| placeholder={t('settings.modelProfilesPromptPlaceholder')} | |
| className="w-full bg-background border border-border rounded-lg px-3 py-2 resize-y min-h-32 text-sm leading-6" | |
| /> | |
| <p className="text-xs text-muted-foreground"> | |
| {t('settings.modelProfilesPromptHint')} | |
| </p> | |
| </div> | |
| )} | |
| </div> | |
| ) | |
| })} | |
| </div> | |
| <div className="flex flex-wrap items-center gap-2"> | |
| <button | |
| type="button" | |
| onClick={addItem} | |
| disabled={items.length >= maxCount} | |
| className="h-9 px-3 rounded-lg border border-border bg-background text-sm hover:bg-secondary/70 disabled:opacity-50 flex items-center gap-1" | |
| > | |
| <Plus className="w-4 h-4" /> | |
| {t('settings.modelProfilesAdd')} | |
| </button> | |
| <button | |
| type="button" | |
| onClick={save} | |
| disabled={saving} | |
| className="h-9 px-3 rounded-lg bg-primary text-primary-foreground text-sm hover:bg-primary/90 disabled:opacity-50 flex items-center gap-1" | |
| > | |
| <Save className="w-4 h-4" /> | |
| {saving ? t('settings.skillsSaving') : t('settings.modelProfilesSave')} | |
| </button> | |
| <span className="text-xs text-muted-foreground">{t('settings.modelProfilesHint')}</span> | |
| </div> | |
| </div> | |
| ) | |
| } | |