import { html, nothing } from 'lit'; import { t } from '../../lib/i18n.js'; /** * The schema-driven settings form, shared by the Config page and the System * agents page. * * A config property renders the same way wherever it is edited — the backend * ships its type, its current value and, for the dropdown types, the choices it * owns (`PropertyType` in `core-api`), and this decides how to frame them. Both * pages write through the same `PUT /api/config/{key}`, so "where is this * setting shown" stays a pure placement question with no second form to keep in * step. * * Adding a property type is still the three-step recipe in `config_property.rs`: * variant, backend mapping + options, and a branch in `_renderInput` here. */ export class ConfigFormController { /** @param requestUpdate host callback, invoked whenever state changes. */ constructor(requestUpdate) { this._requestUpdate = requestUpdate; this._values = {}; this._saving = new Set(); this._saved = new Set(); } /** Seed the editable values from freshly fetched sets. */ seedFromSets(sets) { const vals = {}; for (const s of sets ?? []) for (const p of s?.properties ?? []) vals[p.key] = p.value ?? ''; this._values = vals; this._requestUpdate(); } _setValue(key, val) { this._values = { ...this._values, [key]: val }; this._requestUpdate(); } async _save(prop) { const key = prop.key; const value = this._values[key] ?? ''; this._saving = new Set([...this._saving, key]); this._requestUpdate(); try { const res = await fetch(`/api/config/${encodeURIComponent(key)}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ value }), }); if (!res.ok) throw new Error(await res.text()); this._saved = new Set([...this._saved, key]); this._requestUpdate(); setTimeout(() => { this._saved = new Set([...this._saved].filter(k => k !== key)); this._requestUpdate(); }, 1500); } catch (e) { alert(t('config.error_save', { name: prop.name, msg: e.message })); } finally { this._saving = new Set([...this._saving].filter(k => k !== key)); this._requestUpdate(); } } _renderInput(prop) { const val = this._values[prop.key] ?? ''; if (prop.property_type === 'bool') { const effective = val !== '' ? val : (prop.default_value ?? 'true'); const checked = effective !== 'false'; return html`
{ this._setValue(prop.key, e.target.checked ? 'true' : 'false'); this._save(prop); }} />
`; } if (prop.property_type === 'int') { return html` this._setValue(prop.key, e.target.value)} />`; } // Dropdown-style property types. The backend ships the allowed values in // `prop.options` (a list of {id, name}); we only decide how to frame them. if (prop.property_type === 'security_group') { // Nullable: the empty choice means "fall back to the role default". const groups = prop.options ?? []; return html` `; } if (prop.property_type === 'locale') { // Interface languages the instance supports; labels are native endonyms. // Always a concrete pick (no empty option) — falls back to default_value. const locales = prop.options ?? []; const current = val || prop.default_value || 'en'; return html` `; } if (prop.property_type === 'llm_model') { // Configured LLM models, by name. Nullable: the empty choice means // "auto-select" (the backend's own resolution order applies). const models = prop.options ?? []; return html` `; } return html` this._setValue(prop.key, e.target.value)} />`; } /** * One row per property. `labelFor(prop)` lets the host supply translated * name/description; it returns `{ name, description }`. */ renderRows(properties, labelFor = p => p) { return html`
${(properties ?? []).map(prop => { const saving = this._saving.has(prop.key); const saved = this._saved.has(prop.key); const label = labelFor(prop); // A switch and a language picker save on change; everything else needs // an explicit commit, or every keystroke would be a write. const needsButton = !['bool', 'locale'].includes(prop.property_type); return html`
${label.name}
${label.description}
${this._renderInput(prop)} ${needsButton ? html` ` : nothing}
`; })}
`; } } /** `t(key)` when a translation exists, otherwise the server-supplied text. */ export function maybeT(key, fallback) { const v = t(key); return v !== key ? v : fallback; } /** Config keys are dotted; i18n keys are not. */ export function propKeyId(propKey) { return propKey.replace(/\./g, '__'); }