// 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 = '
' + '
' + '' + t('memory.activeMemory') + '' + '' + (activeMemoryEnabled ? ' ' + t('memory.active') + '' : '' + t('memory.inactive') + '') + '
'; if (activeMemoryEnabled) { var isAutoSync = activeMemoryConfig.interval !== 'manual'; html += '
' + // Interval selector '
' + '' + '' + '
' + // CLI tool selector '
' + '' + '' + '
' + // Auto-sync indicator (isAutoSync ? '
' + t('memory.autoSyncActive') + '
' : '') + '
' + // Sync button and status '
' + '' + (activeMemoryStatus && activeMemoryStatus.lastSync ? '' + t('memory.lastSync') + ': ' + formatTimestamp(activeMemoryStatus.lastSync) + '' : '') + '
'; } html += '
'; return html; } // ========== Data Loading ========== async function loadMemoryStats() { try { var response = await fetch('/api/memory/stats?filter=' + memoryTimeFilter); if (!response.ok) throw new Error('Failed to load memory stats'); var data = await response.json(); memoryStats = data.stats || { mostRead: [], mostEdited: [] }; return memoryStats; } catch (err) { console.error('Failed to load memory stats:', err); memoryStats = { mostRead: [], mostEdited: [] }; return memoryStats; } } async function loadMemoryGraph() { try { var response = await fetch('/api/memory/graph'); if (!response.ok) throw new Error('Failed to load memory graph'); var data = await response.json(); memoryGraphData = data.graph || { nodes: [], edges: [] }; return memoryGraphData; } catch (err) { console.error('Failed to load memory graph:', err); memoryGraphData = { nodes: [], edges: [] }; return memoryGraphData; } } async function loadRecentContext() { try { var response = await fetch('/api/memory/recent'); if (!response.ok) throw new Error('Failed to load recent context'); var data = await response.json(); recentContext = data.recent || []; return recentContext; } catch (err) { console.error('Failed to load recent context:', err); recentContext = []; return []; } } async function loadInsightsHistory() { try { var response = await fetch('/api/memory/insights?limit=10'); if (!response.ok) throw new Error('Failed to load insights history'); var data = await response.json(); insightsHistory = data.insights || []; return insightsHistory; } catch (err) { console.error('Failed to load insights history:', err); insightsHistory = []; return []; } } // ========== Active Memory Functions ========== // Timer management for automatic sync function startActiveMemorySyncTimer() { // Clear any existing timer stopActiveMemorySyncTimer(); // Only start timer if interval is not manual if (activeMemoryConfig.interval === 'manual' || !activeMemoryEnabled) { return; } var intervalMs = parseInt(activeMemoryConfig.interval, 10) * 60 * 1000; // Convert minutes to ms console.log('[ActiveMemory] Starting auto-sync timer:', activeMemoryConfig.interval, 'minutes'); activeMemorySyncTimer = setInterval(function() { console.log('[ActiveMemory] Auto-sync triggered'); syncActiveMemory(); }, intervalMs); } function stopActiveMemorySyncTimer() { if (activeMemorySyncTimer) { console.log('[ActiveMemory] Stopping auto-sync timer'); clearInterval(activeMemorySyncTimer); activeMemorySyncTimer = null; } } async function loadActiveMemoryStatus() { try { var response = await fetch('/api/memory/active/status'); if (!response.ok) throw new Error('Failed to load active memory status'); var data = await response.json(); activeMemoryEnabled = data.enabled || false; activeMemoryStatus = data.status || null; // Load config if available if (data.config) { activeMemoryConfig = Object.assign(activeMemoryConfig, data.config); } // Start timer if active memory is enabled and interval is not manual if (activeMemoryEnabled && activeMemoryConfig.interval !== 'manual') { startActiveMemorySyncTimer(); } return data; } catch (err) { console.error('Failed to load active memory status:', err); activeMemoryEnabled = false; activeMemoryStatus = null; return { enabled: false }; } } async function toggleActiveMemory(enabled) { try { var response = await fetch('/api/memory/active/toggle', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled: enabled, config: activeMemoryConfig }) }); if (!response.ok) throw new Error('Failed to toggle active memory'); var data = await response.json(); activeMemoryEnabled = data.enabled; // Manage auto-sync timer based on enabled state if (activeMemoryEnabled) { startActiveMemorySyncTimer(); } else { stopActiveMemorySyncTimer(); } // Show notification if (window.showToast) { showToast(enabled ? t('memory.activeMemoryEnabled') : t('memory.activeMemoryDisabled'), 'success'); } // Re-render the view to update UI renderMemoryView(); } catch (err) { console.error('Failed to toggle active memory:', err); if (window.showToast) { showToast(t('memory.activeMemoryError'), 'error'); } // Revert checkbox state var checkbox = document.getElementById('activeMemorySwitch'); if (checkbox) checkbox.checked = !enabled; } } async function updateActiveMemoryConfig(key, value) { activeMemoryConfig[key] = value; try { var response = await fetch('/api/memory/active/config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ config: activeMemoryConfig }) }); if (!response.ok) throw new Error('Failed to update config'); // Restart timer if interval changed and active memory is enabled if (key === 'interval' && activeMemoryEnabled) { startActiveMemorySyncTimer(); } if (window.showToast) { showToast(t('memory.configUpdated'), 'success'); } } catch (err) { console.error('Failed to update active memory config:', err); if (window.showToast) { showToast(t('memory.configError'), 'error'); } } } async function syncActiveMemory() { var syncBtn = document.querySelector('.btn-sync'); if (syncBtn) { syncBtn.classList.add('syncing'); syncBtn.disabled = true; } try { var response = await fetch('/api/memory/active/sync', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tool: activeMemoryConfig.tool }) }); if (!response.ok) throw new Error('Failed to sync active memory'); var data = await response.json(); if (window.showToast) { showToast(t('memory.syncComplete') + ' (' + (data.filesAnalyzed || 0) + ' ' + t('memory.filesAnalyzed') + ')', 'success'); } // Refresh data and update last sync time await loadActiveMemoryStatus(); // Update last sync display without full re-render var lastSyncEl = document.querySelector('.last-sync'); if (lastSyncEl && activeMemoryStatus && activeMemoryStatus.lastSync) { lastSyncEl.textContent = t('memory.lastSync') + ': ' + formatTimestamp(activeMemoryStatus.lastSync); } } catch (err) { console.error('Failed to sync active memory:', err); if (window.showToast) { showToast(t('memory.syncError'), 'error'); } } finally { if (syncBtn) { syncBtn.classList.remove('syncing'); syncBtn.disabled = false; } } } // ========== Left Column: Context Hotspots ========== function renderHotspotsColumn() { var container = document.getElementById('memory-hotspots'); if (!container) return; var mostRead = memoryStats.mostRead || []; var mostEdited = memoryStats.mostEdited || []; var mostMentioned = memoryStats.mostMentioned || []; container.innerHTML = '
' + '
' + '
' + '

