// Honcho user opt-in page (page_id `memory`, visible to any user with a // `plugin_access` grant). // // Two halves: // // 1. The per-user consent to long-term memory. Reuses the core per-user config // endpoints — `GET /api/plugins/mine` to read the current flag, // `PUT /api/plugins/honcho/my-config` to save `{ enabled }`. // 2. Once opted in (saved flag, not the draft toggle): the "what does Honcho // remember about me?" debug panel, backed by this plugin's own opt-in-gated // endpoints — `GET ${api}/status` (service health + the caller's own // processing queue), `GET ${api}/overview` (card + facts + summary, no // input), and one text field with two actions: `POST ${api}/search` (raw // ranked facts) and `POST ${api}/ask` (Honcho's server-side LLM answers). // The built-in mini-guide explains the difference, because "words → facts" // vs "question → AI answer" is not obvious. // // Errors from these endpoints arrive already localized *and specific* (the // backend forwards the real Honcho transport/HTTP detail) — they are surfaced // verbatim, never as a generic "unavailable". // // Default-exports the element class; the host registers it. import { html, nothing } from 'lit'; import { HonchoBase, jf, t } from './common.js'; const P = 'plugin.honcho'; const ID = 'honcho'; export default class HonchoMemoryPage extends HonchoBase { static get properties() { return { _row: { state: true }, // UserPluginView | null (null once loaded = not granted) _enabled: { state: true }, // draft toggle _status: { state: true }, // { ok?, err? } for the opt-in save _error: { state: true }, _loading: { state: true }, // Debug panel (only used once the *saved* opt-in flag is on). _svc: { state: true }, // null | { ok, latency_ms?, queue? } | { ok:false, error } _svcBusy: { state: true }, _ov: { state: true }, // null | { card, conclusions, summary } _ovBusy: { state: true }, _ovErr: { state: true }, // string | null _q: { state: true }, // query input value _qBusy: { state: true }, // null | 'search' | 'ask' _qRes: { state: true }, // null | { kind:'search', conclusions, empty } | { kind:'ask', answer } _qErr: { state: true }, // string | null }; } constructor() { super(); this._row = null; this._enabled = false; this._status = {}; this._error = null; this._loading = true; this._svc = null; this._svcBusy = false; this._ov = null; this._ovBusy = false; this._ovErr = null; this._q = ''; this._qBusy = null; this._qRes = null; this._qErr = null; } connectedCallback() { super.connectedCallback(); this._load(); } async _load() { this._loading = true; this._error = null; try { const mine = await jf('/api/plugins/mine'); const row = (mine ?? []).find(x => x.id === ID) ?? null; this._row = row; this._enabled = !!row?.user_config?.enabled; // The panel reads the *saved* flag; when it just turned on (save → reload) // this is also what triggers the first fetch of panel data. if (row?.user_config?.enabled) { this._refreshStatus(); this._refreshOverview(); } } catch (e) { this._error = e.message; } finally { this._loading = false; } } async _save() { this._status = {}; try { await jf(`/api/plugins/${ID}/my-config`, { method: 'PUT', body: JSON.stringify({ enabled: this._enabled }), }); this._status = { ok: t(`${P}.memory.saved`) }; await this._load(); } catch (e) { this._status = { err: e.message }; } } // ── Debug panel: data ───────────────────────────────────────────────────── async _refreshStatus() { this._svcBusy = true; try { // 200 with { ok:false, error } when Honcho is down — the badge wants the // specific message, not an exception. Other statuses (503, 403…) still // throw and land in the same place. this._svc = await jf(`${this.api}/status`); } catch (e) { this._svc = { ok: false, error: e.message }; } finally { this._svcBusy = false; } } async _refreshOverview() { this._ovBusy = true; this._ovErr = null; try { this._ov = await jf(`${this.api}/overview`); } catch (e) { this._ovErr = e.message; } finally { this._ovBusy = false; } } async _run(kind) { const q = this._q.trim(); if (!q || this._qBusy) return; this._qBusy = kind; this._qErr = null; this._qRes = null; try { const r = await jf(`${this.api}/${kind}`, { method: 'POST', body: JSON.stringify({ query: q }), }); this._qRes = kind === 'search' ? { kind, conclusions: r?.conclusions ?? [], empty: !!r?.empty } : { kind, answer: r?.answer ?? '' }; } catch (e) { this._qErr = e.message; } finally { this._qBusy = null; } } // ── Render ──────────────────────────────────────────────────────────────── render() { return html`
${t(`${P}.memory.unavailable`)}
${t(`${P}.memory.intro`)}
${card.raw}`}
` : nothing}
${conclusions.length ? html`
${id} ` : nothing}${content}