feat(complete): Phase 2-5 - Vektor-Embeddings, ChromaDB, Neural Scorer, Streamlit Dashboard, Graph-Visualisierung

This commit is contained in:
2026-05-25 09:43:04 +02:00
parent 08d21f8087
commit 59f4059cd8
6 changed files with 842 additions and 2 deletions

184
src/graph_view.py Normal file
View File

@@ -0,0 +1,184 @@
"""
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)