/**
* Memory Visualization Components
* Handles interactive memory graphs, timelines, clusters, and advanced search
*/
// D3.js import - will be loaded via script tag in webpack
import * as d3 from 'd3';
/**
* Memory Visualization Manager
* Orchestrates all memory visualization components
*/
class MemoryVisualizationManager {
constructor() {
this.memoryGraph = new MemoryGraphViz();
this.memoryTimeline = new MemoryTimelineViz();
this.memoryClusters = new MemoryClustersViz();
this.memorySearch = new MemoryAdvancedSearch();
this.currentMemories = [];
this.currentConcepts = [];
this.initialized = false;
}
/**
* Initialize memory visualization components
*/
async initialize() {
if (this.initialized) return;
try {
// Initialize each component
this.memoryGraph.initialize();
this.memoryTimeline.initialize();
this.memoryClusters.initialize();
this.memorySearch.initialize();
// Set up event listeners
this.setupEventListeners();
this.initialized = true;
console.log('Memory visualization manager initialized');
} catch (error) {
console.error('Error initializing memory visualization:', error);
}
}
/**
* Set up event listeners for memory visualization
*/
setupEventListeners() {
// Memory Graph Events
document.getElementById('load-memory-graph')?.addEventListener('click', () => {
this.loadMemoryGraph();
});
document.getElementById('refresh-memory-graph')?.addEventListener('click', () => {
this.refreshMemoryGraph();
});
// Timeline Events
document.getElementById('load-memory-timeline')?.addEventListener('click', () => {
this.loadMemoryTimeline();
});
// Clusters Events
document.getElementById('load-memory-clusters')?.addEventListener('click', () => {
this.loadMemoryClusters();
});
document.getElementById('recalculate-clusters')?.addEventListener('click', () => {
this.recalculateClusters();
});
// Advanced Search Events
document.getElementById('execute-memory-search')?.addEventListener('click', () => {
this.executeAdvancedSearch();
});
document.getElementById('clear-filters')?.addEventListener('click', () => {
this.clearSearchFilters();
});
// Threshold slider updates
document.getElementById('memory-graph-threshold')?.addEventListener('input', (e) => {
document.getElementById('memory-graph-threshold-value').textContent = e.target.value;
});
document.getElementById('similarity-threshold')?.addEventListener('input', (e) => {
document.getElementById('similarity-threshold-value').textContent = e.target.value;
});
}
/**
* Load and display memory graph
*/
async loadMemoryGraph() {
try {
const limit = document.getElementById('memory-graph-limit')?.value || 50;
const threshold = document.getElementById('memory-graph-threshold')?.value || 0.7;
// Show loading state
this.showGraphLoading('memory-graph-container');
// Fetch memory data
const memories = await this.fetchMemoryData(limit, threshold);
const concepts = await this.fetchConceptData();
// Update data
this.currentMemories = memories;
this.currentConcepts = concepts;
// Render graph
this.memoryGraph.render(memories, concepts, {
threshold: parseFloat(threshold),
container: 'memory-graph-container'
});
// Update info stats
this.updateGraphInfo(memories, concepts);
} catch (error) {
console.error('Error loading memory graph:', error);
this.showGraphError('memory-graph-container', 'Failed to load memory graph');
}
}
/**
* Refresh memory graph with current data
*/
async refreshMemoryGraph() {
await this.loadMemoryGraph();
}
/**
* Load and display memory timeline
*/
async loadMemoryTimeline() {
try {
const period = document.getElementById('timeline-period')?.value || 'week';
const grouping = document.getElementById('timeline-grouping')?.value || 'day';
const showAccess = document.getElementById('show-access-patterns')?.checked || true;
this.showGraphLoading('memory-timeline-container');
const timelineData = await this.fetchTimelineData(period, grouping, showAccess);
this.memoryTimeline.render(timelineData, {
period,
grouping,
showAccess,
container: 'memory-timeline-container'
});
} catch (error) {
console.error('Error loading memory timeline:', error);
this.showGraphError('memory-timeline-container', 'Failed to load memory timeline');
}
}
/**
* Load and display memory clusters
*/
async loadMemoryClusters() {
try {
const clusterCount = document.getElementById('cluster-count')?.value || 5;
const method = document.getElementById('cluster-method')?.value || 'kmeans';
this.showGraphLoading('memory-clusters-container');
const clusterData = await this.fetchClusterData(clusterCount, method);
this.memoryClusters.render(clusterData, {
clusterCount: parseInt(clusterCount),
method,
container: 'memory-clusters-container'
});
this.updateClusterStats(clusterData);
} catch (error) {
console.error('Error loading memory clusters:', error);
this.showGraphError('memory-clusters-container', 'Failed to load memory clusters');
}
}
/**
* Recalculate clusters with new parameters
*/
async recalculateClusters() {
await this.loadMemoryClusters();
}
/**
* Execute advanced memory search
*/
async executeAdvancedSearch() {
try {
const filters = this.memorySearch.getSearchFilters();
const results = await this.searchMemories(filters);
this.memorySearch.displayResults(results);
} catch (error) {
console.error('Error executing memory search:', error);
this.memorySearch.showError('Failed to execute search');
}
}
/**
* Clear all search filters
*/
clearSearchFilters() {
this.memorySearch.clearFilters();
}
/**
* Fetch memory data from API
*/
async fetchMemoryData(limit = 50, threshold = 0.7) {
const response = await fetch('/api/memory/graph', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ limit, threshold })
});
if (!response.ok) {
// Fallback to mock data for development
return this.generateMockMemoryData(limit);
}
return await response.json();
}
/**
* Fetch concept data from API
*/
async fetchConceptData() {
const response = await fetch('/api/memory/concepts');
if (!response.ok) {
return this.generateMockConceptData();
}
return await response.json();
}
/**
* Fetch timeline data from API
*/
async fetchTimelineData(period, grouping, showAccess) {
const response = await fetch('/api/memory/timeline', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ period, grouping, showAccess })
});
if (!response.ok) {
return this.generateMockTimelineData(period, grouping);
}
return await response.json();
}
/**
* Fetch cluster data from API
*/
async fetchClusterData(clusterCount, method) {
const response = await fetch('/api/memory/clusters', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ clusterCount, method })
});
if (!response.ok) {
return this.generateMockClusterData(clusterCount);
}
return await response.json();
}
/**
* Search memories with filters
*/
async searchMemories(filters) {
const response = await fetch('/api/memory/search/advanced', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(filters)
});
if (!response.ok) {
return this.generateMockSearchResults(filters);
}
return await response.json();
}
/**
* Show loading state in graph container
*/
showGraphLoading(containerId) {
const container = document.getElementById(containerId);
if (container) {
container.innerHTML = `
<div class="graph-loading">
<div class="spinner"></div>
<p>Loading visualization...</p>
</div>
`;
}
}
/**
* Show error state in graph container
*/
showGraphError(containerId, message) {
const container = document.getElementById(containerId);
if (container) {
container.innerHTML = `
<div class="graph-error">
<p>❌ ${message}</p>
<button class="btn secondary-btn" onclick="location.reload()">Retry</button>
</div>
`;
}
}
/**
* Update graph info statistics
*/
updateGraphInfo(memories, concepts) {
const memoryCount = memories?.length || 0;
const conceptCount = concepts?.length || 0;
const connectionCount = this.calculateConnections(memories, concepts);
document.getElementById('memory-count').textContent = memoryCount;
document.getElementById('concept-count').textContent = conceptCount;
document.getElementById('connection-count').textContent = connectionCount;
}
/**
* Update cluster statistics
*/
updateClusterStats(clusterData) {
const clusters = clusterData?.clusters || [];
const totalClusters = clusters.length;
const largestCluster = Math.max(...clusters.map(c => c.memories?.length || 0));
const avgClusterSize = totalClusters > 0 ?
clusters.reduce((sum, c) => sum + (c.memories?.length || 0), 0) / totalClusters : 0;
document.getElementById('total-clusters').textContent = totalClusters;
document.getElementById('largest-cluster').textContent = `${largestCluster} memories`;
document.getElementById('average-cluster-size').textContent = avgClusterSize.toFixed(1);
}
/**
* Calculate number of connections between memories and concepts
*/
calculateConnections(memories, concepts) {
let connections = 0;
memories?.forEach(memory => {
const memoryConcepts = memory.concepts || [];
connections += memoryConcepts.length;
});
return connections;
}
// Mock data generators for development/fallback
generateMockMemoryData(limit) {
const memories = [];
for (let i = 0; i < Math.min(limit, 20); i++) {
memories.push({
id: `memory-${i}`,
prompt: `Sample prompt ${i + 1}`,
response: `Sample response ${i + 1}`,
timestamp: Date.now() - (i * 86400000), // Days ago
concepts: [`concept-${i % 5}`, `concept-${(i + 1) % 5}`],
accessCount: Math.floor(Math.random() * 10) + 1,
decayFactor: Math.random(),
type: i % 2 === 0 ? 'user' : 'assistant'
});
}
return memories;
}
generateMockConceptData() {
return [
{ id: 'concept-0', name: 'Programming', weight: 0.8 },
{ id: 'concept-1', name: 'JavaScript', weight: 0.7 },
{ id: 'concept-2', name: 'API Design', weight: 0.6 },
{ id: 'concept-3', name: 'Database', weight: 0.5 },
{ id: 'concept-4', name: 'Visualization', weight: 0.4 }
];
}
generateMockTimelineData(period, grouping) {
const data = [];
const now = new Date();
const days = period === 'day' ? 1 : period === 'week' ? 7 : period === 'month' ? 30 : 90;
for (let i = 0; i < days; i++) {
const date = new Date(now - (i * 86400000));
data.push({
date,
memoryCount: Math.floor(Math.random() * 10),
accessCount: Math.floor(Math.random() * 20)
});
}
return data.reverse();
}
generateMockClusterData(clusterCount) {
const clusters = [];
for (let i = 0; i < clusterCount; i++) {
const memories = [];
const size = Math.floor(Math.random() * 8) + 2;
for (let j = 0; j < size; j++) {
memories.push({
id: `cluster-${i}-memory-${j}`,
prompt: `Cluster ${i} memory ${j}`,
similarity: Math.random()
});
}
clusters.push({
id: i,
label: `Cluster ${i + 1}`,
memories,
centroid: [Math.random() * 100, Math.random() * 100]
});
}
return { clusters };
}
generateMockSearchResults(filters) {
return {
results: this.generateMockMemoryData(10),
totalCount: 10,
executionTime: Math.random() * 100 + 50
};
}
}
/**
* Memory Graph Visualization Component
*/
class MemoryGraphViz {
constructor() {
this.svg = null;
this.simulation = null;
this.nodes = [];
this.links = [];
}
initialize() {
console.log('Memory graph visualization initialized');
}
render(memories, concepts, options = {}) {
const container = document.getElementById(options.container || 'memory-graph-container');
if (!container) return;
// Clear container
container.innerHTML = '';
// Set up dimensions
const width = container.clientWidth || 800;
const height = container.clientHeight || 400;
// Create SVG
this.svg = d3.select(container)
.append('svg')
.attr('width', width)
.attr('height', height);
// Prepare nodes and links
this.prepareGraphData(memories, concepts, options.threshold);
// Create force simulation
this.simulation = d3.forceSimulation(this.nodes)
.force('link', d3.forceLink(this.links).id(d => d.id).distance(50))
.force('charge', d3.forceManyBody().strength(-200))
.force('center', d3.forceCenter(width / 2, height / 2));
// Draw links
const link = this.svg.append('g')
.selectAll('line')
.data(this.links)
.enter().append('line')
.attr('class', 'memory-link')
.classed('strong', d => d.strength > 0.7);
// Draw nodes
const node = this.svg.append('g')
.selectAll('circle')
.data(this.nodes)
.enter().append('circle')
.attr('r', d => d.type === 'concept' ? 8 : 12)
.attr('class', d => d.type === 'concept' ? 'concept-node' : 'memory-node')
.classed('user', d => d.subtype === 'user')
.classed('assistant', d => d.subtype === 'assistant')
.call(d3.drag()
.on('start', (event, d) => this.dragstarted(event, d))
.on('drag', (event, d) => this.dragged(event, d))
.on('end', (event, d) => this.dragended(event, d)))
.on('click', (event, d) => this.nodeClicked(event, d));
// Add labels
const label = this.svg.append('g')
.selectAll('text')
.data(this.nodes)
.enter().append('text')
.attr('class', 'memory-label')
.attr('dy', '.35em')
.text(d => d.label.substring(0, 20) + (d.label.length > 20 ? '...' : ''));
// Update positions on simulation tick
this.simulation.on('tick', () => {
link
.attr('x1', d => d.source.x)
.attr('y1', d => d.source.y)
.attr('x2', d => d.target.x)
.attr('y2', d => d.target.y);
node
.attr('cx', d => d.x)
.attr('cy', d => d.y);
label
.attr('x', d => d.x)
.attr('y', d => d.y + 20);
});
}
prepareGraphData(memories, concepts, threshold) {
this.nodes = [];
this.links = [];
// Ensure memories is an array
const memoriesArray = Array.isArray(memories) ? memories : [];
// Add memory nodes
memoriesArray.forEach(memory => {
this.nodes.push({
id: memory.id,
label: memory.prompt || memory.response || 'Memory',
type: 'memory',
subtype: memory.type || 'user',
data: memory
});
});
// Ensure concepts is an array
const conceptsArray = Array.isArray(concepts) ? concepts : [];
// Add concept nodes
conceptsArray.forEach(concept => {
this.nodes.push({
id: concept.id,
label: concept.name || concept.id,
type: 'concept',
weight: concept.weight || 0.5,
data: concept
});
});
// Add links between memories and concepts
memoriesArray.forEach(memory => {
const memoryConcepts = memory.concepts || [];
memoryConcepts.forEach(conceptId => {
if (conceptsArray.find(c => c.id === conceptId)) {
this.links.push({
source: memory.id,
target: conceptId,
strength: Math.random() * 0.5 + 0.5
});
}
});
});
// Add links between similar memories (based on threshold)
for (let i = 0; i < memories.length; i++) {
for (let j = i + 1; j < memories.length; j++) {
const similarity = this.calculateSimilarity(memories[i], memories[j]);
if (similarity > threshold) {
this.links.push({
source: memories[i].id,
target: memories[j].id,
strength: similarity
});
}
}
}
}
calculateSimilarity(memory1, memory2) {
// Simple concept overlap similarity
const concepts1 = new Set(memory1.concepts || []);
const concepts2 = new Set(memory2.concepts || []);
const intersection = new Set([...concepts1].filter(x => concepts2.has(x)));
const union = new Set([...concepts1, ...concepts2]);
return union.size > 0 ? intersection.size / union.size : 0;
}
dragstarted(event, d) {
if (!event.active) this.simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
}
dragended(event, d) {
if (!event.active) this.simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
nodeClicked(event, d) {
// Update selected memory info
document.getElementById('selected-memory').textContent = d.label;
// Highlight selected node
this.svg.selectAll('.memory-node, .concept-node').classed('selected', false);
d3.select(event.target).classed('selected', true);
// Show memory details if available
if (d.type === 'memory') {
this.showMemoryDetails(d.data);
}
}
showMemoryDetails(memory) {
const detailsPanel = document.getElementById('timeline-memory-details');
if (detailsPanel) {
detailsPanel.innerHTML = `
<h5>Memory Details</h5>
<p><strong>Prompt:</strong> ${memory.prompt || 'N/A'}</p>
<p><strong>Response:</strong> ${memory.response || 'N/A'}</p>
<p><strong>Created:</strong> ${new Date(memory.timestamp).toLocaleString()}</p>
<p><strong>Access Count:</strong> ${memory.accessCount || 0}</p>
<p><strong>Concepts:</strong> ${(memory.concepts || []).join(', ') || 'None'}</p>
`;
}
}
}
/**
* Memory Timeline Visualization Component
*/
class MemoryTimelineViz {
constructor() {
this.svg = null;
}
initialize() {
console.log('Memory timeline visualization initialized');
}
render(data, options = {}) {
const container = document.getElementById(options.container || 'memory-timeline-container');
if (!container) return;
container.innerHTML = '';
const width = container.clientWidth || 800;
const height = container.clientHeight || 400;
const margin = { top: 20, right: 30, bottom: 40, left: 50 };
this.svg = d3.select(container)
.append('svg')
.attr('width', width)
.attr('height', height);
const g = this.svg.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);
const xScale = d3.scaleTime()
.domain(d3.extent(data, d => d.date))
.range([0, width - margin.left - margin.right]);
const yScale = d3.scaleLinear()
.domain([0, d3.max(data, d => Math.max(d.memoryCount, d.accessCount))])
.range([height - margin.top - margin.bottom, 0]);
// Add axes
g.append('g')
.attr('transform', `translate(0,${height - margin.top - margin.bottom})`)
.call(d3.axisBottom(xScale).tickFormat(d3.timeFormat('%m/%d')));
g.append('g')
.call(d3.axisLeft(yScale));
// Add memory creation bars
g.selectAll('.timeline-bar')
.data(data)
.enter().append('rect')
.attr('class', 'timeline-bar')
.attr('x', d => xScale(d.date) - 5)
.attr('y', d => yScale(d.memoryCount))
.attr('width', 10)
.attr('height', d => height - margin.top - margin.bottom - yScale(d.memoryCount))
.on('click', (event, d) => this.timelinePointClicked(event, d));
// Add access pattern line if enabled
if (options.showAccess) {
const line = d3.line()
.x(d => xScale(d.date))
.y(d => yScale(d.accessCount))
.curve(d3.curveMonotoneX);
g.append('path')
.datum(data)
.attr('class', 'access-pattern-line')
.attr('d', line);
g.selectAll('.timeline-point')
.data(data)
.enter().append('circle')
.attr('class', 'timeline-point')
.attr('cx', d => xScale(d.date))
.attr('cy', d => yScale(d.accessCount))
.attr('r', 3)
.on('click', (event, d) => this.timelinePointClicked(event, d));
}
}
timelinePointClicked(event, d) {
const detailsPanel = document.getElementById('timeline-memory-details');
if (detailsPanel) {
detailsPanel.innerHTML = `
<h5>Timeline Point</h5>
<p><strong>Date:</strong> ${d.date.toLocaleDateString()}</p>
<p><strong>Memories Created:</strong> ${d.memoryCount}</p>
<p><strong>Total Accesses:</strong> ${d.accessCount}</p>
`;
}
}
}
/**
* Memory Clusters Visualization Component
*/
class MemoryClustersViz {
constructor() {
this.svg = null;
}
initialize() {
console.log('Memory clusters visualization initialized');
}
render(clusterData, options = {}) {
const container = document.getElementById(options.container || 'memory-clusters-container');
if (!container) return;
container.innerHTML = '';
const width = container.clientWidth || 800;
const height = container.clientHeight || 400;
this.svg = d3.select(container)
.append('svg')
.attr('width', width)
.attr('height', height);
const clusters = clusterData.clusters || [];
// Create cluster groups
const clusterGroups = this.svg.selectAll('.cluster-group')
.data(clusters)
.enter().append('g')
.attr('class', 'cluster-group');
// Position clusters in a grid
const cols = Math.ceil(Math.sqrt(clusters.length));
const cellWidth = width / cols;
const cellHeight = height / Math.ceil(clusters.length / cols);
clusters.forEach((cluster, i) => {
const col = i % cols;
const row = Math.floor(i / cols);
const centerX = col * cellWidth + cellWidth / 2;
const centerY = row * cellHeight + cellHeight / 2;
const group = d3.select(clusterGroups.nodes()[i]);
// Draw cluster boundary
group.append('circle')
.attr('class', 'cluster-group')
.attr('cx', centerX)
.attr('cy', centerY)
.attr('r', Math.min(cellWidth, cellHeight) / 3);
// Draw cluster memories as nodes
const memories = cluster.memories || [];
const angleStep = (2 * Math.PI) / memories.length;
const radius = Math.min(cellWidth, cellHeight) / 6;
memories.forEach((memory, j) => {
const angle = j * angleStep;
const x = centerX + Math.cos(angle) * radius;
const y = centerY + Math.sin(angle) * radius;
group.append('circle')
.attr('class', `cluster-node cluster-${i}`)
.attr('cx', x)
.attr('cy', y)
.attr('r', 4)
.on('click', () => this.clusterNodeClicked(memory));
});
// Add cluster label
group.append('text')
.attr('class', 'cluster-label')
.attr('x', centerX)
.attr('y', centerY + Math.min(cellWidth, cellHeight) / 2.5)
.text(`${cluster.label} (${memories.length})`);
});
}
clusterNodeClicked(memory) {
console.log('Cluster node clicked:', memory);
// Could show memory details or navigate to memory
}
}
/**
* Advanced Memory Search Component
*/
class MemoryAdvancedSearch {
constructor() {
this.currentResults = [];
}
initialize() {
console.log('Memory advanced search initialized');
this.setupDatePresets();
}
setupDatePresets() {
const presetSelect = document.getElementById('time-preset');
if (presetSelect) {
presetSelect.addEventListener('change', (e) => {
this.applyDatePreset(e.target.value);
});
}
}
applyDatePreset(preset) {
const dateFrom = document.getElementById('date-from');
const dateTo = document.getElementById('date-to');
const now = new Date();
if (!dateFrom || !dateTo) return;
switch (preset) {
case 'today':
dateFrom.value = now.toISOString().split('T')[0];
dateTo.value = now.toISOString().split('T')[0];
break;
case 'yesterday':
const yesterday = new Date(now - 86400000);
dateFrom.value = yesterday.toISOString().split('T')[0];
dateTo.value = yesterday.toISOString().split('T')[0];
break;
case 'week':
const weekAgo = new Date(now - (7 * 86400000));
dateFrom.value = weekAgo.toISOString().split('T')[0];
dateTo.value = now.toISOString().split('T')[0];
break;
case 'month':
const monthAgo = new Date(now - (30 * 86400000));
dateFrom.value = monthAgo.toISOString().split('T')[0];
dateTo.value = now.toISOString().split('T')[0];
break;
}
}
getSearchFilters() {
return {
query: document.getElementById('search-query')?.value || '',
searchIn: Array.from(document.getElementById('search-in')?.selectedOptions || [])
.map(option => option.value),
dateFrom: document.getElementById('date-from')?.value || null,
dateTo: document.getElementById('date-to')?.value || null,
accessCountMin: parseInt(document.getElementById('access-count-min')?.value) || 0,
similarityThreshold: parseFloat(document.getElementById('similarity-threshold')?.value) || 0.7,
highFrequencyOnly: document.getElementById('high-frequency-only')?.checked || false,
recentOnly: document.getElementById('recent-only')?.checked || false
};
}
clearFilters() {
document.getElementById('search-query').value = '';
document.getElementById('search-in').selectedIndex = -1;
document.getElementById('date-from').value = '';
document.getElementById('date-to').value = '';
document.getElementById('access-count-min').value = '0';
document.getElementById('similarity-threshold').value = '0.7';
document.getElementById('similarity-threshold-value').textContent = '0.7';
document.getElementById('high-frequency-only').checked = false;
document.getElementById('recent-only').checked = false;
document.getElementById('time-preset').value = '';
}
displayResults(results) {
const resultsContainer = document.getElementById('memory-search-results');
if (!resultsContainer) return;
this.currentResults = results.results || [];
if (this.currentResults.length === 0) {
resultsContainer.innerHTML = `
<div class="results-placeholder">
<p>No memories found matching your criteria</p>
</div>
`;
return;
}
resultsContainer.innerHTML = `
<div class="search-results-header">
<h4>Search Results (${this.currentResults.length} found)</h4>
<p>Execution time: ${results.executionTime?.toFixed(2) || 0}ms</p>
</div>
<div class="search-results-list">
${this.currentResults.map(memory => this.renderMemoryResult(memory)).join('')}
</div>
`;
}
renderMemoryResult(memory) {
return `
<div class="memory-result-item" data-memory-id="${memory.id}">
<div class="memory-result-header">
<span class="memory-type">${memory.type || 'memory'}</span>
<span class="memory-date">${new Date(memory.timestamp).toLocaleDateString()}</span>
<span class="memory-score">${(memory.score || 0).toFixed(3)}</span>
</div>
<div class="memory-result-content">
<p><strong>Prompt:</strong> ${(memory.prompt || '').substring(0, 100)}...</p>
<p><strong>Response:</strong> ${(memory.response || '').substring(0, 100)}...</p>
<p><strong>Concepts:</strong> ${(memory.concepts || []).join(', ')}</p>
</div>
<div class="memory-result-stats">
<span>Access Count: ${memory.accessCount || 0}</span>
<span>Decay Factor: ${(memory.decayFactor || 0).toFixed(3)}</span>
</div>
</div>
`;
}
showError(message) {
const resultsContainer = document.getElementById('memory-search-results');
if (resultsContainer) {
resultsContainer.innerHTML = `
<div class="search-error">
<p>❌ ${message}</p>
</div>
`;
}
}
}
// Initialize and export
let memoryVizManager = null;
export function initMemoryVisualization() {
if (!memoryVizManager) {
memoryVizManager = new MemoryVisualizationManager();
}
return memoryVizManager.initialize();
}
export function getMemoryVisualizationManager() {
return memoryVizManager;
}