App._memoryCategory = '';
App._memoryFilter = '';
App._memoryPage = 0;
App.renderMemory = function() {
var tab = this._memoryTab;
var sessionId = this._memorySessionId || '';
var cat = this._memoryCategory || '';
var analyticsPromise = api('/api/stats/memory-analytics').catch(function() { return null; });
var promises;
if (tab === 'search') {
promises = Promise.resolve({ entries: [], _mode: 'search' });
} else if (tab === 'working') {
promises = api('/api/memory/working' + (sessionId ? '/' + encodeURIComponent(sessionId) : ''))
.catch(function() { return { entries: [] }; })
.then(function(d) { d._mode = 'working'; return d; });
} else if (tab === 'semantic') {
var catPromise = api('/api/memory/semantic/categories').catch(function() { return { categories: [] }; });
var entriesUrl = cat ? '/api/memory/semantic/' + encodeURIComponent(cat) : '/api/memory/semantic?limit=100';
var dataPromise = api(entriesUrl).catch(function() { return { entries: [] }; });
promises = Promise.all([catPromise, dataPromise]).then(function(r) {
return { categories: r[0].categories || [], entries: r[1].entries || [], _mode: 'semantic' };
});
} else {
promises = api('/api/memory/episodic').catch(function() { return { entries: [] }; })
.then(function(d) { d._mode = 'episodic'; return d; });
}
var self = this;
return Promise.all([promises, analyticsPromise]).then(function(pair) {
var data = pair[0];
var analytics = pair[1] || {};
var allEntries = data.entries || data.results || [];
var mode = data._mode || tab;
var MEM_PAGE_SIZE = 25;
var filterQ = (self._memoryFilter || '').toLowerCase();
var entries = filterQ ? allEntries.filter(function(e) { var txt = (e.content || e.value || e.key || '').toLowerCase(); return txt.indexOf(filterQ) >= 0; }) : allEntries;
var memPage = self._memoryPage || 0;
var memTotalPages = Math.ceil(entries.length / MEM_PAGE_SIZE) || 1;
if (memPage >= memTotalPages) memPage = 0;
var memPaged = entries.slice(memPage * MEM_PAGE_SIZE, (memPage + 1) * MEM_PAGE_SIZE);
function memFilterBar() { return '<div style="margin-bottom:0.75rem"><input type="text" id="mem-filter-input" placeholder="Filter entries\u2026" value="' + esc(self._memoryFilter || '') + '" style="padding:0.4rem 0.75rem;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);width:100%;max-width:300px;font-family:var(--font);font-size:0.8125rem"></div>'; }
function memPager() { if (memTotalPages <= 1) return ''; return '<div style="display:flex;gap:0.5rem;align-items:center;margin-top:0.75rem;justify-content:center"><button class="btn secondary" id="mem-prev" style="font-size:0.75rem;padding:0.2rem 0.6rem"' + (memPage === 0 ? ' disabled' : '') + '>\u2190 Prev</button><span style="font-size:0.75rem;color:var(--muted)">Page ' + (memPage + 1) + ' of ' + memTotalPages + ' (' + entries.length + ' entries)</span><button class="btn secondary" id="mem-next" style="font-size:0.75rem;padding:0.2rem 0.6rem"' + (memPage >= memTotalPages - 1 ? ' disabled' : '') + '>Next \u2192</button></div>'; }
var tabButtons = '<div class="tabs">'
+ '<button class="' + (tab === 'working' ? 'active' : '') + '" data-tab="working">Working</button>'
+ '<button class="' + (tab === 'episodic' ? 'active' : '') + '" data-tab="episodic">Episodic</button>'
+ '<button class="' + (tab === 'semantic' ? 'active' : '') + '" data-tab="semantic">Semantic</button>'
+ '<button class="' + (tab === 'search' ? 'active' : '') + '" data-tab="search">Search</button>'
+ '</div>';
// ── Retrieval Health strip ────────────────────────────────────────────
var healthStripHtml = '';
if (analytics && (analytics.total_turns != null)) {
var hitRatePct = analytics.hit_rate != null ? (analytics.hit_rate * 100).toFixed(1) + '%' : '\u2014';
var avgSim = analytics.avg_similarity != null ? Number(analytics.avg_similarity).toFixed(3) : '\u2014';
var budgetPct = analytics.avg_budget_utilization != null ? (analytics.avg_budget_utilization * 100).toFixed(1) + '%' : '\u2014';
var tierDist = analytics.tier_distribution || {};
var tierKeys = ['episodic', 'semantic', 'procedural', 'relationship', 'working'];
var tierItems = tierKeys.map(function(k) {
var v = tierDist[k];
return v != null ? '<span style="font-size:0.6875rem;color:var(--muted)">' + k + ': <strong style="color:var(--text)">' + v + '</strong></span>' : null;
}).filter(Boolean).join('<span style="color:var(--border);margin:0 0.25rem">/</span>');
healthStripHtml = '<div style="display:flex;flex-wrap:wrap;align-items:center;gap:1rem;padding:0.6rem 0.85rem;margin-bottom:0.85rem;background:var(--surface-2);border:1px solid var(--border-ghost);border-radius:var(--radius)">'
+ '<span style="font-family:var(--font-headline);font-size:0.5625rem;font-weight:700;letter-spacing:0.15em;text-transform:uppercase;color:var(--muted);white-space:nowrap">Retrieval Health</span>'
+ '<div style="display:flex;flex-wrap:wrap;gap:1.25rem;align-items:center">'
+ '<div style="display:flex;flex-direction:column;align-items:center"><span style="font-size:0.9375rem;font-weight:700;color:var(--accent)">' + hitRatePct + '</span><span style="font-size:0.625rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.1em">Hit Rate</span></div>'
+ '<div style="display:flex;flex-direction:column;align-items:center"><span style="font-size:0.9375rem;font-weight:700;color:var(--tertiary)">' + avgSim + '</span><span style="font-size:0.625rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.1em">Avg Similarity</span></div>'
+ '<div style="display:flex;flex-direction:column;align-items:center"><span style="font-size:0.9375rem;font-weight:700;color:var(--secondary)">' + budgetPct + '</span><span style="font-size:0.625rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.1em">Budget Util</span></div>'
+ (tierItems ? '<div style="display:flex;flex-direction:column"><span style="font-size:0.625rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.1em;margin-bottom:0.2rem">Tier Breakdown</span><div style="display:flex;flex-wrap:wrap;gap:0.4rem;align-items:center">' + tierItems + '</div></div>' : '')
+ '</div></div>';
}
var filterHtml = '';
var semanticNavHtml = '';
if (mode === 'semantic' && data.categories && data.categories.length > 0) {
var cats = data.categories;
var totalEntries = cats.reduce(function(s, c) { return s + c.count; }, 0);
var navRows = '<div data-mem-cat="" style="cursor:pointer;padding:0.65rem 0.8rem;border:1px solid ' + (!cat ? 'var(--accent)' : 'var(--border-soft)') + ';border-radius:6px;background:' + (!cat ? 'var(--accent-dim)' : 'rgba(255,255,255,0.01)') + ';display:flex;justify-content:space-between;align-items:center;gap:0.75rem">'
+ '<div><div style="font-size:0.78rem;font-weight:600;color:var(--text)">All Categories</div><div style="font-size:0.68rem;color:var(--muted)">Browse the full semantic memory set</div></div>'
+ '<span class="badge ' + (!cat ? 'active' : 'muted') + '" style="white-space:nowrap">' + totalEntries + '</span>'
+ '</div>';
cats.forEach(function(c) {
var active = cat === c.category;
navRows += '<div data-mem-cat="' + esc(c.category) + '" style="cursor:pointer;padding:0.65rem 0.8rem;border:1px solid ' + (active ? 'var(--accent)' : 'var(--border-soft)') + ';border-radius:6px;background:' + (active ? 'var(--accent-dim)' : 'rgba(255,255,255,0.01)') + ';display:flex;justify-content:space-between;align-items:center;gap:0.75rem">'
+ '<div><div style="font-size:0.78rem;font-weight:600;color:var(--text)">' + esc(c.category) + '</div><div style="font-size:0.68rem;color:var(--muted)">Semantic category</div></div>'
+ '<span class="badge ' + (active ? 'active' : 'muted') + '" style="white-space:nowrap">' + c.count + '</span>'
+ '</div>';
});
semanticNavHtml = '<div class="card" style="padding:0.85rem">'
+ '<div class="card-title" style="margin-bottom:0.25rem">Categories</div>'
+ '<div style="font-size:0.75rem;color:var(--muted);margin-bottom:0.75rem">Navigate semantic memory by category.</div>'
+ '<div style="display:flex;flex-direction:column;gap:0.45rem">' + navRows + '</div>'
+ '</div>';
}
if (mode === 'semantic') {
var list = memPaged.map(function(e) {
return '<div class="card" style="margin-bottom:0.5rem">'
+ '<div style="display:flex;justify-content:space-between;align-items:baseline">'
+ '<strong style="color:var(--text);font-size:0.875rem">' + esc(e.key || '') + '</strong>'
+ '<span class="badge muted" style="font-size:0.7rem">' + esc(e.category || '') + '</span>'
+ '</div>'
+ '<div style="margin-top:4px">' + formatMemContent(e.value || e.content || '') + '</div>'
+ '<div style="margin-top:6px"><span class="badge">conf: ' + esc(String(e.confidence != null ? e.confidence : '—')) + '</span></div>'
+ '</div>';
}).join('') || '<p style="color:var(--muted)">No entries' + (cat ? ' in "' + esc(cat) + '"' : '') + '. Try a different category filter.</p>';
var contentHtml = '<div>'
+ (cat ? '<div style="margin-bottom:0.75rem;font-size:0.78rem;color:var(--muted)">Showing category: <strong style="color:var(--text)">' + esc(cat) + '</strong></div>' : '')
+ memFilterBar()
+ '<div id="memory-list">' + list + '</div>'
+ memPager()
+ '</div>';
if (semanticNavHtml) {
return healthStripHtml + tabButtons + '<div style="display:grid;grid-template-columns:minmax(240px,280px) minmax(0,1fr);gap:1rem;align-items:start">' + semanticNavHtml + contentHtml + '</div>';
}
return healthStripHtml + tabButtons + filterHtml + memFilterBar() + '<div id="memory-list">' + list + '</div>' + memPager();
}
if (mode === 'working') {
var sessionIds = [];
entries.forEach(function(e) { if (e.session_id && sessionIds.indexOf(e.session_id) === -1) sessionIds.push(e.session_id); });
if (sessionIds.length > 1 || (sessionIds.length === 1 && !sessionId)) {
filterHtml = '<div style="margin-bottom:1rem;display:flex;align-items:center;gap:0.5rem">'
+ '<label style="font-size:0.8rem;color:var(--muted)">Session:</label>'
+ '<select id="mem-session-select" style="flex:1;max-width:400px;padding:0.4rem;border:1px solid var(--border);border-radius:4px;background:var(--surface);color:var(--text);font-family:var(--mono);font-size:0.8rem">'
+ '<option value="">All sessions (' + sessionIds.length + ')</option>';
sessionIds.forEach(function(sid) {
var firstEntry = entries.find(function(e) { return e.session_id === sid; });
var dateLabel = firstEntry && firstEntry.created_at ? firstEntry.created_at.substring(0, 16).replace('T', ' ') : '';
var label = dateLabel + ' (' + sid.substring(0, 8) + '\u2026)';
var sel = sessionId === sid ? ' selected' : '';
filterHtml += '<option value="' + esc(sid) + '"' + sel + '>' + esc(label) + '</option>';
});
filterHtml += '</select></div>';
}
var list = memPaged.map(function(e) {
var ts = e.created_at ? e.created_at.substring(0, 16).replace('T', ' ') : '';
return '<div class="card" style="margin-bottom:0.5rem">'
+ '<div>' + formatMemContent(e.content || '') + '</div>'
+ '<div style="margin-top:6px">'
+ '<span class="badge muted">' + esc(e.entry_type || '—') + '</span> '
+ '<span class="badge">imp: ' + esc(String(e.importance != null ? e.importance : '—')) + '</span> '
+ '<span style="font-size:0.7rem;color:var(--muted)">' + esc((e.session_id || '').substring(0, 8)) + '</span>'
+ (ts ? ' <span style="font-size:0.7rem;color:var(--muted)">' + esc(ts) + '</span>' : '')
+ '</div></div>';
}).join('') || '<p style="color:var(--muted)">No working memory entries. Open a session and send a message to populate this view.</p>';
return healthStripHtml + tabButtons + filterHtml + memFilterBar() + '<div id="memory-list">' + list + '</div>' + memPager();
}
if (mode === 'search') {
var searchHtml = '<div style="margin-bottom:1rem"><input type="text" id="memory-search-q" placeholder="Search across all memory\u2026" style="padding:0.5rem 1rem;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);width:100%;max-width:400px;font-family:var(--font)"><button class="btn" style="margin-left:8px" id="memory-search-btn">' + uiBtnLabel('search', 'Search') + '</button></div>';
return healthStripHtml + tabButtons + searchHtml + '<div id="memory-search-results"><p style="color:var(--muted)">Enter a query and click Search (example: <code>token budget</code>).</p></div>';
}
var list = memPaged.map(function(e) {
var type = e.entry_type || e.classification || e.category || '\u2014';
var imp = e.importance != null ? e.importance : (e.confidence != null ? e.confidence : '\u2014');
var content = e.content || e.value || '';
if (!content && e.key) content = e.key + ': ' + (e.summary || '');
if (!content) { try { var ek = Object.keys(e).filter(function(k) { return k !== 'id' && k !== 'created_at' && k !== 'updated_at'; }); content = ek.map(function(k) { return k + ': ' + String(e[k] || ''); }).join(' | '); } catch(_) { content = '(no content)'; } }
var ts = e.created_at ? e.created_at.substring(0, 16).replace('T', ' ') : (e.updated_at ? e.updated_at.substring(0, 16).replace('T', ' ') : '');
return '<div class="card" style="margin-bottom:0.5rem"><div>' + formatMemContent(content) + '</div><div style="margin-top:6px"><span class="badge muted">' + esc(type) + '</span> <span class="badge">imp: ' + esc(String(imp)) + '</span>' + (ts ? ' <span style="font-size:0.7rem;color:var(--muted)">' + esc(ts) + '</span>' : '') + '</div></div>';
}).join('') || '<p style="color:var(--muted)">No entries found for this memory view yet.</p>';
return healthStripHtml + tabButtons + memFilterBar() + '<div id="memory-list">' + list + '</div>' + memPager();
});
};