feat: fullscreen graph layout, category legend, and node labels

- Graph container fills viewport (calc(100vh - 180px)) instead of 500px
- Node detail panel overlays graph as absolute-positioned panel
- Category legend with colored dots fixed in SVG top-left corner
- High-importance nodes (>= 0.7) show truncated content labels
This commit is contained in:
Viktor Barzin 2026-03-22 23:55:51 +02:00
parent 20f2f02dea
commit 44338d0eff
No known key found for this signature in database
GPG key ID: 0EB088298288D958
3 changed files with 45 additions and 4 deletions

View file

@ -280,7 +280,7 @@ textarea.input-field {
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 8px;
min-height: 500px;
height: calc(100vh - 180px);
}
.graph-container svg {

View file

@ -479,11 +479,11 @@
<span x-show="loading" class="spinner ml-2"></span>
</div>
<div class="flex gap-4">
<div class="flex-1 graph-container" x-ref="graphContainer"></div>
<div class="relative">
<div class="graph-container" x-ref="graphContainer"></div>
<template x-if="selectedNode">
<div class="node-detail w-80 shrink-0">
<div class="node-detail" style="position:absolute; top:1rem; right:1rem; width:20rem; z-index:10;">
<div class="flex items-center gap-2 mb-2">
<span class="text-xs" style="color: var(--text-muted); font-family: 'JetBrains Mono', monospace">#<span x-text="selectedNode.id"></span></span>
<span class="badge badge-category" x-text="selectedNode.category"></span>

View file

@ -141,6 +141,44 @@ function graphComponent() {
node.append('title').text(d => d.content ? d.content.substring(0, 60) : '');
// Node labels for high-importance memories
const labels = g.append('g')
.selectAll('text')
.data(nodes.filter(d => d.importance >= 0.7))
.join('text')
.attr('font-size', '10px')
.attr('fill', '#c4b8a8')
.attr('font-family', "'JetBrains Mono', monospace")
.attr('pointer-events', 'none')
.text(d => d.content ? d.content.substring(0, 25) + '\u2026' : '');
// Legend (appended to svg, not g, so it stays fixed during zoom/pan)
const legend = svg.append('g')
.attr('class', 'graph-legend')
.attr('transform', 'translate(16, 16)');
const cats = Object.entries(categoryColors);
cats.forEach(([cat, color], i) => {
const row = legend.append('g').attr('transform', `translate(0, ${i * 22})`);
row.append('circle').attr('r', 6).attr('cx', 6).attr('cy', 6).attr('fill', color);
row.append('text').attr('x', 18).attr('y', 10)
.attr('fill', '#c4b8a8')
.attr('font-size', '12px')
.attr('font-family', "'JetBrains Mono', monospace")
.text(cat);
});
// Semi-transparent background behind legend
const legendBBox = legend.node().getBBox();
legend.insert('rect', ':first-child')
.attr('x', legendBBox.x - 8)
.attr('y', legendBBox.y - 8)
.attr('width', legendBBox.width + 16)
.attr('height', legendBBox.height + 16)
.attr('rx', 6)
.attr('fill', '#1a1613')
.attr('fill-opacity', 0.85);
simulation.on('tick', () => {
link
.attr('x1', d => d.source.x)
@ -150,6 +188,9 @@ function graphComponent() {
node
.attr('cx', d => d.x)
.attr('cy', d => d.y);
labels
.attr('x', d => d.x + 8 + d.importance * 15)
.attr('y', d => d.y + 4);
});
},