import { html, nothing } from 'lit'; import { LightElement } from '../lib/base.js'; import { t } from '../lib/i18n.js'; function _maybeT(key, fallback) { const v = t(key); return v !== key ? v : fallback; } function _configSetSlug(name) { const slugs = { 'Interface': 'interface', 'TIC Agent': 'tic_agent', 'Compaction': 'compaction', }; return slugs[name] ?? null; } function _propKeyId(propKey) { return propKey.replace(/\./g, '__'); } export class ConfigPage extends LightElement { static properties = { _open: { state: true }, _properties: { state: true }, _values: { state: true }, // { [key]: string } _saving: { state: true }, // Set _saved: { state: true }, // Set (brief flash) _error: { state: true }, _debugMode: { state: true }, _debugLoading: { state: true }, }; constructor() { super(); this._open = false; this._properties = []; this._values = {}; this._saving = new Set(); this._saved = new Set(); this._error = null; this._debugMode = false; this._debugLoading = true; } connectedCallback() { super.connectedCallback(); this.__onLocaleChanged = () => this.requestUpdate(); window.addEventListener('locale-changed', this.__onLocaleChanged); window.addEventListener('llm-page-change', (e) => { this._open = e.detail.page === 'config'; this.style.display = this._open ? 'flex' : 'none'; if (this._open) { this._load(); this._loadDebugMode(); } }); } disconnectedCallback() { window.removeEventListener('locale-changed', this.__onLocaleChanged); super.disconnectedCallback(); } async _loadDebugMode() { try { const res = await fetch('/api/dev/debug_mode'); if (!res.ok) throw new Error(); const data = await res.json(); this._debugMode = data.enabled; } catch { // ignore, keep current value } finally { this._debugLoading = false; } } async _toggleDebugMode() { const next = !this._debugMode; this._debugMode = next; try { const res = await fetch('/api/dev/debug_mode', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled: next }), }); if (!res.ok) throw new Error(); window.dispatchEvent(new CustomEvent('debug-mode-change', { detail: { enabled: next } })); } catch { this._debugMode = !next; } } async _load() { this._error = null; try { const res = await fetch('/api/config'); if (!res.ok) throw new Error(await res.text()); const data = await res.json(); this._properties = data.sets ?? []; const vals = {}; for (const s of this._properties) for (const p of s.properties) vals[p.key] = p.value ?? ''; this._values = vals; } catch (e) { this._error = e.message; } } _setValue(key, val) { this._values = { ...this._values, [key]: val }; } 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]); setTimeout(() => { this._saved = new Set([...this._saved].filter(k => k !== key)); }, 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)); } } _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. // Adding a new custom type from a config section? Give it a `property_type` // on the backend, attach its `options`, and add a branch like these — a // free-text box becomes a proper picker for the price of a few lines. if (prop.property_type === 'security_group') { // Nullable: the empty choice means "fall back to the instance 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)} />`; } _renderSet(set) { const slug = _configSetSlug(set.name); const sName = slug ? _maybeT(`config.set.${slug}.name`, set.name) : set.name; const sDesc = slug ? _maybeT(`config.set.${slug}.desc`, set.description) : set.description; return html`
${sName}
${sDesc}
${set.properties.map(p => this._renderRow(p))}
`; } _renderRow(prop) { const saving = this._saving.has(prop.key); const saved = this._saved.has(prop.key); const pk = _propKeyId(prop.key); const pName = _maybeT(`config.prop.${pk}.name`, prop.name); const pDesc = _maybeT(`config.prop.${pk}.desc`, prop.description); return html`
${pName}
${pDesc}
${this._renderInput(prop)} ${!['bool', 'locale'].includes(prop.property_type) ? html` ` : nothing}
`; } render() { return html`

${t('config.title')}

${this._error ? html`
${this._error}
` : nothing} ${this._properties.length === 0 && !this._error ? html`

${t('config.loading')}

` : nothing}
${this._properties.map(s => this._renderSet(s))}
${t('config.developer')}
${t('config.debug')}
${t('config.debug.desc')}
this._toggleDebugMode()} />
`; } }