' + t('memory.contextHotspots') + '

' + '
' + '
' + '' + '
' + '
' + '
' + '' + '' + '' + '
' + '
' + '
' + '

' + t('memory.mostRead') + '

' + renderHotspotList(mostRead, 'read') + '
' + '
' + '

' + t('memory.mostEdited') + '

' + renderHotspotList(mostEdited, 'edit') + '
' + '
' + '

' + t('memory.mostMentioned') + '

' + renderTopicList(mostMentioned) + '
' + '
' + '
'; if (window.lucide) lucide.createIcons(); } function renderHotspotList(items, type) { if (!items || items.length === 0) { return '
' + '' + '

' + t('memory.noData') + '

' + '
'; } return '
' + items.map(function(item, index) { var heat = item.heat || item.count || 0; var heatClass = heat > 50 ? 'high' : heat > 20 ? 'medium' : 'low'; var path = item.path || item.file || 'Unknown'; var fileName = path.split('/').pop().split('\\').pop(); return '
' + '
' + (index + 1) + '
' + '
' + '
' + escapeHtml(fileName) + '
' + '
' + escapeHtml(path.substring(0, path.lastIndexOf(fileName))) + '
' + '
' + '
' + '' + heat + '' + '' + '
' + '
'; }).join('') + '
'; } function renderTopicList(items) { if (!items || items.length === 0) { return '
' + '' + '

