Update
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
knowledge
|
||||
/.env
|
||||
/staging
|
||||
|
||||
@@ -23,3 +23,17 @@ KB staging/ ──────rw───┘ │
|
||||
## Datenhoheit
|
||||
|
||||
Produktive JSON-Dateien werden niemals verändert. Der einzige schreibende Fremdpfad ist das explizit konfigurierte Staging-Verzeichnis. Alle Dateien werden atomar über Temporärdatei und Rename geschrieben.
|
||||
|
||||
## Hierarchische Render-Schicht
|
||||
|
||||
Der persistierte Wissensgraph bleibt unverändert vollständig. Das Frontend leitet daraus eine temporäre, hierarchische Render-Schicht ab:
|
||||
|
||||
```text
|
||||
Full Graph
|
||||
└── Cortex-Region
|
||||
└── Themenwolke (LOD 2)
|
||||
└── lokale Gruppe (LOD 1)
|
||||
└── einzelner Wissensknoten (LOD 0)
|
||||
```
|
||||
|
||||
Die sichtbare Ebene wird durch Zoom, Fokus und Aktivitätszeiten bestimmt. Außenkanten werden nach sichtbaren Endpunkten, Relationstyp, Richtung, Herkunft und Status aggregiert. Eine Aktivierung öffnet ausschließlich den betroffenen Pfad der Hierarchie; nach Ablauf der Offenhaltezeit wird dieser wieder verdichtet. Die KI-, Retrieval- und Persistenzlogik arbeitet weiterhin ausschließlich mit den echten Node- und Edge-IDs.
|
||||
|
||||
14
README.md
14
README.md
@@ -29,6 +29,20 @@ Die Themenstruktur wird nicht nur aus dem ersten Kategorie-Feld abgeleitet. Kate
|
||||
|
||||
Der Aktivitätsfeed zeigt nur relevante Ereignisse und ergänzt – sofern vorhanden – Cortex-Bereich, Trefferzahl, verwendete Quellen, Laufzeit, semantische Nähe, Relationstyp, Konfidenz, Recherchequellen, Ticket-ID und Staging-Pfad. Der Statusblock zeigt außerdem, ob der autonome Worker läuft, wartet, keine Kandidaten findet oder Ollama nicht erreicht. Über den wieder eingeblendeten **AI-THINK**-Button kann jederzeit ein manueller Zyklus angestoßen werden.
|
||||
|
||||
### Hierarchisches Semantic-LOD
|
||||
|
||||
Die Visualisierung rendert nicht mehr permanent jeden Knoten und jede Kante. Räumlich nahe, inaktive Elemente derselben Cortex-Region werden in zwei Stufen verdichtet:
|
||||
|
||||
- **Themenwolke (Level 2):** größere ruhende Bereiche;
|
||||
- **lokale Wissensgruppe (Level 1):** kleinere Gruppen innerhalb einer Themenwolke;
|
||||
- **Einzel-Node (Level 0):** konkrete Wissenseinträge bei Aktivität oder starkem Zoom.
|
||||
|
||||
Alle Originaldaten bleiben im Browser erhalten. Nur der abgeleitete `renderGraph` wird reduziert. Außenkanten einer Gruppe werden nach Quellgruppe, Zielgruppe, Relationstyp, Richtung, Herkunft und Status dedupliziert. `edgeCount` und `weightSum` bleiben an der aggregierten Kante erhalten; interne Kanten werden am Supernode gezählt.
|
||||
|
||||
Wird ein enthaltenes Wissenselement durch Agent, Knowledgebase, AI-THINK oder Recherche aktiviert, öffnet sich zunächst seine Themenwolke in engere Gruppen und anschließend die betroffene lokale Gruppe in Einzel-Nodes. Nach 30 bis 42 Sekunden ohne erneute Aktivität fällt der Bereich wieder zusammen. Zoom-Hysterese verhindert Flackern. Der **LOD**-Schalter kann die Verdichtung zu Diagnosezwecken deaktivieren; die Kennzahl **Render** im Header zeigt die tatsächlich pro Frame gezeichneten Nodes.
|
||||
|
||||
Die Grenzwerte stehen am Anfang von `internal/web/static/app.js` in `LOD_CONFIG` (`localDistance`, `coarseDistance`, Gruppengrößen und Offenhaltezeiten).
|
||||
|
||||
## Schutz der Basisprojekte
|
||||
|
||||
Das Brain bekommt nur:
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
6e7da366f9f451117f3d80acce5c94e551b7838eaa063ee887aeea51ab148197 ./.env.example
|
||||
9f81f9f707d706c92059977b7c5a2ec68ed15c39dcd6d2d57aadee1cbb1e3bb7 ./ARCHITECTURE.md
|
||||
b3704d36a5da4bca7faab55ad945a026e93843d169be7c942a3ebe66cb241e2e ./.env.example
|
||||
cd2a385faa5059dc1c756533bce56666a124272682b738629831f042c7a0e0fb ./ARCHITECTURE.md
|
||||
99171b262752a66278da5a4a8b2b48e83fbdb1eb9cdca3efa944d260d6e6bc01 ./Dockerfile
|
||||
adcd3c9ba3bdf366afcc4e15a25423e068dd761e5d5d2d6f8cb20a3686302045 ./Makefile
|
||||
23f3748f6adef25034c32832272a5c0d03700aed0619742e6a8c450b84f95a07 ./README.md
|
||||
221c2e42a6234fa3bae31eb71745182489f466fcedbc8f3d3e9d918ce6d35adf ./README.md
|
||||
cf53db59fb1b3057019d54166bfbb0c95fbf84cb5032cad3ebea30af3aab69b3 ./cmd/brain/main.go
|
||||
f854ec4e9b1a5190a294529b58759c0e8535a1715bf12bb24f5b588af9b98b72 ./data/graph-state.json
|
||||
418310e2660f34890c4be97d6e4a0873cce57fe5b8eb6bfdff25bcd2d5faa7bf ./data/graph-state.json
|
||||
2404066ac6c893852a3f2bba00b33883b7e0eeb542448caf733fff4862715ed0 ./deployment/README.md
|
||||
d2c5ea4134a3df1ffa9c6409603f2fcf7bc30d16e3ac66e46328d059de5b5eff ./deployment/docker-compose.full.yml
|
||||
b8c9c01bdf731530252bbc109fd898d3c76b6e44ec6d589ffd0c87a60111ea04 ./docker-compose.yml
|
||||
2f3d69501091987783f8fd5e4d677d1191fd95fc435b12e907b0bcbf53d540aa ./deployment/docker-compose.full.yml
|
||||
63b043e874d4cb8130d7bc5a8eca58cfb4cdd1069ff18c20a568ada0d373f9cd ./docker-compose.yml
|
||||
9126f8bca1144abfc77747063b2c8f31acf12839a75e060b98369e10a00061ca ./go.mod
|
||||
bf239391e61040b00d2f49d805b0d49a44bd3679f3c53849dfbc5e3b5a3fcc7a ./integrations/agent/README.md
|
||||
a0105475dc054977223fac36618b8cd8137c55be1d11fddcd24e9a4d3074c170 ./integrations/agent/glpi-ai-agent-neural-brain.patch
|
||||
73ab7c49600cdaa4795e76c50e66ce919dec171b3a07f2d16ff6f45ce5f73365 ./integrations/knowledgebase/README.md
|
||||
36666043ebf4139e13610e5fcd0b0c6f45e4e6a53fed33ec47d03a66878e7b08 ./integrations/knowledgebase/glpi-ai-knowledgebase-neural-brain.patch
|
||||
50d05fa2a183f5f3eaab0545cb48d3abb64be62eb5d84dc2c6d99c7125f7344b ./internal/activity/broker.go
|
||||
92f34f0f6535daffbbd405c5c0957b5367189420dc5b3ecbc505435534d8a464 ./internal/config/config.go
|
||||
862b2eb57019d87f2221698341faf180820062fb309bdc6047129f25e902ed74 ./internal/engine/engine.go
|
||||
9fa32035e4a606e69597329f78cbe6cadcaa47eec50d77299841ef049725c6a6 ./internal/engine/engine_test.go
|
||||
0bf06e95cfafc65862f873a648240b192e158b814ffdd24c30d353ceb1042f82 ./internal/graph/store.go
|
||||
2a7a62411c1a6eb70440ac13ce6bc9eef10914ea62ebd270bfc6e8444c036250 ./internal/graph/store_test.go
|
||||
094c75a5335170e300d9c88ad183db40fa54cc0070dad3f0fdbb62c719adc1e8 ./internal/config/config.go
|
||||
686a2d9b0eb741e7ec163883e5e407e2b629e13b7b0f420158b5a71608345c0a ./internal/engine/engine.go
|
||||
55af6324acec36d5a4ce9e6c48ade1dcba9a4acbd2eda00a572ab71ca576bfea ./internal/engine/engine_test.go
|
||||
64f896f3421ffc41836e0dce7fdcf8beb639258960fee7df07552539c0db2038 ./internal/graph/store.go
|
||||
5c32a4e4fca939165a2fa7c0f380fdf79cfab7cc65924e21fe7b9ac5e85a2b6d ./internal/graph/store_test.go
|
||||
4476351d388d11c6becd78b4c918fc8d47dfd70500d8b3e2ddf81f7ff61a2cea ./internal/ingest/agent.go
|
||||
fe0a813efe140fdc8961015cb2a493928c97e456a80c2d8cf2ddbe7c6e335018 ./internal/ingest/knowledge.go
|
||||
5ed96aec4c3d2599cb9af3f6e951533271dc2b9a4826667482da814f9b450da1 ./internal/ingest/knowledge_test.go
|
||||
@@ -26,9 +26,9 @@ fb1af93afeb4ebc89cad95badeaee9a0cc3a236b9761e15a41a7b4b2c7192d00 ./internal/mod
|
||||
2ea6284f6aa2c23c6f6583ba66647ae8f48432527746b99898bc567c1be64a52 ./internal/ollama/client.go
|
||||
2eb686cfc9016b9b0cf15e5c342f693ec9b60c454bc4814c1c9583f6d5b5b806 ./internal/research/searxng.go
|
||||
6346f7b213aa3fb36bc9f43134bba75e0b368c9a005cc1aef8d265f553ff8cef ./internal/research/searxng_test.go
|
||||
0f54fd249ef02616378b3630951eee4526f69593e54911058271c9eeb076874b ./internal/web/server.go
|
||||
b5b46e461473e21c5bb5e1cd014c6cd1a1db4c0b734a52d93a47ea4bb73f9e7d ./internal/web/static/app.css
|
||||
28c3461026cfdac5e0c4b08c92ede18c3ae0427ba4d9ad1193daf60c9974f9f3 ./internal/web/static/app.js
|
||||
acbc97be2fa1d495270751a4a7b2ef0a58a9c7301e82a3cac901cda0f78f7600 ./internal/web/static/index.html
|
||||
c81571f39dfa36c540c3f9b55ac05db90ffc85dd8ccf0da5fecd454a7f65b19d ./neural-brain
|
||||
dda84763f509405c39cb533ee1c1f2bbeed48718cd7de9cbb66dd1adc5582e2c ./internal/web/server.go
|
||||
644105a4921f0394bc0ab3cc4c8658053e456e593acf7573255bd867d6cde8dd ./internal/web/static/app.css
|
||||
c310356d4bfe2c02a7bf078140c9c49febf8a7c4f9ab2dfacd1016e918953b0d ./internal/web/static/app.js
|
||||
02007a3143603cda56794e36613f9a3cfc3cdbaf66279af0b3e4b9b5e8f4cbc2 ./internal/web/static/index.html
|
||||
fcc7348cf65c04d023b0d0c1fa391e7628ae326d394e44c6fde9bb8eb6f665b5 ./neural-brain
|
||||
83aded814b6225395935e61fe957963c3c470f368fc9089f505b6de23e959115 ./preview.png
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -29,3 +29,4 @@ html,body{margin:0;width:100%;height:100%;overflow:hidden;background:radial-grad
|
||||
/* AI-THINK-Steuerung und echter Backend-Status */
|
||||
.autonomy-status{display:flex;align-items:center;gap:8px;margin:-1px 0 10px;padding:8px 9px;border:1px solid rgba(82,231,255,.12);border-radius:10px;background:rgba(82,231,255,.035);color:#8fa9ba;font-size:10px;line-height:1.35}.autonomy-status i{flex:0 0 auto;width:7px;height:7px;border-radius:50%;background:var(--cyan);box-shadow:0 0 10px var(--cyan)}.autonomy-status.running{border-color:rgba(255,180,82,.28);background:rgba(255,180,82,.06);color:#ffd9a7}.autonomy-status.running i{background:var(--amber);box-shadow:0 0 12px var(--amber);animation:pulse .72s infinite}.autonomy-status.offline{border-color:rgba(255,95,136,.22);background:rgba(255,95,136,.05);color:#ffb0c5}.autonomy-status.offline i{background:var(--red);box-shadow:0 0 10px var(--red)}.autonomy-status.waiting{color:#a8bdca}.autonomy-status.waiting i{background:var(--blue);box-shadow:0 0 10px var(--blue)}
|
||||
.dock .think-action{display:flex;align-items:center;gap:7px;border-color:rgba(255,180,82,.22);background:rgba(255,180,82,.07);color:var(--amber)}.dock .think-action small{font-size:8px;letter-spacing:.11em;opacity:.68}.dock .think-action:hover{background:rgba(255,180,82,.14);box-shadow:0 0 22px rgba(255,180,82,.08)}.dock .think-action.running{border-color:rgba(255,180,82,.48);background:rgba(255,180,82,.13);box-shadow:0 0 24px rgba(255,180,82,.11)}.dock .think-action:disabled{cursor:wait;opacity:.72}
|
||||
.metrics span[title]{cursor:help}.dock #toggleLOD.active{color:var(--green);border-color:rgba(93,255,189,.22);background:rgba(93,255,189,.065)}
|
||||
|
||||
@@ -17,6 +17,15 @@
|
||||
processing: {label: 'PROCESSING', detail: 'Agent- und Wissenssuche', className: 'processing', color: [183, 117, 255], duration: 7800},
|
||||
learning: {label: 'LEARNING', detail: 'indexiert bei stabiler Ansicht', className: 'learning', color: [75, 123, 255], duration: 6500}
|
||||
};
|
||||
const LOD_CONFIG = {
|
||||
localDistance: 0.085,
|
||||
coarseDistance: 0.23,
|
||||
localMaxMembers: 24,
|
||||
coarseMaxChildren: 18,
|
||||
revealNodeMS: 30000,
|
||||
revealParentMS: 42000,
|
||||
rebuildThrottleMS: 120
|
||||
};
|
||||
|
||||
const state = {
|
||||
nodes: [], edges: [], clusters: [], clusterByKey: new Map(), nodeById: new Map(), edgeById: new Map(), adjacency: new Map(),
|
||||
@@ -27,7 +36,11 @@
|
||||
lastLogFingerprint: new Map(), recentImportant: [], clusterActive: new Map(),
|
||||
mode: 'living', modeUntil: 0, activityEnergy: 0, targetEnergy: 0, focusClusterKey: '', focusNodeID: '',
|
||||
cameraTargetYaw: null, cameraTargetPitch: null, zoomTarget: 1.02, nextAmbientAt: performance.now() + 1800,
|
||||
ambientCursor: 0, bursts: [], brainStatus: null
|
||||
ambientCursor: 0, bursts: [], brainStatus: null,
|
||||
lodEnabled: true, lodLeaves: [], lodCoarse: [], lodGroupById: new Map(), lodLeafByNode: new Map(), lodCoarseByNode: new Map(),
|
||||
lodOpenUntil: new Map(), lodHotUntil: new Map(), lodDirty: true, lodLastBuild: 0, lodNextExpiry: 0, lodZoomBand: 2,
|
||||
renderNodes: [], renderEdges: [], renderIdleEdges: [], renderNodeById: new Map(), renderEdgeById: new Map(), visibleForNode: new Map(),
|
||||
edgeRenderMap: new Map(), renderActive: new Map(), renderEdgeActive: new Map(), renderStats: {nodes: 0, edges: 0, hiddenNodes: 0, hiddenEdges: 0}
|
||||
};
|
||||
|
||||
function resize() {
|
||||
@@ -67,6 +80,8 @@
|
||||
state.adjacency.get(e.target).push(e);
|
||||
}
|
||||
buildLayout(old, oldClusters);
|
||||
buildLODHierarchy();
|
||||
rebuildRenderGraph(performance.now(), true);
|
||||
$('nodeCount').textContent = state.nodes.length.toLocaleString('de-DE');
|
||||
$('edgeCount').textContent = state.edges.length.toLocaleString('de-DE');
|
||||
} catch {
|
||||
@@ -411,6 +426,272 @@
|
||||
}
|
||||
}
|
||||
|
||||
function splitSpatial(items, maxItems) {
|
||||
if (items.length <= maxItems) return [items];
|
||||
const ranges = ['x', 'y', 'z'].map(axis => {
|
||||
let min = Infinity, max = -Infinity;
|
||||
for (const item of items) {
|
||||
min = Math.min(min, item[axis]);
|
||||
max = Math.max(max, item[axis]);
|
||||
}
|
||||
return {axis, range: max - min};
|
||||
}).sort((a, b) => b.range - a.range);
|
||||
const axis = ranges[0].axis;
|
||||
const sorted = [...items].sort((a, b) => a[axis] - b[axis] || String(a.id).localeCompare(String(b.id)));
|
||||
const parts = [];
|
||||
for (let i = 0; i < sorted.length; i += maxItems) parts.push(sorted.slice(i, i + maxItems));
|
||||
return parts;
|
||||
}
|
||||
|
||||
function spatialBuckets(items, cellSize, maxItems) {
|
||||
const buckets = new Map();
|
||||
for (const item of items) {
|
||||
const key = `${Math.floor(item.x / cellSize)}:${Math.floor(item.y / cellSize)}:${Math.floor(item.z / cellSize)}`;
|
||||
if (!buckets.has(key)) buckets.set(key, []);
|
||||
buckets.get(key).push(item);
|
||||
}
|
||||
const result = [];
|
||||
const entries = [...buckets.entries()].sort((a, b) => a[0].localeCompare(b[0]));
|
||||
for (const [, bucket] of entries) result.push(...splitSpatial(bucket, maxItems));
|
||||
return result;
|
||||
}
|
||||
|
||||
function makeLODGroup(level, cluster, members, children, ordinal) {
|
||||
let sumX = 0, sumY = 0, sumZ = 0, mass = 0;
|
||||
for (const member of members) {
|
||||
const weight = 1 + Math.min(8, Math.sqrt((state.adjacency.get(member.id) || []).length));
|
||||
sumX += member.x * weight;
|
||||
sumY += member.y * weight;
|
||||
sumZ += member.z * weight;
|
||||
mass += weight;
|
||||
}
|
||||
const signature = `${cluster.key}|${level}|${members[0]?.id || ordinal}|${members.length}|${ordinal}`;
|
||||
const id = `lod:${level}:${hashString(signature).toString(36)}`;
|
||||
const statuses = new Set(members.map(member => member.status).filter(Boolean));
|
||||
return {
|
||||
id, kind: 'supernode', renderKind: 'supernode', lodLevel: level, clusterKey: cluster.key, cluster,
|
||||
label: cluster.label, summary: `${members.length.toLocaleString('de-DE')} verdichtete Wissenselemente`,
|
||||
status: statuses.size === 1 ? [...statuses][0] : 'aggregate', origin: 'lod', categories: [cluster.label],
|
||||
x: sumX / Math.max(1, mass), y: sumY / Math.max(1, mass), z: sumZ / Math.max(1, mass),
|
||||
weight: Math.max(1, Math.log2(members.length + 1)), clusterColor: cluster.color,
|
||||
members, memberIDs: members.map(member => member.id), memberCount: members.length, children: children || [],
|
||||
internalEdgeCount: 0, externalEdgeCount: 0, screen: null, transitionFrom: null, transitionStart: 0
|
||||
};
|
||||
}
|
||||
|
||||
function buildLODHierarchy() {
|
||||
state.lodLeaves = [];
|
||||
state.lodCoarse = [];
|
||||
state.lodGroupById = new Map();
|
||||
state.lodLeafByNode = new Map();
|
||||
state.lodCoarseByNode = new Map();
|
||||
for (const cluster of state.clusters) {
|
||||
const localParts = spatialBuckets(cluster.nodes, LOD_CONFIG.localDistance, LOD_CONFIG.localMaxMembers);
|
||||
const leaves = localParts.map((members, index) => makeLODGroup(1, cluster, members, [], index));
|
||||
for (const leaf of leaves) {
|
||||
state.lodLeaves.push(leaf);
|
||||
state.lodGroupById.set(leaf.id, leaf);
|
||||
for (const id of leaf.memberIDs) state.lodLeafByNode.set(id, leaf);
|
||||
}
|
||||
const coarseParts = spatialBuckets(leaves, LOD_CONFIG.coarseDistance, LOD_CONFIG.coarseMaxChildren);
|
||||
const coarseGroups = coarseParts.map((children, index) => {
|
||||
const members = children.flatMap(child => child.members);
|
||||
const group = makeLODGroup(2, cluster, members, children, index);
|
||||
for (const child of children) child.parentID = group.id;
|
||||
return group;
|
||||
});
|
||||
cluster.lodLeaves = leaves;
|
||||
cluster.lodCoarse = coarseGroups;
|
||||
for (const coarse of coarseGroups) {
|
||||
state.lodCoarse.push(coarse);
|
||||
state.lodGroupById.set(coarse.id, coarse);
|
||||
for (const id of coarse.memberIDs) state.lodCoarseByNode.set(id, coarse);
|
||||
}
|
||||
}
|
||||
state.lodDirty = true;
|
||||
}
|
||||
|
||||
function lodGroupOpen(group, now) {
|
||||
return (state.lodOpenUntil.get(group.id) || 0) > now;
|
||||
}
|
||||
|
||||
function nodeHot(id, now) {
|
||||
return (state.lodHotUntil.get(id) || 0) > now || (state.active.get(id) || 0) > 0.04 || state.focusNodeID === id || state.selected?.id === id;
|
||||
}
|
||||
|
||||
function groupHasHotNode(group, now) {
|
||||
if (lodGroupOpen(group, now)) return true;
|
||||
for (const id of group.memberIDs) if (nodeHot(id, now)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function revealLODNodes(ids, duration = LOD_CONFIG.revealNodeMS) {
|
||||
const now = performance.now();
|
||||
for (const id of ids || []) {
|
||||
if (!state.nodeById.has(id)) continue;
|
||||
state.lodHotUntil.set(id, Math.max(state.lodHotUntil.get(id) || 0, now + duration));
|
||||
const leaf = state.lodLeafByNode.get(id);
|
||||
const coarse = state.lodCoarseByNode.get(id);
|
||||
if (leaf) state.lodOpenUntil.set(leaf.id, Math.max(state.lodOpenUntil.get(leaf.id) || 0, now + duration));
|
||||
if (coarse) state.lodOpenUntil.set(coarse.id, Math.max(state.lodOpenUntil.get(coarse.id) || 0, now + Math.max(duration, LOD_CONFIG.revealParentMS)));
|
||||
}
|
||||
state.lodDirty = true;
|
||||
}
|
||||
|
||||
function openLODGroup(group) {
|
||||
const now = performance.now();
|
||||
const duration = group.lodLevel === 2 ? LOD_CONFIG.revealParentMS : LOD_CONFIG.revealNodeMS;
|
||||
state.lodOpenUntil.set(group.id, now + duration);
|
||||
if (group.lodLevel === 1) {
|
||||
const parent = state.lodGroupById.get(group.parentID);
|
||||
if (parent) state.lodOpenUntil.set(parent.id, now + LOD_CONFIG.revealParentMS);
|
||||
}
|
||||
state.lodDirty = true;
|
||||
rebuildRenderGraph(now, true);
|
||||
}
|
||||
|
||||
function updateLODZoomBand() {
|
||||
const before = state.lodZoomBand;
|
||||
if (state.lodZoomBand === 2 && state.zoom > 1.12) state.lodZoomBand = 1;
|
||||
else if (state.lodZoomBand === 1 && state.zoom < 0.96) state.lodZoomBand = 2;
|
||||
else if (state.lodZoomBand === 1 && state.zoom > 1.72) state.lodZoomBand = 0;
|
||||
else if (state.lodZoomBand === 0 && state.zoom < 1.48) state.lodZoomBand = 1;
|
||||
if (before !== state.lodZoomBand) state.lodDirty = true;
|
||||
}
|
||||
|
||||
function addVisibleEntity(entity, renderNodes, renderNodeById, visibleForNode, oldRenderNodeById, oldVisibleForNode, now) {
|
||||
entity.renderKind = entity.kind === 'supernode' ? 'supernode' : 'node';
|
||||
entity.memberCount = entity.kind === 'supernode' ? entity.memberCount : 1;
|
||||
entity.internalEdgeCount = 0;
|
||||
entity.externalEdgeCount = 0;
|
||||
const sampleID = entity.kind === 'supernode' ? entity.memberIDs[0] : entity.id;
|
||||
const oldVisibleID = oldVisibleForNode.get(sampleID);
|
||||
const oldEntity = oldVisibleID ? oldRenderNodeById.get(oldVisibleID) : null;
|
||||
if (!oldRenderNodeById.has(entity.id) && oldEntity) {
|
||||
entity.transitionFrom = {x: oldEntity.x, y: oldEntity.y, z: oldEntity.z};
|
||||
entity.transitionStart = now;
|
||||
}
|
||||
renderNodes.push(entity);
|
||||
renderNodeById.set(entity.id, entity);
|
||||
if (entity.kind === 'supernode') {
|
||||
for (const id of entity.memberIDs) visibleForNode.set(id, entity.id);
|
||||
} else {
|
||||
visibleForNode.set(entity.id, entity.id);
|
||||
}
|
||||
}
|
||||
|
||||
function rebuildRenderGraph(now = performance.now(), force = false) {
|
||||
if (!force && (!state.lodDirty || now - state.lodLastBuild < LOD_CONFIG.rebuildThrottleMS)) return;
|
||||
const oldRenderNodeById = state.renderNodeById;
|
||||
const oldVisibleForNode = state.visibleForNode;
|
||||
const renderNodes = [];
|
||||
const renderNodeById = new Map();
|
||||
const visibleForNode = new Map();
|
||||
|
||||
if (!state.lodEnabled) {
|
||||
for (const node of state.nodes) addVisibleEntity(node, renderNodes, renderNodeById, visibleForNode, oldRenderNodeById, oldVisibleForNode, now);
|
||||
} else {
|
||||
for (const coarse of state.lodCoarse) {
|
||||
const focused = coarse.clusterKey === state.focusClusterKey;
|
||||
const coarseOpen = state.lodZoomBand <= 1 || focused || groupHasHotNode(coarse, now);
|
||||
if (!coarseOpen && coarse.memberCount > 1) {
|
||||
addVisibleEntity(coarse, renderNodes, renderNodeById, visibleForNode, oldRenderNodeById, oldVisibleForNode, now);
|
||||
continue;
|
||||
}
|
||||
for (const leaf of coarse.children) {
|
||||
const leafOpen = state.lodZoomBand === 0 || groupHasHotNode(leaf, now);
|
||||
if (!leafOpen && leaf.memberCount > 1) {
|
||||
addVisibleEntity(leaf, renderNodes, renderNodeById, visibleForNode, oldRenderNodeById, oldVisibleForNode, now);
|
||||
} else {
|
||||
for (const node of leaf.members) addVisibleEntity(node, renderNodes, renderNodeById, visibleForNode, oldRenderNodeById, oldVisibleForNode, now);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const renderEdgesByKey = new Map();
|
||||
const edgeRenderMap = new Map();
|
||||
for (const edge of state.edges) {
|
||||
const source = visibleForNode.get(edge.source);
|
||||
const target = visibleForNode.get(edge.target);
|
||||
if (!source || !target) continue;
|
||||
if (source === target) {
|
||||
const entity = renderNodeById.get(source);
|
||||
if (entity) entity.internalEdgeCount++;
|
||||
edgeRenderMap.set(edge.id, '');
|
||||
continue;
|
||||
}
|
||||
const key = `${source}\u0000${target}\u0000${edge.type || ''}\u0000${edge.origin || ''}\u0000${edge.status || ''}`;
|
||||
let aggregate = renderEdgesByKey.get(key);
|
||||
if (!aggregate) {
|
||||
aggregate = {
|
||||
id: `lod-edge:${hashString(key).toString(36)}`, source, target, type: edge.type, origin: edge.origin, status: edge.status,
|
||||
confidence: edge.confidence || 0, weight: 0, edgeCount: 0, weightSum: 0, maxConfidence: edge.confidence || 0,
|
||||
memberEdgeIDs: [], explanation: edge.explanation || ''
|
||||
};
|
||||
renderEdgesByKey.set(key, aggregate);
|
||||
}
|
||||
aggregate.edgeCount++;
|
||||
aggregate.weightSum += edge.weight || 1;
|
||||
aggregate.maxConfidence = Math.max(aggregate.maxConfidence, edge.confidence || 0);
|
||||
aggregate.confidence = aggregate.maxConfidence;
|
||||
aggregate.weight = Math.log1p(aggregate.weightSum);
|
||||
if (aggregate.memberEdgeIDs.length < 32) aggregate.memberEdgeIDs.push(edge.id);
|
||||
edgeRenderMap.set(edge.id, aggregate.id);
|
||||
}
|
||||
const renderEdges = [...renderEdgesByKey.values()];
|
||||
for (const edge of renderEdges) {
|
||||
const source = renderNodeById.get(edge.source);
|
||||
const target = renderNodeById.get(edge.target);
|
||||
if (source) source.externalEdgeCount += edge.edgeCount;
|
||||
if (target) target.externalEdgeCount += edge.edgeCount;
|
||||
}
|
||||
|
||||
state.renderNodes = renderNodes;
|
||||
state.renderEdges = renderEdges;
|
||||
state.renderIdleEdges = renderEdges.filter(edge => edge.edgeCount > 1 || edge.origin === 'ai-inference' || edge.type === 'contradicts' || edge.type === 'supports');
|
||||
state.renderNodeById = renderNodeById;
|
||||
state.renderEdgeById = new Map(renderEdges.map(edge => [edge.id, edge]));
|
||||
state.visibleForNode = visibleForNode;
|
||||
state.edgeRenderMap = edgeRenderMap;
|
||||
state.renderActive = new Map();
|
||||
for (const [id, strength] of state.active) {
|
||||
const visibleID = visibleForNode.get(id);
|
||||
if (visibleID) state.renderActive.set(visibleID, Math.max(state.renderActive.get(visibleID) || 0, strength));
|
||||
}
|
||||
state.renderEdgeActive = new Map();
|
||||
for (const [id, strength] of state.edgeActive) {
|
||||
const visibleID = edgeRenderMap.get(id);
|
||||
if (visibleID) state.renderEdgeActive.set(visibleID, Math.max(state.renderEdgeActive.get(visibleID) || 0, strength));
|
||||
}
|
||||
state.renderStats = {
|
||||
nodes: renderNodes.length,
|
||||
edges: renderEdges.length,
|
||||
hiddenNodes: Math.max(0, state.nodes.length - renderNodes.length),
|
||||
hiddenEdges: Math.max(0, state.edges.length - renderEdges.length)
|
||||
};
|
||||
const renderCount = $('renderCount');
|
||||
if (renderCount) {
|
||||
renderCount.textContent = renderNodes.length.toLocaleString('de-DE');
|
||||
renderCount.parentElement.title = `${renderNodes.length.toLocaleString('de-DE')} von ${state.nodes.length.toLocaleString('de-DE')} Nodes · ${renderEdges.length.toLocaleString('de-DE')} aggregierte Edges`;
|
||||
}
|
||||
const lodButton = $('toggleLOD');
|
||||
if (lodButton) lodButton.title = `Hierarchisches LOD: ${renderNodes.length.toLocaleString('de-DE')} Render-Nodes / ${renderEdges.length.toLocaleString('de-DE')} Render-Edges`;
|
||||
state.lodLastBuild = now;
|
||||
state.lodDirty = false;
|
||||
state.lodNextExpiry = Infinity;
|
||||
for (const expires of state.lodOpenUntil.values()) if (expires > now) state.lodNextExpiry = Math.min(state.lodNextExpiry, expires);
|
||||
for (const expires of state.lodHotUntil.values()) if (expires > now) state.lodNextExpiry = Math.min(state.lodNextExpiry, expires);
|
||||
if (!Number.isFinite(state.lodNextExpiry)) state.lodNextExpiry = 0;
|
||||
if (state.hover && !renderNodeById.has(state.hover.id)) state.hover = null;
|
||||
}
|
||||
|
||||
function updateLOD(now) {
|
||||
updateLODZoomBand();
|
||||
if (state.lodNextExpiry && now >= state.lodNextExpiry) state.lodDirty = true;
|
||||
if (state.lodDirty) rebuildRenderGraph(now);
|
||||
}
|
||||
|
||||
function rotatePoint(n) {
|
||||
const cy = Math.cos(state.yaw), sy = Math.sin(state.yaw);
|
||||
const cp = Math.cos(state.pitch), sp = Math.sin(state.pitch);
|
||||
@@ -543,14 +824,25 @@
|
||||
function projectAnimatedNode(node, now) {
|
||||
const cluster = node.cluster;
|
||||
let x = node.x, y = node.y, z = node.z;
|
||||
if (node.transitionFrom && node.transitionStart) {
|
||||
const t = Math.max(0, Math.min(1, (now - node.transitionStart) / 520));
|
||||
const eased = t * t * (3 - 2 * t);
|
||||
x = node.transitionFrom.x + (x - node.transitionFrom.x) * eased;
|
||||
y = node.transitionFrom.y + (y - node.transitionFrom.y) * eased;
|
||||
z = node.transitionFrom.z + (z - node.transitionFrom.z) * eased;
|
||||
if (t >= 1) {
|
||||
node.transitionFrom = null;
|
||||
node.transitionStart = 0;
|
||||
}
|
||||
}
|
||||
if (cluster) {
|
||||
const clusterEnergy = state.clusterActive.get(cluster.key) || 0;
|
||||
const learning = state.mode === 'learning';
|
||||
const ambient = 1 + Math.sin(now * 0.00072 + hashString(cluster.key) * 0.00001) * (learning ? 0.0025 : 0.012);
|
||||
const expansion = ambient + clusterEnergy * (learning ? 0.006 : 0.055) + (cluster.key === state.focusClusterKey && !learning ? state.activityEnergy * 0.028 : 0);
|
||||
x = cluster.x + (node.x - cluster.x) * expansion;
|
||||
y = cluster.y + (node.y - cluster.y) * expansion;
|
||||
z = cluster.z + (node.z - cluster.z) * expansion;
|
||||
x = cluster.x + (x - cluster.x) * expansion;
|
||||
y = cluster.y + (y - cluster.y) * expansion;
|
||||
z = cluster.z + (z - cluster.z) * expansion;
|
||||
}
|
||||
const cy = Math.cos(state.yaw), sy = Math.sin(state.yaw);
|
||||
const cp = Math.cos(state.pitch), sp = Math.sin(state.pitch);
|
||||
@@ -688,20 +980,27 @@
|
||||
if (!state.edgesVisible) return;
|
||||
ctx.save();
|
||||
ctx.globalCompositeOperation = 'screen';
|
||||
for (const e of state.edges) {
|
||||
const a = state.nodeById.get(e.source), b = state.nodeById.get(e.target);
|
||||
const edges = [...state.renderIdleEdges];
|
||||
const included = new Set(edges.map(edge => edge.id));
|
||||
for (const id of state.renderEdgeActive.keys()) {
|
||||
const edge = state.renderEdgeById.get(id);
|
||||
if (edge && !included.has(id)) { edges.push(edge); included.add(id); }
|
||||
}
|
||||
for (const edge of edges) {
|
||||
const a = state.renderNodeById.get(edge.source), b = state.renderNodeById.get(edge.target);
|
||||
if (!a?.screen || !b?.screen) continue;
|
||||
const active = state.edgeActive.get(e.id) || 0;
|
||||
const base = active > 0.01 ? 0.08 + active * 0.7 : e.origin === 'ai-inference' ? 0.03 : e.type === 'categorized_as' ? 0.02 : 0.009;
|
||||
const alpha = Math.min(0.85, base);
|
||||
if (alpha < 0.012) continue;
|
||||
const active = state.renderEdgeActive.get(edge.id) || 0;
|
||||
const density = Math.min(1, Math.log1p(edge.edgeCount || 1) / 5.5);
|
||||
const base = active > 0.01 ? 0.08 + active * 0.7 : edge.origin === 'ai-inference' ? 0.026 : edge.type === 'categorized_as' ? 0.017 : 0.006 + density * 0.014;
|
||||
const alpha = Math.min(0.88, base);
|
||||
if (alpha < 0.01) continue;
|
||||
const mx = (a.screen.x + b.screen.x) / 2;
|
||||
const my = (a.screen.y + b.screen.y) / 2 - Math.abs(a.screen.x - b.screen.x) * 0.035;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(a.screen.x, a.screen.y);
|
||||
ctx.quadraticCurveTo(mx, my, b.screen.x, b.screen.y);
|
||||
ctx.strokeStyle = e.origin === 'ai-inference' ? `rgba(255,180,82,${alpha})` : `rgba(82,181,255,${alpha})`;
|
||||
ctx.lineWidth = 0.25 + active * 1.7 + (e.confidence || 0) * 0.2;
|
||||
ctx.strokeStyle = edge.origin === 'ai-inference' ? `rgba(255,180,82,${alpha})` : `rgba(82,181,255,${alpha})`;
|
||||
ctx.lineWidth = 0.25 + active * 1.7 + (edge.confidence || 0) * 0.18 + density * 0.65;
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.restore();
|
||||
@@ -711,21 +1010,41 @@
|
||||
ctx.save();
|
||||
ctx.globalCompositeOperation = 'lighter';
|
||||
for (let i = state.particles.length - 1; i >= 0; i--) {
|
||||
const p = state.particles[i];
|
||||
p.t += dt * p.speed;
|
||||
const e = state.edgeById.get(p.edgeId), a = e && state.nodeById.get(e.source), b = e && state.nodeById.get(e.target);
|
||||
if (!e || !a?.screen || !b?.screen || p.t > 1.08) {
|
||||
const particle = state.particles[i];
|
||||
particle.t += dt * particle.speed;
|
||||
const original = state.edgeById.get(particle.edgeId);
|
||||
const renderEdgeID = state.edgeRenderMap.get(particle.edgeId);
|
||||
const renderEdge = renderEdgeID ? state.renderEdgeById.get(renderEdgeID) : null;
|
||||
let a = renderEdge && state.renderNodeById.get(renderEdge.source);
|
||||
let b = renderEdge && state.renderNodeById.get(renderEdge.target);
|
||||
if (!renderEdge && original) {
|
||||
const visibleID = state.visibleForNode.get(original.source);
|
||||
const sameID = state.visibleForNode.get(original.target);
|
||||
if (visibleID && visibleID === sameID) {
|
||||
a = state.renderNodeById.get(visibleID);
|
||||
b = a;
|
||||
}
|
||||
}
|
||||
if (!original || !a?.screen || !b?.screen || particle.t > 1.08) {
|
||||
state.particles.splice(i, 1);
|
||||
continue;
|
||||
}
|
||||
const q = Math.min(1, p.t);
|
||||
const q = Math.min(1, particle.t);
|
||||
const ease = q * q * (3 - 2 * q);
|
||||
const x = a.screen.x + (b.screen.x - a.screen.x) * ease;
|
||||
const y = a.screen.y + (b.screen.y - a.screen.y) * ease - Math.sin(q * Math.PI) * 18;
|
||||
const size = 8 * (p.size || 1) * (0.82 + state.activityEnergy * 0.28);
|
||||
let x, y;
|
||||
if (a === b) {
|
||||
const angle = q * Math.PI * 2 + hashString(particle.edgeId) * 0.00001;
|
||||
const orbit = 8 + Math.min(18, Math.log1p(a.memberCount || 1) * 3);
|
||||
x = a.screen.x + Math.cos(angle) * orbit;
|
||||
y = a.screen.y + Math.sin(angle) * orbit * 0.55;
|
||||
} else {
|
||||
x = a.screen.x + (b.screen.x - a.screen.x) * ease;
|
||||
y = a.screen.y + (b.screen.y - a.screen.y) * ease - Math.sin(q * Math.PI) * 18;
|
||||
}
|
||||
const size = 8 * (particle.size || 1) * (0.82 + state.activityEnergy * 0.28);
|
||||
const grad = ctx.createRadialGradient(x, y, 0, x, y, size);
|
||||
grad.addColorStop(0, 'rgba(255,255,255,.95)');
|
||||
grad.addColorStop(0.22, p.color);
|
||||
grad.addColorStop(0.22, particle.color);
|
||||
grad.addColorStop(1, 'rgba(82,231,255,0)');
|
||||
ctx.fillStyle = grad;
|
||||
ctx.beginPath();
|
||||
@@ -736,48 +1055,74 @@
|
||||
}
|
||||
|
||||
function renderNodes(now, dt) {
|
||||
for (const [id, value] of state.active) {
|
||||
const next = Math.max(0, value - dt * 0.44);
|
||||
if (next > 0) state.active.set(id, next); else state.active.delete(id);
|
||||
}
|
||||
for (const [id, value] of state.renderActive) {
|
||||
const next = Math.max(0, value - dt * 0.44);
|
||||
if (next > 0) state.renderActive.set(id, next); else state.renderActive.delete(id);
|
||||
}
|
||||
|
||||
state.projected = [];
|
||||
for (const n of state.nodes) {
|
||||
n.screen = projectAnimatedNode(n, now);
|
||||
state.projected.push(n);
|
||||
for (const node of state.renderNodes) {
|
||||
node.screen = projectAnimatedNode(node, now);
|
||||
state.projected.push(node);
|
||||
}
|
||||
state.projected.sort((a, b) => a.screen.z - b.screen.z);
|
||||
|
||||
ctx.save();
|
||||
ctx.globalCompositeOperation = 'lighter';
|
||||
for (const n of state.projected) {
|
||||
let act = state.active.get(n.id) || 0;
|
||||
act = Math.max(0, act - dt * 0.44);
|
||||
if (act > 0) state.active.set(n.id, act); else state.active.delete(n.id);
|
||||
const hover = state.hover?.id === n.id;
|
||||
const selected = state.selected?.id === n.id;
|
||||
const degree = Math.min(18, (state.adjacency.get(n.id) || []).length);
|
||||
const baseWeight = Math.max(0.25, Math.min(2.5, (n.weight || 1) * 0.85 + Math.sqrt(degree) * 0.05));
|
||||
const breathe = 0.5 + 0.5 * Math.sin(now * 0.0014 + n.x * 7 + n.y * 9);
|
||||
const clusterEnergy = state.clusterActive.get(n.clusterKey) || 0;
|
||||
const focused = n.clusterKey === state.focusClusterKey ? state.activityEnergy : 0;
|
||||
const r = (1.05 + baseWeight * 0.7 + n.screen.p * 0.55) * (1 + act * 0.48 + clusterEnergy * 0.06 + focused * 0.025) + (hover || selected ? 1.6 : 0);
|
||||
for (const node of state.projected) {
|
||||
const isGroup = node.kind === 'supernode';
|
||||
const act = state.renderActive.get(node.id) || (isGroup ? 0 : state.active.get(node.id) || 0);
|
||||
const hover = state.hover?.id === node.id;
|
||||
const selected = state.selected?.id === node.id;
|
||||
const degree = Math.min(60, isGroup ? node.externalEdgeCount || 0 : (state.adjacency.get(node.id) || []).length);
|
||||
const memberScale = isGroup ? Math.min(7.5, 1.1 + Math.log2((node.memberCount || 1) + 1) * 0.72) : 0;
|
||||
const baseWeight = isGroup ? memberScale : Math.max(0.25, Math.min(2.5, (node.weight || 1) * 0.85 + Math.sqrt(degree) * 0.05));
|
||||
const breathe = 0.5 + 0.5 * Math.sin(now * 0.0014 + node.x * 7 + node.y * 9);
|
||||
const clusterEnergy = state.clusterActive.get(node.clusterKey) || 0;
|
||||
const focused = node.clusterKey === state.focusClusterKey ? state.activityEnergy : 0;
|
||||
const r = (isGroup ? 2.4 + baseWeight : 1.05 + baseWeight * 0.7 + node.screen.p * 0.55) * (1 + act * 0.48 + clusterEnergy * 0.06 + focused * 0.025) + (hover || selected ? 1.6 : 0);
|
||||
const idleDim = state.mode === 'living' ? 0 : 0.025;
|
||||
const alpha = Math.min(1, 0.17 - idleDim + n.screen.p * 0.1 + Math.min(0.22, degree * 0.006) + act * 0.46 + clusterEnergy * 0.08 + focused * 0.04 + (hover || selected ? 0.18 : 0));
|
||||
if (act > 0.05 || hover || selected || n.kind === 'ai-think') {
|
||||
const halo = ctx.createRadialGradient(n.screen.x, n.screen.y, 0, n.screen.x, n.screen.y, r * (3 + act * 4));
|
||||
halo.addColorStop(0, nodeColor(n, 0.48 + act * 0.35));
|
||||
halo.addColorStop(0.24, nodeColor(n, 0.14 + act * 0.2));
|
||||
halo.addColorStop(1, nodeColor(n, 0));
|
||||
const alpha = Math.min(1, (isGroup ? 0.42 : 0.17) - idleDim + node.screen.p * 0.1 + Math.min(0.22, degree * 0.004) + act * 0.46 + clusterEnergy * 0.08 + focused * 0.04 + (hover || selected ? 0.18 : 0));
|
||||
if (act > 0.05 || hover || selected || node.kind === 'ai-think' || isGroup) {
|
||||
const haloRadius = r * (isGroup ? 2.35 + act * 2.4 : 3 + act * 4);
|
||||
const halo = ctx.createRadialGradient(node.screen.x, node.screen.y, 0, node.screen.x, node.screen.y, haloRadius);
|
||||
halo.addColorStop(0, nodeColor(node, (isGroup ? 0.28 : 0.48) + act * 0.35));
|
||||
halo.addColorStop(0.24, nodeColor(node, (isGroup ? 0.09 : 0.14) + act * 0.2));
|
||||
halo.addColorStop(1, nodeColor(node, 0));
|
||||
ctx.fillStyle = halo;
|
||||
ctx.beginPath();
|
||||
ctx.arc(n.screen.x, n.screen.y, r * (3 + act * 4), 0, Math.PI * 2);
|
||||
ctx.arc(node.screen.x, node.screen.y, haloRadius, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
ctx.fillStyle = nodeColor(n, alpha);
|
||||
ctx.fillStyle = nodeColor(node, alpha);
|
||||
ctx.beginPath();
|
||||
ctx.arc(n.screen.x, n.screen.y, r * (0.84 + breathe * 0.1), 0, Math.PI * 2);
|
||||
ctx.arc(node.screen.x, node.screen.y, r * (0.84 + breathe * 0.1), 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
if (n.kind === 'ai-think') {
|
||||
ctx.strokeStyle = nodeColor(n, 0.55);
|
||||
if (isGroup) {
|
||||
ctx.strokeStyle = nodeColor(node, 0.4 + act * 0.3);
|
||||
ctx.lineWidth = 0.7 + Math.min(1.4, Math.log1p(node.internalEdgeCount || 0) * 0.12);
|
||||
ctx.beginPath();
|
||||
ctx.arc(node.screen.x, node.screen.y, r * 1.28 + breathe * 1.3, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
if (node.memberCount >= 8 && r > 5) {
|
||||
ctx.save();
|
||||
ctx.globalCompositeOperation = 'source-over';
|
||||
ctx.font = '600 8px Inter, system-ui';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillStyle = 'rgba(235,248,255,.82)';
|
||||
ctx.fillText(node.memberCount > 999 ? `${Math.round(node.memberCount / 100) / 10}k` : String(node.memberCount), node.screen.x, node.screen.y + 0.5);
|
||||
ctx.restore();
|
||||
}
|
||||
} else if (node.kind === 'ai-think') {
|
||||
ctx.strokeStyle = nodeColor(node, 0.55);
|
||||
ctx.lineWidth = 0.7;
|
||||
ctx.beginPath();
|
||||
ctx.arc(n.screen.x, n.screen.y, r * 2 + breathe * 1.8, 0, Math.PI * 2);
|
||||
ctx.arc(node.screen.x, node.screen.y, r * 2 + breathe * 1.8, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
@@ -787,15 +1132,17 @@
|
||||
ctx.save();
|
||||
ctx.font = '10px Inter, system-ui';
|
||||
ctx.textBaseline = 'middle';
|
||||
for (const n of state.projected) {
|
||||
const act = state.active.get(n.id) || 0;
|
||||
if (!(act > 0.45 || state.hover?.id === n.id || state.selected?.id === n.id || (n.kind === 'ai-think' && n.screen.p > 1.02))) continue;
|
||||
const label = n.label.length > 36 ? n.label.slice(0, 34) + '…' : n.label;
|
||||
for (const node of state.projected) {
|
||||
const act = state.renderActive.get(node.id) || (node.kind === 'supernode' ? 0 : state.active.get(node.id) || 0);
|
||||
const isGroup = node.kind === 'supernode';
|
||||
if (!(act > 0.45 || state.hover?.id === node.id || state.selected?.id === node.id || (!isGroup && node.kind === 'ai-think' && node.screen.p > 1.02))) continue;
|
||||
const raw = isGroup ? `${node.label} · ${node.memberCount}` : node.label;
|
||||
const label = raw.length > 42 ? raw.slice(0, 40) + '…' : raw;
|
||||
const w = ctx.measureText(label).width + 12;
|
||||
ctx.fillStyle = 'rgba(2,7,14,.82)';
|
||||
ctx.fillRect(n.screen.x + 8, n.screen.y - 8, w, 16);
|
||||
ctx.fillStyle = nodeColor(n, 0.95);
|
||||
ctx.fillText(label, n.screen.x + 14, n.screen.y);
|
||||
ctx.fillStyle = 'rgba(2,7,14,.84)';
|
||||
ctx.fillRect(node.screen.x + 8, node.screen.y - 8, w, 16);
|
||||
ctx.fillStyle = nodeColor(node, 0.95);
|
||||
ctx.fillText(label, node.screen.x + 14, node.screen.y);
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
@@ -849,6 +1196,7 @@
|
||||
const dt = Math.min(0.05, (now - state.last) / 1000);
|
||||
state.last = now;
|
||||
updateVisualState(now, dt);
|
||||
updateLOD(now);
|
||||
drawBackground(now);
|
||||
renderActivityAura(now);
|
||||
renderClusterClouds(now);
|
||||
@@ -861,6 +1209,10 @@
|
||||
const next = value - dt * (state.mode === 'living' ? 0.34 : 0.5);
|
||||
if (next <= 0) state.edgeActive.delete(id); else state.edgeActive.set(id, next);
|
||||
}
|
||||
for (const [id, value] of state.renderEdgeActive) {
|
||||
const next = value - dt * (state.mode === 'living' ? 0.34 : 0.5);
|
||||
if (next <= 0) state.renderEdgeActive.delete(id); else state.renderEdgeActive.set(id, next);
|
||||
}
|
||||
requestAnimationFrame(frame);
|
||||
}
|
||||
requestAnimationFrame(frame);
|
||||
@@ -870,6 +1222,11 @@
|
||||
const mode = eventMode(evt);
|
||||
const substep = evt.type === 'node.activated' || evt.type === 'edges.traversed';
|
||||
if (evt.type !== 'brain.idle' && !substep) setVisualMode(mode, evt, strength);
|
||||
if (evt.type !== 'brain.idle' && mode !== 'learning' && evt.node_ids?.length) {
|
||||
const duration = mode === 'thinking' || mode === 'researching' ? 45000 : LOD_CONFIG.revealNodeMS;
|
||||
revealLODNodes(evt.node_ids, duration);
|
||||
rebuildRenderGraph(performance.now(), !substep);
|
||||
}
|
||||
if (substep && !state.focusClusterKey && evt.node_ids?.length) {
|
||||
const focus = state.nodeById.get(evt.node_ids[0]);
|
||||
if (focus) focusCamera(focus);
|
||||
@@ -878,10 +1235,14 @@
|
||||
for (const id of evt.node_ids || []) {
|
||||
const node = state.nodeById.get(id);
|
||||
state.active.set(id, Math.max(state.active.get(id) || 0, strength));
|
||||
const visibleID = state.visibleForNode.get(id);
|
||||
if (visibleID) state.renderActive.set(visibleID, Math.max(state.renderActive.get(visibleID) || 0, strength));
|
||||
if (node?.clusterKey) state.clusterActive.set(node.clusterKey, Math.max(state.clusterActive.get(node.clusterKey) || 0, strength));
|
||||
}
|
||||
for (const id of evt.edge_ids || []) {
|
||||
state.edgeActive.set(id, Math.max(state.edgeActive.get(id) || 0, strength));
|
||||
const renderID = state.edgeRenderMap.get(id);
|
||||
if (renderID) state.renderEdgeActive.set(renderID, Math.max(state.renderEdgeActive.get(renderID) || 0, strength));
|
||||
const count = evt.type === 'brain.idle' ? 1 : Math.ceil(3 + strength * (mode === 'thinking' ? 7 : 5));
|
||||
for (let i = 0; i < count; i++) {
|
||||
state.particles.push({edgeId: id, t: -i * 0.065, speed: 0.32 + Math.random() * (mode === 'thinking' ? 0.9 : 0.62), color: modeParticleColor(mode), size: mode === 'thinking' ? 1.12 : mode === 'researching' ? 0.95 : 0.86});
|
||||
@@ -891,8 +1252,10 @@
|
||||
state.pulses++;
|
||||
$('pulseCount').textContent = state.pulses.toLocaleString('de-DE');
|
||||
const focus = evt.node_ids?.[0] ? state.nodeById.get(evt.node_ids[0]) : null;
|
||||
const x = focus?.screen?.x ?? state.width / 2;
|
||||
const y = focus?.screen?.y ?? state.height / 2;
|
||||
const visibleID = focus ? state.visibleForNode.get(focus.id) : '';
|
||||
const visibleFocus = visibleID ? state.renderNodeById.get(visibleID) : null;
|
||||
const x = visibleFocus?.screen?.x ?? focus?.screen?.x ?? state.width / 2;
|
||||
const y = visibleFocus?.screen?.y ?? focus?.screen?.y ?? state.height / 2;
|
||||
const waveCount = mode === 'thinking' ? 3 : mode === 'researching' ? 2 : 1;
|
||||
for (let i = 0; i < waveCount; i++) {
|
||||
state.waves.push({x, y, r: 18 + i * 17, life: 1 - i * 0.08, color: modeColor, speed: 118 + i * 35, width: mode === 'thinking' ? 1.8 : 1.25, alpha: mode === 'thinking' ? 0.3 : 0.24, dashed: mode === 'researching' && i === 1});
|
||||
@@ -1014,10 +1377,18 @@
|
||||
state.cortexVisible = !state.cortexVisible;
|
||||
e.currentTarget.classList.toggle('active', state.cortexVisible);
|
||||
});
|
||||
$('toggleLOD').addEventListener('click', e => {
|
||||
state.lodEnabled = !state.lodEnabled;
|
||||
e.currentTarget.classList.toggle('active', state.lodEnabled);
|
||||
state.lodDirty = true;
|
||||
rebuildRenderGraph(performance.now(), true);
|
||||
});
|
||||
$('resetView').addEventListener('click', () => {
|
||||
state.yaw = 0.18;
|
||||
state.pitch = -0.12;
|
||||
state.zoom = 1.02;
|
||||
state.zoomTarget = 1.02;
|
||||
state.lodDirty = true;
|
||||
});
|
||||
$('clearLog').addEventListener('click', () => {$('activityLog').innerHTML = '';});
|
||||
|
||||
@@ -1069,20 +1440,32 @@
|
||||
state.hover = best;
|
||||
const tip = $('tooltip');
|
||||
if (best) {
|
||||
const degree = (state.adjacency.get(best.id) || []).length;
|
||||
tip.classList.remove('hidden');
|
||||
tip.innerHTML = `<strong>${escapeHTML(best.label)}</strong><small>${escapeHTML((best.categories || []).slice(0, 3).join(' · ') || best.kind || 'Knoten')}<br>${degree} Verbindungen · ${escapeHTML(best.status || 'aktiv')}</small>`;
|
||||
tip.style.left = Math.min(state.width - 260, e.clientX + 14) + 'px';
|
||||
tip.style.top = Math.min(state.height - 70, e.clientY + 14) + 'px';
|
||||
if (best.kind === 'supernode') {
|
||||
const level = best.lodLevel === 2 ? 'Themenwolke' : 'lokale Wissensgruppe';
|
||||
tip.innerHTML = `<strong>${escapeHTML(best.label)}</strong><small>${level} · ${best.memberCount.toLocaleString('de-DE')} Elemente<br>${best.internalEdgeCount.toLocaleString('de-DE')} interne · ${best.externalEdgeCount.toLocaleString('de-DE')} externe Edges<br>Klicken zum Auflösen</small>`;
|
||||
} else {
|
||||
const degree = (state.adjacency.get(best.id) || []).length;
|
||||
tip.innerHTML = `<strong>${escapeHTML(best.label)}</strong><small>${escapeHTML((best.categories || []).slice(0, 3).join(' · ') || best.kind || 'Knoten')}<br>${degree} Verbindungen · ${escapeHTML(best.status || 'aktiv')}</small>`;
|
||||
}
|
||||
tip.style.left = Math.min(state.width - 280, e.clientX + 14) + 'px';
|
||||
tip.style.top = Math.min(state.height - 86, e.clientY + 14) + 'px';
|
||||
} else {
|
||||
tip.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
canvas.addEventListener('pointerup', e => {
|
||||
canvas.addEventListener('pointerup', () => {
|
||||
if (!state.moved && state.hover) {
|
||||
state.selected = state.hover;
|
||||
state.active.set(state.hover.id, 1.2);
|
||||
if (state.hover.kind === 'supernode') {
|
||||
openLODGroup(state.hover);
|
||||
state.renderActive.set(state.hover.id, 1.1);
|
||||
} else {
|
||||
state.selected = state.hover;
|
||||
state.active.set(state.hover.id, 1.2);
|
||||
revealLODNodes([state.hover.id], LOD_CONFIG.revealNodeMS);
|
||||
rebuildRenderGraph(performance.now(), true);
|
||||
}
|
||||
}
|
||||
state.dragging = false;
|
||||
});
|
||||
@@ -1090,6 +1473,8 @@
|
||||
canvas.addEventListener('wheel', e => {
|
||||
e.preventDefault();
|
||||
state.zoom = Math.max(0.55, Math.min(2.4, state.zoom * Math.exp(-e.deltaY * 0.001)));
|
||||
state.zoomTarget = state.zoom;
|
||||
state.lodDirty = true;
|
||||
}, {passive: false});
|
||||
|
||||
function escapeHTML(v) {
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
<div class="metrics">
|
||||
<span><b id="nodeCount">0</b> Nodes</span>
|
||||
<span><b id="edgeCount">0</b> Edges</span>
|
||||
<span title="Aktuell gerenderte LOD-Knoten"><b id="renderCount">0</b> Render</span>
|
||||
<span><b id="pulseCount">0</b> Impulse</span>
|
||||
<span class="state" id="systemState"><i></i> verbindet</span>
|
||||
</div>
|
||||
@@ -48,6 +49,7 @@
|
||||
<button id="toggleEdges" class="active">Synapsen</button>
|
||||
<button id="toggleRotate" class="active">Autopilot</button>
|
||||
<button id="toggleCortex" class="active">Cortex</button>
|
||||
<button id="toggleLOD" class="active" title="Hierarchische dynamische Verdichtung">LOD</button>
|
||||
<button id="resetView">Zentrieren</button>
|
||||
<button id="enrichNow" class="think-action" title="Einen autonomen AI-THINK-Zyklus sofort starten"><span>AI-THINK</span><small>STARTEN</small></button>
|
||||
</nav>
|
||||
|
||||
BIN
neural-brain
BIN
neural-brain
Binary file not shown.
Reference in New Issue
Block a user