Honeycomb und Control-Patch

This commit is contained in:
2026-08-04 05:28:51 +02:00
parent 23339a8be3
commit 3af51bf888
21 changed files with 1222 additions and 79 deletions
+425 -49
View File
@@ -40,7 +40,10 @@
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}
edgeRenderMap: new Map(), renderActive: new Map(), renderEdgeActive: new Map(), renderStats: {nodes: 0, edges: 0, hiddenNodes: 0, hiddenEdges: 0},
fullSnapshot: null, runtimeSettings: {learning_enabled: true, thinking_enabled: true, learning_categories: [], display_categories: [], thinking_categories: [], view_mode: 'neural'},
availableCategories: [], viewMode: 'neural', honeycombNodes: [], honeycombSpacing: 0, settingsOpen: false, settingsDraft: null,
graphVersion: null, displaySignature: ''
};
function resize() {
@@ -63,13 +66,147 @@
return data;
}
function categoryFilterMatches(node, categories) {
if (!categories || categories.length === 0) return true;
const wanted = new Set(categories.map(value => String(value).trim().toLowerCase()).filter(Boolean));
if (wanted.has('*')) return true;
const nodeCategories = (node.categories || []).map(value => String(value).trim().toLowerCase()).filter(Boolean);
if (nodeCategories.length === 0) return wanted.has('__uncategorized__');
return nodeCategories.some(category => wanted.has(category));
}
function filteredSnapshot(snapshot) {
const filters = state.runtimeSettings.display_categories || [];
if (!filters.length) return snapshot;
const visible = new Set();
const noteKinds = new Set(['knowledge', 'ai-think', 'external']);
for (const node of snapshot.nodes) {
if (noteKinds.has(node.kind) && categoryFilterMatches(node, filters)) visible.add(node.id);
if (node.kind === 'category' && filters.some(value => String(value).toLowerCase() === String(node.label || '').toLowerCase())) visible.add(node.id);
}
for (const edge of snapshot.edges) {
if (visible.has(edge.source)) visible.add(edge.target);
if (visible.has(edge.target)) visible.add(edge.source);
}
return {
...snapshot,
nodes: snapshot.nodes.filter(node => visible.has(node.id)),
edges: snapshot.edges.filter(edge => visible.has(edge.source) && visible.has(edge.target))
};
}
async function loadRuntimeConfiguration() {
try {
const [settings, categories] = await Promise.all([api('/api/runtime-settings'), api('/api/categories')]);
state.runtimeSettings = {...state.runtimeSettings, ...settings};
state.availableCategories = categories.categories || [];
state.viewMode = state.runtimeSettings.view_mode === 'honeycomb' ? 'honeycomb' : 'neural';
syncRuntimeControls();
renderCategoryFilters();
} catch {
syncRuntimeControls();
}
}
function syncRuntimeControls() {
const learning = Boolean(state.runtimeSettings.learning_enabled);
const thinking = Boolean(state.runtimeSettings.thinking_enabled);
const panelSettings = state.settingsOpen && state.settingsDraft ? state.settingsDraft : state.runtimeSettings;
const learningButton = $('toggleLearning');
const thinkingButton = $('toggleThinking');
if (learningButton) learningButton.classList.toggle('active', learning);
if (thinkingButton) thinkingButton.classList.toggle('active', thinking);
if ($('settingsLearning')) $('settingsLearning').checked = Boolean(panelSettings.learning_enabled);
if ($('settingsThinking')) $('settingsThinking').checked = Boolean(panelSettings.thinking_enabled);
if ($('settingsViewNeural')) $('settingsViewNeural').classList.toggle('active', panelSettings.view_mode !== 'honeycomb');
if ($('settingsViewHoneycomb')) $('settingsViewHoneycomb').classList.toggle('active', panelSettings.view_mode === 'honeycomb');
const enrichButton = $('enrichNow');
if (enrichButton && !state.brainStatus?.enrich_running) enrichButton.disabled = !thinking;
if (!learning && !thinking) setVisualMode('living');
updateViewButtons();
}
function categoryLabel(name) {
return name === '__uncategorized__' ? 'Ohne Kategorie' : name;
}
function filterSet(key) {
const source = state.settingsDraft || state.runtimeSettings;
return new Set((source[`${key}_categories`] || []).map(value => String(value).toLowerCase()));
}
function renderCategoryFilters(search = '') {
const query = String(search || '').trim().toLowerCase();
const targets = {learning: $('learningCategoryList'), display: $('displayCategoryList'), thinking: $('thinkingCategoryList')};
for (const [key, target] of Object.entries(targets)) {
if (!target) continue;
const selected = filterSet(key);
target.innerHTML = '';
const categories = state.availableCategories.filter(category => !query || categoryLabel(category.name).toLowerCase().includes(query));
for (const category of categories) {
const label = document.createElement('label');
label.className = 'category-option';
const checked = selected.has(String(category.name).toLowerCase());
label.innerHTML = `<input type="checkbox" data-category-filter="${key}" value="${escapeHTML(category.name)}" ${checked ? 'checked' : ''}><span>${escapeHTML(categoryLabel(category.name))}<em>${Number(category.count || 0).toLocaleString('de-DE')}</em></span>`;
target.appendChild(label);
}
if (!categories.length) target.innerHTML = '<span class="empty-filter">Keine passende Kategorie</span>';
}
}
function collectCategoryFilter(key) {
return [...document.querySelectorAll(`[data-category-filter="${key}"]:checked`)].map(input => input.value);
}
async function persistRuntimeSettings(settings = state.runtimeSettings) {
const normalized = {
learning_enabled: Boolean(settings.learning_enabled),
thinking_enabled: Boolean(settings.thinking_enabled),
learning_categories: [...(settings.learning_categories || [])],
display_categories: [...(settings.display_categories || [])],
thinking_categories: [...(settings.thinking_categories || [])],
view_mode: settings.view_mode === 'honeycomb' ? 'honeycomb' : 'neural'
};
const previousDisplay = JSON.stringify(state.runtimeSettings.display_categories || []);
const updated = await api('/api/runtime-settings', {method: 'PUT', body: JSON.stringify(normalized)});
state.runtimeSettings = {...normalized, ...updated};
state.viewMode = state.runtimeSettings.view_mode;
syncRuntimeControls();
applyViewMode(state.viewMode, false);
if (previousDisplay !== JSON.stringify(state.runtimeSettings.display_categories || [])) await loadGraph();
await loadStatus();
return state.runtimeSettings;
}
function openSettingsPanel() {
state.settingsOpen = true;
state.settingsDraft = JSON.parse(JSON.stringify(state.runtimeSettings));
$('settingsPanel')?.classList.remove('hidden');
$('settingsBackdrop')?.classList.remove('hidden');
syncRuntimeControls();
renderCategoryFilters($('categorySearch')?.value || '');
}
function closeSettingsPanel() {
state.settingsOpen = false;
state.settingsDraft = null;
$('settingsPanel')?.classList.add('hidden');
$('settingsBackdrop')?.classList.add('hidden');
}
async function loadGraph() {
try {
const snap = await api('/api/graph');
const displaySignature = JSON.stringify(state.runtimeSettings.display_categories || []);
if (state.fullSnapshot && state.graphVersion === snap.version && state.displaySignature === displaySignature) return;
state.graphVersion = snap.version;
state.displaySignature = displaySignature;
state.fullSnapshot = snap;
const filtered = filteredSnapshot(snap);
const old = state.nodeById;
const oldClusters = state.clusterByKey;
state.nodes = snap.nodes.map(n => ({...n, glow: old.get(n.id)?.glow || 0, screen: null, clusterKey: '', clusterColor: old.get(n.id)?.clusterColor || ''}));
state.edges = snap.edges;
state.nodes = filtered.nodes.map(n => ({...n, glow: old.get(n.id)?.glow || 0, screen: null, clusterKey: '', clusterColor: old.get(n.id)?.clusterColor || ''}));
state.edges = filtered.edges;
state.nodeById = new Map(state.nodes.map(n => [n.id, n]));
state.edgeById = new Map(state.edges.map(e => [e.id, e]));
state.adjacency = new Map();
@@ -80,9 +217,16 @@
state.adjacency.get(e.target).push(e);
}
buildLayout(old, oldClusters);
for (const node of state.nodes) {
node.neuralX = node.x;
node.neuralY = node.y;
node.neuralZ = node.z;
}
buildHoneycombLayout();
buildLODHierarchy();
rebuildRenderGraph(performance.now(), true);
applyViewMode(state.runtimeSettings.view_mode || state.viewMode, false);
$('nodeCount').textContent = state.nodes.length.toLocaleString('de-DE');
$('nodeCount').parentElement.title = `${state.nodes.length.toLocaleString('de-DE')} sichtbar · ${snap.nodes.length.toLocaleString('de-DE')} insgesamt`;
$('edgeCount').textContent = state.edges.length.toLocaleString('de-DE');
} catch {
setSystem('offline', false);
@@ -98,6 +242,10 @@
try {
const status = await api('/api/status');
state.brainStatus = status;
if (status.runtime_settings) {
state.runtimeSettings = {...state.runtimeSettings, ...status.runtime_settings};
syncRuntimeControls();
}
renderAutonomyStatus(status);
} catch {
renderAutonomyStatus({ollama_ok: false, auto_enrich: false, enrich_error: 'Status nicht erreichbar'});
@@ -108,11 +256,17 @@
const panel = $('autonomyStatus');
const button = $('enrichNow');
if (!panel || !button) return;
const runtime = status.runtime_settings || state.runtimeSettings;
const thinkingEnabled = runtime.thinking_enabled !== false;
const learningEnabled = runtime.learning_enabled !== false;
const queued = status.enrich_result === 'queued' && !status.enrich_running;
const running = Boolean(status.enrich_running || queued);
let text = '';
let cls = 'waiting';
if (status.enrich_running) {
if (!thinkingEnabled) {
text = `Living-only · Thinking pausiert${learningEnabled ? '' : ' · Learning pausiert'}.`;
cls = 'waiting';
} else if (status.enrich_running) {
text = `AI-THINK läuft · ${status.enrich_trigger === 'manual' ? 'manuell' : 'automatisch'} · Batch ${status.enrich_batch_size || 1}`;
cls = 'running';
} else if (queued) {
@@ -140,10 +294,10 @@
panel.title = diagnostics.join(' · ');
const span = panel.querySelector('span');
if (span) span.textContent = text;
button.disabled = running;
button.disabled = running || !thinkingEnabled;
button.classList.toggle('running', running);
const small = button.querySelector('small');
if (small) small.textContent = status.enrich_running ? 'LÄUFT' : queued ? 'WARTET' : 'STARTEN';
if (small) small.textContent = !thinkingEnabled ? 'PAUSIERT' : status.enrich_running ? 'LÄUFT' : queued ? 'WARTET' : 'STARTEN';
}
function nextRunText(value) {
@@ -256,6 +410,112 @@
return out;
}
function honeycombPointCount(spacing, stopAt = Infinity, collect = false) {
const points = collect ? [] : null;
let count = 0;
const yStep = spacing * Math.sqrt(3) / 2;
const zStep = spacing * Math.sqrt(2 / 3);
const kMin = Math.floor(-0.72 / zStep) - 1;
const kMax = Math.ceil(0.72 / zStep) + 1;
const rMin = Math.floor(-0.9 / yStep) - 1;
const rMax = Math.ceil(0.9 / yStep) + 1;
for (let k = kMin; k <= kMax; k++) {
const layer = Math.abs(k) % 2;
const z = k * zStep;
const layerX = layer * spacing * 0.5;
const layerY = layer * yStep / 3;
for (let r = rMin; r <= rMax; r++) {
const y = r * yStep + layerY;
const rowX = (Math.abs(r) % 2) * spacing * 0.5;
const qMin = Math.floor(-1.02 / spacing) - 1;
const qMax = Math.ceil(1.02 / spacing) + 1;
for (let q = qMin; q <= qMax; q++) {
const x = q * spacing + rowX + layerX;
if (!insideBrain(x, y, z)) continue;
count++;
if (collect) points.push({x, y, z});
if (count >= stopAt) return collect ? points : count;
}
}
}
return collect ? points : count;
}
function buildHoneycombLayout() {
const noteKinds = new Set(['knowledge', 'ai-think', 'external']);
const notes = state.nodes.filter(node => noteKinds.has(node.kind));
state.honeycombNodes = notes;
if (!notes.length) {
state.honeycombSpacing = 0;
return;
}
let low = 0.008;
let high = 0.32;
while (honeycombPointCount(low, notes.length) < notes.length && low > 0.0025) low *= 0.75;
for (let i = 0; i < 12; i++) {
const mid = (low + high) / 2;
const count = honeycombPointCount(mid, notes.length);
if (count >= notes.length) low = mid; else high = mid;
}
const spacing = Math.max(0.0025, low * 0.985);
let points = honeycombPointCount(spacing, Infinity, true);
if (points.length < notes.length) points = honeycombPointCount(Math.max(0.0025, spacing * 0.96), Infinity, true);
points.sort((a, b) => hashString(`${a.x.toFixed(5)}:${a.y.toFixed(5)}:${a.z.toFixed(5)}`) - hashString(`${b.x.toFixed(5)}:${b.y.toFixed(5)}:${b.z.toFixed(5)}`));
notes.sort((a, b) => hashString(a.id) - hashString(b.id));
const step = points.length / notes.length;
for (let i = 0; i < notes.length; i++) {
const point = points[Math.min(points.length - 1, Math.floor(i * step))];
notes[i].honeyX = point.x;
notes[i].honeyY = point.y;
notes[i].honeyZ = point.z;
}
state.honeycombSpacing = spacing;
}
function updateViewButtons() {
const neural = $('viewNeural');
const honey = $('viewHoneycomb');
if (neural) neural.classList.toggle('active', state.viewMode === 'neural');
if (honey) honey.classList.toggle('active', state.viewMode === 'honeycomb');
document.body.classList.toggle('honeycomb-view', state.viewMode === 'honeycomb');
const edgeButton = $('toggleEdges');
const cortexButton = $('toggleCortex');
const lodButton = $('toggleLOD');
for (const button of [edgeButton, cortexButton, lodButton]) {
if (button) button.disabled = state.viewMode === 'honeycomb';
}
}
function applyViewMode(mode, persist = true) {
mode = mode === 'honeycomb' ? 'honeycomb' : 'neural';
state.viewMode = mode;
state.runtimeSettings.view_mode = mode;
state.hover = null;
state.selected = null;
state.particles.length = 0;
state.edgeRenderMap.clear();
for (const node of state.nodes) {
node.transitionFrom = null;
node.transitionStart = 0;
}
if (mode === 'honeycomb') {
state.renderNodes = state.honeycombNodes;
state.renderEdges = [];
state.renderIdleEdges = [];
state.renderNodeById = new Map(state.honeycombNodes.map(node => [node.id, node]));
state.renderEdgeById = new Map();
state.visibleForNode = new Map(state.honeycombNodes.map(node => [node.id, node.id]));
state.renderStats = {nodes: state.honeycombNodes.length, edges: 0, hiddenNodes: Math.max(0, state.nodes.length - state.honeycombNodes.length), hiddenEdges: state.edges.length};
const renderCount = $('renderCount');
if (renderCount) renderCount.textContent = state.honeycombNodes.length.toLocaleString('de-DE');
} else {
state.lodDirty = true;
rebuildRenderGraph(performance.now(), true);
}
updateViewButtons();
if (persist) persistRuntimeSettings().catch(() => {});
}
function buildLayout(previousNodes = new Map(), previousClusters = new Map()) {
const degree = new Map(state.nodes.map(n => [n.id, (state.adjacency.get(n.id) || []).length]));
const labels = new Map();
@@ -731,6 +991,9 @@
}
function setVisualMode(mode, evt = null, strength = 1) {
if (!state.runtimeSettings.learning_enabled && !state.runtimeSettings.thinking_enabled) mode = 'living';
if (mode === 'learning' && !state.runtimeSettings.learning_enabled) mode = 'living';
if ((mode === 'thinking' || mode === 'researching') && !state.runtimeSettings.thinking_enabled) mode = 'living';
const cfg = MODE_CONFIG[mode] || MODE_CONFIG.living;
const now = performance.now();
state.mode = mode;
@@ -760,9 +1023,12 @@
function focusCamera(node) {
state.focusNodeID = node.id;
state.focusClusterKey = node.clusterKey || '';
const radial = Math.max(0.05, Math.hypot(node.x, node.z));
state.cameraTargetYaw = nearestAngle(state.yaw, Math.atan2(node.x, node.z));
state.cameraTargetPitch = Math.max(-0.62, Math.min(0.62, Math.atan2(node.y, radial)));
const x = state.viewMode === 'honeycomb' && Number.isFinite(node.honeyX) ? node.honeyX : node.x;
const y = state.viewMode === 'honeycomb' && Number.isFinite(node.honeyY) ? node.honeyY : node.y;
const z = state.viewMode === 'honeycomb' && Number.isFinite(node.honeyZ) ? node.honeyZ : node.z;
const radial = Math.max(0.05, Math.hypot(x, z));
state.cameraTargetYaw = nearestAngle(state.yaw, Math.atan2(x, z));
state.cameraTargetPitch = Math.max(-0.62, Math.min(0.62, Math.atan2(y, radial)));
}
function updateVisualState(now, dt) {
@@ -792,6 +1058,7 @@
}
function autonomousLivingPulse(now) {
if (state.viewMode === 'honeycomb') return;
if (state.mode !== 'living' || now < state.nextAmbientAt || !state.clusters.length) return;
const candidates = state.clusters.slice(0, Math.min(18, state.clusters.length));
const cluster = candidates[state.ambientCursor % candidates.length];
@@ -815,11 +1082,12 @@
}
function eventMode(evt) {
if (!state.runtimeSettings.learning_enabled && !state.runtimeSettings.thinking_enabled) return 'living';
if (!evt || evt.type === 'brain.idle') return 'living';
if (evt.type?.includes('research')) return 'researching';
if (evt.type?.includes('think')) return 'thinking';
if (evt.type?.includes('research')) return state.runtimeSettings.thinking_enabled ? 'researching' : 'living';
if (evt.type?.includes('think')) return state.runtimeSettings.thinking_enabled ? 'thinking' : 'living';
if (evt.source === 'agent' || evt.source === 'knowledgebase' || evt.type?.includes('query')) return 'processing';
if (evt.type === 'graph.updated' || evt.type === 'scan.started' || evt.type === 'embedding.batch') return 'learning';
if (evt.type === 'graph.updated' || evt.type === 'scan.started' || evt.type === 'embedding.batch' || evt.type?.startsWith('glpi.kb')) return state.runtimeSettings.learning_enabled ? 'learning' : 'living';
return 'processing';
}
@@ -828,8 +1096,11 @@
}
function projectAnimatedNode(node, now) {
const cluster = node.cluster;
let x = node.x, y = node.y, z = node.z;
const honeycomb = state.viewMode === 'honeycomb' && Number.isFinite(node.honeyX);
const cluster = honeycomb ? null : node.cluster;
let x = honeycomb ? node.honeyX : node.x;
let y = honeycomb ? node.honeyY : node.y;
let z = honeycomb ? node.honeyZ : 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);
@@ -911,6 +1182,7 @@
}
function renderClusterClouds(now) {
if (state.viewMode === 'honeycomb') return;
ctx.save();
ctx.globalCompositeOperation = 'screen';
for (const cluster of state.clusters) {
@@ -944,6 +1216,7 @@
}
function renderCortexLabels() {
if (state.viewMode === 'honeycomb') return;
if (!state.cortexVisible || state.width < 850) return;
const visible = state.clusters
.filter(cluster => cluster.screen && (cluster.importance > 0.52 || cluster.key === state.focusClusterKey))
@@ -983,6 +1256,7 @@
}
function renderEdges() {
if (state.viewMode === 'honeycomb') return;
if (!state.edgesVisible) return;
ctx.save();
ctx.globalCompositeOperation = 'screen';
@@ -1013,6 +1287,7 @@
}
function renderParticles(dt) {
if (state.viewMode === 'honeycomb') return;
ctx.save();
ctx.globalCompositeOperation = 'lighter';
for (let i = state.particles.length - 1; i >= 0; i--) {
@@ -1080,20 +1355,22 @@
ctx.save();
ctx.globalCompositeOperation = 'lighter';
for (const node of state.projected) {
const honeycomb = state.viewMode === 'honeycomb';
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 degree = honeycomb ? 0 : 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 baseWeight = honeycomb ? 0.72 : 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 clusterEnergy = honeycomb ? 0 : state.clusterActive.get(node.clusterKey) || 0;
const focused = honeycomb ? 0 : 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, (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 baseAlpha = honeycomb ? 0.23 : (isGroup ? 0.42 : 0.17);
const alpha = Math.min(1, baseAlpha - idleDim + node.screen.p * (honeycomb ? 0.035 : 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 || (!honeycomb && (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));
@@ -1124,7 +1401,7 @@
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') {
} else if (!honeycomb && node.kind === 'ai-think') {
ctx.strokeStyle = nodeColor(node, 0.55);
ctx.lineWidth = 0.7;
ctx.beginPath();
@@ -1141,7 +1418,7 @@
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;
if (!(act > 0.45 || state.hover?.id === node.id || state.selected?.id === node.id || (state.viewMode !== 'honeycomb' && !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;
@@ -1202,15 +1479,22 @@
const dt = Math.min(0.05, (now - state.last) / 1000);
state.last = now;
updateVisualState(now, dt);
updateLOD(now);
if (state.viewMode === 'neural') updateLOD(now);
drawBackground(now);
renderActivityAura(now);
renderClusterClouds(now);
renderEdges();
renderParticles(dt);
if (state.viewMode === 'neural') {
renderActivityAura(now);
renderClusterClouds(now);
renderEdges();
renderParticles(dt);
}
renderNodes(now, dt);
renderWaves(dt);
renderCortexLabels();
if (state.viewMode === 'neural') {
renderWaves(dt);
renderCortexLabels();
} else {
state.waves.length = 0;
state.bursts.length = 0;
}
for (const [id, value] of state.edgeActive) {
const next = value - dt * (state.mode === 'living' ? 0.34 : 0.5);
if (next <= 0) state.edgeActive.delete(id); else state.edgeActive.set(id, next);
@@ -1228,7 +1512,7 @@
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) {
if (state.viewMode === 'neural' && 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);
@@ -1245,28 +1529,32 @@
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});
if (state.viewMode === 'neural') {
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});
}
}
}
if (evt.type !== 'brain.idle') {
state.pulses++;
$('pulseCount').textContent = state.pulses.toLocaleString('de-DE');
const focus = evt.node_ids?.[0] ? state.nodeById.get(evt.node_ids[0]) : null;
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});
if (state.viewMode === 'neural') {
const focus = evt.node_ids?.[0] ? state.nodeById.get(evt.node_ids[0]) : null;
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});
}
if (mode === 'thinking' || mode === 'researching') state.bursts.push({x, y, life: 1, color: modeColor, rays: mode === 'thinking' ? 22 : 16, length: mode === 'thinking' ? 150 : 115, spin: pseudo(evt.id || evt.type, 9) * Math.PI});
}
if (mode === 'thinking' || mode === 'researching') state.bursts.push({x, y, life: 1, color: modeColor, rays: mode === 'thinking' ? 22 : 16, length: mode === 'thinking' ? 150 : 115, spin: pseudo(evt.id || evt.type, 9) * Math.PI});
}
addLog(evt);
if (evt.type === 'graph.updated') loadGraph();
@@ -1374,6 +1662,91 @@
stream.onerror = () => setSystem('verbindet', false);
stream.onopen = () => setSystem('lebt', true);
$('viewNeural').addEventListener('click', () => applyViewMode('neural', true));
$('viewHoneycomb').addEventListener('click', () => applyViewMode('honeycomb', true));
$('toggleLearning').addEventListener('click', async e => {
const button = e.currentTarget;
button.disabled = true;
state.runtimeSettings.learning_enabled = !state.runtimeSettings.learning_enabled;
try {
await persistRuntimeSettings();
if (!state.runtimeSettings.learning_enabled && !state.runtimeSettings.thinking_enabled) setVisualMode('living');
} catch (err) {
state.runtimeSettings.learning_enabled = !state.runtimeSettings.learning_enabled;
addLog({type: 'runtime.settings.failed', source: 'ui', phase: 'control', message: `Learning konnte nicht umgestellt werden: ${err.message}`, timestamp: new Date().toISOString()});
} finally {
button.disabled = false;
syncRuntimeControls();
}
});
$('toggleThinking').addEventListener('click', async e => {
const button = e.currentTarget;
button.disabled = true;
state.runtimeSettings.thinking_enabled = !state.runtimeSettings.thinking_enabled;
try {
await persistRuntimeSettings();
if (!state.runtimeSettings.learning_enabled && !state.runtimeSettings.thinking_enabled) setVisualMode('living');
} catch (err) {
state.runtimeSettings.thinking_enabled = !state.runtimeSettings.thinking_enabled;
addLog({type: 'runtime.settings.failed', source: 'ui', phase: 'control', message: `Thinking konnte nicht umgestellt werden: ${err.message}`, timestamp: new Date().toISOString()});
} finally {
button.disabled = false;
syncRuntimeControls();
}
});
$('openSettings').addEventListener('click', openSettingsPanel);
$('closeSettings').addEventListener('click', closeSettingsPanel);
$('settingsBackdrop').addEventListener('click', closeSettingsPanel);
$('categorySearch').addEventListener('input', e => renderCategoryFilters(e.currentTarget.value));
$('settingsLearning').addEventListener('change', e => { if (state.settingsDraft) state.settingsDraft.learning_enabled = e.currentTarget.checked; });
$('settingsThinking').addEventListener('change', e => { if (state.settingsDraft) state.settingsDraft.thinking_enabled = e.currentTarget.checked; });
$('settingsViewNeural').addEventListener('click', () => {
if (state.settingsDraft) state.settingsDraft.view_mode = 'neural';
$('settingsViewNeural').classList.add('active');
$('settingsViewHoneycomb').classList.remove('active');
});
$('settingsViewHoneycomb').addEventListener('click', () => {
if (state.settingsDraft) state.settingsDraft.view_mode = 'honeycomb';
$('settingsViewHoneycomb').classList.add('active');
$('settingsViewNeural').classList.remove('active');
});
$('settingsPanel').addEventListener('change', e => {
const input = e.target.closest('[data-category-filter]');
if (!input || !state.settingsDraft) return;
const key = input.dataset.categoryFilter;
const property = `${key}_categories`;
const selected = new Map((state.settingsDraft[property] || []).map(value => [String(value).toLowerCase(), value]));
const normalized = String(input.value).toLowerCase();
if (input.checked) selected.set(normalized, input.value); else selected.delete(normalized);
state.settingsDraft[property] = [...selected.values()];
});
document.querySelectorAll('[data-clear-filter]').forEach(button => button.addEventListener('click', () => {
if (!state.settingsDraft) return;
state.settingsDraft[`${button.dataset.clearFilter}_categories`] = [];
renderCategoryFilters($('categorySearch').value);
}));
$('saveSettings').addEventListener('click', async e => {
if (!state.settingsDraft) return;
const button = e.currentTarget;
const feedback = $('settingsFeedback');
button.disabled = true;
feedback.textContent = 'Einstellungen werden übernommen …';
try {
state.settingsDraft.learning_enabled = $('settingsLearning').checked;
state.settingsDraft.thinking_enabled = $('settingsThinking').checked;
await persistRuntimeSettings(state.settingsDraft);
feedback.textContent = 'Aktiv · Speicherung erfolgt gebündelt mit dem nächsten Flush.';
setTimeout(closeSettingsPanel, 550);
} catch (err) {
feedback.textContent = `Fehler: ${err.message}`;
} finally {
button.disabled = false;
}
});
$('toggleLabels').addEventListener('click', e => {
state.labels = !state.labels;
e.currentTarget.classList.toggle('active', state.labels);
@@ -1494,8 +1867,11 @@
return String(v ?? '').replace(/[&<>'"]/g, c => ({'&': '&amp;', '<': '&lt;', '>': '&gt;', "'": '&#39;', '"': '&quot;'}[c]));
}
loadGraph();
loadStatus();
(async () => {
await loadRuntimeConfiguration();
await loadGraph();
await loadStatus();
})();
setInterval(loadGraph, 30000);
setInterval(loadStatus, 3000);
})();