fleet-memory/memory_graph_interactive.html
Nicolò Boschi bc7d9fe07f improvements
2025-10-30 18:54:06 +01:00

423 lines
14 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<title>Memory Graph - Interactive Visualization</title>
<meta charset="utf-8">
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.28.1/cytoscape.min.js"></script>
<style>
body {
font-family: Tahoma, sans-serif;
margin: 0;
padding: 0;
background: #f5f5f5;
}
.tab-container {
background: white;
}
.tab-buttons {
background: #f0f0f0;
border-bottom: 2px solid #333;
padding: 0;
margin: 0;
}
.tab-button {
background: #e0e0e0;
border: none;
padding: 12px 24px;
cursor: pointer;
font-size: 16px;
font-weight: bold;
border-top: 2px solid transparent;
border-left: 2px solid transparent;
border-right: 2px solid transparent;
transition: background 0.2s;
}
.tab-button:hover {
background: #d0d0d0;
}
.tab-button.active {
background: white;
border-top: 2px solid #333;
border-left: 2px solid #333;
border-right: 2px solid #333;
border-bottom: 2px solid white;
margin-bottom: -2px;
}
.tab-content {
display: none;
background: white;
}
.tab-content.active {
display: block;
}
#cy {
width: 100%;
height: 800px;
background: #ffffff;
}
#graph-tab {
position: relative;
}
#table-tab {
padding: 20px;
}
.legend {
position: absolute;
top: 20px;
left: 20px;
background: white;
padding: 15px;
border: 2px solid #333;
border-radius: 8px;
box-shadow: 2px 2px 8px rgba(0,0,0,0.3);
z-index: 1000;
max-width: 250px;
}
.legend h3 {
margin-top: 0;
border-bottom: 2px solid #333;
padding-bottom: 5px;
}
.legend-item {
margin: 8px 0;
display: flex;
align-items: center;
}
.legend-line {
width: 30px;
height: 2px;
margin-right: 10px;
}
.legend-node {
width: 20px;
height: 20px;
margin-right: 10px;
border: 1px solid #999;
border-radius: 3px;
}
#table-filter {
width: 100%;
max-width: 600px;
padding: 10px;
margin-bottom: 15px;
border: 2px solid #ccc;
border-radius: 4px;
font-size: 14px;
box-sizing: border-box;
}
#memory-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
max-width: 1400px;
}
#memory-table th {
padding: 10px;
text-align: left;
border: 1px solid #ddd;
background: #f0f0f0;
}
#memory-table td {
padding: 8px;
border: 1px solid #ddd;
}
.tooltip {
position: absolute;
background: white;
border: 2px solid #333;
border-radius: 4px;
padding: 10px;
box-shadow: 2px 2px 8px rgba(0,0,0,0.3);
max-width: 300px;
font-size: 12px;
pointer-events: none;
z-index: 9999;
}
</style>
</head>
<body>
<div class="tab-container">
<div class="tab-buttons">
<button class="tab-button active" onclick="switchTab('graph')">Graph View</button>
<button class="tab-button" onclick="switchTab('table')">Table View</button>
</div>
<div id="graph-tab" class="tab-content active">
<div style="padding: 15px; background: #f9f9f9; border-bottom: 2px solid #333;">
<div style="display: flex; gap: 15px; align-items: center; flex-wrap: wrap;">
<div>
<label style="font-weight: bold; margin-right: 5px;">Limit nodes:</label>
<input type="number" id="node-limit" value="50" min="10" max="1000" step="10"
style="width: 80px; padding: 5px; border: 1px solid #ccc; border-radius: 4px;">
</div>
<div>
<label style="font-weight: bold; margin-right: 5px;">Layout:</label>
<select id="layout-select" style="padding: 5px; border: 1px solid #ccc; border-radius: 4px;">
<option value="circle">Circle (fast)</option>
<option value="grid">Grid (fast)</option>
<option value="cose">Force-directed (slow)</option>
</select>
</div>
<button onclick="reloadGraph()" style="padding: 6px 15px; background: #42a5f5; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold;">
Apply
</button>
<span id="node-count" style="color: #666; font-size: 14px;"></span>
</div>
</div>
<div id="cy"></div>
<div class="legend">
<h3>Legend</h3>
<h4 style="margin: 10px 0 5px 0;">Link Types:</h4>
<div class="legend-item">
<div class="legend-line" style="background: #00bcd4; border-top: 1px dashed #00bcd4;"></div>
<span><b>Temporal</b></span>
</div>
<div class="legend-item">
<div class="legend-line" style="background: #ff69b4;"></div>
<span><b>Semantic</b></span>
</div>
<div class="legend-item">
<div class="legend-line" style="background: #ffd700;"></div>
<span><b>Entity</b></span>
</div>
<h4 style="margin: 15px 0 5px 0;">Nodes:</h4>
<div class="legend-item">
<div class="legend-node" style="background: #e0e0e0;"></div>
<span>No entities</span>
</div>
<div class="legend-item">
<div class="legend-node" style="background: #90caf9;"></div>
<span>1 entity</span>
</div>
<div class="legend-item">
<div class="legend-node" style="background: #42a5f5;"></div>
<span>2+ entities</span>
</div>
</div>
</div>
<div id="table-tab" class="tab-content">
<h2>Memory Units (0)</h2>
<input type="text" id="table-filter" placeholder="Filter by text, context, or entities...">
<div style="overflow-x: auto;">
<table id="memory-table">
<thead>
<tr>
<th>ID</th>
<th>Text</th>
<th>Context</th>
<th>Date</th>
<th>Entities</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
<script>
// Graph data
const allGraphData = {"nodes": [], "edges": []};
let cy = null;
// Initialize graph with filtering
function initGraph(nodeLimit, layoutName) {
// Filter nodes to limit
const limitedNodes = allGraphData.nodes.slice(0, nodeLimit);
const nodeIds = new Set(limitedNodes.map(n => n.data.id));
// Filter edges to only include those between visible nodes
const limitedEdges = allGraphData.edges.filter(e =>
nodeIds.has(e.data.source) && nodeIds.has(e.data.target)
);
// Update count display
document.getElementById('node-count').textContent =
`Showing ${limitedNodes.length} of ${allGraphData.nodes.length} nodes`;
// Destroy existing graph if any
if (cy) {
cy.destroy();
}
// Layout configurations
const layouts = {
'circle': {
name: 'circle',
animate: false,
radius: 300,
spacingFactor: 1.5
},
'grid': {
name: 'grid',
animate: false,
rows: Math.ceil(Math.sqrt(limitedNodes.length)),
cols: Math.ceil(Math.sqrt(limitedNodes.length)),
spacingFactor: 2
},
'cose': {
name: 'cose',
animate: false,
nodeRepulsion: 15000,
idealEdgeLength: 150,
edgeElasticity: 100,
nestingFactor: 1.2,
gravity: 1,
numIter: 1000,
initialTemp: 200,
coolingFactor: 0.95,
minTemp: 1.0
}
};
// Initialize Cytoscape
cy = cytoscape({
container: document.getElementById('cy'),
elements: [
...limitedNodes.map(n => ({ data: n.data })),
...limitedEdges.map(e => ({ data: e.data }))
],
style: [
{
selector: 'node',
style: {
'background-color': 'data(color)',
'label': 'data(label)',
'text-valign': 'center',
'text-halign': 'center',
'font-size': '10px',
'font-weight': 'bold',
'text-wrap': 'wrap',
'text-max-width': '100px',
'width': 40,
'height': 40,
'border-width': 2,
'border-color': '#333'
}
},
{
selector: 'edge',
style: {
'width': 1,
'line-color': 'data(color)',
'line-style': 'data(lineStyle)',
'target-arrow-shape': 'triangle',
'target-arrow-color': 'data(color)',
'curve-style': 'bezier',
'opacity': 0.7
}
},
{
selector: 'node:selected',
style: {
'border-width': 4,
'border-color': '#000'
}
}
],
layout: layouts[layoutName] || layouts['circle']
});
// Simple tooltip on hover
let tooltip = null;
cy.on('mouseover', 'node', function(evt) {
const node = evt.target;
const data = node.data();
const renderedPosition = node.renderedPosition();
// Create tooltip
tooltip = document.createElement('div');
tooltip.className = 'tooltip';
tooltip.innerHTML = `
<b>Text:</b> ${data.text}<br>
<b>Context:</b> ${data.context}<br>
<b>Date:</b> ${data.date}<br>
<b>Entities:</b> ${data.entities}
`;
tooltip.style.left = renderedPosition.x + 20 + 'px';
tooltip.style.top = renderedPosition.y + 'px';
document.body.appendChild(tooltip);
});
cy.on('mouseout', 'node', function(evt) {
if (tooltip) {
tooltip.remove();
tooltip = null;
}
});
}
// Reload graph with current settings
function reloadGraph() {
const nodeLimit = parseInt(document.getElementById('node-limit').value) || 50;
const layoutName = document.getElementById('layout-select').value;
initGraph(nodeLimit, layoutName);
}
// Initialize with default settings (50 nodes, circle layout)
initGraph(50, 'circle');
// Tab switching
function switchTab(tabName) {
document.querySelectorAll('.tab-content').forEach(tab => {
tab.classList.remove('active');
});
document.querySelectorAll('.tab-button').forEach(btn => {
btn.classList.remove('active');
});
if (tabName === 'graph') {
document.getElementById('graph-tab').classList.add('active');
document.querySelectorAll('.tab-button')[0].classList.add('active');
cy.resize(); // Resize graph when switching to it
} else if (tabName === 'table') {
document.getElementById('table-tab').classList.add('active');
document.querySelectorAll('.tab-button')[1].classList.add('active');
}
}
// Table filtering
document.getElementById('table-filter').addEventListener('input', function() {
const filterValue = this.value.toLowerCase();
const rows = document.querySelectorAll('#memory-table tbody tr');
rows.forEach(row => {
const text = row.textContent.toLowerCase();
if (text.includes(filterValue)) {
row.style.display = '';
} else {
row.style.display = 'none';
}
});
});
</script>
</body>
</html>