Add memory graph visualization UI
This commit is contained in:
@@ -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' })
|
||||
|
||||
Reference in New Issue
Block a user