import { html, nothing } from 'lit'; import { LightElement } from '../lib/base.js'; import { t } from '../lib/i18n.js'; import { jf, schemaFields } from './shared/plugin-common.js'; // Plugins page (`#plugins`) — the user-facing half of the plugin split. // // Shows the plugins the caller has been granted (`plugin_access`, admin-granted). // When a plugin declares a `user_config_schema` the card carries a small // schema-driven form — e.g. Telegram's pairing code — saved via // `PUT /api/plugins/{id}/my-config`. // // The admin half (enable/disable, instance config, access grants) lives on // `#plugin-catalog` + `#plugin-detail` — see `plugin-catalog.js`. // // Styling reuses the connectors card grid (`web/css/connectors.css`). export class PluginsPage extends LightElement { static get properties() { return { _open: { state: true }, _mine: { state: true }, // UserPluginView[] — granted + enabled plugins _error: { state: true }, _uDrafts: { state: true }, // user config drafts: { [pluginId]: {key: value} } _uStatus: { state: true }, // { [pluginId]: { ok?: string, err?: string } } }; } constructor() { super(); this._open = false; this._reset(); } _reset() { this._mine = null; this._error = null; this._uDrafts = {}; this._uStatus = {}; } connectedCallback() { super.connectedCallback(); this.__onLocaleChanged = () => this.requestUpdate(); window.addEventListener('locale-changed', this.__onLocaleChanged); window.addEventListener('llm-page-change', (e) => { this._open = e.detail.page === 'plugins'; this.style.display = this._open ? 'flex' : 'none'; if (this._open) this._load(); }); } disconnectedCallback() { window.removeEventListener('locale-changed', this.__onLocaleChanged); super.disconnectedCallback(); } async _load() { this._error = null; try { this._mine = await jf('/api/plugins/mine'); } catch (e) { this._error = e.message; } } _uDraft(p) { if (!this._uDrafts[p.id]) { // Seed the form from the stored config for keys the schema knows. const draft = {}; for (const f of schemaFields(p.user_config_schema)) { const v = p.user_config?.[f.key]; draft[f.key] = v ?? (f.type === 'boolean' ? false : ''); } this._uDrafts = { ...this._uDrafts, [p.id]: draft }; } return this._uDrafts[p.id]; } _setUDraft(id, key, value) { this._uDrafts = { ...this._uDrafts, [id]: { ...this._uDrafts[id], [key]: value } }; } async _saveUserConfig(p) { const draft = this._uDraft(p); for (const f of schemaFields(p.user_config_schema)) { if (f.required && !draft[f.key]) { this._uStatus = { ...this._uStatus, [p.id]: { err: t('plugins.error.required', { field: f.label }) } }; return; } } this._uStatus = { ...this._uStatus, [p.id]: {} }; try { await jf(`/api/plugins/${encodeURIComponent(p.id)}/my-config`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(draft), }); this._uStatus = { ...this._uStatus, [p.id]: { ok: t('plugins.saved') } }; // Drop the draft so the reloaded status blob re-seeds the form. const drafts = { ...this._uDrafts }; delete drafts[p.id]; this._uDrafts = drafts; this._mine = await jf('/api/plugins/mine'); } catch (e) { this._uStatus = { ...this._uStatus, [p.id]: { err: e.message } }; } } // ── Render ───────────────────────────────────────────────────────────────── render() { if (!this._open) return nothing; const loading = this._mine === null && !this._error; return html`
${t('plugins.empty.mine')}
${t('plugins.empty.ask_admin')}