Build second brain graph 2.0 view
This commit is contained in:
@@ -630,10 +630,14 @@ def _graph_payload_from_rows(rows: list[sqlite3.Row], link_rows: list[sqlite3.Ro
|
|||||||
|
|
||||||
content = (r["content"] or "").strip()
|
content = (r["content"] or "").strip()
|
||||||
source = str(meta.get("source", "unknown") or "unknown")
|
source = str(meta.get("source", "unknown") or "unknown")
|
||||||
|
tags = [t for t in _safe_json_extract_tags(r["metadata_json"]) if t.strip()]
|
||||||
|
primary_cluster = tags[0] if tags else source
|
||||||
add_node(eid, "engram", label=(content[:54] or eid[:8]), weight=float(meta.get("access_count", 0) or 0))
|
add_node(eid, "engram", label=(content[:54] or eid[:8]), weight=float(meta.get("access_count", 0) or 0))
|
||||||
nodes[eid].update(
|
nodes[eid].update(
|
||||||
{
|
{
|
||||||
"source": source,
|
"source": source,
|
||||||
|
"cluster": primary_cluster,
|
||||||
|
"tags": tags[:8],
|
||||||
"confidence": float(meta.get("confidence", 0.0) or 0.0),
|
"confidence": float(meta.get("confidence", 0.0) or 0.0),
|
||||||
"created": meta.get("created", r["created_at"]),
|
"created": meta.get("created", r["created_at"]),
|
||||||
"modified": meta.get("modified", r["modified_at"]),
|
"modified": meta.get("modified", r["modified_at"]),
|
||||||
@@ -646,17 +650,20 @@ def _graph_payload_from_rows(rows: list[sqlite3.Row], link_rows: list[sqlite3.Ro
|
|||||||
|
|
||||||
sid = f"source:{source}"
|
sid = f"source:{source}"
|
||||||
add_node(sid, "source", label=source, weight=5)
|
add_node(sid, "source", label=source, weight=5)
|
||||||
|
nodes[sid]["cluster"] = source
|
||||||
add_edge(eid, sid, "from_source", 0.45)
|
add_edge(eid, sid, "from_source", 0.45)
|
||||||
|
|
||||||
for t in _safe_json_extract_tags(r["metadata_json"]):
|
for t in tags:
|
||||||
tid = f"tag:{t}"
|
tid = f"tag:{t}"
|
||||||
add_node(tid, "tag", label=t, weight=2)
|
add_node(tid, "tag", label=t, weight=2)
|
||||||
|
nodes[tid]["cluster"] = t
|
||||||
add_edge(eid, tid, "has_tag", 0.35)
|
add_edge(eid, tid, "has_tag", 0.35)
|
||||||
|
|
||||||
host = _host_from_meta(r["metadata_json"])
|
host = _host_from_meta(r["metadata_json"])
|
||||||
if host:
|
if host:
|
||||||
hid = f"host:{host}"
|
hid = f"host:{host}"
|
||||||
add_node(hid, "host", label=host, weight=3)
|
add_node(hid, "host", label=host, weight=3)
|
||||||
|
nodes[hid]["cluster"] = source
|
||||||
add_edge(eid, hid, "grounded_at", 0.25)
|
add_edge(eid, hid, "grounded_at", 0.25)
|
||||||
|
|
||||||
for lr in link_rows:
|
for lr in link_rows:
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.app {
|
.app {
|
||||||
max-width: 480px;
|
max-width: 1180px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: #141419;
|
background: radial-gradient(circle at 50% 12%, #162238 0%, #11131d 42%, #0b0d13 100%);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,11 +165,14 @@ body {
|
|||||||
/* Graph canvas */
|
/* Graph canvas */
|
||||||
#graphCanvas{
|
#graphCanvas{
|
||||||
display:block;
|
display:block;
|
||||||
margin: 8px auto 0;
|
margin: 10px auto 0;
|
||||||
background:#02040a;
|
background:#01030a;
|
||||||
border:1px solid #172033;
|
border:0;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
box-shadow: 0 0 28px rgba(34,211,238,0.08), inset 0 0 42px rgba(124,58,237,0.08);
|
box-shadow:
|
||||||
|
0 0 48px rgba(34,211,238,0.14),
|
||||||
|
0 0 120px rgba(236,72,153,0.08),
|
||||||
|
inset 0 0 56px rgba(124,58,237,0.10);
|
||||||
touch-action: none;
|
touch-action: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,8 +184,8 @@ body {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
.graph-controls .btn{
|
.graph-controls .btn{
|
||||||
background:#1e1e28;
|
background:rgba(12,18,31,0.88);
|
||||||
border:1px solid #2a2a3a;
|
border:1px solid rgba(74,94,130,0.55);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
color:#cfd3ff;
|
color:#cfd3ff;
|
||||||
@@ -194,19 +197,19 @@ body {
|
|||||||
box-shadow:0 0 0 1px rgba(108,138,245,0.18) inset;
|
box-shadow:0 0 0 1px rgba(108,138,245,0.18) inset;
|
||||||
}
|
}
|
||||||
.graph-mode{
|
.graph-mode{
|
||||||
color:#8ef6e4;
|
color:#a7f3d0;
|
||||||
font-size:0.78rem;
|
font-size:0.78rem;
|
||||||
font-weight:700;
|
font-weight:700;
|
||||||
padding: 6px 8px;
|
padding: 6px 8px;
|
||||||
border:1px solid #173c42;
|
border:1px solid rgba(52,211,153,0.35);
|
||||||
background:#07151b;
|
background:rgba(4,18,20,0.82);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
.graph-legend{
|
.graph-legend{
|
||||||
margin: 8px 12px 0;
|
margin: 8px 12px 0;
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
background:#1a1a24;
|
background:rgba(11,15,25,0.72);
|
||||||
border:1px solid #252533;
|
border:1px solid rgba(65,78,112,0.4);
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
color:#b9b9c9;
|
color:#b9b9c9;
|
||||||
font-size:0.8rem;
|
font-size:0.8rem;
|
||||||
@@ -222,12 +225,45 @@ body {
|
|||||||
.graph-live{
|
.graph-live{
|
||||||
margin: 8px 12px 0;
|
margin: 8px 12px 0;
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
background:#101820;
|
background:rgba(5,12,20,0.78);
|
||||||
border:1px solid #22303d;
|
border:1px solid rgba(56,189,248,0.22);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
color:#bfe8df;
|
color:#bfe8df;
|
||||||
font-size:0.78rem;
|
font-size:0.78rem;
|
||||||
}
|
}
|
||||||
|
.graph-insights{
|
||||||
|
display:grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||||
|
gap: 8px;
|
||||||
|
margin: 8px 12px 0;
|
||||||
|
}
|
||||||
|
.graph-chip{
|
||||||
|
min-height: 44px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border: 1px solid rgba(80,96,130,0.42);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(10,15,26,0.72);
|
||||||
|
color: #dbeafe;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.graph-chip b{
|
||||||
|
display:block;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.graph-chip span{
|
||||||
|
display:block;
|
||||||
|
color:#8aa0c7;
|
||||||
|
font-size:0.72rem;
|
||||||
|
margin-top:2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 560px) {
|
||||||
|
.app { max-width: 100%; }
|
||||||
|
.graph-controls .btn { padding: 8px 9px; }
|
||||||
|
}
|
||||||
.graph-live-title{
|
.graph-live-title{
|
||||||
color:#e8fffb;
|
color:#e8fffb;
|
||||||
font-weight:700;
|
font-weight:700;
|
||||||
|
|||||||
@@ -67,16 +67,17 @@
|
|||||||
<option value="5000">Nodes: 5000</option>
|
<option value="5000">Nodes: 5000</option>
|
||||||
</select>
|
</select>
|
||||||
<button class="btn" onclick="reloadGraph()">Reload</button>
|
<button class="btn" onclick="reloadGraph()">Reload</button>
|
||||||
<span class="graph-mode">Live physics on</span>
|
<span class="graph-mode">Graph 2.0 live</span>
|
||||||
</div>
|
</div>
|
||||||
<canvas id="graphCanvas" width="440" height="520"></canvas>
|
<canvas id="graphCanvas" width="440" height="520"></canvas>
|
||||||
<div class="graph-hint muted small" id="graphHint">Lade Graph…</div>
|
<div class="graph-hint muted small" id="graphHint">Lade Graph…</div>
|
||||||
|
<div class="graph-insights" id="graphInsights"></div>
|
||||||
<div class="graph-live" id="graphLive">
|
<div class="graph-live" id="graphLive">
|
||||||
<div class="graph-live-title">Live</div>
|
<div class="graph-live-title">Live</div>
|
||||||
<div id="graphLiveFeed" class="graph-live-feed"></div>
|
<div id="graphLiveFeed" class="graph-live-feed"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="graph-legend">
|
<div class="graph-legend">
|
||||||
<div><strong>Graph</strong>: Live Cluster wie Obsidian. Zoom per Pinch, Pan per Drag. Tap auf Engram öffnet Details, Tap auf Tag setzt Suche.</div>
|
<div><strong>Graph 2.0</strong>: Topic-Cluster, Brücken und Live-Deltas. Zoom per Pinch, Pan per Drag. Tap auf Engram öffnet Details, Tap auf Tag setzt Suche.</div>
|
||||||
<div class="legend-row"><span class="legend-dot engram"></span> Engram</div>
|
<div class="legend-row"><span class="legend-dot engram"></span> Engram</div>
|
||||||
<div class="legend-row"><span class="legend-dot tag"></span> Tag</div>
|
<div class="legend-row"><span class="legend-dot tag"></span> Tag</div>
|
||||||
<div class="legend-row"><span class="legend-dot source"></span> Quelle</div>
|
<div class="legend-row"><span class="legend-dot source"></span> Quelle</div>
|
||||||
@@ -348,7 +349,7 @@ async function loadGraph() {
|
|||||||
_graphDraw();
|
_graphDraw();
|
||||||
try {
|
try {
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
const chunkSize = maxEngrams && maxEngrams <= 1000 ? 400 : 900;
|
const chunkSize = maxEngrams && maxEngrams <= 1000 ? 500 : 1800;
|
||||||
while (token === graphState.loadingToken) {
|
while (token === graphState.loadingToken) {
|
||||||
const limit = maxEngrams ? Math.min(chunkSize, Math.max(0, maxEngrams - offset)) : chunkSize;
|
const limit = maxEngrams ? Math.min(chunkSize, Math.max(0, maxEngrams - offset)) : chunkSize;
|
||||||
if (limit <= 0) break;
|
if (limit <= 0) break;
|
||||||
@@ -420,7 +421,10 @@ let graphState = {
|
|||||||
edgeKeys: new Set(),
|
edgeKeys: new Set(),
|
||||||
sourceIndex: new Map(),
|
sourceIndex: new Map(),
|
||||||
sourceCounts: new Map(),
|
sourceCounts: new Map(),
|
||||||
|
clusterIndex: new Map(),
|
||||||
|
clusterCounts: new Map(),
|
||||||
nextSourceIndex: 0,
|
nextSourceIndex: 0,
|
||||||
|
nextClusterIndex: 0,
|
||||||
loadingToken: 0,
|
loadingToken: 0,
|
||||||
totalEngrams: 0,
|
totalEngrams: 0,
|
||||||
loadedEngrams: 0,
|
loadedEngrams: 0,
|
||||||
@@ -454,7 +458,10 @@ function _graphResetData() {
|
|||||||
graphState.edgeKeys = new Set();
|
graphState.edgeKeys = new Set();
|
||||||
graphState.sourceIndex = new Map();
|
graphState.sourceIndex = new Map();
|
||||||
graphState.sourceCounts = new Map();
|
graphState.sourceCounts = new Map();
|
||||||
|
graphState.clusterIndex = new Map();
|
||||||
|
graphState.clusterCounts = new Map();
|
||||||
graphState.nextSourceIndex = 0;
|
graphState.nextSourceIndex = 0;
|
||||||
|
graphState.nextClusterIndex = 0;
|
||||||
graphState.totalEngrams = 0;
|
graphState.totalEngrams = 0;
|
||||||
graphState.loadedEngrams = 0;
|
graphState.loadedEngrams = 0;
|
||||||
graphState.lastModified = null;
|
graphState.lastModified = null;
|
||||||
@@ -477,18 +484,32 @@ function _graphHashUnit(s) {
|
|||||||
|
|
||||||
function _graphPalette(seed) {
|
function _graphPalette(seed) {
|
||||||
const palette = [
|
const palette = [
|
||||||
[34, 211, 238], // cyan
|
[34, 211, 238], [236, 72, 153], [168, 85, 247],
|
||||||
[244, 114, 182], // pink
|
[74, 222, 128], [248, 113, 113], [96, 165, 250],
|
||||||
[168, 85, 247], // purple
|
[251, 191, 36], [45, 212, 191], [250, 204, 21],
|
||||||
[74, 222, 128], // green
|
[129, 140, 248], [251, 113, 133], [52, 211, 153],
|
||||||
[248, 113, 113], // red
|
|
||||||
[96, 165, 250], // blue
|
|
||||||
[251, 191, 36], // amber
|
|
||||||
[45, 212, 191], // teal
|
|
||||||
];
|
];
|
||||||
return palette[Math.floor(_graphHashUnit(seed) * palette.length) % palette.length];
|
return palette[Math.floor(_graphHashUnit(seed) * palette.length) % palette.length];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _graphClusterKey(n) {
|
||||||
|
if (!n) return 'unknown';
|
||||||
|
const raw = n.cluster || (Array.isArray(n.tags) && n.tags[0]) || n.source || n.label || n.id || 'unknown';
|
||||||
|
return String(raw || 'unknown').slice(0, 80);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _graphClusterCenter(cluster) {
|
||||||
|
const key = cluster || 'unknown';
|
||||||
|
if (!graphState.clusterIndex.has(key)) {
|
||||||
|
graphState.clusterIndex.set(key, graphState.nextClusterIndex++);
|
||||||
|
}
|
||||||
|
const i = graphState.clusterIndex.get(key);
|
||||||
|
if (i === 0) return {x: 0, y: 0};
|
||||||
|
const angle = i * 2.399963 + _graphHashUnit(key) * 0.85;
|
||||||
|
const ring = 190 + Math.sqrt(i) * 78;
|
||||||
|
return {x: Math.cos(angle) * ring, y: Math.sin(angle) * ring};
|
||||||
|
}
|
||||||
|
|
||||||
function _graphSourceCenter(source) {
|
function _graphSourceCenter(source) {
|
||||||
const key = source || 'unknown';
|
const key = source || 'unknown';
|
||||||
if (!graphState.sourceIndex.has(key)) {
|
if (!graphState.sourceIndex.has(key)) {
|
||||||
@@ -505,28 +526,30 @@ function _graphSourceCenter(source) {
|
|||||||
|
|
||||||
function _graphPlaceNode(n) {
|
function _graphPlaceNode(n) {
|
||||||
if (n.kind === 'source') {
|
if (n.kind === 'source') {
|
||||||
return _graphSourceCenter((n.label || n.id || '').replace(/^source:/, ''));
|
const p = _graphSourceCenter((n.label || n.id || '').replace(/^source:/, ''));
|
||||||
|
return {x: p.x * 0.55, y: p.y * 0.55};
|
||||||
}
|
}
|
||||||
if (n.kind === 'tag') {
|
if (n.kind === 'tag') {
|
||||||
|
const p = _graphClusterCenter(n.label || n.id);
|
||||||
const angle = _graphHashUnit(n.id) * Math.PI * 2;
|
const angle = _graphHashUnit(n.id) * Math.PI * 2;
|
||||||
const ring = 470 + (_graphHashUnit(n.id + ':r') * 260);
|
const ring = 24 + (_graphHashUnit(n.id + ':r') * 54);
|
||||||
return {x: Math.cos(angle) * ring, y: Math.sin(angle) * ring};
|
return {x: p.x + Math.cos(angle) * ring, y: p.y + Math.sin(angle) * ring};
|
||||||
}
|
}
|
||||||
if (n.kind === 'host') {
|
if (n.kind === 'host') {
|
||||||
const angle = _graphHashUnit(n.id) * Math.PI * 2;
|
const angle = _graphHashUnit(n.id) * Math.PI * 2;
|
||||||
const ring = 640 + (_graphHashUnit(n.id + ':r') * 220);
|
const ring = 540 + (_graphHashUnit(n.id + ':r') * 260);
|
||||||
return {x: Math.cos(angle) * ring, y: Math.sin(angle) * ring};
|
return {x: Math.cos(angle) * ring, y: Math.sin(angle) * ring};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Obsidian-like "brain": each source gets a lobe, entries fill it as a
|
// Graph 2.0: nodes live in topic lobes. This gives an InfraNodus-like
|
||||||
// golden-angle cloud so 50k+ nodes appear immediately without edge layout.
|
// overview immediately, before expensive edge physics has any work to do.
|
||||||
const source = n.source || 'unknown';
|
const cluster = _graphClusterKey(n);
|
||||||
const local = (graphState.sourceCounts.get(source) || 0) + 1;
|
const local = (graphState.clusterCounts.get(cluster) || 0) + 1;
|
||||||
graphState.sourceCounts.set(source, local);
|
graphState.clusterCounts.set(cluster, local);
|
||||||
const center = _graphSourceCenter(source);
|
const center = _graphClusterCenter(cluster);
|
||||||
const angle = local * 2.399963 + _graphHashUnit(n.id) * 0.7;
|
const angle = local * 2.399963 + _graphHashUnit(n.id) * 0.9;
|
||||||
const radius = 18 + Math.sqrt(local) * 7.5 + _graphHashUnit(n.id + ':r') * 28;
|
const radius = 14 + Math.sqrt(local) * 6.6 + _graphHashUnit(n.id + ':r') * 30;
|
||||||
const squash = 0.94 + (_graphHashUnit(source) - 0.5) * 0.16;
|
const squash = 0.9 + (_graphHashUnit(cluster) - 0.5) * 0.24;
|
||||||
return {
|
return {
|
||||||
x: center.x + Math.cos(angle) * radius * squash,
|
x: center.x + Math.cos(angle) * radius * squash,
|
||||||
y: center.y + Math.sin(angle) * radius / squash,
|
y: center.y + Math.sin(angle) * radius / squash,
|
||||||
@@ -546,6 +569,8 @@ function _graphEnsureSimNode(n) {
|
|||||||
modified: n.modified ?? existing.modified,
|
modified: n.modified ?? existing.modified,
|
||||||
last_accessed: n.last_accessed ?? existing.last_accessed,
|
last_accessed: n.last_accessed ?? existing.last_accessed,
|
||||||
source: n.source ?? existing.source,
|
source: n.source ?? existing.source,
|
||||||
|
cluster: n.cluster ?? existing.cluster,
|
||||||
|
tags: n.tags ?? existing.tags,
|
||||||
predict_locked: n.predict_locked ?? existing.predict_locked,
|
predict_locked: n.predict_locked ?? existing.predict_locked,
|
||||||
createdMs: Date.parse(n.created || existing.created || '') || existing.createdMs || 0,
|
createdMs: Date.parse(n.created || existing.created || '') || existing.createdMs || 0,
|
||||||
modifiedMs: Date.parse(n.modified || existing.modified || '') || existing.modifiedMs || 0,
|
modifiedMs: Date.parse(n.modified || existing.modified || '') || existing.modifiedMs || 0,
|
||||||
@@ -567,6 +592,8 @@ function _graphEnsureSimNode(n) {
|
|||||||
modifiedMs: Date.parse(n.modified || '') || 0,
|
modifiedMs: Date.parse(n.modified || '') || 0,
|
||||||
last_accessed: n.last_accessed,
|
last_accessed: n.last_accessed,
|
||||||
source: n.source,
|
source: n.source,
|
||||||
|
cluster: n.cluster,
|
||||||
|
tags: n.tags,
|
||||||
predict_locked: n.predict_locked,
|
predict_locked: n.predict_locked,
|
||||||
x: p.x,
|
x: p.x,
|
||||||
y: p.y,
|
y: p.y,
|
||||||
@@ -649,11 +676,11 @@ function _graphNodeFill(n) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (n.kind === 'engram') {
|
if (n.kind === 'engram') {
|
||||||
let base = _graphPalette(n.source || n.id);
|
let base = _graphPalette(_graphClusterKey(n));
|
||||||
const verdict = (n.verdict || '').toString();
|
const verdict = (n.verdict || '').toString();
|
||||||
if (verdict === 'confirmed_false') base = [248, 113, 113];
|
if (verdict === 'confirmed_false') base = [248, 113, 113];
|
||||||
else if (verdict === 'confirmed_true') {
|
else if (verdict === 'confirmed_true') {
|
||||||
const src = _graphPalette(n.source || n.id);
|
const src = _graphPalette(_graphClusterKey(n));
|
||||||
base = [Math.round((src[0] + 74) / 2), Math.round((src[1] + 222) / 2), Math.round((src[2] + 128) / 2)];
|
base = [Math.round((src[0] + 74) / 2), Math.round((src[1] + 222) / 2), Math.round((src[2] + 128) / 2)];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1107,21 +1134,21 @@ function _graphStepAmbient(alpha = 1.0) {
|
|||||||
if (!count) return;
|
if (!count) return;
|
||||||
// Keep the full 50k+ graph alive without doing an expensive all-node force
|
// Keep the full 50k+ graph alive without doing an expensive all-node force
|
||||||
// solve: tiny deterministic drift plus stronger movement on fresh/hub nodes.
|
// solve: tiny deterministic drift plus stronger movement on fresh/hub nodes.
|
||||||
const sample = Math.min(count, 900);
|
const sample = Math.min(count, 1400);
|
||||||
const start = Math.floor((t * 97) % count);
|
const start = Math.floor((t * 97) % count);
|
||||||
for (let k = 0; k < sample; k++) {
|
for (let k = 0; k < sample; k++) {
|
||||||
const n = sim[(start + k * 13) % count];
|
const n = sim[(start + k * 13) % count];
|
||||||
const amp = (n.kind === 'engram') ? 0.018 : 0.05;
|
const amp = (n.kind === 'engram') ? 0.026 : 0.07;
|
||||||
n.x += Math.sin(t * 0.7 + _graphHashUnit(n.id) * 6.283) * amp * alpha;
|
n.x += Math.sin(t * 0.7 + _graphHashUnit(n.id) * 6.283) * amp * alpha;
|
||||||
n.y += Math.cos(t * 0.6 + _graphHashUnit(n.id + ':y') * 6.283) * amp * alpha;
|
n.y += Math.cos(t * 0.6 + _graphHashUnit(n.id + ':y') * 6.283) * amp * alpha;
|
||||||
}
|
}
|
||||||
const recentCutoff = now - 12 * 60 * 1000;
|
const recentCutoff = now - 12 * 60 * 1000;
|
||||||
for (const n of sim) {
|
for (const n of sim) {
|
||||||
if (n.kind === 'source' || n.kind === 'tag' || (n.modifiedMs || n.createdMs || 0) > recentCutoff) {
|
if (n.kind === 'source' || n.kind === 'tag' || (n.modifiedMs || n.createdMs || 0) > recentCutoff) {
|
||||||
const home = n.kind === 'engram' ? _graphSourceCenter(n.source || 'unknown') : null;
|
const home = n.kind === 'engram' ? _graphClusterCenter(_graphClusterKey(n)) : (n.kind === 'tag' ? _graphClusterCenter(n.label || n.id) : null);
|
||||||
if (home) {
|
if (home) {
|
||||||
n.vx += (home.x - n.x) * 0.00003 * alpha;
|
n.vx += (home.x - n.x) * 0.00004 * alpha;
|
||||||
n.vy += (home.y - n.y) * 0.00003 * alpha;
|
n.vy += (home.y - n.y) * 0.00004 * alpha;
|
||||||
}
|
}
|
||||||
n.vx *= 0.92; n.vy *= 0.92;
|
n.vx *= 0.92; n.vy *= 0.92;
|
||||||
n.x += n.vx; n.y += n.vy;
|
n.x += n.vx; n.y += n.vy;
|
||||||
@@ -1133,8 +1160,38 @@ function _graphEdgeColor(kind) {
|
|||||||
const k = (kind || '').toString().toLowerCase();
|
const k = (kind || '').toString().toLowerCase();
|
||||||
if (k.includes('tag')) return '#7c3aed';
|
if (k.includes('tag')) return '#7c3aed';
|
||||||
if (k.includes('host')) return '#f59e0b';
|
if (k.includes('host')) return '#f59e0b';
|
||||||
|
if (k.includes('source')) return '#22d3ee';
|
||||||
if (k.includes('ref')) return '#10b981';
|
if (k.includes('ref')) return '#10b981';
|
||||||
return '#3a3a55';
|
return '#64748b';
|
||||||
|
}
|
||||||
|
|
||||||
|
function _graphDenseEdgeVisible(l) {
|
||||||
|
if (!l || !l.a || !l.b) return false;
|
||||||
|
if (l.kind === 'link') return true;
|
||||||
|
const da = graphState.degree.get(l.a.id) || 0;
|
||||||
|
const db = graphState.degree.get(l.b.id) || 0;
|
||||||
|
if (l.a.kind !== 'engram' || l.b.kind !== 'engram') return Math.max(da, db) >= 28;
|
||||||
|
const h = _graphHashUnit(`${l.a.id}:${l.b.id}:${l.kind}`);
|
||||||
|
return h > 0.982;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _graphRenderInsights() {
|
||||||
|
const el = document.getElementById('graphInsights');
|
||||||
|
if (!el) return;
|
||||||
|
const clusters = Array.from(graphState.clusterCounts.entries())
|
||||||
|
.sort((a, b) => b[1] - a[1])
|
||||||
|
.slice(0, 5);
|
||||||
|
const live = graphState.sim.filter(n => {
|
||||||
|
const ms = n.modifiedMs || n.createdMs || 0;
|
||||||
|
return ms && (graphState.drawNow - ms) < 15 * 60 * 1000;
|
||||||
|
}).length;
|
||||||
|
const html = [
|
||||||
|
`<div class="graph-chip"><b>${graphState.sim.length}</b><span>Knoten live</span></div>`,
|
||||||
|
`<div class="graph-chip"><b>${graphState.links.length}</b><span>Beziehungen</span></div>`,
|
||||||
|
`<div class="graph-chip"><b>${live}</b><span>aktiv / neu</span></div>`,
|
||||||
|
...clusters.map(([name, count]) => `<div class="graph-chip"><b>${escapeHtml(name)}</b><span>${count} Einträge</span></div>`),
|
||||||
|
].join('');
|
||||||
|
el.innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
function _graphDraw() {
|
function _graphDraw() {
|
||||||
@@ -1163,14 +1220,17 @@ function _graphDraw() {
|
|||||||
|
|
||||||
const term = (graphState.search || '').trim();
|
const term = (graphState.search || '').trim();
|
||||||
const dense = graphState.sim.length > 12000;
|
const dense = graphState.sim.length > 12000;
|
||||||
const drawEdges = !dense || term || graphState.selectedId;
|
let drawnEdges = 0;
|
||||||
if (drawEdges) for (const l of graphState.links) {
|
const maxDenseEdges = graphState.selectedId || term ? 5000 : 2400;
|
||||||
|
for (const l of graphState.links) {
|
||||||
const isMatchEdge = term && (_graphMatches(l.a, term) || _graphMatches(l.b, term));
|
const isMatchEdge = term && (_graphMatches(l.a, term) || _graphMatches(l.b, term));
|
||||||
const selectedEdge = graphState.selectedId && (l.a.id === graphState.selectedId || l.b.id === graphState.selectedId);
|
const selectedEdge = graphState.selectedId && (l.a.id === graphState.selectedId || l.b.id === graphState.selectedId);
|
||||||
if (dense && !isMatchEdge && !selectedEdge && l.kind !== 'link') continue;
|
if (dense && !isMatchEdge && !selectedEdge && !_graphDenseEdgeVisible(l)) continue;
|
||||||
|
if (dense && !isMatchEdge && !selectedEdge && drawnEdges >= maxDenseEdges) continue;
|
||||||
|
drawnEdges++;
|
||||||
const w = Math.max(0.2, Math.min(3.0, (l.weight || 1.0)));
|
const w = Math.max(0.2, Math.min(3.0, (l.weight || 1.0)));
|
||||||
ctx.lineWidth = (0.6 + w) / graphState.zoom;
|
ctx.lineWidth = (0.6 + w) / graphState.zoom;
|
||||||
ctx.globalAlpha = isMatchEdge || selectedEdge ? 0.9 : (dense ? 0.12 : (0.20 + Math.min(0.35, w * 0.18)));
|
ctx.globalAlpha = isMatchEdge || selectedEdge ? 0.9 : (dense ? 0.16 : (0.20 + Math.min(0.35, w * 0.18)));
|
||||||
ctx.strokeStyle = isMatchEdge ? '#f7d154' : _graphEdgeColor(l.kind);
|
ctx.strokeStyle = isMatchEdge ? '#f7d154' : _graphEdgeColor(l.kind);
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(l.a.x, l.a.y);
|
ctx.moveTo(l.a.x, l.a.y);
|
||||||
@@ -1249,12 +1309,16 @@ function _graphDraw() {
|
|||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(cx, cy, viewR, 0, Math.PI * 2);
|
ctx.arc(cx, cy, viewR, 0, Math.PI * 2);
|
||||||
ctx.strokeStyle = 'rgba(34, 211, 238, 0.18)';
|
ctx.strokeStyle = 'rgba(34, 211, 238, 0.05)';
|
||||||
ctx.lineWidth = 1.2;
|
ctx.lineWidth = 0.8;
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
const loaded = graphState.totalEngrams ? ` | engrams=${graphState.loadedEngrams}/${graphState.totalEngrams}` : '';
|
const loaded = graphState.totalEngrams ? ` | engrams=${graphState.loadedEngrams}/${graphState.totalEngrams}` : '';
|
||||||
hint.textContent = `nodes=${graphState.nodes.length} edges=${graphState.edges.length}${loaded}` + (term ? ` | match=${matches}` : '');
|
hint.textContent = `nodes=${graphState.nodes.length} edges=${graphState.edges.length}${loaded}` + (term ? ` | match=${matches}` : '');
|
||||||
|
if (!graphState._lastInsightsAt || graphState.drawNow - graphState._lastInsightsAt > 1500) {
|
||||||
|
graphState._lastInsightsAt = graphState.drawNow;
|
||||||
|
_graphRenderInsights();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function _graphLoop() {
|
function _graphLoop() {
|
||||||
|
|||||||
Reference in New Issue
Block a user