Add memory graph visualization UI
Some checks failed
Tests / lint (pull_request) Failing after 16s
Tests / test (pull_request) Has been skipped

This commit is contained in:
2026-03-22 23:22:32 +00:00
parent 9f69a8a8be
commit bf0199c0df

View File

@@ -31,7 +31,19 @@
{% endfor %}
</div>
<!-- Search -->
<div class="mc-tabs" style="margin-bottom: 1rem;">
<button class="mc-tab-btn active" onclick="switchTab('list')">List View</button>
<button class="mc-tab-btn" onclick="switchTab('graph')">Graph View</button>
</div>
<!-- Graph View -->
<div id="graph-view" class="mc-panel" style="display:none; height: 500px; position: relative; overflow: hidden;">
<div id="memory-graph" style="width:100%; height:100%;"></div>
<div id="graph-tooltip" style="position: absolute; display: none; background: var(--bg-color); border: 1px solid var(--border-color); padding: 0.5rem; border-radius: 4px; pointer-events: none; z-index: 100; font-size: 0.8rem; max-width: 200px;"></div>
</div>
<div id="list-view">
<!-- Search -->
<div class="mc-search-section">
<form method="get" action="/memory" class="mc-search-form">
<input
@@ -120,8 +132,109 @@
</div>
</div>
</div>
</div>
<script src="https://d3js.org/d3.v7.min.js"></script>
<script>
function switchTab(tab) {
document.querySelectorAll('.mc-tab-btn').forEach(b => b.classList.remove('active'));
event.target.classList.add('active');
if (tab === 'graph') {
document.getElementById('list-view').style.display = 'none';
document.getElementById('graph-view').style.display = 'block';
renderGraph();
} else {
document.getElementById('list-view').style.display = 'block';
document.getElementById('graph-view').style.display = 'none';
}
}
let graphRendered = false;
async function renderGraph() {
if (graphRendered) return;
graphRendered = true;
const res = await fetch('/memory/graph_data');
const data = await res.json();
const width = document.getElementById('memory-graph').clientWidth;
const height = document.getElementById('memory-graph').clientHeight;
const svg = d3.select("#memory-graph")
.append("svg")
.attr("width", width)
.attr("height", height);
const simulation = d3.forceSimulation(data.nodes)
.force("link", d3.forceLink(data.links).id(d(d.id)).distance(50))
.force("charge", d3.forceManyBody().strength(-100))
.force("center", d3.forceCenter(width / 2, height / 2));
const link = svg.append("g")
.attr("stroke", "var(--border-color)")
.attr("stroke-opacity", 0.6)
.selectAll("line")
.data(data.links)
.join("line")
.attr("stroke-width", d => Math.sqrt(d.value));
const node = svg.append("g")
.attr("stroke", "#fff")
.attr("stroke-width", 1.5)
.selectAll("circle")
.data(data.nodes)
.join("circle")
.attr("r", 8)
.attr("fill", d => {
if (d.type === 'fact') return '#ff4444';
if (d.type === 'conversation') return '#44ff44';
return '#4444ff';
})
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
const tooltip = d3.select("#graph-tooltip");
node.on("mouseover", (event, d) => {
tooltip.style("display", "block")
.html("<strong>" + d.type.toUpperCase() + "</strong><br>" + d.content)
.style("left", (event.pageX - 250) + "px")
.style("top", (event.pageY - 100) + "px");
}).on("mouseout", () => {
tooltip.style("display", "none");
});
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);
});
function dragstarted(event) {
if (!event.active) simulation.alphaTarget(0.3).restart();
event.subject.fx = event.subject.x;
event.subject.fy = event.subject.y;
}
function dragged(event) {
event.subject.fx = event.x;
event.subject.fy = event.y;
}
function dragended(event) {
if (!event.active) simulation.alphaTarget(0);
event.subject.fx = null;
event.subject.fy = null;
}
}
function deleteFact(id) {
function deleteFact(id) {
if (!confirm('Delete this fact?')) return;
fetch('/memory/fact/' + id, { method: 'DELETE' })