' + t('memory.noData') + '

' + '
'; } return '
' + items.map(function(item, index) { var heat = item.heat || item.count || 0; var heatClass = heat > 10 ? 'high' : heat > 5 ? 'medium' : 'low'; var preview = item.preview || item.topic || 'Unknown'; return '
' + '
' + (index + 1) + '
' + '
' + '
' + escapeHtml(preview) + '
' + '
' + '
' + '' + heat + '' + '' + '
' + '
'; }).join('') + '
'; } // ========== 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 = '
' + '
' + '
' + '

' + t('memory.memoryGraph') + '

' + '' + (memoryGraphData.nodes || []).length + ' ' + t('memory.nodes') + '' + '
' + '
' + '' + '' + '' + '' + '
' + '
' + '
' + '
' + '
' + t('memory.file') + '
' + '
' + t('memory.module') + '
' + '
' + t('memory.component') + '
' + '
' + '
'; // 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 = '
' + '
' + '
' + '

' + t('memory.recentContext') + '

' + '' + recentContext.length + ' ' + t('memory.activities') + '' + '
' + '
' + '' + renderContextTimeline(recentContext) + renderContextStats() + '
'; if (window.lucide) lucide.createIcons(); } function renderContextTimeline(prompts) { if (!prompts || prompts.length === 0) { return '
' + '' + '

' + t('memory.noRecentActivity') + '

' + '
'; } return '
' + prompts.map(function(item, index) { var timestamp = item.timestamp ? formatTimestamp(item.timestamp) : 'Unknown time'; var type = item.type || 'unknown'; var typeIcon = type === 'read' ? 'eye' : type === 'write' ? 'pencil' : type === 'edit' ? 'pencil' : 'file-text'; var files = item.files || []; var description = item.prompt || item.description || 'No description'; return '
' + '
' + '' + '
' + '
' + '
' + '' + escapeHtml(type.charAt(0).toUpperCase() + type.slice(1)) + '' + '' + timestamp + '' + '
' + '
' + escapeHtml(description) + '
' + (files.length > 0 ? '
' + files.map(function(f) { return '' + ' ' + escapeHtml(f.split('/').pop().split('\\').pop()) + ''; }).join('') + '
' : '') + '
' + '
'; }).join('') + '
'; } /** * Toggle timeline item expansion */ function toggleTimelineItem(element) { element.classList.toggle('expanded'); } function renderContextStats() { var totalReads = recentContext.filter(function(c) { return c.type === 'read'; }).length; var totalEdits = recentContext.filter(function(c) { return c.type === 'edit' || c.type === 'write'; }).length; var totalMentions = recentContext.filter(function(c) { return c.type === 'mention'; }).length; return '
' + '
' + '' + '' + t('memory.reads') + '' + '' + totalReads + '' + '
' + '
' + '' + '' + t('memory.edits') + '' + '' + totalEdits + '' + '
' + '
' + '' + '' + t('memory.mentions') + '' + '' + totalMentions + '' + '
' + '
'; } function showNodeDetails(node) { var container = document.getElementById('memory-context'); if (!container) return; var associations = memoryGraphData.edges .filter(function(e) { return e.source.id === node.id || e.target.id === node.id; }) .map(function(e) { var other = e.source.id === node.id ? e.target : e.source; return { node: other, weight: e.weight || 1 }; }) .sort(function(a, b) { return b.weight - a.weight; }); container.innerHTML = '
' + '
' + '
' + '

