// ========================================== // Chat Switchboard – Memory UI (v0.18.0 Phase 3) // ========================================== // User-facing memory management in Settings, // admin memory review in Admin Panel, // and persona memory config in preset forms. const MemoryUI = { // ── State ──────────────────────────────── _memories: [], _pendingMemories: [], _filter: { scope: 'user', status: 'active', query: '' }, // ═════════════════════════════════════════ // User Settings Tab // ═════════════════════════════════════════ async openSettingsPanel() { const panel = document.getElementById('settingsMemoryContent'); if (!panel) return; panel.innerHTML = '
Loading memories...
'; try { // Load memory count summary const count = await API.getMemoryCount(); const active = count.active || 0; const pending = count.pending || 0; panel.innerHTML = `
${active} Active
${pending} Pending Review
${pending > 0 ? `` : ''}
`; // Wire events document.getElementById('memoryStatusFilter')?.addEventListener('change', (e) => { MemoryUI._filter.status = e.target.value; MemoryUI.loadMemories(); }); document.getElementById('memorySearchInput')?.addEventListener('input', _debounce((e) => { MemoryUI._filter.query = e.target.value; MemoryUI.loadMemories(); }, 300)); document.getElementById('memoryRefreshBtn')?.addEventListener('click', () => MemoryUI.openSettingsPanel()); document.getElementById('memoryApproveAllBtn')?.addEventListener('click', () => MemoryUI.approveAllPending()); // Load initial list await this.loadMemories(); } catch (e) { panel.innerHTML = `
${esc(e.message)}
`; } }, async loadMemories() { const el = document.getElementById('memoryList'); if (!el) return; el.innerHTML = '
Loading...
'; try { const data = await API.listMemories(this._filter.status, this._filter.query); this._memories = data.memories || []; if (!this._memories.length) { el.innerHTML = `
No ${this._filter.status.replace('_', ' ')} memories${this._filter.query ? ' matching "' + esc(this._filter.query) + '"' : ''}
`; return; } el.innerHTML = this._memories.map(m => this._renderMemoryCard(m)).join(''); this._wireMemoryCards(el); } catch (e) { el.innerHTML = `
${esc(e.message)}
`; } }, _renderMemoryCard(m) { const confidence = Math.round(m.confidence * 100); const confidenceClass = confidence >= 80 ? 'high' : confidence >= 50 ? 'medium' : 'low'; const updated = m.updated_at ? new Date(m.updated_at).toLocaleDateString() : ''; const scopeLabel = m.scope === 'persona_user' ? 'persona' : m.scope; return `
${esc(m.key)}
${scopeLabel} ${confidence}%
${esc(m.value)}
`; }, _wireMemoryCards(container) { container.querySelectorAll('[data-action]').forEach(btn => { btn.addEventListener('click', async (e) => { const id = btn.dataset.id; const action = btn.dataset.action; if (action === 'approve') await this.approveMemory(id); else if (action === 'reject') await this.rejectMemory(id); else if (action === 'delete') await this.deleteMemory(id); else if (action === 'edit') this.editMemory(id); }); }); }, async approveMemory(id) { try { await API.approveMemory(id); UI.toast('Memory approved', 'success'); this.loadMemories(); } catch (e) { UI.toast(e.message, 'error'); } }, async rejectMemory(id) { try { await API.rejectMemory(id); UI.toast('Memory rejected', 'success'); this.loadMemories(); } catch (e) { UI.toast(e.message, 'error'); } }, async deleteMemory(id) { if (!confirm('Delete this memory permanently?')) return; try { await API.deleteMemory(id); UI.toast('Memory deleted', 'success'); this.loadMemories(); } catch (e) { UI.toast(e.message, 'error'); } }, editMemory(id) { const m = this._memories.find(x => x.id === id); if (!m) return; const card = document.querySelector(`.memory-card[data-id="${id}"]`); if (!card) return; card.innerHTML = `
`; document.getElementById(`memEditSave_${id}`)?.addEventListener('click', async () => { const key = document.getElementById(`memEdit_key_${id}`).value.trim(); const value = document.getElementById(`memEdit_val_${id}`).value.trim(); if (!key || !value) return UI.toast('Key and value are required', 'warning'); try { await API.updateMemory(id, { key, value }); UI.toast('Memory updated', 'success'); this.loadMemories(); } catch (e) { UI.toast(e.message, 'error'); } }); document.getElementById(`memEditCancel_${id}`)?.addEventListener('click', () => { this.loadMemories(); }); }, async approveAllPending() { try { const data = await API.listMemories('pending_review', ''); const ids = (data.memories || []).map(m => m.id); if (!ids.length) return UI.toast('No pending memories', 'info'); if (!confirm(`Approve all ${ids.length} pending memories?`)) return; await API.bulkApproveMemories(ids); UI.toast(`${ids.length} memories approved`, 'success'); this.openSettingsPanel(); } catch (e) { UI.toast(e.message, 'error'); } }, // ═════════════════════════════════════════ // Admin Panel — Memory Review // ═════════════════════════════════════════ async openAdminPanel() { const el = document.getElementById('adminMemoryContent'); if (!el) return; el.innerHTML = '
Loading...
'; try { const data = await API.adminListPendingMemories(); const pending = data.memories || []; el.innerHTML = `
${pending.length} pending memories ${pending.length > 0 ? ` ` : ''}
${pending.length === 0 ? '
No memories pending review
' : ''} ${pending.map(m => this._renderAdminMemoryRow(m)).join('')}
`; // Wire events document.getElementById('adminBulkApproveBtn')?.addEventListener('click', async () => { const ids = pending.map(m => m.id); if (!confirm(`Approve all ${ids.length} pending memories?`)) return; try { await API.bulkApproveMemories(ids); UI.toast(`${ids.length} memories approved`, 'success'); MemoryUI.openAdminPanel(); } catch (e) { UI.toast(e.message, 'error'); } }); document.getElementById('adminMemoryRefreshBtn')?.addEventListener('click', () => MemoryUI.openAdminPanel()); // Wire per-row actions el.querySelectorAll('[data-action]').forEach(btn => { btn.addEventListener('click', async () => { const id = btn.dataset.id; try { if (btn.dataset.action === 'approve') { await API.approveMemory(id); UI.toast('Approved', 'success'); } else if (btn.dataset.action === 'reject') { await API.rejectMemory(id); UI.toast('Rejected', 'success'); } MemoryUI.openAdminPanel(); } catch (e) { UI.toast(e.message, 'error'); } }); }); } catch (e) { el.innerHTML = `
${esc(e.message)}
`; } }, _renderAdminMemoryRow(m) { const confidence = Math.round(m.confidence * 100); const scope = m.scope === 'persona_user' ? 'persona' : m.scope; return `
${esc(m.key)}
${scope} ${confidence}% ${esc(m.owner_id?.substring(0, 8) || '?')}…
${esc(m.value)}
`; }, // ═════════════════════════════════════════ // Persona Form Memory Fields // ═════════════════════════════════════════ // Call this after renderPresetForm to append memory fields. // container: the form container element // prefix: the form prefix (e.g. 'pf', 'apf') appendMemoryFields(container, prefix) { const section = document.createElement('div'); section.className = 'memory-persona-section'; section.innerHTML = `

Memory

When enabled, this persona can save and recall memories from conversations.

`; // Insert before the submit/cancel buttons const submitRow = container.querySelector('.form-row:last-child'); if (submitRow) { container.insertBefore(section, submitRow); } else { container.appendChild(section); } }, // Read memory values from a persona form getMemoryValues(prefix) { const enabled = document.getElementById(`${prefix}_memoryEnabled`)?.checked ?? true; const prompt = document.getElementById(`${prefix}_memoryExtractionPrompt`)?.value.trim() || null; return { memory_enabled: enabled, memory_extraction_prompt: prompt }; }, // Set memory values in a persona form setMemoryValues(prefix, persona) { const enabledEl = document.getElementById(`${prefix}_memoryEnabled`); if (enabledEl) enabledEl.checked = persona.memory_enabled !== false; const promptEl = document.getElementById(`${prefix}_memoryExtractionPrompt`); if (promptEl) promptEl.value = persona.memory_extraction_prompt || ''; }, }; // ── Debounce helper ────────────────────────── function _debounce(fn, ms) { let timer; return function(...args) { clearTimeout(timer); timer = setTimeout(() => fn.apply(this, args), ms); }; }