|
|
|
@@ -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 => ({'&': '&', '<': '<', '>': '>', "'": ''', '"': '"'}[c]));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
loadGraph();
|
|
|
|
|
loadStatus();
|
|
|
|
|
(async () => {
|
|
|
|
|
await loadRuntimeConfiguration();
|
|
|
|
|
await loadGraph();
|
|
|
|
|
await loadStatus();
|
|
|
|
|
})();
|
|
|
|
|
setInterval(loadGraph, 30000);
|
|
|
|
|
setInterval(loadStatus, 3000);
|
|
|
|
|
})();
|
|
|
|
|