' + t('memory.nodeDetails') + '

' + '
' + '
' + '' + '
' + '
' + '
' + '
' + '
' + '' + '
' + '
' + '
' + escapeHtml(node.name || node.id) + '
' + '
' + escapeHtml(node.path || node.id) + '
' + '
' + '
' + '
' + '
' + '' + t('memory.heat') + '' + '' + (node.heat || 0) + '' + '
' + '
' + '' + t('memory.associations') + '' + '' + associations.length + '' + '
' + '
' + '' + t('memory.type') + '' + '' + (node.type || 'file') + '' + '
' + '
' + (associations.length > 0 ? '
' + '

' + t('memory.relatedNodes') + '

' + '
' + associations.slice(0, 10).map(function(a) { return '
' + '
' + '' + '' + escapeHtml(a.node.name || a.node.id) + '' + '
' + '
' + a.weight + '
' + '
'; }).join('') + (associations.length > 10 ? '
+' + (associations.length - 10) + ' more
' : '') + '
' + '
' : '
' + t('memory.noAssociations') + '
') + '
' + '
'; if (window.lucide) lucide.createIcons(); } // ========== Insights Section ========== function renderInsightsSection() { var container = document.getElementById('memory-insights'); if (!container) return; container.innerHTML = '
' + '
' + '
' + '

' + t('memory.insightsHistory') + '

' + '' + insightsHistory.length + ' ' + t('memory.analyses') + '' + '
' + '
' + '' + '
' + '
' + '
' + renderInsightsCards() + '
' + (selectedInsight ? '
' + renderInsightDetail(selectedInsight) + '
' : '') + '
'; 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 '
' + '
' + '
' + '' + '' + insight.tool + '' + '
' + '
' + timeAgo + '
' + '
' + '
' + '
' + '' + patternCount + '' + '' + t('memory.patterns') + '' + '
' + '
' + '' + suggestionCount + '' + '' + t('memory.suggestions') + '' + '
' + '
' + '' + insight.prompt_count + '' + '' + t('memory.prompts') + '' + '
' + '
' + '
' + (insight.patterns && insight.patterns.length > 0 ? '
' + '' + escapeHtml(insight.patterns[0].type || 'pattern') + '' + '' + escapeHtml((insight.patterns[0].description || '').substring(0, 60)) + '...' + '
' : '') + '
' + '
'; }).join('') + '
'; } function getInsightSeverity(patterns) { if (!patterns || patterns.length === 0) return 'low'; var hasHigh = patterns.some(function(p) { return p.severity === 'high'; }); var hasMedium = patterns.some(function(p) { return p.severity === 'medium'; }); return hasHigh ? 'high' : (hasMedium ? 'medium' : 'low'); } function getToolIcon(tool) { switch(tool) { case 'gemini': return 'sparkles'; case 'qwen': return 'bot'; case 'codex': return 'code-2'; default: return 'cpu'; } } async function showInsightDetail(insightId) { try { var response = await fetch('/api/memory/insights/' + insightId); if (!response.ok) throw new Error('Failed to load insight detail'); var data = await response.json(); selectedInsight = data.insight; renderInsightsSection(); } catch (err) { console.error('Failed to load insight detail:', err); if (window.showToast) { showToast(t('memory.loadInsightError'), 'error'); } } } function closeInsightDetail() { selectedInsight = null; renderInsightsSection(); } function renderInsightDetail(insight) { if (!insight) return ''; var html = '
' + '
' + '

' + t('memory.insightDetail') + '

