-
-
Memory Survival by Day
-
-
-
-
Feedback Quality Trend
-
+
+
+
+
+
+
Memory Lifecycle (14d)
+
+
+
-
Encoding Pipeline
-
+
Signal Quality by Source
+
-
Salience Distribution
-
+
Recall Learning Curve
+
-
Session Timeline
-
+
Session Activity
+
@@ -1760,6 +1941,7 @@
Activity
localStorage.setItem('mnemonic-theme', name);
var sel = document.getElementById('themeSelect');
if (sel) sel.value = name;
+ if (state.mindLoaded) updateMindColors();
}
// Sync dropdown to current theme on load
@@ -1788,6 +1970,14 @@
Activity
agentData: null,
llmLoaded: false,
toolsLoaded: false,
+ dreamSessionTotals: { cycles: 0, replayed: 0, strengthened: 0, newLinks: 0, insights: 0, crossProject: 0, demoted: 0 },
+ mindLoaded: false,
+ mindSimulation: null,
+ mindData: null,
+ mindSelectedNode: null,
+ mindSearchTerm: '',
+ mindView: 'memories',
+ mindAdjacency: null,
};
const CONFIG = {
@@ -1847,6 +2037,7 @@
Activity
if (name === 'agent' && !state.agentLoaded) loadAgentData();
if (name === 'llm' && !state.llmLoaded) loadLLMUsage();
if (name === 'tools' && !state.toolsLoaded) loadToolUsage();
+ if (name === 'mind' && !state.mindLoaded) loadMindGraph();
}
function switchExploreTab(tab) {
@@ -1860,7 +2051,7 @@
Activity
function handleHash() {
var hash = window.location.hash.replace('#', '');
- if (['recall', 'explore', 'timeline', 'agent', 'llm'].includes(hash)) switchView(hash);
+ if (['recall', 'explore', 'timeline', 'mind', 'agent', 'llm', 'tools'].includes(hash)) switchView(hash);
}
window.addEventListener('hashchange', handleHash);
@@ -2796,12 +2987,84 @@
Activity
else { badge.classList.remove('visible'); }
}
- function addEvent(type, description, timestamp) {
- var dotColors = { raw_memory_created: 'green', memory_encoded: 'blue', memory_accessed: 'violet', query_executed: 'cyan', consolidation_started: 'orange', consolidation_completed: 'orange', dream_cycle_completed: 'violet', meta_cycle_completed: 'violet', watcher_event: 'yellow', system_health: 'cyan', episode_closed: 'green' };
+ var _eventDotColors = { raw_memory_created: 'green', memory_encoded: 'blue', memory_accessed: 'violet', query_executed: 'cyan', consolidation_started: 'orange', consolidation_completed: 'orange', dream_cycle_completed: 'violet', meta_cycle_completed: 'violet', watcher_event: 'yellow', system_health: 'cyan', episode_closed: 'green', pattern_discovered: 'cyan', abstraction_created: 'violet', memory_amended: 'blue', session_ended: 'green' };
+ var _eventNavTargets = { pattern_discovered: ['explore', 'patterns'], abstraction_created: ['explore', 'abstractions'], episode_closed: ['explore', 'episodes'], memory_encoded: ['explore', 'memories'] };
+
+ function _nonZero(items) {
+ return items.filter(function(i) { return i[0] > 0; }).map(function(i) { return i[0] + ' ' + i[1]; }).join(' \u00b7 ');
+ }
+
+ function buildEventDetail(type, payload) {
+ var p = payload || {};
+ switch (type) {
+ case 'consolidation_completed': {
+ var line1 = _nonZero([[p.memories_processed, 'processed'], [p.memories_decayed, 'decayed'], [p.merged_clusters, 'merged']]);
+ var line2 = _nonZero([[p.patterns_extracted, 'patterns found'], [p.never_recalled_archived, 'noise archived'], [p.transitioned_fading, 'fading'], [p.transitioned_archived, 'archived']]);
+ var dur = p.duration_ms ? (p.duration_ms / 1000).toFixed(1) + 's' : '';
+ var html = '';
+ if (line1) html += '
' + line1 + '
';
+ if (line2) html += '
' + line2 + '
';
+ if (dur) html += '
' + dur + '
';
+ return html;
+ }
+ case 'dream_cycle_completed': {
+ var l1 = _nonZero([[p.memories_replayed, 'replayed'], [p.associations_strengthened, 'strengthened'], [p.new_associations_created, 'new links']]);
+ var l2 = _nonZero([[p.insights_generated, 'insights'], [p.cross_project_links, 'cross-project'], [p.noisy_memories_demoted, 'demoted']]);
+ var d = p.duration_ms ? (p.duration_ms / 1000).toFixed(1) + 's' : '';
+ var h = '';
+ if (l1) h += '
' + l1 + '
';
+ if (l2) h += '
' + l2 + '
';
+ if (d) h += '
' + d + '
';
+ return h;
+ }
+ case 'memory_encoded': {
+ var concepts = (p.concepts || []).length;
+ var assocs = p.associations_created || 0;
+ var parts = _nonZero([[concepts, 'concepts'], [assocs, 'associations']]);
+ return parts ? '
' + parts + '
' : '';
+ }
+ case 'pattern_discovered': {
+ var html = '';
+ if (p.title) html += '
' + escapeHtml(p.title.slice(0, 60)) + '
';
+ var meta = _nonZero([[1, p.pattern_type || 'pattern'], [p.evidence_count, 'evidence']]);
+ if (meta) html += '
' + meta + '
';
+ return html;
+ }
+ case 'abstraction_created': {
+ var html = '';
+ if (p.title) html += '
' + escapeHtml(p.title.slice(0, 60)) + '
';
+ if (p.source_count) html += '
from ' + p.source_count + ' sources
';
+ return html;
+ }
+ case 'episode_closed': {
+ var html = '';
+ if (p.title) html += '
' + escapeHtml(p.title.slice(0, 60)) + '
';
+ var meta = _nonZero([[p.event_count, 'events']]);
+ if (p.duration_sec) meta += (meta ? ' \u00b7 ' : '') + Math.round(p.duration_sec / 60) + 'min';
+ if (meta) html += '
' + meta + '
';
+ return html;
+ }
+ case 'query_executed': {
+ return '
' + (p.results_returned || 0) + ' results \u00b7 ' + (p.took_ms || 0) + 'ms
';
+ }
+ default: return '';
+ }
+ }
+
+ function addEvent(type, description, timestamp, payload) {
var container = document.getElementById('eventsContainer');
var el = document.createElement('div');
- el.className = 'event-item';
- el.innerHTML = '
' + escapeHtml(type.replace(/_/g, ' ')) + '
' + escapeHtml(description) + '
' + relativeTime(new Date(timestamp)) + '
';
+ var navTarget = _eventNavTargets[type];
+ el.className = 'event-item' + (navTarget ? ' event-clickable' : '');
+ if (navTarget) {
+ el.onclick = function() {
+ toggleDrawer();
+ switchView(navTarget[0]);
+ if (navTarget[1]) switchExploreTab(navTarget[1]);
+ };
+ }
+ var detail = buildEventDetail(type, payload);
+ el.innerHTML = '
' + escapeHtml(type.replace(/_/g, ' ')) + '
' + escapeHtml(description) + '
' + detail + '
' + relativeTime(new Date(timestamp)) + '
';
container.prepend(el);
while (container.children.length > CONFIG.MAX_EVENTS) container.removeChild(container.lastChild);
if (!state.drawerOpen) { state.unreadEvents++; updateBadge(); }
@@ -2934,13 +3197,13 @@
Activity
case 'query_executed': desc = 'Query: "' + (payload.query_text || '').slice(0, 40) + '" (' + (payload.results_returned || 0) + ' results)'; break;
case 'consolidation_started': desc = 'Consolidation cycle started'; break;
case 'consolidation_completed': desc = 'Consolidated: ' + (payload.memories_processed || 0) + ' processed'; state.exploreLoaded.episodes = false; state.exploreLoaded.patterns = false; state.exploreLoaded.abstractions = false; break;
- case 'dream_cycle_completed': desc = 'Dream cycle completed'; state.exploreLoaded.abstractions = false; break;
+ case 'dream_cycle_completed': desc = 'Dream cycle'; state.exploreLoaded.abstractions = false; state.dreamSessionTotals.cycles++; state.dreamSessionTotals.replayed += (payload.memories_replayed || 0); state.dreamSessionTotals.strengthened += (payload.associations_strengthened || 0); state.dreamSessionTotals.newLinks += (payload.new_associations_created || 0); state.dreamSessionTotals.insights += (payload.insights_generated || 0); state.dreamSessionTotals.crossProject += (payload.cross_project_links || 0); state.dreamSessionTotals.demoted += (payload.noisy_memories_demoted || 0); break;
case 'pattern_discovered': desc = 'Pattern: ' + (payload.title || '').slice(0, 40); state.exploreLoaded.patterns = false; break;
case 'abstraction_created': desc = (payload.level === 3 ? 'Axiom' : 'Principle') + ': ' + (payload.title || '').slice(0, 40); state.exploreLoaded.abstractions = false; break;
case 'episode_closed': desc = 'Episode closed: ' + (payload.title || '').slice(0, 40); state.exploreLoaded.episodes = false; break;
default: desc = type.replace(/_/g, ' ');
}
- addEvent(type, desc, msg.timestamp || new Date().toISOString());
+ addEvent(type, desc, msg.timestamp || new Date().toISOString(), payload);
if (['memory_encoded', 'consolidation_completed', 'dream_cycle_completed'].includes(type) && state.currentView === 'timeline') {
clearTimeout(state._timelineLiveReload);
state._timelineLiveReload = setTimeout(function() { loadTimelineData(false); }, 5000);
@@ -3208,6 +3471,18 @@
Activity
// ── Tool Usage Analytics ──
var _toolRange = '24h';
+ var _toolLog = [];
+ var _analyticsRange = 14;
+
+ function setAnalyticsRange(days) {
+ _analyticsRange = days;
+ document.querySelectorAll('#analyticsRangeTabs .llm-range-tab').forEach(function(t) {
+ t.classList.toggle('active', t.getAttribute('data-range') === String(days));
+ });
+ document.getElementById('lifecycleTitle').textContent = 'Memory Lifecycle (' + days + 'd)';
+ loadAnalytics();
+ loadSessionTimeline();
+ }
function setToolRange(range) {
_toolRange = range;
@@ -3250,6 +3525,7 @@
Activity
document.getElementById('toolUpdated').textContent = 'Updated ' + new Date().toLocaleTimeString();
var log = data.log || [];
+ _toolLog = log; // expose for session enrichment
// Per-tool extras from log (avg latency, avg size)
var toolExtras = {};
@@ -3405,6 +3681,43 @@
Activity
}
}
+ // ── Research Analytics ──
+
+ var _raSourceColors = { mcp: 'var(--accent-green)', filesystem: 'var(--accent-cyan)', terminal: 'var(--accent-yellow)', git: 'var(--accent-violet)', clipboard: 'var(--text-muted)', ingest: 'var(--accent-green)', benchmark: 'var(--accent-red)', system: 'var(--text-muted)' };
+
+ function _thresholdColor(value, greenAbove, yellowAbove) {
+ if (value >= greenAbove) return 'var(--accent-green)';
+ if (value >= yellowAbove) return 'var(--accent-yellow)';
+ return 'var(--accent-red)';
+ }
+
+ function _renderSparkline(container, data, color) {
+ if (!data || data.length < 2) return;
+ var w = 64, h = 24;
+ var svg = d3.select(container).append('svg').attr('width', w).attr('height', h);
+ var ext = d3.extent(data);
+ if (ext[0] === ext[1]) { ext[0] -= 1; ext[1] += 1; }
+ var x = d3.scaleLinear().domain([0, data.length - 1]).range([2, w - 2]);
+ var y = d3.scaleLinear().domain(ext).range([h - 2, 2]);
+ var line = d3.line().x(function(d, i) { return x(i); }).y(function(d) { return y(d); }).curve(d3.curveMonotoneX);
+ var area = d3.area().x(function(d, i) { return x(i); }).y0(h).y1(function(d) { return y(d); }).curve(d3.curveMonotoneX);
+ svg.append('path').datum(data).attr('d', area).attr('fill', color).attr('opacity', 0.1);
+ svg.append('path').datum(data).attr('d', line).attr('fill', 'none').attr('stroke', color).attr('stroke-width', 1.5);
+ svg.append('circle').attr('cx', x(data.length - 1)).attr('cy', y(data[data.length - 1])).attr('r', 2.5).attr('fill', color);
+ }
+
+ function _computeDelta(data) {
+ if (!data || data.length < 2) return { text: '', cls: 'neutral' };
+ var recent = data.slice(-Math.min(3, data.length));
+ var older = data.slice(0, Math.max(1, data.length - 3));
+ var avgRecent = recent.reduce(function(a, b) { return a + b; }, 0) / recent.length;
+ var avgOlder = older.reduce(function(a, b) { return a + b; }, 0) / older.length;
+ var delta = avgRecent - avgOlder;
+ if (Math.abs(delta) < 0.5) return { text: 'stable', cls: 'neutral' };
+ var sign = delta > 0 ? '+' : '';
+ return { text: sign + delta.toFixed(1), cls: delta > 0 ? 'positive' : 'negative' };
+ }
+
async function loadAnalytics() {
try {
var data = await fetchJSON('/analytics');
@@ -3413,193 +3726,488 @@
Activity
var re = data.recall_effectiveness || [];
var fb = data.feedback_trend || [];
var sv = data.memory_survival || [];
+ var ch = data.consolidation_history || [];
+ var days = _analyticsRange;
+
+ // Slice data to selected range
+ var svData = sv.slice().reverse().slice(-days);
+ var fbData = fb.slice().reverse().slice(-days);
+ var chData = ch.slice().reverse().slice(-days);
+
+ // ── KPI Cards ──
+ var kpiContainer = document.getElementById('raKpis');
+
+ // Pipeline Health: encoding rate
+ var encodingRate = (p.encoding_rate || 0) * 100;
+ var pipelineSpark = svData.map(function(d) {
+ return d.created > 0 ? (d.active / d.created) * 100 : 0;
+ });
+ var pipelineDelta = _computeDelta(pipelineSpark);
- // Pipeline summary cards
- document.getElementById('raRaw').textContent = (p.total_raw || 0).toLocaleString();
- document.getElementById('raEncoded').textContent = (p.total_encoded || 0).toLocaleString();
- var dedupEl = document.getElementById('raDedupRate');
- dedupEl.textContent = ((p.dedup_rate || 0) * 100).toFixed(1) + '%';
- dedupEl.style.color = 'var(--accent-cyan)';
-
- // MCP survival rate
- var mcpData = sn.mcp || {};
- var mcpSurv = document.getElementById('raMCPSurvival');
- mcpSurv.textContent = ((mcpData.survival_rate || 0) * 100).toFixed(0) + '%';
- mcpSurv.style.color = 'var(--accent-green)';
-
- // Filesystem (noise) survival rate
- var fsData = sn.filesystem || {};
- var noiseSurv = document.getElementById('raNoiseSurvival');
- noiseSurv.textContent = ((fsData.survival_rate || 0) * 100).toFixed(0) + '%';
- noiseSurv.style.color = (fsData.survival_rate || 0) > 0.5 ? 'var(--accent-yellow)' : 'var(--accent-green)';
-
- // Recall learning indicator: ratio of 6+ salience to never-recalled salience
+ // MCP Survival
+ var mcpSurv = ((sn.mcp || {}).survival_rate || 0) * 100;
+
+ // Recall Learning
var neverBucket = re.find(function(b) { return b.bucket.indexOf('never') >= 0; });
var highBucket = re.find(function(b) { return b.bucket.indexOf('6+') >= 0; });
- var learningEl = document.getElementById('raLearning');
- if (neverBucket && highBucket && neverBucket.avg_salience > 0) {
- var ratio = (highBucket.avg_salience / neverBucket.avg_salience).toFixed(1) + 'x';
- learningEl.textContent = ratio;
- learningEl.style.color = 'var(--accent-green)';
- } else {
- learningEl.textContent = '-';
- }
+ var learningRatio = (neverBucket && highBucket && neverBucket.avg_salience > 0)
+ ? highBucket.avg_salience / neverBucket.avg_salience : 0;
- // Signal vs Noise — horizontal bars showing survival rate per source
- var snEl = document.getElementById('signalNoiseChart');
- var sources = Object.keys(sn).sort(function(a, b) { return (sn[b].total || 0) - (sn[a].total || 0); });
- var sourceColors = { mcp: 'var(--accent-green)', filesystem: 'var(--accent-cyan)', terminal: 'var(--accent-yellow)', git: 'var(--accent-purple)', clipboard: 'var(--text-muted)', ingest: 'var(--accent-green)', benchmark: 'var(--accent-red)', system: 'var(--text-muted)' };
- snEl.innerHTML = sources.map(function(src) {
- var s = sn[src];
- var survPct = ((s.survival_rate || 0) * 100).toFixed(1);
- var barWidth = Math.max(2, (s.survival_rate || 0) * 100);
- var color = sourceColors[src] || 'var(--text-muted)';
- return '
' +
- '
' +
- '' + src + ' (' + s.total + ' memories)' +
- '' + s.active + ' active (' + survPct + '%)
' +
- '
';
- }).join('');
+ // Recall Quality
+ var qualitySpark = fbData.map(function(d) {
+ var total = d.helpful + d.partial + d.irrelevant;
+ return total > 0 ? (d.helpful / total) * 100 : 0;
+ });
+ var totalFb = fbData.reduce(function(a, d) { return a + d.helpful + d.partial + d.irrelevant; }, 0);
+ var totalHelpful = fbData.reduce(function(a, d) { return a + d.helpful; }, 0);
+ var qualityPct = totalFb > 0 ? (totalHelpful / totalFb) * 100 : 0;
+ var qualityDelta = _computeDelta(qualitySpark);
+
+ kpiContainer.innerHTML = '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
';
+
+ // Pipeline Health card
+ var el1 = document.getElementById('kpiPipeline');
+ el1.innerHTML = '
Pipeline Health
' +
+ '
' + encodingRate.toFixed(0) + '%
' +
+ '';
+ _renderSparkline(document.getElementById('sparkPipeline'), pipelineSpark, _thresholdColor(encodingRate, 50, 30));
+
+ // MCP Survival card
+ var el2 = document.getElementById('kpiMcp');
+ el2.innerHTML = '
MCP Survival
' +
+ '
' + mcpSurv.toFixed(0) + '%
' +
+ '';
+
+ // Recall Learning card
+ var el3 = document.getElementById('kpiLearning');
+ var lColor = _thresholdColor(learningRatio, 1.5, 1.0);
+ el3.innerHTML = '
Recall Learning
' +
+ '
' + (learningRatio > 0 ? learningRatio.toFixed(1) + 'x' : '-') + '
' +
+ '';
+
+ // Recall Quality card
+ var el4 = document.getElementById('kpiQuality');
+ el4.innerHTML = '
Recall Quality
' +
+ '
' + (totalFb > 0 ? qualityPct.toFixed(0) + '%' : '-') + '
' +
+ '';
+ _renderSparkline(document.getElementById('sparkQuality'), qualitySpark, _thresholdColor(qualityPct, 70, 50));
+
+ // Network Density KPI (from /api/v1/stats, already loaded at init)
+ var el5 = document.getElementById('kpiDensity');
+ try {
+ var statsResp = await fetchJSON('/stats');
+ var avgAssoc = (statsResp.store || {}).avg_associations_per_memory || 0;
+ el5.innerHTML = '
Network Density
' +
+ '
' + avgAssoc.toFixed(1) + '
' +
+ '';
+ } catch(e) { el5.innerHTML = '
Network Density
-
'; }
+
+ // Retrieval Performance KPI (from new endpoint)
+ var el6 = document.getElementById('kpiRetrieval');
+ try {
+ var retStats = await fetchJSON('/retrieval/stats');
+ var avgPerQuery = retStats.avg_memories_per_query || 0;
+ var totalQ = retStats.total_queries || 0;
+ var avgMs = retStats.avg_synthesis_ms || 0;
+ el6.innerHTML = '
Retrieval Perf
' +
+ '
' + (totalQ > 0 ? avgPerQuery.toFixed(1) : '-') + '
' +
+ '';
+ } catch(e) { el6.innerHTML = '
Retrieval Perf
-
'; }
+
+ // ── Cognitive Agents Panel ──
+ try {
+ var absResp = await fetchJSON('/abstractions?limit=500');
+ var abstractions = absResp.abstractions || absResp || [];
+ if (!Array.isArray(abstractions)) abstractions = [];
+ var principles = abstractions.filter(function(a) { return a.level === 2; }).length;
+ var axioms = abstractions.filter(function(a) { return a.level === 3; }).length;
+
+ var chTotals = (ch || []).reduce(function(acc, c) {
+ acc.cycles++; acc.processed += (c.processed || 0); acc.merged += (c.merged || 0); acc.decayed += (c.decayed || 0);
+ return acc;
+ }, { cycles: 0, processed: 0, merged: 0, decayed: 0 });
+
+ var ds = state.dreamSessionTotals;
+ var cogGrid = document.getElementById('cognitiveGrid');
+ cogGrid.innerHTML =
+ '
Encoding
' + (p.total_encoded || 0) + '
' + (encodingRate > 0 ? encodingRate.toFixed(0) + '% rate' : 'no data') + '
' +
+ '
Consolidation
' + chTotals.cycles + '
' + chTotals.merged + ' merged \u00b7 ' + chTotals.decayed + ' decayed
' +
+ '
Dreaming
' + ds.cycles + '
' + ds.insights + ' insights \u00b7 ' + ds.newLinks + ' new links
' +
+ '
Abstraction
' + principles + '
' + axioms + ' axioms \u00b7 ' + (principles + axioms) + ' total
';
+ } catch(e) { console.error('Cognitive panel load failed:', e); }
+
+ // ── System Analysis ──
+ try {
+ var briefEl = document.getElementById('raBrief');
+ var statsResp2 = await fetchJSON('/stats');
+ var st = statsResp2.store || {};
+ var sessResp = await fetchJSON('/sessions?days=' + days + '&limit=100');
+ var sessCount = sessResp.count || 0;
+ var avgAssoc = st.avg_associations_per_memory || 0;
+
+ var lines = [];
+
+ // Overall health sentence
+ var healthIssues = 0;
+ if (encodingRate < 50) healthIssues++;
+ if (mcpSurv < 60) healthIssues++;
+ if (qualityPct < 50 && totalFb > 5) healthIssues++;
+ if (learningRatio < 1.0 && learningRatio > 0) healthIssues++;
+
+ if (healthIssues === 0) {
+ lines.push('Mnemonic is performing well.
' + (st.total_memories || 0) + ' memories across
' + sessCount + ' sessions, with a well-connected network (' + avgAssoc.toFixed(1) + ' associations per memory).');
+ } else if (healthIssues <= 2) {
+ lines.push('Mnemonic is running but has
areas that need attention.
' + (st.total_memories || 0) + ' memories across
' + sessCount + ' sessions.');
+ } else {
+ lines.push('Mnemonic has
several metrics below target.
' + (st.total_memories || 0) + ' memories across
' + sessCount + ' sessions. This is expected early on \u2014 the system improves with use.');
+ }
- // Recall Effectiveness — bar chart showing access buckets vs avg salience
- var recallEl = document.getElementById('recallChart');
- if (re.length === 0) {
- recallEl.innerHTML = '
No recall data';
- } else {
- var maxSal = Math.max.apply(null, re.map(function(b) { return b.avg_salience; }));
- recallEl.innerHTML = '
Memories recalled more often have higher salience = system is learning
' +
- re.map(function(b) {
- var barWidth = maxSal > 0 ? (b.avg_salience / maxSal * 100) : 0;
- var color = b.bucket.indexOf('never') >= 0 ? 'var(--accent-red)' : b.bucket.indexOf('6+') >= 0 ? 'var(--accent-green)' : 'var(--accent-cyan)';
- return '
' +
- '
' +
- '' + b.bucket + ' (' + b.count + ')' +
- '' + b.avg_salience.toFixed(3) + '
' +
- '
';
- }).join('');
- }
+ // Encoding pipeline insight
+ if (encodingRate >= 80) {
+ lines.push('The encoding pipeline is converting ' + encodingRate.toFixed(0) + '% of observations into memories \u2014 minimal signal loss.');
+ } else if (encodingRate >= 50) {
+ lines.push('The encoding pipeline is at ' + encodingRate.toFixed(0) + '%. Some observations are being filtered or failing to encode.');
+ } else if (encodingRate > 0) {
+ lines.push('
Encoding is struggling at ' + encodingRate.toFixed(0) + '%. Check LLM availability \u2014 most observations are failing to encode.');
+ }
- // Memory Survival by day — stacked text table (D3 chart would be ideal but text works)
- var survEl = document.getElementById('survivalChart');
- var survData = sv.slice().reverse().slice(-14); // last 14 days, chronological
- if (survData.length === 0) {
- survEl.innerHTML = '
No survival data';
- } else {
- survEl.innerHTML = '
| Date | Created | Active | Fading | Archived | Merged |
' +
- survData.map(function(d) {
- var total = d.created || 1;
- var activePct = ((d.active / total) * 100).toFixed(0);
- return '| ' + d.date.substring(5) + ' | ' + d.created + ' | ' +
- '' + d.active + ' (' + activePct + '%) | ' +
- '' + d.fading + ' | ' +
- '' + d.archived + ' | ' +
- '' + d.merged + ' |
';
- }).join('') + '
';
- }
+ // MCP survival insight
+ if (mcpSurv > 0 && mcpSurv < 60) {
+ var mcpActive = (sn.mcp || {}).active || 0;
+ var mcpTotal = (sn.mcp || {}).total || 0;
+ lines.push('Only
' + mcpSurv.toFixed(0) + '% of MCP memories survive (' + mcpActive + '/' + mcpTotal + ' active). Older memories naturally decay over time \u2014 this ratio improves as you build fresh, high-quality memories through active use.');
+ } else if (mcpSurv >= 60 && mcpSurv < 80) {
+ lines.push('MCP survival is ' + mcpSurv.toFixed(0) + '% \u2014 some older memories have been pruned, which is healthy.');
+ }
- // Feedback trend
- var fbEl = document.getElementById('feedbackTrendChart');
- var fbData = fb.slice().reverse().slice(-14);
- if (fbData.length === 0) {
- fbEl.innerHTML = '
No feedback data';
- } else {
- fbEl.innerHTML = '
| Date | Helpful | Partial | Irrelevant | Quality |
' +
- fbData.map(function(d) {
- var total = d.helpful + d.partial + d.irrelevant;
- var quality = total > 0 ? ((d.helpful / total) * 100).toFixed(0) + '%' : '-';
- var qColor = total > 0 && (d.helpful / total) >= 0.5 ? 'var(--accent-green)' : 'var(--accent-yellow)';
- return '| ' + d.date.substring(5) + ' | ' +
- '' + d.helpful + ' | ' +
- '' + d.partial + ' | ' +
- '' + d.irrelevant + ' | ' +
- '' + quality + ' |
';
- }).join('') + '
';
- }
+ // Recall quality insight
+ if (totalFb > 5) {
+ if (qualityPct >= 70) {
+ lines.push('Recall quality is
strong at ' + qualityPct.toFixed(0) + '% \u2014 the system is returning useful memories most of the time.');
+ } else if (qualityDelta.cls === 'positive') {
+ lines.push('Recall quality is at ' + qualityPct.toFixed(0) + '% but
trending up (' + qualityDelta.text + '). The feedback loop is working \u2014 keep rating recalls.');
+ } else {
+ lines.push('Recall quality is
below target at ' + qualityPct.toFixed(0) + '%. More feedback will help the system learn which memories matter.');
+ }
+ } else {
+ lines.push('Not enough recall feedback yet to assess quality. Rate your recalls (helpful/partial/irrelevant) to train the system.');
+ }
- // Encoding Pipeline — funnel visualization
- var epEl = document.getElementById('encodingPipelineChart');
- var totalRaw = p.total_raw || 0;
- var totalEncoded = p.total_encoded || 0;
- var totalMerged = p.total_merged || 0;
- var maxVal = Math.max(totalRaw, 1);
- var stages = [
- { label: 'Raw', value: totalRaw, color: 'var(--accent-blue)' },
- { label: 'Encoded', value: totalEncoded, color: 'var(--accent-cyan)' },
- { label: 'Merged', value: totalMerged, color: 'var(--accent-violet)' },
- { label: 'Active', value: totalEncoded - totalMerged, color: 'var(--accent-green)' }
- ];
- epEl.innerHTML = stages.map(function(s) {
- var pct = Math.max(((s.value / maxVal) * 100), 2);
- return '
' + s.label + '' +
- '
' +
- '
' + (s.value >= 0 ? s.value.toLocaleString() : 0) + ' ';
- }).join('');
+ // Learning signal
+ if (learningRatio > 1.5) {
+ lines.push('Frequently recalled memories have
' + learningRatio.toFixed(1) + 'x higher salience than unused ones \u2014 the system is learning what matters.');
+ } else if (learningRatio > 0 && learningRatio < 1.0) {
+ lines.push('Frequently recalled memories don\u2019t yet have higher salience than unused ones. This is normal early on \u2014 the recall learning signal strengthens over time with more feedback.');
+ }
- // Salience Distribution — stacked bar chart by tier
- var sdEl = document.getElementById('salienceHistogram');
- var sd = data.salience_distribution || {};
- var sourceColors = {
- mcp: 'var(--accent-cyan)', filesystem: 'var(--accent-blue)',
- git: 'var(--accent-violet)', terminal: 'var(--accent-green)',
- clipboard: 'var(--accent-yellow)', benchmark: 'var(--accent-pink)',
- ingest: 'var(--accent-red)', system: 'var(--text-muted)'
- };
- var tiers = ['high', 'medium', 'low', 'noise'];
- var allSources = new Set();
- tiers.forEach(function(t) { Object.keys(sd[t] || {}).forEach(function(s) { allSources.add(s); }); });
- var sources = Array.from(allSources);
- var maxTierTotal = 0;
- tiers.forEach(function(t) {
- var total = 0;
- sources.forEach(function(s) { total += (sd[t] || {})[s] || 0; });
- if (total > maxTierTotal) maxTierTotal = total;
- });
- if (maxTierTotal === 0) {
- sdEl.innerHTML = '
No salience data';
- } else {
- sdEl.innerHTML = tiers.map(function(tier) {
- var tierData = sd[tier] || {};
- var tierTotal = 0;
- sources.forEach(function(s) { tierTotal += tierData[s] || 0; });
- var segs = sources.map(function(s) {
- var val = tierData[s] || 0;
- if (val === 0) return '';
- var pct = (val / maxTierTotal) * 100;
- return '
';
- }).join('');
- return '
' + tier + '' +
- '
' + segs + '
' +
- '
' + tierTotal + ' ';
- }).join('') +
- '
' +
- sources.map(function(s) {
- return '' + s + '';
- }).join('') + '
';
- }
+ // Abstraction formation
+ if (principles > 0) {
+ var axiomText = axioms > 0 ? ' and
' + axioms + ' axiom' + (axioms !== 1 ? 's' : '') + '' : '';
+ lines.push('The abstraction agent has synthesized
' + principles + ' principle' + (principles !== 1 ? 's' : '') + '' + axiomText + ' from your memory network \u2014 higher-order patterns that inform future recall.');
+ }
+
+ briefEl.innerHTML = lines.join(' ');
+ } catch(e) { /* analysis is optional, fail silently */ }
+
+ // ── Memory Lifecycle Chart (D3 stacked area) ──
+ renderLifecycleChart(svData, fbData, chData);
+
+ // ── Signal Quality by Source (D3 horizontal bars) ──
+ renderSignalChart(sn);
+
+ // ── Recall Learning Curve (D3 connected dots) ──
+ renderRecallChart(re);
} catch(e) {
console.error('Analytics load failed:', e);
}
}
- // Session Timeline — swimlane visualization
- function _fmtTimelineAxis(d) {
- var now = new Date();
- if (d.toDateString() === now.toDateString()) {
- return String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0');
+ function renderLifecycleChart(svData, fbData, chData) {
+ var container = document.getElementById('lifecycleChart');
+ var legendEl = document.getElementById('lifecycleLegend');
+ container.innerHTML = '';
+ if (!svData || svData.length === 0) {
+ container.innerHTML = '
No lifecycle data yet
';
+ legendEl.innerHTML = '';
+ return;
+ }
+
+ var w = container.clientWidth || 600;
+ var h = 220;
+ var margin = { top: 10, right: 50, bottom: 28, left: 45 };
+ var iw = w - margin.left - margin.right;
+ var ih = h - margin.top - margin.bottom;
+
+ var svg = d3.select(container).append('svg').attr('width', w).attr('height', h);
+ var g = svg.append('g').attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
+
+ // Prepare stack data
+ var stackKeys = ['active', 'merged', 'fading', 'archived'];
+ var stackColors = { active: 'var(--accent-green)', merged: 'var(--accent-cyan)', fading: 'var(--accent-yellow)', archived: 'var(--text-dim)' };
+
+ var x = d3.scaleBand().domain(svData.map(function(d) { return d.date; })).range([0, iw]).padding(0.1);
+ var maxY = d3.max(svData, function(d) { return (d.active || 0) + (d.merged || 0) + (d.fading || 0) + (d.archived || 0); }) || 1;
+ var y = d3.scaleLinear().domain([0, maxY]).nice().range([ih, 0]);
+
+ var stack = d3.stack().keys(stackKeys).value(function(d, key) { return d[key] || 0; });
+ var series = stack(svData);
+
+ var area = d3.area()
+ .x(function(d) { return x(d.data.date) + x.bandwidth() / 2; })
+ .y0(function(d) { return y(d[0]); })
+ .y1(function(d) { return y(d[1]); })
+ .curve(d3.curveMonotoneX);
+
+ series.forEach(function(s) {
+ g.append('path').datum(s).attr('d', area)
+ .attr('fill', stackColors[s.key])
+ .attr('opacity', s.key === 'archived' ? 0.3 : 0.6);
+ });
+
+ // Feedback quality overlay line (secondary Y axis)
+ if (fbData && fbData.length > 0) {
+ var yRight = d3.scaleLinear().domain([0, 100]).range([ih, 0]);
+ // Map feedback dates to survival dates
+ var fbMap = {};
+ fbData.forEach(function(d) { var t = d.helpful + d.partial + d.irrelevant; fbMap[d.date] = t > 0 ? (d.helpful / t) * 100 : null; });
+ var fbPoints = svData.map(function(d) { return { date: d.date, quality: fbMap[d.date] }; }).filter(function(d) { return d.quality !== null && d.quality !== undefined; });
+
+ if (fbPoints.length > 1) {
+ var fbLine = d3.line()
+ .x(function(d) { return x(d.date) + x.bandwidth() / 2; })
+ .y(function(d) { return yRight(d.quality); })
+ .curve(d3.curveMonotoneX);
+ g.append('path').datum(fbPoints).attr('d', fbLine)
+ .attr('fill', 'none').attr('stroke', 'var(--accent-violet)').attr('stroke-width', 2).attr('stroke-dasharray', '4,2');
+
+ // Right axis label
+ g.append('text').attr('x', iw + 8).attr('y', yRight(50)).attr('fill', 'var(--text-dim)')
+ .attr('font-size', '0.6rem').attr('dominant-baseline', 'middle').text('quality %');
+ }
+ }
+
+ // Consolidation diamond markers
+ if (chData && chData.length > 0) {
+ var chMap = {};
+ chData.forEach(function(d) { if (d.processed > 0) chMap[d.date] = d.processed; });
+ svData.forEach(function(d) {
+ if (chMap[d.date]) {
+ var cx = x(d.date) + x.bandwidth() / 2;
+ g.append('path')
+ .attr('d', 'M' + cx + ',' + (y(maxY) - 2) + ' l4,6 l-4,6 l-4,-6 z')
+ .attr('fill', 'var(--accent-orange)').attr('opacity', 0.7);
+ }
+ });
}
+
+ // X axis — show every Nth date label to avoid crowding
+ var labelEvery = Math.max(1, Math.ceil(svData.length / 7));
var months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
- return months[d.getMonth()] + ' ' + d.getDate() + ' ' +
- String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0');
+ g.append('g').attr('transform', 'translate(0,' + ih + ')').call(
+ d3.axisBottom(x).tickValues(svData.filter(function(d, i) { return i % labelEvery === 0; }).map(function(d) { return d.date; }))
+ .tickFormat(function(d) { var parts = d.split('-'); return months[parseInt(parts[1]) - 1] + ' ' + parseInt(parts[2]); })
+ ).selectAll('text').attr('fill', 'var(--text-dim)').attr('font-size', '0.6rem');
+ g.selectAll('.domain,.tick line').attr('stroke', 'var(--border-subtle)');
+
+ // Y axis
+ g.append('g').call(d3.axisLeft(y).ticks(4).tickFormat(d3.format('d')))
+ .selectAll('text').attr('fill', 'var(--text-dim)').attr('font-size', '0.6rem');
+ g.selectAll('.domain,.tick line').attr('stroke', 'var(--border-subtle)');
+
+ // Tooltip on hover
+ var tooltipRect = g.append('rect').attr('width', iw).attr('height', ih).attr('fill', 'transparent');
+ var tooltipLine = g.append('line').attr('stroke', 'var(--text-dim)').attr('stroke-dasharray', '2,2').attr('y1', 0).attr('y2', ih).style('display', 'none');
+ var tooltipDiv = document.getElementById('lifecycleTooltip');
+
+ tooltipRect.on('mousemove', function(event) {
+ var mx = d3.pointer(event, this)[0];
+ var dates = svData.map(function(d) { return d.date; });
+ var idx = Math.round(mx / iw * (dates.length - 1));
+ idx = Math.max(0, Math.min(idx, dates.length - 1));
+ var d = svData[idx];
+ tooltipLine.attr('x1', x(d.date) + x.bandwidth() / 2).attr('x2', x(d.date) + x.bandwidth() / 2).style('display', null);
+ var fb = fbData.find(function(f) { return f.date === d.date; });
+ var qStr = '';
+ if (fb) { var t = fb.helpful + fb.partial + fb.irrelevant; qStr = t > 0 ? ' · quality: ' + ((fb.helpful / t) * 100).toFixed(0) + '%' : ''; }
+ tooltipDiv.style.display = 'block';
+ tooltipDiv.style.left = (event.offsetX + 12) + 'px';
+ tooltipDiv.style.top = (event.offsetY - 10) + 'px';
+ tooltipDiv.innerHTML = '
' + d.date + 'Active: ' + (d.active || 0) + ' · Merged: ' + (d.merged || 0) + ' · Fading: ' + (d.fading || 0) + ' · Archived: ' + (d.archived || 0) + qStr;
+ }).on('mouseleave', function() {
+ tooltipLine.style('display', 'none');
+ tooltipDiv.style.display = 'none';
+ });
+
+ // Legend
+ legendEl.innerHTML = stackKeys.map(function(k) {
+ return '
' + k + '';
+ }).join('') + '
quality' +
+ '
consolidation';
+ }
+
+ function renderSignalChart(sn) {
+ var container = document.getElementById('signalNoiseChart');
+ container.innerHTML = '';
+ var sources = Object.keys(sn).sort(function(a, b) { return (sn[b].total || 0) - (sn[a].total || 0); });
+ if (sources.length === 0) { container.innerHTML = '
No source data
'; return; }
+
+ var avgSurv = 0;
+ var totalAll = 0;
+ sources.forEach(function(s) { avgSurv += (sn[s].survival_rate || 0) * (sn[s].total || 0); totalAll += (sn[s].total || 0); });
+ avgSurv = totalAll > 0 ? avgSurv / totalAll : 0;
+
+ var w = container.clientWidth || 400;
+ var h = sources.length * 36 + 10;
+ var margin = { top: 4, right: 12, bottom: 4, left: 80 };
+ var iw = w - margin.left - margin.right;
+
+ var svg = d3.select(container).append('svg').attr('width', w).attr('height', h);
+ var g = svg.append('g').attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
+
+ var y = d3.scaleBand().domain(sources).range([0, h - margin.top - margin.bottom]).padding(0.25);
+ var x = d3.scaleLinear().domain([0, 100]).range([0, iw]);
+
+ // Background tracks
+ g.selectAll('.track').data(sources).enter().append('rect')
+ .attr('x', 0).attr('y', function(d) { return y(d); })
+ .attr('width', iw).attr('height', y.bandwidth())
+ .attr('fill', 'var(--bg-tertiary)').attr('rx', 3);
+
+ // Bars
+ g.selectAll('.bar').data(sources).enter().append('rect')
+ .attr('x', 0).attr('y', function(d) { return y(d); })
+ .attr('width', function(d) { return Math.max(2, x((sn[d].survival_rate || 0) * 100)); })
+ .attr('height', y.bandwidth())
+ .attr('fill', function(d) { return _raSourceColors[d] || 'var(--text-muted)'; })
+ .attr('opacity', 0.7).attr('rx', 3);
+
+ // Average line
+ g.append('line').attr('x1', x(avgSurv * 100)).attr('x2', x(avgSurv * 100))
+ .attr('y1', 0).attr('y2', h - margin.top - margin.bottom)
+ .attr('stroke', 'var(--text-muted)').attr('stroke-dasharray', '3,3').attr('stroke-width', 1);
+
+ // Labels (left)
+ g.selectAll('.label').data(sources).enter().append('text')
+ .attr('x', -6).attr('y', function(d) { return y(d) + y.bandwidth() / 2; })
+ .attr('text-anchor', 'end').attr('dominant-baseline', 'middle')
+ .attr('fill', function(d) { return _raSourceColors[d] || 'var(--text-muted)'; })
+ .attr('font-size', '0.72rem').attr('font-weight', 600)
+ .text(function(d) { return d; });
+
+ // Value labels (on bar)
+ g.selectAll('.val').data(sources).enter().append('text')
+ .attr('x', function(d) { return Math.max(2, x((sn[d].survival_rate || 0) * 100)) + 4; })
+ .attr('y', function(d) { return y(d) + y.bandwidth() / 2; })
+ .attr('dominant-baseline', 'middle')
+ .attr('fill', 'var(--text-muted)').attr('font-size', '0.65rem')
+ .text(function(d) {
+ var s = sn[d];
+ return s.active + '/' + s.total + ' (' + ((s.survival_rate || 0) * 100).toFixed(0) + '%)' +
+ (s.avg_salience ? ' · sal ' + s.avg_salience.toFixed(2) : '');
+ });
+ }
+
+ function renderRecallChart(re) {
+ var container = document.getElementById('recallChart');
+ container.innerHTML = '';
+ if (!re || re.length === 0) { container.innerHTML = '
No recall data yet
'; return; }
+
+ var w = container.clientWidth || 400;
+ var h = 180;
+ var margin = { top: 12, right: 16, bottom: 36, left: 45 };
+ var iw = w - margin.left - margin.right;
+ var ih = h - margin.top - margin.bottom;
+
+ var svg = d3.select(container).append('svg').attr('width', w).attr('height', h);
+ var g = svg.append('g').attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
+
+ var buckets = re.map(function(b) { return b.bucket; });
+ var x = d3.scalePoint().domain(buckets).range([20, iw - 20]).padding(0.5);
+ var maxSal = d3.max(re, function(b) { return b.avg_salience; }) || 1;
+ var y = d3.scaleLinear().domain([0, Math.max(maxSal * 1.1, 0.5)]).range([ih, 0]);
+ var maxCount = d3.max(re, function(b) { return b.count; }) || 1;
+
+ // Reference line at first bucket's salience (to show slope)
+ if (re.length > 1) {
+ g.append('line').attr('x1', 0).attr('x2', iw).attr('y1', y(re[0].avg_salience)).attr('y2', y(re[0].avg_salience))
+ .attr('stroke', 'var(--border-subtle)').attr('stroke-dasharray', '3,3');
+ }
+
+ // Connecting line
+ var line = d3.line().x(function(d) { return x(d.bucket); }).y(function(d) { return y(d.avg_salience); }).curve(d3.curveMonotoneX);
+ var isLearning = re.length >= 2 && re[re.length - 1].avg_salience > re[0].avg_salience;
+ g.append('path').datum(re).attr('d', line)
+ .attr('fill', 'none').attr('stroke', isLearning ? 'var(--accent-green)' : 'var(--accent-red)')
+ .attr('stroke-width', 2);
+
+ // Dots (sized by count)
+ var rScale = d3.scaleSqrt().domain([0, maxCount]).range([4, 12]);
+ re.forEach(function(b) {
+ var color = b.bucket.indexOf('never') >= 0 ? 'var(--accent-red)' : b.bucket.indexOf('6+') >= 0 ? 'var(--accent-green)' : 'var(--accent-cyan)';
+ g.append('circle').attr('cx', x(b.bucket)).attr('cy', y(b.avg_salience))
+ .attr('r', rScale(b.count)).attr('fill', color).attr('opacity', 0.7);
+ g.append('text').attr('x', x(b.bucket)).attr('y', y(b.avg_salience) - rScale(b.count) - 5)
+ .attr('text-anchor', 'middle').attr('fill', 'var(--text-secondary)').attr('font-size', '0.68rem').attr('font-weight', 600)
+ .text(b.avg_salience.toFixed(2));
+ });
+
+ // X axis
+ g.append('g').attr('transform', 'translate(0,' + ih + ')').call(d3.axisBottom(x))
+ .selectAll('text').attr('fill', 'var(--text-dim)').attr('font-size', '0.65rem');
+ g.selectAll('.domain,.tick line').attr('stroke', 'var(--border-subtle)');
+
+ // Y axis
+ g.append('g').call(d3.axisLeft(y).ticks(4)).selectAll('text').attr('fill', 'var(--text-dim)').attr('font-size', '0.6rem');
+ g.selectAll('.domain,.tick line').attr('stroke', 'var(--border-subtle)');
+
+ // Verdict text
+ var ratio = (re.length >= 2 && re[0].avg_salience > 0) ? (re[re.length - 1].avg_salience / re[0].avg_salience) : 0;
+ var verdictColor = isLearning ? 'var(--accent-green)' : 'var(--accent-red)';
+ var verdictText = isLearning
+ ? 'Learning: ' + ratio.toFixed(1) + 'x salience increase from "' + re[0].bucket + '" to "' + re[re.length - 1].bucket + '"'
+ : 'No clear learning signal — salience flat or declining';
+ svg.append('text').attr('x', margin.left).attr('y', h - 2)
+ .attr('fill', verdictColor).attr('font-size', '0.65rem').text(verdictText);
+ }
+
+ // ── Session Activity (expandable rows with quality enrichment) ──
+
+ function _buildSessionEnrichment() {
+ var enrichment = {};
+ _toolLog.forEach(function(r) {
+ var sid = r.session_id || '';
+ if (!sid) return;
+ if (!enrichment[sid]) enrichment[sid] = { tools: {}, types: {}, fbH: 0, fbP: 0, fbI: 0 };
+ var e = enrichment[sid];
+ var tool = r.tool_name || r.tool || '';
+ if (tool) e.tools[tool] = (e.tools[tool] || 0) + 1;
+ if ((tool === 'remember') && r.memory_type) e.types[r.memory_type] = (e.types[r.memory_type] || 0) + 1;
+ if (tool === 'feedback') {
+ var rating = r.rating || r.quality || '';
+ if (rating === 'helpful') e.fbH++;
+ else if (rating === 'partial') e.fbP++;
+ else if (rating === 'irrelevant') e.fbI++;
+ }
+ });
+ return enrichment;
+ }
+
+ function toggleSession(sessionId) {
+ var detail = document.getElementById('detail-' + sessionId);
+ var chevron = document.getElementById('chevron-' + sessionId);
+ if (detail) detail.classList.toggle('open');
+ if (chevron) chevron.classList.toggle('open');
}
async function loadSessionTimeline() {
try {
- var resp = await fetch(CONFIG.API_BASE + '/sessions?days=7&limit=15');
+ var resp = await fetch(CONFIG.API_BASE + '/sessions?days=' + _analyticsRange + '&limit=15');
if (!resp.ok) return;
var data = await resp.json();
var sessions = data.sessions || [];
@@ -3609,54 +4217,77 @@
Activity
return;
}
- // Filter out sessions with no memories
var activeSessions = sessions.filter(function(s) { return s.memory_count > 0; });
if (activeSessions.length === 0) {
el.innerHTML = '
No sessions with memories';
return;
}
- // Find time range across all sessions
- var minT = Infinity, maxT = -Infinity;
- activeSessions.forEach(function(s) {
- var st = new Date(s.start_time).getTime();
- var et = new Date(s.end_time).getTime();
- if (st < minT) minT = st;
- if (et > maxT) maxT = et;
- });
- var rangeMs = maxT - minT || 1;
-
+ var enrichment = _buildSessionEnrichment();
+ var maxCount = Math.max.apply(null, activeSessions.map(function(s) { return s.memory_count; }));
var colors = ['var(--accent-cyan)', 'var(--accent-green)', 'var(--accent-violet)', 'var(--accent-blue)', 'var(--accent-pink)', 'var(--accent-yellow)'];
-
- var html = activeSessions.map(function(s, i) {
- var st = new Date(s.start_time).getTime();
- var et = new Date(s.end_time).getTime();
- var left = ((st - minT) / rangeMs) * 100;
- var width = Math.max(((et - st) / rangeMs) * 100, 2);
- var label = s.session_id.replace('mcp-', '').substring(0, 8);
- var dur = Math.round((et - st) / 60000);
+ var now = new Date();
+ var today = now.toDateString();
+ var yesterday = new Date(now.getTime() - 86400000).toDateString();
+ var months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
+
+ var rows = activeSessions.map(function(s, i) {
+ var st = new Date(s.start_time);
+ var et = new Date(s.end_time);
+ var durMin = Math.round((et - st) / 60000);
+ var durStr = durMin >= 60 ? Math.floor(durMin / 60) + 'h ' + (durMin % 60) + 'm' : durMin + 'm';
+ if (durMin === 0) durStr = '<1m';
+ var dateStr = st.toDateString() === today ? 'Today' : st.toDateString() === yesterday ? 'Yesterday' : months[st.getMonth()] + ' ' + st.getDate();
+ var timeStr = String(st.getHours()).padStart(2, '0') + ':' + String(st.getMinutes()).padStart(2, '0');
+
+ var barPct = Math.max((s.memory_count / maxCount) * 100, 8);
var color = colors[i % colors.length];
- var concepts = (s.top_concepts && s.top_concepts.length > 0) ? ' \u00b7 ' + s.top_concepts.slice(0, 3).join(', ') : '';
- return '
' +
- '
' + escapeHtml(label) + '' +
- '
' +
- '
' +
- s.memory_count + '
' +
- '
' + dur + 'm / ' + s.memory_count + concepts + '';
- }).join('');
- // Time axis labels
- var tickCount = Math.min(5, activeSessions.length);
- var axisHtml = '
';
- for (var ti = 0; ti < tickCount; ti++) {
- var pct = tickCount > 1 ? (ti / (tickCount - 1)) * 100 : 50;
- var tickMs = minT + (pct / 100) * rangeMs;
- var tickLabel = _fmtTimelineAxis(new Date(tickMs));
- axisHtml += '' + tickLabel + '';
- }
- axisHtml += '
';
+ // Enrichment from tool log
+ var e = enrichment[s.session_id] || { tools: {}, types: {}, fbH: 0, fbP: 0, fbI: 0 };
+ var totalFb = e.fbH + e.fbP + e.fbI;
+ var qualityDotColor = 'var(--text-dim)';
+ var fbBadge = '';
+ if (totalFb > 0) {
+ var helpfulPct = (e.fbH / totalFb) * 100;
+ qualityDotColor = helpfulPct >= 70 ? 'var(--accent-green)' : helpfulPct >= 50 ? 'var(--accent-yellow)' : 'var(--accent-red)';
+ fbBadge = '
' + e.fbH + '/' + totalFb + ' helpful';
+ }
- el.innerHTML = html + axisHtml;
+ // Bar color reflects quality
+ if (totalFb > 0) {
+ var hp = (e.fbH / totalFb) * 100;
+ color = hp >= 70 ? 'var(--accent-green)' : hp >= 50 ? 'var(--accent-yellow)' : 'var(--accent-cyan)';
+ }
+
+ var concepts = (s.top_concepts || []).slice(0, 2);
+ var pillsHtml = concepts.map(function(c) { return '
' + escapeHtml(c) + ''; }).join('');
+
+ var sid = s.session_id.replace(/[^a-zA-Z0-9]/g, '');
+
+ // Expanded detail
+ var toolStr = Object.keys(e.tools).map(function(t) { return t + '(' + e.tools[t] + ')'; }).join(' ');
+ var typeStr = Object.keys(e.types).map(function(t) { return t + '(' + e.types[t] + ')'; }).join(' ');
+ var allConcepts = (s.top_concepts || []).map(function(c) { return '
' + escapeHtml(c) + ''; }).join('');
+
+ var detailHtml = '
';
+ if (toolStr) detailHtml += '
Tools: ' + toolStr + '
';
+ if (typeStr) detailHtml += '
Types: ' + typeStr + '
';
+ if (allConcepts) detailHtml += '
Topics: ' + allConcepts + '
';
+ detailHtml += '
';
+
+ return '
' +
+ '' +
+ '' + dateStr + '' + timeStr + '' +
+ '' + s.memory_count + '' +
+ '' + durStr + '' +
+ fbBadge +
+ '' + pillsHtml + '' +
+ '▸' +
+ '
' + detailHtml;
+ }).join('');
+
+ el.innerHTML = rows;
} catch(e) { /* ignore */ }
}
@@ -3765,8 +4396,10 @@
Activity
case '1': switchView('recall'); break;
case '2': switchView('explore'); break;
case '3': switchView('timeline'); break;
- case '4': switchView('agent'); break;
- case '5': switchView('llm'); break;
+ case '4': switchView('mind'); break;
+ case '5': switchView('agent'); break;
+ case '6': switchView('llm'); break;
+ case '7': switchView('tools'); break;
}
});
@@ -4614,6 +5247,564 @@
Activity
}
}
+ // ── Mind View ──
+ var _mindFilterTimer = null;
+ var _mindSearchTimer = null;
+ var _mindResizeObserver = null;
+ var _mindColorCache = {};
+
+ function getGraphColor(varName) {
+ if (!_mindColorCache[varName]) {
+ _mindColorCache[varName] = getComputedStyle(document.documentElement).getPropertyValue(varName).trim() || '#888';
+ }
+ return _mindColorCache[varName];
+ }
+ function invalidateColorCache() { _mindColorCache = {}; }
+
+ function mindEdgeStyle(type) {
+ var styles = {
+ similar: { color: function() { return getGraphColor('--text-dim'); }, dash: '', opacity: 0.5 },
+ caused_by: { color: function() { return getGraphColor('--accent-orange'); }, dash: '', opacity: 0.8 },
+ part_of: { color: function() { return getGraphColor('--accent-blue'); }, dash: '', opacity: 0.7 },
+ reinforces: { color: function() { return getGraphColor('--accent-green'); }, dash: '', opacity: 0.7 },
+ temporal: { color: function() { return getGraphColor('--text-dim'); }, dash: '4,3', opacity: 0.3 },
+ contradicts: { color: function() { return getGraphColor('--accent-red'); }, dash: '', opacity: 0.8 },
+ };
+ return styles[type] || styles.similar;
+ }
+
+ function mindColorScale() {
+ var dim = document.getElementById('mindColorBy').value;
+ var maps = {
+ source: { mcp: '--accent-cyan', filesystem: '--accent-violet', terminal: '--accent-orange', clipboard: '--accent-green', consolidation: '--accent-blue', git: '--accent-pink' },
+ emotional_tone: { neutral: '--text-dim', frustrating: '--accent-red', satisfying: '--accent-green', surprising: '--accent-yellow' },
+ significance: { routine: '--text-dim', notable: '--accent-blue', important: '--accent-orange', critical: '--accent-red', success: '--accent-green', failure: '--accent-red', blocked: '--accent-yellow' },
+ state: { active: '--accent-green', fading: '--accent-yellow', archived: '--text-dim', merged: '--accent-blue' },
+ };
+ var m = maps[dim] || maps.source;
+ return function(node) {
+ var val = node[dim] || '';
+ var varName = m[val] || '--text-muted';
+ return getGraphColor(varName);
+ };
+ }
+
+ function nodeRadius(salience) { return 4 + (salience || 0) * 16; }
+
+ function computeMindStats(data) {
+ var nCount = data.nodes.length;
+ var eCount = data.edges.length;
+ if (nCount === 0) return { nodes: 0, edges: 0, clusters: 0, orphans: 0, avgDegree: 0 };
+
+ // Build adjacency for cluster detection
+ var adj = {};
+ data.nodes.forEach(function(n) { adj[n.id] = []; });
+ data.edges.forEach(function(e) {
+ if (adj[e.source] || adj[e.source.id]) {
+ var sid = typeof e.source === 'object' ? e.source.id : e.source;
+ var tid = typeof e.target === 'object' ? e.target.id : e.target;
+ if (adj[sid]) adj[sid].push(tid);
+ if (adj[tid]) adj[tid].push(sid);
+ }
+ });
+
+ // BFS connected components
+ var visited = {};
+ var clusters = 0;
+ var orphans = 0;
+ data.nodes.forEach(function(n) {
+ if (visited[n.id]) return;
+ clusters++;
+ var queue = [n.id];
+ visited[n.id] = true;
+ var size = 0;
+ while (queue.length > 0) {
+ var cur = queue.shift();
+ size++;
+ (adj[cur] || []).forEach(function(neighbor) {
+ if (!visited[neighbor]) { visited[neighbor] = true; queue.push(neighbor); }
+ });
+ }
+ if (size === 1 && (!adj[n.id] || adj[n.id].length === 0)) orphans++;
+ });
+
+ return {
+ nodes: nCount,
+ edges: eCount,
+ clusters: clusters,
+ orphans: orphans,
+ avgDegree: nCount > 0 ? ((eCount * 2) / nCount).toFixed(1) : '0.0',
+ };
+ }
+
+ function renderMindStats(stats) {
+ var el = document.getElementById('mindStats');
+ var items = [
+ { v: stats.nodes, l: 'nodes' },
+ { v: stats.edges, l: 'edges' },
+ { v: stats.clusters, l: stats.clusters === 1 ? 'cluster' : 'clusters' },
+ { v: stats.orphans, l: 'orphans' },
+ { v: stats.avgDegree, l: 'avg degree' },
+ ];
+ el.innerHTML = items.map(function(s) {
+ return '
' + s.v + '' + s.l + '
';
+ }).join('');
+ }
+
+ function buildMindLegend() {
+ var el = document.getElementById('mindLegend');
+ var types = [
+ { type: 'similar', label: 'similar' },
+ { type: 'caused_by', label: 'caused by' },
+ { type: 'part_of', label: 'part of' },
+ { type: 'reinforces', label: 'reinforces' },
+ { type: 'temporal', label: 'temporal' },
+ { type: 'contradicts', label: 'contradicts' },
+ ];
+ el.innerHTML = types.map(function(t) {
+ var s = mindEdgeStyle(t.type);
+ var dashAttr = s.dash ? ' stroke-dasharray="' + s.dash + '"' : '';
+ return '
'
+ + ''
+ + '' + t.label + '
';
+ }).join('');
+ }
+
+ function buildMindAdjacency(data) {
+ var adj = {};
+ data.nodes.forEach(function(n) { adj[n.id] = new Set(); });
+ data.edges.forEach(function(e) {
+ var sid = typeof e.source === 'object' ? e.source.id : e.source;
+ var tid = typeof e.target === 'object' ? e.target.id : e.target;
+ if (adj[sid]) adj[sid].add(tid);
+ if (adj[tid]) adj[tid].add(sid);
+ });
+ return adj;
+ }
+
+ function getNeighborhood(nodeId, hops) {
+ var adj = state.mindAdjacency;
+ if (!adj) return new Set([nodeId]);
+ var visited = new Set([nodeId]);
+ var frontier = [nodeId];
+ for (var h = 0; h < hops; h++) {
+ var next = [];
+ frontier.forEach(function(id) {
+ (adj[id] || new Set()).forEach(function(neighbor) {
+ if (!visited.has(neighbor)) { visited.add(neighbor); next.push(neighbor); }
+ });
+ });
+ frontier = next;
+ }
+ return visited;
+ }
+
+ function showMindDetail(node) {
+ var el = document.getElementById('mindDetail');
+ state.mindSelectedNode = node;
+
+ // Count connections by type
+ var connCounts = {};
+ if (state.mindData) {
+ state.mindData.edges.forEach(function(e) {
+ var sid = typeof e.source === 'object' ? e.source.id : e.source;
+ var tid = typeof e.target === 'object' ? e.target.id : e.target;
+ if (sid === node.id || tid === node.id) {
+ connCounts[e.relation_type] = (connCounts[e.relation_type] || 0) + 1;
+ }
+ });
+ }
+ var totalConns = Object.values(connCounts).reduce(function(a, b) { return a + b; }, 0);
+
+ var date = node.timestamp ? new Date(node.timestamp) : null;
+ var dateStr = date ? date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) + ' ' + date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }) : '-';
+
+ var rows = [
+ { l: 'Salience', v: (node.salience || 0).toFixed(2) },
+ { l: 'Source', v: node.source || '-' },
+ { l: 'State', v: node.state || '-' },
+ ];
+ if (node.significance) rows.push({ l: 'Significance', v: node.significance });
+ if (node.emotional_tone) rows.push({ l: 'Tone', v: node.emotional_tone });
+ rows.push({ l: 'Created', v: dateStr });
+ rows.push({ l: 'Connections', v: totalConns });
+ if (node.event_count) rows.push({ l: 'Events', v: node.event_count });
+
+ var html = '
';
+ html += '
' + escapeHtml(node.summary || '') + '
';
+ html += rows.map(function(r) {
+ return '
' + r.l + '' + escapeHtml(String(r.v)) + '
';
+ }).join('');
+
+ if (totalConns > 0) {
+ html += '
Connections
';
+ Object.keys(connCounts).sort().forEach(function(type) {
+ var s = mindEdgeStyle(type);
+ var dashStyle = s.dash ? ' stroke-dasharray="' + s.dash + '"' : '';
+ html += '
'
+ + ''
+ + '' + type.replace(/_/g, ' ') + ' (' + connCounts[type] + ')
';
+ });
+ html += '
';
+ }
+
+ if (node.concepts && node.concepts.length > 0) {
+ html += '
Concepts
';
+ node.concepts.forEach(function(c) {
+ html += '' + escapeHtml(c) + '';
+ });
+ html += '
';
+ }
+
+ if (node.files_modified && node.files_modified.length > 0) {
+ html += '
Files
';
+ node.files_modified.slice(0, 8).forEach(function(f) { html += '
' + escapeHtml(f) + '
'; });
+ if (node.files_modified.length > 8) html += '
+' + (node.files_modified.length - 8) + ' more
';
+ html += '
';
+ }
+
+ el.innerHTML = html;
+ el.style.display = 'block';
+ }
+
+ function closeMindDetail() {
+ document.getElementById('mindDetail').style.display = 'none';
+ state.mindSelectedNode = null;
+ clearMindHighlight();
+ }
+
+ function mindSearchConcept(concept) {
+ var input = document.getElementById('mindSearch');
+ input.value = concept;
+ document.getElementById('mindSearchClear').style.display = '';
+ state.mindSearchTerm = concept.toLowerCase();
+ applyMindSearch();
+ }
+
+ function onMindSearch() {
+ clearTimeout(_mindSearchTimer);
+ _mindSearchTimer = setTimeout(function() {
+ var val = document.getElementById('mindSearch').value.trim();
+ document.getElementById('mindSearchClear').style.display = val ? '' : 'none';
+ state.mindSearchTerm = val.toLowerCase();
+ applyMindSearch();
+ }, 200);
+ }
+
+ function clearMindSearch() {
+ document.getElementById('mindSearch').value = '';
+ document.getElementById('mindSearchClear').style.display = 'none';
+ state.mindSearchTerm = '';
+ clearMindHighlight();
+ }
+
+ function applyMindSearch() {
+ var canvas = document.getElementById('mindCanvas');
+ var term = state.mindSearchTerm;
+ if (!term || !state.mindData) { clearMindHighlight(); return; }
+
+ var matchIds = new Set();
+ state.mindData.nodes.forEach(function(n) {
+ var haystack = ((n.summary || '') + ' ' + (n.concepts || []).join(' ')).toLowerCase();
+ if (haystack.indexOf(term) !== -1) matchIds.add(n.id);
+ });
+
+ d3.select(canvas).selectAll('circle.mind-node')
+ .attr('opacity', function(d) { return matchIds.has(d.id) ? 1 : 0.08; })
+ .style('animation', function(d) { return matchIds.has(d.id) ? 'mindPulse 1.5s ease-in-out infinite' : 'none'; });
+ d3.select(canvas).selectAll('line.mind-edge')
+ .attr('opacity', function(d) {
+ var sid = typeof d.source === 'object' ? d.source.id : d.source;
+ var tid = typeof d.target === 'object' ? d.target.id : d.target;
+ return (matchIds.has(sid) || matchIds.has(tid)) ? mindEdgeStyle(d.relation_type).opacity : 0.03;
+ });
+ d3.select(canvas).selectAll('text.mind-label')
+ .attr('opacity', function(d) { return matchIds.has(d.id) ? 1 : 0.08; });
+ }
+
+ function applyMindNeighborhood(nodeId) {
+ var canvas = document.getElementById('mindCanvas');
+ var neighborhood = getNeighborhood(nodeId, 2);
+
+ d3.select(canvas).selectAll('circle.mind-node')
+ .attr('opacity', function(d) { return neighborhood.has(d.id) ? 1 : 0.08; })
+ .attr('stroke', function(d) { return d.id === nodeId ? getGraphColor('--accent-cyan') : getGraphColor('--border-color'); })
+ .attr('stroke-width', function(d) { return d.id === nodeId ? 2.5 : 1; })
+ .style('animation', 'none');
+ d3.select(canvas).selectAll('line.mind-edge')
+ .attr('opacity', function(d) {
+ var sid = typeof d.source === 'object' ? d.source.id : d.source;
+ var tid = typeof d.target === 'object' ? d.target.id : d.target;
+ return (neighborhood.has(sid) && neighborhood.has(tid)) ? mindEdgeStyle(d.relation_type).opacity : 0.03;
+ });
+ d3.select(canvas).selectAll('text.mind-label')
+ .attr('opacity', function(d) { return neighborhood.has(d.id) ? 1 : 0.08; });
+ }
+
+ function clearMindHighlight() {
+ var canvas = document.getElementById('mindCanvas');
+ d3.select(canvas).selectAll('circle.mind-node')
+ .attr('opacity', 1)
+ .attr('stroke', getGraphColor('--border-color'))
+ .attr('stroke-width', 1)
+ .style('animation', 'none');
+ d3.select(canvas).selectAll('line.mind-edge')
+ .attr('opacity', function(d) { return mindEdgeStyle(d.relation_type).opacity; });
+ d3.select(canvas).selectAll('text.mind-label').attr('opacity', 1);
+ }
+
+ function updateMindColors() {
+ invalidateColorCache();
+ var canvas = document.getElementById('mindCanvas');
+ var color = mindColorScale();
+ d3.select(canvas).selectAll('circle.mind-node')
+ .attr('fill', function(d) { return color(d); });
+ // Rebuild legend with new colors
+ buildMindLegend();
+ }
+
+ function setMindView(view) {
+ state.mindView = view;
+ document.querySelectorAll('.mind-toggle').forEach(function(b) {
+ b.classList.toggle('active', b.getAttribute('data-mind-view') === view);
+ });
+ state.mindLoaded = false;
+ loadMindGraph();
+ }
+
+ function onMindFilterChange() {
+ document.getElementById('mindSalienceVal').textContent = parseFloat(document.getElementById('mindSalience').value).toFixed(2);
+ document.getElementById('mindStrengthVal').textContent = parseFloat(document.getElementById('mindStrength').value).toFixed(2);
+ clearTimeout(_mindFilterTimer);
+ _mindFilterTimer = setTimeout(function() {
+ state.mindLoaded = false;
+ loadMindGraph();
+ }, 300);
+ }
+
+ function buildMindGraph(data) {
+ var canvas = document.getElementById('mindCanvas');
+ // Remove existing SVG but keep overlays
+ var existingSvg = canvas.querySelector('svg');
+ if (existingSvg) existingSvg.remove();
+
+ var rect = canvas.getBoundingClientRect();
+ var w = rect.width || 800;
+ var h = rect.height || 500;
+
+ var svg = d3.select(canvas).append('svg')
+ .attr('width', w).attr('height', h)
+ .style('position', 'absolute').style('top', '0').style('left', '0');
+
+ var g = svg.append('g');
+
+ // Zoom
+ var zoom = d3.zoom()
+ .scaleExtent([0.15, 5])
+ .on('zoom', function(event) { g.attr('transform', event.transform); });
+ svg.call(zoom);
+ // Click on canvas background to deselect
+ svg.on('click', function(event) {
+ if (event.target === svg.node()) {
+ closeMindDetail();
+ clearMindSearch();
+ }
+ });
+
+ var color = mindColorScale();
+ var nodes = data.nodes;
+ var edges = data.edges;
+
+ // Build adjacency
+ state.mindAdjacency = buildMindAdjacency(data);
+
+ // Edges
+ var edgeG = g.append('g').attr('class', 'mind-edges');
+ var link = edgeG.selectAll('line')
+ .data(edges).enter().append('line')
+ .attr('class', 'mind-edge')
+ .attr('stroke', function(d) { return mindEdgeStyle(d.relation_type).color(); })
+ .attr('stroke-width', function(d) { return 1 + (d.strength || 0) * 3; })
+ .attr('stroke-dasharray', function(d) { return mindEdgeStyle(d.relation_type).dash || null; })
+ .attr('opacity', function(d) { return mindEdgeStyle(d.relation_type).opacity; });
+
+ // Nodes
+ var nodeG = g.append('g').attr('class', 'mind-nodes');
+ var node = nodeG.selectAll('circle')
+ .data(nodes).enter().append('circle')
+ .attr('class', 'mind-node')
+ .attr('r', function(d) { return nodeRadius(d.salience); })
+ .attr('fill', function(d) { return color(d); })
+ .attr('stroke', getGraphColor('--border-color'))
+ .attr('stroke-width', 1)
+ .attr('cursor', 'pointer')
+ .style('transition', 'opacity 0.2s');
+
+ // Labels for high-salience nodes
+ var labelG = g.append('g').attr('class', 'mind-labels');
+ var labels = labelG.selectAll('text')
+ .data(nodes.filter(function(d) { return d.salience > 0.6; }))
+ .enter().append('text')
+ .attr('class', 'mind-label')
+ .text(function(d) { var s = d.summary || ''; return s.length > 22 ? s.slice(0, 20) + '...' : s; })
+ .attr('font-size', '9px')
+ .attr('fill', getGraphColor('--text-muted'))
+ .attr('text-anchor', 'middle')
+ .attr('pointer-events', 'none')
+ .attr('dy', function(d) { return nodeRadius(d.salience) + 12; });
+
+ // Tooltip
+ var tooltip = document.getElementById('mindTooltip');
+
+ node.on('mouseenter', function(event, d) {
+ var concepts = (d.concepts || []).slice(0, 5).join(', ');
+ tooltip.innerHTML = '
' + escapeHtml((d.summary || '').slice(0, 80)) + '
'
+ + '
'
+ + '' + (d.source || '') + ''
+ + 'salience: ' + (d.salience || 0).toFixed(2) + ''
+ + (concepts ? '' + escapeHtml(concepts) + '' : '')
+ + '
';
+ tooltip.style.display = 'block';
+ var cr = canvas.getBoundingClientRect();
+ tooltip.style.left = (event.clientX - cr.left + 14) + 'px';
+ tooltip.style.top = (event.clientY - cr.top + 14) + 'px';
+ })
+ .on('mousemove', function(event) {
+ var cr = canvas.getBoundingClientRect();
+ var x = event.clientX - cr.left + 14;
+ var y = event.clientY - cr.top + 14;
+ // Keep tooltip in view
+ if (x + 300 > cr.width) x = event.clientX - cr.left - 310;
+ if (y + 100 > cr.height) y = event.clientY - cr.top - 80;
+ tooltip.style.left = x + 'px';
+ tooltip.style.top = y + 'px';
+ })
+ .on('mouseleave', function() { tooltip.style.display = 'none'; });
+
+ // Click: neighborhood focus + detail
+ node.on('click', function(event, d) {
+ event.stopPropagation();
+ applyMindNeighborhood(d.id);
+ showMindDetail(d);
+ });
+
+ // Drag
+ var drag = d3.drag()
+ .on('start', function(event, d) {
+ if (event.sourceEvent) event.sourceEvent.stopPropagation();
+ if (!event.active) simulation.alphaTarget(0.3).restart();
+ d.fx = d.x; d.fy = d.y;
+ })
+ .on('drag', function(event, d) { d.fx = event.x; d.fy = event.y; })
+ .on('end', function(event, d) {
+ if (!event.active) simulation.alphaTarget(0);
+ d.fx = null; d.fy = null;
+ });
+ node.call(drag);
+
+ // Force simulation
+ var simulation = d3.forceSimulation(nodes)
+ .force('link', d3.forceLink(edges).id(function(d) { return d.id; })
+ .distance(function(d) { return 60 + (1 - (d.strength || 0)) * 140; })
+ .strength(function(d) { return 0.2 + (d.strength || 0) * 0.6; }))
+ .force('charge', d3.forceManyBody().strength(-120).distanceMax(350))
+ .force('center', d3.forceCenter(w / 2, h / 2).strength(0.04))
+ .force('collide', d3.forceCollide().radius(function(d) { return nodeRadius(d.salience) + 3; }).strength(0.7))
+ .force('x', d3.forceX(w / 2).strength(0.02))
+ .force('y', d3.forceY(h / 2).strength(0.02))
+ .alphaDecay(0.015)
+ .velocityDecay(0.4)
+ .on('tick', function() {
+ link.attr('x1', function(d) { return d.source.x; }).attr('y1', function(d) { return d.source.y; })
+ .attr('x2', function(d) { return d.target.x; }).attr('y2', function(d) { return d.target.y; });
+ node.attr('cx', function(d) { return d.x; }).attr('cy', function(d) { return d.y; });
+ labels.attr('x', function(d) { return d.x; }).attr('y', function(d) { return d.y; });
+ });
+
+ state.mindSimulation = simulation;
+
+ // Resize observer
+ if (_mindResizeObserver) _mindResizeObserver.disconnect();
+ _mindResizeObserver = new ResizeObserver(function() {
+ var r = canvas.getBoundingClientRect();
+ if (r.width < 10 || r.height < 10) return;
+ svg.attr('width', r.width).attr('height', r.height);
+ simulation.force('center', d3.forceCenter(r.width / 2, r.height / 2).strength(0.04));
+ simulation.force('x', d3.forceX(r.width / 2).strength(0.02));
+ simulation.force('y', d3.forceY(r.height / 2).strength(0.02));
+ simulation.alpha(0.1).restart();
+ });
+ _mindResizeObserver.observe(canvas);
+
+ // Escape key to clear selection
+ function onMindEscape(e) {
+ if (e.key === 'Escape' && state.currentView === 'mind') {
+ closeMindDetail();
+ clearMindSearch();
+ }
+ }
+ // Remove previous listener before adding (avoid stacking)
+ document.removeEventListener('keydown', onMindEscape);
+ document.addEventListener('keydown', onMindEscape);
+ }
+
+ async function loadMindGraph() {
+ var canvas = document.getElementById('mindCanvas');
+
+ // Show loading state
+ var existingSvg = canvas.querySelector('svg');
+ if (existingSvg) existingSvg.remove();
+
+ // Stop existing simulation
+ if (state.mindSimulation) { state.mindSimulation.stop(); state.mindSimulation = null; }
+
+ var view = state.mindView;
+ var limit = parseInt(document.getElementById('mindLimit').value) || 150;
+ var minSalience = parseFloat(document.getElementById('mindSalience').value) || 0;
+ var minStrength = parseFloat(document.getElementById('mindStrength').value) || 0;
+
+ var url = '/graph?view=' + view + '&limit=' + limit;
+ if (minSalience > 0) url += '&min_salience=' + minSalience;
+ if (minStrength > 0) url += '&min_strength=' + minStrength;
+
+ try {
+ var data = await fetchJSON(url);
+ state.mindData = data;
+
+ // Remove any previous empty state
+ var prevEmpty = canvas.querySelector('.mind-empty');
+ if (prevEmpty) prevEmpty.remove();
+
+ if (!data.nodes || data.nodes.length === 0) {
+ var emptyDiv = document.createElement('div');
+ emptyDiv.className = 'mind-empty';
+ emptyDiv.innerHTML = '
◉
'
+ + '
No memories to visualize
'
+ + '
Associations form as your memory network grows
';
+ canvas.appendChild(emptyDiv);
+ renderMindStats({ nodes: 0, edges: 0, clusters: 0, orphans: 0, avgDegree: '0.0' });
+ state.mindLoaded = true;
+ return;
+ }
+
+ var stats = computeMindStats(data);
+ renderMindStats(stats);
+ buildMindGraph(data);
+ buildMindLegend();
+ state.mindLoaded = true;
+ } catch (err) {
+ var prevEmpty = canvas.querySelector('.mind-empty');
+ if (prevEmpty) prevEmpty.remove();
+ var errDiv = document.createElement('div');
+ errDiv.className = 'mind-empty';
+ errDiv.innerHTML = '
⚠
'
+ + '
Failed to load graph
'
+ + '
' + escapeHtml(err.message) + '
';
+ canvas.appendChild(errDiv);
+ renderMindStats({ nodes: 0, edges: 0, clusters: 0, orphans: 0, avgDegree: '0.0' });
+ }
+ }
+
// ── Init ──
async function initializeApp() {
loadStats();