// ==========================================
// 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
`;
// 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 `
`;
},
_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 ? `
✓ Approve All
` : ''}
↻ Refresh
${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 `
`;
},
// ═════════════════════════════════════════
// 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 = `
Enable memory for this persona
When enabled, this persona can save and recall memories from conversations.
Custom Extraction Prompt (optional)
`;
// 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);
};
}