' + '' + '
' + '
' + ' ' + insight.tool + '' + ' ' + formatTimestamp(insight.created_at) + '' + ' ' + insight.prompt_count + ' ' + t('memory.promptsAnalyzed') + '' + '
'; // Patterns if (insight.patterns && insight.patterns.length > 0) { html += '
' + '
' + t('memory.patternsFound') + ' (' + insight.patterns.length + ')
' + '
' + insight.patterns.map(function(p) { return '
' + '
' + '' + escapeHtml(p.type || 'pattern') + '' + '' + (p.severity || 'low') + '' + (p.occurrences ? '' + p.occurrences + 'x' : '') + '
' + '
' + escapeHtml(p.description || '') + '
' + (p.suggestion ? '
' + escapeHtml(p.suggestion) + '
' : '') + '
'; }).join('') + '
' + '
'; } // Suggestions if (insight.suggestions && insight.suggestions.length > 0) { html += '
' + '
' + t('memory.suggestionsProvided') + ' (' + insight.suggestions.length + ')
' + '
' + insight.suggestions.map(function(s) { return '
' + '
' + escapeHtml(s.title || '') + '
' + '
' + escapeHtml(s.description || '') + '
' + (s.example ? '
' + escapeHtml(s.example) + '
' : '') + '
'; }).join('') + '
' + '
'; } html += '
' + '' + '
' + '
'; return html; } async function deleteInsight(insightId) { if (!confirm(t('memory.confirmDeleteInsight'))) return; try { var response = await csrfFetch('/api/memory/insights/' + insightId, { method: 'DELETE' }); if (!response.ok) throw new Error('Failed to delete insight'); selectedInsight = null; await loadInsightsHistory(); renderInsightsSection(); if (window.showToast) { showToast(t('memory.insightDeleted'), 'success'); } } catch (err) { console.error('Failed to delete insight:', err); if (window.showToast) { showToast(t('memory.deleteInsightError'), 'error'); } } } async function refreshInsightsHistory() { await loadInsightsHistory(); renderInsightsSection(); } // ========== Actions ========== async function setMemoryTimeFilter(filter) { memoryTimeFilter = filter; await loadMemoryStats(); renderHotspotsColumn(); } async function refreshMemoryData() { await Promise.all([ loadMemoryStats(), loadMemoryGraph(), loadRecentContext() ]); renderHotspotsColumn(); renderGraphColumn(); renderContextColumn(); } function filterRecentContext(query) { var filtered = recentContext; if (query && query.trim()) { var q = query.toLowerCase(); filtered = recentContext.filter(function(item) { var promptMatch = (item.prompt || '').toLowerCase().includes(q); var filesMatch = (item.files || []).some(function(f) { return f.toLowerCase().includes(q); }); return promptMatch || filesMatch; }); } var container = document.getElementById('memory-context'); if (!container) return; var timeline = container.querySelector('.context-timeline'); if (timeline) { timeline.outerHTML = renderContextTimeline(filtered); if (window.lucide) lucide.createIcons(); } } // ========== WebSocket Event Handlers ========== function handleMemoryUpdated(payload) { // Refresh graph and stats without full re-render if (payload.type === 'stats') { loadMemoryStats().then(function() { renderHotspotsColumn(); }); } else if (payload.type === 'graph') { loadMemoryGraph().then(function() { renderGraphColumn(); }); } else if (payload.type === 'context') { loadRecentContext().then(function() { renderContextColumn(); }); } else { // Full refresh refreshMemoryData(); } // Highlight updated node if provided if (payload.nodeId) { highlightNode(payload.nodeId); } } // ========== Utilities ========== function formatTimestamp(timestamp) { var date = new Date(timestamp); var now = new Date(); var diff = now - date; // Less than 1 minute if (diff < 60000) { return t('memory.justNow'); } // Less than 1 hour if (diff < 3600000) { var minutes = Math.floor(diff / 60000); return minutes + ' ' + t('memory.minutesAgo'); } // Less than 1 day if (diff < 86400000) { var hours = Math.floor(diff / 3600000); return hours + ' ' + t('memory.hoursAgo'); } // Otherwise show date return date.toLocaleDateString() + ' ' + date.toLocaleTimeString(); }