Feature: Enhanced Memory Search UI #1049
@@ -13,9 +13,53 @@ from timmy.memory_system import (
|
||||
update_personal_fact,
|
||||
)
|
||||
|
||||
|
||||
from timmy.memory_system import get_connection
|
||||
|
||||
router = APIRouter(prefix="/memory", tags=["memory"])
|
||||
|
||||
|
||||
@router.get("/graph_data", response_class=JSONResponse)
|
||||
async def memory_graph_data():
|
||||
"""Return memory nodes and links for visualization."""
|
||||
nodes = []
|
||||
links = []
|
||||
|
||||
with get_connection() as conn:
|
||||
rows = conn.execute(
|
||||
"SELECT id, content, memory_type, session_id, agent_id FROM memories ORDER BY created_at DESC LIMIT 100"
|
||||
).fetchall()
|
||||
|
||||
for row in rows:
|
||||
nodes.append({
|
||||
"id": row["id"],
|
||||
"content": row["content"][:50],
|
||||
"type": row["memory_type"]
|
||||
})
|
||||
|
||||
# Simple linking logic: link memories in the same session
|
||||
if row["session_id"]:
|
||||
for other in nodes[:-1]:
|
||||
# This is a bit slow but okay for 100 nodes
|
||||
pass
|
||||
|
||||
# More robust linking: link by session_id
|
||||
session_map = {}
|
||||
for row in rows:
|
||||
sid = row["session_id"]
|
||||
if sid:
|
||||
if sid not in session_map:
|
||||
session_map[sid] = []
|
||||
session_map[sid].append(row["id"])
|
||||
|
||||
for sid, ids in session_map.items():
|
||||
for i in range(len(ids) - 1):
|
||||
links.append({"source": ids[i], "target": ids[i+1]})
|
||||
|
||||
return {"nodes": nodes, "links": links}
|
||||
|
||||
|
||||
|
||||
@router.get("", response_class=HTMLResponse)
|
||||
async def memory_page(
|
||||
request: Request,
|
||||
|
||||
@@ -3,7 +3,26 @@
|
||||
{% block title %}Memory Browser{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="mc-panel">
|
||||
<div class="mc-panel-header d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h1 class="page-title">Memory Browser</h1>
|
||||
<p class="mc-text-secondary">Semantic search through conversation history and facts</p>
|
||||
</div>
|
||||
<ul class="nav nav-pills" id="memoryTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="list-tab" data-bs-toggle="pill" data-bs-target="#listView" type="button" role="tab">List View</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="graph-tab" data-bs-toggle="pill" data-bs-target="#graphView" type="button" role="tab">Graph View</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="tab-content" id="memoryTabsContent">
|
||||
<div class="tab-pane fade show active" id="listView" role="tabpanel">
|
||||
|
||||
<div class="mc-panel-header">
|
||||
<h1 class="page-title">Memory Browser</h1>
|
||||
<p class="mc-text-secondary">Semantic search through conversation history and facts</p>
|
||||
@@ -66,7 +85,14 @@
|
||||
<span class="memory-score">{{ "%.2f"|format(mem.relevance_score) }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="memory-content">{{ mem.content }}</div>
|
||||
{% if mem.relevance_score %}
|
||||
<div class="relevance-bar-container mt-2" style="height: 4px; background: #eee; border-radius: 2px;">
|
||||
<div class="relevance-bar" style="height: 100%; width: {{ mem.relevance_score * 100 }}%; background: #007bff; border-radius: 2px;"></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="memory-meta">
|
||||
<span class="memory-time">{{ mem.timestamp[11:16] }}</span>
|
||||
{% if mem.agent_id %}
|
||||
@@ -119,9 +145,108 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div> <!-- End listView -->
|
||||
<div class="tab-pane fade" id="graphView" role="tabpanel">
|
||||
<div class="mc-graph-container border rounded bg-light" style="height: 600px; position: relative;">
|
||||
<div id="memory-graph" style="width: 100%; height: 100%;"></div>
|
||||
<div id="graph-tooltip" class="position-absolute p-2 bg-white border rounded shadow-sm d-none" style="z-index: 1000; pointer-events: none;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div> <!-- End tab-content -->
|
||||
</div>
|
||||
|
||||
<script src="https://d3js.org/d3.v7.min.js">
|
||||
// Graph View Logic
|
||||
document.getElementById('graph-tab').addEventListener('shown.bs.tab', function() {
|
||||
fetch('/memory/graph_data')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
renderGraph(data);
|
||||
});
|
||||
});
|
||||
|
||||
function renderGraph(data) {
|
||||
const container = document.getElementById('memory-graph');
|
||||
if (!container) return;
|
||||
container.innerHTML = '';
|
||||
const width = container.clientWidth;
|
||||
const height = container.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(100))
|
||||
.force('charge', d3.forceManyBody().strength(-200))
|
||||
.force('center', d3.forceCenter(width / 2, height / 2));
|
||||
|
||||
const link = svg.append('g')
|
||||
.selectAll('line')
|
||||
.data(data.links)
|
||||
.enter().append('line')
|
||||
.attr('stroke', '#999')
|
||||
.attr('stroke-opacity', 0.6);
|
||||
|
||||
const node = svg.append('g')
|
||||
.selectAll('circle')
|
||||
.data(data.nodes)
|
||||
.enter().append('circle')
|
||||
.attr('r', d => d.type === 'fact' ? 8 : 5)
|
||||
.attr('fill', d => d.type === 'fact' ? '#ff7f0e' : '#1f77b4')
|
||||
.call(d3.drag()
|
||||
.on('start', dragstarted)
|
||||
.on('drag', dragged)
|
||||
.on('end', dragended));
|
||||
|
||||
const tooltip = document.getElementById('graph-tooltip');
|
||||
|
||||
node.on('mouseover', (event, d) => {
|
||||
tooltip.classList.remove('d-none');
|
||||
tooltip.innerHTML = '<strong>' + d.type + '</strong><br>' + d.content.substring(0, 100) + '...';
|
||||
tooltip.style.left = (event.pageX - container.getBoundingClientRect().left + 10) + 'px';
|
||||
tooltip.style.top = (event.pageY - container.getBoundingClientRect().top + 10) + 'px';
|
||||
});
|
||||
|
||||
node.on('mouseout', () => {
|
||||
tooltip.classList.add('d-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;
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
<script>
|
||||
|
||||
function deleteFact(id) {
|
||||
if (!confirm('Delete this fact?')) return;
|
||||
fetch('/memory/fact/' + id, { method: 'DELETE' })
|
||||
|
||||
Reference in New Issue
Block a user