// Memory Module View
// Three-column layout: Context Hotspots | Memory Graph | Recent Context
// ========== Memory State ==========
var memoryStats = null;
var memoryGraphData = null;
var recentContext = [];
var memoryTimeFilter = 'all'; // 'today', 'week', 'all'
var selectedNode = null;
var activeMemoryEnabled = false;
var activeMemoryStatus = null;
var activeMemoryConfig = {
interval: 'manual', // manual, 5, 15, 30, 60 (minutes)
tool: 'gemini' // gemini, qwen
};
var activeMemorySyncTimer = null; // Timer for automatic periodic sync
var insightsHistory = []; // Insights analysis history
var selectedInsight = null; // Currently selected insight for detail view
// ========== Main Render Function ==========
async function renderMemoryView() {
var container = document.getElementById('mainContent');
if (!container) return;
// Hide stats grid and search for memory view
var statsGrid = document.getElementById('statsGrid');
var searchInput = document.getElementById('searchInput');
if (statsGrid) statsGrid.style.display = 'none';
if (searchInput) searchInput.parentElement.style.display = 'none';
// Show loading state
container.innerHTML = '
' +
'
' +
'
' + t('common.loading') + '
' +
'
';
// Load data
await Promise.all([
loadMemoryStats(),
loadMemoryGraph(),
loadRecentContext(),
loadActiveMemoryStatus(),
loadInsightsHistory()
]);
// Render layout with Active Memory header
container.innerHTML = '
' +
'
' +
'
' +
'
' + t('memory.title') + '
' +
'
' +
'
' +
renderActiveMemoryControls() +
'
' +
'
' +
'
' +
'' +
'' +
'' +
'
' +
'
';
// Render each column
renderHotspotsColumn();
renderGraphColumn();
renderContextColumn();
// Initialize Lucide icons
if (window.lucide) lucide.createIcons();
}
function renderActiveMemoryControls() {
var html = '
';
}
// ========== Center Column: Memory Graph ==========
// Store graph state for zoom/pan
var graphZoom = null;
var graphSvg = null;
var graphGroup = null;
var graphSimulation = null;
function renderGraphColumn() {
var container = document.getElementById('memory-graph');
if (!container) return;
container.innerHTML = '
';
// Render D3 graph
renderMemoryGraph(memoryGraphData);
if (window.lucide) lucide.createIcons();
}
function renderMemoryGraph(graphData) {
if (!graphData || !graphData.nodes || graphData.nodes.length === 0) {
var container = document.getElementById('memoryGraphSvg');
if (container) {
container.innerHTML = '
' +
'' +
'
' + t('memory.noGraphData') + '
' +
'
';
if (window.lucide) lucide.createIcons();
}
return;
}
// Check if D3 is available
if (typeof d3 === 'undefined') {
var container = document.getElementById('memoryGraphSvg');
if (container) {
container.innerHTML = '
' +
'' +
'
' + t('memory.d3NotLoaded') + '
' +
'
';
if (window.lucide) lucide.createIcons();
}
return;
}
var container = document.getElementById('memoryGraphSvg');
if (!container) return;
var width = container.clientWidth || 600;
var height = container.clientHeight || 400;
// Clear existing
container.innerHTML = '';
// Filter and clean nodes - remove invalid names (like JSON data)
var cleanNodes = graphData.nodes.filter(function(node) {
var name = node.name || node.id || '';
// Filter out JSON-like data, error messages, and very long strings
if (name.length > 100) return false;
if (name.includes('"status"') || name.includes('"content"')) return false;
if (name.includes('"todos"') || name.includes('"activeForm"')) return false;
if (name.startsWith('{') || name.startsWith('[')) return false;
// Allow all valid node types: file, module, component
return true;
}).map(function(node) {
// Truncate long names for display
var displayName = node.name || node.id || 'Unknown';
if (displayName.length > 25) {
displayName = displayName.substring(0, 22) + '...';
}
return Object.assign({}, node, { displayName: displayName });
});
// Filter edges to only include valid nodes
var nodeIds = new Set(cleanNodes.map(function(n) { return n.id; }));
var cleanEdges = graphData.edges.filter(function(edge) {
var sourceId = typeof edge.source === 'object' ? edge.source.id : edge.source;
var targetId = typeof edge.target === 'object' ? edge.target.id : edge.target;
return nodeIds.has(sourceId) && nodeIds.has(targetId);
});
// Create SVG with zoom support
graphSvg = d3.select('#memoryGraphSvg')
.append('svg')
.attr('width', width)
.attr('height', height)
.attr('class', 'memory-graph-svg')
.attr('viewBox', [0, 0, width, height]);
// Create a group for zoom/pan transformations
graphGroup = graphSvg.append('g').attr('class', 'graph-content');
// Setup zoom behavior
graphZoom = d3.zoom()
.scaleExtent([0.1, 4])
.on('zoom', function(event) {
graphGroup.attr('transform', event.transform);
});
graphSvg.call(graphZoom);
// Create force simulation
graphSimulation = d3.forceSimulation(cleanNodes)
.force('link', d3.forceLink(cleanEdges).id(function(d) { return d.id; }).distance(80))
.force('charge', d3.forceManyBody().strength(-200))
.force('center', d3.forceCenter(width / 2, height / 2))
.force('collision', d3.forceCollide().radius(function(d) { return Math.max(15, (d.heat || 10) + 10); }))
.force('x', d3.forceX(width / 2).strength(0.05))
.force('y', d3.forceY(height / 2).strength(0.05));
// Draw edges
var link = graphGroup.append('g')
.attr('class', 'graph-links')
.selectAll('line')
.data(cleanEdges)
.enter()
.append('line')
.attr('class', 'graph-edge')
.attr('stroke-width', function(d) { return Math.sqrt(d.weight || 1); });
// Draw nodes
var node = graphGroup.append('g')
.attr('class', 'graph-nodes')
.selectAll('g')
.data(cleanNodes)
.enter()
.append('g')
.attr('class', function(d) { return 'graph-node-group ' + (d.type || 'file'); })
.call(d3.drag()
.on('start', dragstarted)
.on('drag', dragged)
.on('end', dragended))
.on('click', function(event, d) {
event.stopPropagation();
selectNode(d);
});
// Add circles to nodes
node.append('circle')
.attr('class', function(d) { return 'graph-node ' + (d.type || 'file'); })
.attr('r', function(d) { return Math.max(8, Math.min(20, (d.heat || 10))); })
.attr('data-id', function(d) { return d.id; });
// Add labels to nodes
node.append('text')
.attr('class', 'graph-label')
.text(function(d) {
// Show file count for modules
if (d.type === 'module' && d.fileCount) {
return d.displayName + ' (' + d.fileCount + ')';
}
return d.displayName;
})
.attr('x', function(d) { return Math.max(10, (d.heat || 10)) + 4; })
.attr('y', 4)
.attr('font-size', '11px');
// Update positions on simulation tick
graphSimulation.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('transform', function(d) {
return 'translate(' + d.x + ',' + d.y + ')';
});
});
// Auto-fit after simulation stabilizes
graphSimulation.on('end', function() {
fitGraphToView();
});
// Also fit after initial layout
setTimeout(function() {
fitGraphToView();
}, 1000);
// Drag functions
function dragstarted(event, d) {
if (!event.active) graphSimulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
}
function dragended(event, d) {
if (!event.active) graphSimulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
}
// ========== Graph Zoom Controls ==========
function zoomGraphIn() {
if (graphSvg && graphZoom) {
graphSvg.transition().duration(300).call(graphZoom.scaleBy, 1.3);
}
}
function zoomGraphOut() {
if (graphSvg && graphZoom) {
graphSvg.transition().duration(300).call(graphZoom.scaleBy, 0.7);
}
}
function fitGraphToView() {
if (!graphSvg || !graphGroup || !graphZoom) return;
var container = document.getElementById('memoryGraphSvg');
if (!container) return;
var width = container.clientWidth || 600;
var height = container.clientHeight || 400;
// Get the bounds of all nodes
var bounds = graphGroup.node().getBBox();
if (bounds.width === 0 || bounds.height === 0) return;
// Calculate scale to fit with padding
var padding = 40;
var scale = Math.min(
(width - padding * 2) / bounds.width,
(height - padding * 2) / bounds.height
);
scale = Math.min(Math.max(scale, 0.2), 2); // Clamp scale between 0.2 and 2
// Calculate translation to center
var tx = (width - bounds.width * scale) / 2 - bounds.x * scale;
var ty = (height - bounds.height * scale) / 2 - bounds.y * scale;
// Apply transform with animation
graphSvg.transition()
.duration(500)
.call(graphZoom.transform, d3.zoomIdentity.translate(tx, ty).scale(scale));
}
function centerGraphOnNode(nodeId) {
if (!graphSvg || !graphGroup || !graphZoom) return;
var container = document.getElementById('memoryGraphSvg');
if (!container) return;
var width = container.clientWidth || 600;
var height = container.clientHeight || 400;
// Find the node
var nodeData = null;
graphGroup.selectAll('.graph-node-group').each(function(d) {
if (d.id === nodeId) nodeData = d;
});
if (!nodeData || nodeData.x === undefined) return;
// Calculate translation to center on node
var scale = 1.2;
var tx = width / 2 - nodeData.x * scale;
var ty = height / 2 - nodeData.y * scale;
graphSvg.transition()
.duration(500)
.call(graphZoom.transform, d3.zoomIdentity.translate(tx, ty).scale(scale));
}
function selectNode(node) {
selectedNode = node;
// Highlight in graph
if (graphGroup) {
graphGroup.selectAll('.graph-node').classed('selected', false);
graphGroup.selectAll('.graph-node[data-id="' + node.id + '"]').classed('selected', true);
}
// Center graph on selected node
centerGraphOnNode(node.id);
// Show node details in context column
showNodeDetails(node);
}
function highlightNode(path) {
var node = memoryGraphData.nodes.find(function(n) { return n.path === path || n.id === path; });
if (node) {
selectNode(node);
}
}
function resetGraphView() {
selectedNode = null;
if (graphGroup) {
graphGroup.selectAll('.graph-node').classed('selected', false);
}
fitGraphToView();
renderContextColumn();
}
// ========== Right Column: Recent Context ==========
function renderContextColumn() {
var container = document.getElementById('memory-context');
if (!container) return;
if (selectedNode) {
showNodeDetails(selectedNode);
return;
}
container.innerHTML = '
';
if (window.lucide) lucide.createIcons();
}
function renderInsightsCards() {
if (!insightsHistory || insightsHistory.length === 0) {
return '
' +
'' +
'
' + t('memory.noInsightsYet') + '
' +
'
' + t('memory.triggerAnalysis') + '
' +
'
';
}
return '
' +
insightsHistory.map(function(insight) {
var patternCount = (insight.patterns || []).length;
var suggestionCount = (insight.suggestions || []).length;
var severity = getInsightSeverity(insight.patterns);
var date = new Date(insight.created_at);
var timeAgo = formatTimestamp(insight.created_at);
return '