185 lines
5.4 KiB
Python
185 lines
5.4 KiB
Python
"""
|
|
graph_view.py - Generiert interaktive Graph-Visualisierung (Cytoscape.js).
|
|
"""
|
|
|
|
import json
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
from .store import EngramStore
|
|
|
|
|
|
_HTML_TEMPLATE = """<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>Second Brain Graph</title>
|
|
<script src="https://unpkg.com/cytoscape@3.26.0/dist/cytoscape.min.js"></script>
|
|
<style>
|
|
body {{ margin:0; padding:0; background:#1a1a2e; color:#eee; font-family: sans-serif; }}
|
|
#cy {{ width: 100vw; height: 100vh; }}
|
|
#info {{ position: absolute; top: 10px; left: 10px; background: rgba(0,0,0,0.8); padding: 15px; border-radius: 8px; max-width: 300px; }}
|
|
#controls {{ position: absolute; bottom: 10px; left: 10px; }}
|
|
.btn {{ background: #e94560; border: none; color: white; padding: 8px 16px; border-radius: 4px; cursor: pointer; margin-right: 5px; }}
|
|
.filter {{ background: #0f3460; border: none; color: white; padding: 6px 12px; border-radius: 4px; margin-right: 5px; cursor: pointer; }}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="info">
|
|
<h3>🧠 Second Brain Graph</h3>
|
|
<p>Knoten: Engramme (Farbe = Confidence)</p>
|
|
<p>Grün=hoch, Gelb=mittel, Rot=niedrig</p>
|
|
<p>Links: Verknüpfungen</p>
|
|
<p><strong>Klicke</strong> für Details</p>
|
|
</div>
|
|
<div id="controls">
|
|
<button class="btn" onclick="cy.fit()">Fit</button>
|
|
<button class="filter" onclick="filterHigh()">Nur High-Conf</button>
|
|
<button class="filter" onclick="filterConfirmed()">Nur Confirmed</button>
|
|
<button class="filter" onclick="showAll()">Alle</button>
|
|
</div>
|
|
<div id="cy"></div>
|
|
<script>
|
|
var cy = cytoscape({{
|
|
container: document.getElementById('cy'),
|
|
elements: {elements_json},
|
|
style: [
|
|
{{ selector: 'node', style: {{
|
|
'background-color': 'data(color)',
|
|
'width': 'data(size)',
|
|
'height': 'data(size)',
|
|
'label': 'data(label)',
|
|
'color': '#fff',
|
|
'font-size': '10px',
|
|
'text-outline-color': '#000',
|
|
'text-outline-width': 1,
|
|
'border-width': 2,
|
|
'border-color': '#333'
|
|
}} }},
|
|
{{ selector: 'edge', style: {{
|
|
'width': 2,
|
|
'line-color': '#555',
|
|
'target-arrow-color': '#555',
|
|
'target-arrow-shape': 'triangle',
|
|
'curve-style': 'bezier'
|
|
}} }},
|
|
{{ selector: '.highlighted', style: {{
|
|
'border-color': '#e94560',
|
|
'border-width': 4
|
|
}} }}
|
|
],
|
|
layout: {{
|
|
name: 'cose',
|
|
idealEdgeLength: 100,
|
|
nodeOverlap: 20,
|
|
refresh: 20,
|
|
fit: true,
|
|
padding: 30,
|
|
randomize: false,
|
|
componentSpacing: 100,
|
|
nodeRepulsion: 400000,
|
|
edgeElasticity: 100,
|
|
nestingFactor: 5,
|
|
gravity: 80,
|
|
numIter: 1000,
|
|
initialTemp: 200,
|
|
coolingFactor: 0.95,
|
|
minTemp: 1.0
|
|
}}
|
|
}});
|
|
|
|
cy.on('tap', 'node', function(evt){{
|
|
var node = evt.target;
|
|
var info = document.getElementById('info');
|
|
info.innerHTML = '<h3>#' + node.id() + '</h3>'
|
|
+ '<p><strong>' + node.data('title') + '</strong></p>'
|
|
+ '<p>Confidence: ' + node.data('confidence').toFixed(2) + '</p>'
|
|
+ '<p>Confirmed: ' + (node.data('confirmed') ? '✅' : '❓') + '</p>'
|
|
+ '<p>Source: ' + node.data('source') + '</p>'
|
|
+ '<p>Tags: ' + node.data('tags') + '</p>';
|
|
}});
|
|
|
|
function filterHigh(){{
|
|
cy.elements().hide();
|
|
cy.nodes().filter(function(n){{ return n.data('confidence') >= 0.7; }}).show();
|
|
cy.edges().filter(function(e){{ return e.source().visible() && e.target().visible(); }}).show();
|
|
}}
|
|
|
|
function filterConfirmed(){{
|
|
cy.elements().hide();
|
|
cy.nodes().filter(function(n){{ return n.data('confirmed'); }}).show();
|
|
cy.edges().filter(function(e){{ return e.source().visible() && e.target().visible(); }}).show();
|
|
}}
|
|
|
|
function showAll(){{
|
|
cy.elements().show();
|
|
}}
|
|
</script>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
|
|
def _confidence_color(conf: float) -> str:
|
|
if conf >= 0.8:
|
|
return "#27ae60" # Green
|
|
elif conf >= 0.5:
|
|
return "#f39c12" # Yellow
|
|
else:
|
|
return "#e74c3c" # Red
|
|
|
|
|
|
def _node_size(access_count: int) -> float:
|
|
return max(20, min(60, 20 + access_count * 5))
|
|
|
|
|
|
def generate_graph_html(store: EngramStore, output_path: str) -> str:
|
|
"""Generiert interaktive HTML-Graph-Visualisierung."""
|
|
engrams = store.get_all()
|
|
|
|
nodes = []
|
|
edges = []
|
|
node_ids = set()
|
|
|
|
for eg in engrams:
|
|
eid = str(eg.id)
|
|
conf = eg.compute_confidence()
|
|
color = _confidence_color(conf)
|
|
size = _node_size(eg.metadata.get("access_count", 0))
|
|
tags = ", ".join(eg.metadata.get("tags", []))
|
|
|
|
nodes.append({
|
|
"data": {
|
|
"id": eid,
|
|
"label": eg.content[:40] + ("..." if len(eg.content) > 40 else ""),
|
|
"title": eg.content,
|
|
"color": color,
|
|
"size": size,
|
|
"confidence": conf,
|
|
"confirmed": eg.correctness.confirmed,
|
|
"source": eg.metadata.get("source", "?"),
|
|
"tags": tags,
|
|
}
|
|
})
|
|
node_ids.add(eid)
|
|
|
|
for lid in eg.links:
|
|
lid_s = str(lid)
|
|
if lid_s in node_ids:
|
|
edges.append({
|
|
"data": {
|
|
"id": f"{eid}_{lid_s}",
|
|
"source": eid,
|
|
"target": lid_s,
|
|
}
|
|
})
|
|
|
|
elements = {"nodes": nodes, "edges": edges}
|
|
html = _HTML_TEMPLATE.format(elements_json=json.dumps(elements, ensure_ascii=False))
|
|
|
|
out = Path(output_path)
|
|
out.parent.mkdir(parents=True, exist_ok=True)
|
|
with open(out, "w", encoding="utf-8") as f:
|
|
f.write(html)
|
|
|
|
return str(out)
|