feat(mindmap): enhance interaction and initialization logic in mindmap files

This commit is contained in:
2025-05-06 21:53:54 +01:00
parent 49e5e19b7c
commit aeb829e36a
6 changed files with 1806 additions and 2096 deletions

View File

@@ -1,648 +1,214 @@
/**
* Mindmap-Initialisierer
* Lädt und initialisiert die Mindmap-Visualisierung
* Mindmap Initialisierung und Event-Handling
*/
// Warte bis DOM geladen ist
document.addEventListener('DOMContentLoaded', function() {
// Prüfe, ob wir auf der Mindmap-Seite sind
const cyContainer = document.getElementById('cy');
if (!cyContainer) {
console.log('Kein Mindmap-Container gefunden, überspringe Initialisierung.');
return;
}
console.log('Initialisiere Mindmap-Visualisierung...');
// Prüfe, ob Cytoscape.js verfügbar ist
if (typeof cytoscape === 'undefined') {
loadScript('/static/js/cytoscape.min.js', initMindmap);
} else {
initMindmap();
}
});
// Warte auf die Cytoscape-Instanz
document.addEventListener('mindmap-loaded', function() {
const cy = window.cy;
if (!cy) return;
/**
* Lädt ein Script dynamisch
* @param {string} src - Quelldatei
* @param {Function} callback - Callback nach dem Laden
*/
function loadScript(src, callback) {
const script = document.createElement('script');
script.src = src;
script.onload = callback;
document.head.appendChild(script);
}
/**
* Initialisiert die Mindmap-Visualisierung
*/
function initMindmap() {
const cyContainer = document.getElementById('cy');
// Erstelle Cytoscape-Instanz
const cy = cytoscape({
container: cyContainer,
style: getNeuralNetworkStyles(),
layout: {
name: 'cose',
animate: true,
animationDuration: 1500,
nodeDimensionsIncludeLabels: true,
padding: 100,
spacingFactor: 1.5,
randomize: true,
componentSpacing: 100,
nodeRepulsion: 8000,
edgeElasticity: 100,
nestingFactor: 1.2,
gravity: 80,
idealEdgeLength: 150
},
wheelSensitivity: 0.1, // Sanfterer Zoom
minZoom: 0.3,
maxZoom: 2.5,
});
// Daten vom Server laden
loadMindmapData(cy);
// Event-Handler zuweisen
setupEventListeners(cy);
// Globale Referenz für Externe Zugriffe
window.cy = cy;
window.mindmapInstance = {
cy: cy,
selectedNode: null,
centerNodeInView: function(node) {
cy.animate({
center: { eles: node },
zoom: 1.2,
duration: 800,
easing: 'ease-in-out-cubic'
});
}
};
}
/**
* Lädt die Mindmap-Daten vom Server
* @param {Object} cy - Cytoscape-Instanz
*/
function loadMindmapData(cy) {
fetch('/api/mindmap')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP Fehler: ${response.status}`);
}
return response.json();
})
.then(data => {
if (!data.nodes || data.nodes.length === 0) {
console.log('Keine Daten gefunden, versuche Refresh-API...');
return fetch('/api/refresh-mindmap')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP Fehler beim Refresh: ${response.status}`);
}
return response.json();
});
}
return data;
})
.then(data => {
console.log('Mindmap-Daten geladen:', data);
// Cytoscape-Elemente vorbereiten
const elements = [];
// Prüfen, ob "Wissen"-Knoten existiert
let rootNode = data.nodes.find(node => node.name === "Wissen");
// Wenn nicht, Root-Knoten hinzufügen
if (!rootNode) {
rootNode = {
id: 'root',
name: 'Wissen',
description: 'Zentrale Wissensbasis',
color_code: '#4299E1'
};
data.nodes.unshift(rootNode);
}
// Knoten hinzufügen mit zufälligen Werten für neuronales Netzwerk
data.nodes.forEach(node => {
// Neuronenzell-Größe und Aktivität
const neuronSize = Math.floor(Math.random() * 8) + 3;
const neuronActivity = Math.random() * 0.7 + 0.3; // Aktivitätslevel zwischen 0.3 und 1.0
elements.push({
group: 'nodes',
data: {
id: node.id.toString(),
name: node.name,
description: node.description || '',
color: node.color_code || '#8B5CF6',
isRoot: node.name === 'Wissen',
neuronSize: neuronSize,
neuronActivity: neuronActivity
}
});
});
// Kanten hinzufügen, wenn vorhanden
if (data.edges && data.edges.length > 0) {
data.edges.forEach(edge => {
elements.push({
group: 'edges',
data: {
id: `${edge.source}-${edge.target}`,
source: edge.source.toString(),
target: edge.target.toString(),
strength: Math.random() * 0.6 + 0.2 // Zufällige Verbindungsstärke zwischen 0.2 und 0.8
}
});
});
} else {
// Wenn keine Kanten definiert sind, verbinde alle Knoten mit dem Root-Knoten
const rootId = rootNode.id.toString();
data.nodes.forEach(node => {
if (node.id.toString() !== rootId) {
elements.push({
group: 'edges',
data: {
id: `${rootId}-${node.id}`,
source: rootId,
target: node.id.toString(),
strength: Math.random() * 0.6 + 0.2
}
});
}
});
}
// Elemente zu Cytoscape hinzufügen
cy.elements().remove();
cy.add(elements);
// Layout anwenden
cy.layout({
name: 'cose',
animate: true,
animationDuration: 1800,
nodeDimensionsIncludeLabels: true,
padding: 100,
spacingFactor: 1.8,
randomize: false,
fit: true,
componentSpacing: 100,
nodeRepulsion: 8000,
edgeElasticity: 100
}).run();
// Neuronale Netzwerk-Effekte hinzufügen
setTimeout(() => {
addNeuralNetworkEffects(cy);
}, 2000);
// Nach dem Laden Event auslösen
document.dispatchEvent(new CustomEvent('mindmap-loaded'));
})
.catch(error => {
console.error('Fehler beim Laden der Mindmap-Daten:', error);
// Fallback mit Standard-Daten
const fallbackData = {
nodes: [
{ id: 1, name: 'Wissen', description: 'Zentrale Wissensbasis', color_code: '#4299E1' },
{ id: 2, name: 'Philosophie', description: 'Philosophisches Denken', color_code: '#9F7AEA' },
{ id: 3, name: 'Wissenschaft', description: 'Wissenschaftliche Erkenntnisse', color_code: '#48BB78' },
{ id: 4, name: 'Technologie', description: 'Technologische Entwicklungen', color_code: '#ED8936' },
{ id: 5, name: 'Künste', description: 'Künstlerische Ausdrucksformen', color_code: '#ED64A6' }
],
edges: [
{ source: 1, target: 2 },
{ source: 1, target: 3 },
{ source: 1, target: 4 },
{ source: 1, target: 5 }
]
};
const fallbackElements = [];
// Knoten hinzufügen mit Neuronen-Eigenschaften
fallbackData.nodes.forEach(node => {
const neuronSize = Math.floor(Math.random() * 8) + 3;
const neuronActivity = Math.random() * 0.7 + 0.3;
fallbackElements.push({
group: 'nodes',
data: {
id: node.id.toString(),
name: node.name,
description: node.description || '',
color: node.color_code || '#8B5CF6',
isRoot: node.name === 'Wissen',
neuronSize: neuronSize,
neuronActivity: neuronActivity
}
});
});
// Kanten hinzufügen
fallbackData.edges.forEach(edge => {
fallbackElements.push({
group: 'edges',
data: {
id: `${edge.source}-${edge.target}`,
source: edge.source.toString(),
target: edge.target.toString(),
strength: Math.random() * 0.6 + 0.2
}
});
});
// Elemente zu Cytoscape hinzufügen
cy.elements().remove();
cy.add(fallbackElements);
// Layout anwenden
cy.layout({
name: 'cose',
animate: true,
animationDuration: 1800,
nodeDimensionsIncludeLabels: true,
fit: true
}).run();
// Neuronale Netzwerk-Effekte hinzufügen
setTimeout(() => {
addNeuralNetworkEffects(cy);
}, 2000);
// Nach dem Laden Event auslösen
document.dispatchEvent(new CustomEvent('mindmap-loaded'));
});
}
// Neuronales Netzwerk-Effekte hinzufügen
function addNeuralNetworkEffects(cy) {
cy.nodes().forEach(node => {
const originalPos = node.position();
const activity = node.data('neuronActivity') || 0.5;
// Subtile Pulsierende Bewegung für Neuronen
setInterval(() => {
const randomFactor = Math.random() * 0.5 + 0.5; // Zufälliger Faktor zwischen 0.5 und 1
const offset = (Math.random() - 0.5) * activity;
const pulseIntensity = 0.7 + (Math.sin(Date.now() / 2000) * 0.3 * activity * randomFactor);
// Leichtes Pulsieren und Bewegung basierend auf "Neuronenaktivität"
node.animate({
position: {
x: originalPos.x + offset,
y: originalPos.y + offset
},
style: {
'background-opacity': Math.max(0.7, pulseIntensity),
'shadow-opacity': Math.max(0.5, pulseIntensity * 0.8)
},
duration: 3000 + (Math.random() * 2000),
easing: 'ease-in-out-cubic',
complete: function() {
node.animate({
position: { x: originalPos.x, y: originalPos.y },
style: {
'background-opacity': 0.9,
'shadow-opacity': 0.6
},
duration: 3000 + (Math.random() * 2000),
easing: 'ease-in-out-cubic'
});
}
});
}, 6000 + Math.random() * 4000); // Leicht zufällige Intervalle für organischeres Aussehen
// Zusätzliche "synaptische" Effekte für Kanten
node.connectedEdges().forEach(edge => {
const strength = edge.data('strength') || 0.5;
// Pulsierende Aktivität entlang der Kanten
setInterval(() => {
const pulseIntensity = 0.5 + (Math.sin(Date.now() / 1500) * 0.5 * strength);
edge.animate({
style: {
'line-opacity': Math.max(0.3, pulseIntensity),
'width': 1 + (pulseIntensity * 0.6)
},
duration: 1200,
easing: 'ease-in-out'
});
}, 3000 + (Math.random() * 2000));
});
});
}
/**
* Richtet Event-Listener für die Mindmap ein
* @param {Object} cy - Cytoscape-Instanz
*/
function setupEventListeners(cy) {
// Klick auf Knoten
// Event-Listener für Knoten-Klicks
cy.on('tap', 'node', function(evt) {
const node = evt.target;
// Alle vorherigen Hervorhebungen zurücksetzen
cy.nodes().forEach(n => {
n.removeStyle();
n.connectedEdges().removeStyle();
});
// Speichere ausgewählten Knoten
window.mindmapInstance.selectedNode = node;
// Aktiviere leuchtenden Effekt statt Umkreisung
node.style({
'background-opacity': 1,
'background-color': node.data('color'),
'shadow-color': node.data('color'),
'shadow-opacity': 1,
'shadow-blur': 15,
'shadow-offset-x': 0,
'shadow-offset-y': 0
});
// Verbundene Kanten und Knoten hervorheben
const connectedEdges = node.connectedEdges();
const connectedNodes = node.neighborhood('node');
connectedEdges.style({
'line-color': '#a78bfa',
'target-arrow-color': '#a78bfa',
'source-arrow-color': '#a78bfa',
'line-opacity': 0.8,
'width': 2
});
connectedNodes.style({
'shadow-opacity': 0.7,
'shadow-blur': 10,
'shadow-color': '#a78bfa'
});
// Info-Panel aktualisieren
updateInfoPanel(node);
// Seitenleiste aktualisieren
updateSidebar(node);
});
// Klick auf Hintergrund - Auswahl zurücksetzen
cy.on('tap', function(evt) {
if (evt.target === cy) {
resetSelection(cy);
}
});
// Smooth Zoom mit Mouse Wheel
cy.on('mousewheel', function(evt) {
const delta = evt.originalEvent.deltaY;
const factor = delta > 0 ? 0.95 : 1.05;
cy.animate({
zoom: cy.zoom() * factor,
duration: 100
});
});
}
// Aktualisiert das Info-Panel mit Daten des ausgewählten Knotens
function updateInfoPanel(node) {
const nodeInfoPanel = document.getElementById('node-info-panel');
const nodeDescription = document.getElementById('node-description');
const connectedNodes = document.getElementById('connected-nodes');
const panelTitle = nodeInfoPanel.querySelector('.info-panel-title');
if (!nodeInfoPanel || !nodeDescription || !connectedNodes) return;
// Titel und Beschreibung aktualisieren
panelTitle.textContent = node.data('name');
nodeDescription.textContent = node.data('description') || 'Keine Beschreibung verfügbar.';
// Verbundene Knoten anzeigen
connectedNodes.innerHTML = '';
// Verbundene Knoten sammeln (direktes Neighborhood)
const connectedNodesList = [];
node.neighborhood('node').forEach(n => {
if (!connectedNodesList.includes(n) && n.id() !== node.id()) {
connectedNodesList.push(n);
}
});
// Verbundene Knoten im Panel anzeigen
if (connectedNodesList.length > 0) {
connectedNodesList.forEach(connectedNode => {
const nodeLink = document.createElement('span');
nodeLink.className = 'inline-block px-2 py-1 text-xs rounded-md m-1 cursor-pointer';
nodeLink.style.backgroundColor = connectedNode.data('color');
nodeLink.textContent = connectedNode.data('name');
// Beim Klick auf den verbundenen Knoten zu diesem wechseln
nodeLink.addEventListener('click', function() {
resetSelection(cy);
// Verzögerung vor der neuen Auswahl für besseren visuellen Übergang
setTimeout(() => {
connectedNode.trigger('tap');
}, 50);
});
connectedNodes.appendChild(nodeLink);
});
} else {
connectedNodes.innerHTML = '<span class="text-sm italic">Keine verbundenen Knoten</span>';
}
// Panel anzeigen mit Animation
nodeInfoPanel.style.transition = 'all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1)';
nodeInfoPanel.classList.add('visible');
}
// Aktualisiert die Seitenleiste
function updateSidebar(node) {
// Alle standard Panels ausblenden
document.querySelectorAll('[data-sidebar]').forEach(panel => {
if (panel.getAttribute('data-sidebar') === 'node-description') {
// Beschreibungs-Panel anzeigen
panel.classList.remove('hidden');
// Titel und Beschreibung aktualisieren
const titleElement = panel.querySelector('[data-node-title]');
const descriptionElement = panel.querySelector('[data-node-description]');
if (titleElement) {
titleElement.textContent = node.data('name');
}
if (descriptionElement) {
descriptionElement.innerHTML = formatDescription(node.data('description') || 'Keine Beschreibung verfügbar.');
}
} else {
// Andere Panels ausblenden
panel.classList.add('hidden');
}
});
}
// Formatiert die Beschreibung mit etwas HTML-Markup
function formatDescription(text) {
return text
.replace(/\n/g, '<br>')
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.*?)\*/g, '<em>$1</em>')
.replace(/`(.*?)`/g, '<code>$1</code>');
}
// Setzt die Auswahl zurück
function resetSelection(cy) {
// Alle Stile zurücksetzen
// Alle vorherigen Hervorhebungen zurücksetzen
cy.nodes().forEach(n => {
n.removeStyle();
n.removeStyle();
n.connectedEdges().removeStyle();
});
cy.edges().forEach(e => {
e.removeStyle();
// Speichere ausgewählten Knoten
window.mindmapInstance.selectedNode = node;
// Aktiviere leuchtenden Effekt statt Umkreisung
node.style({
'background-opacity': 1,
'background-color': node.data('color'),
'shadow-color': node.data('color'),
'shadow-opacity': 1,
'shadow-blur': 15,
'shadow-offset-x': 0,
'shadow-offset-y': 0
});
// Kein Knoten ausgewählt
window.mindmapInstance.selectedNode = null;
// Verbundene Kanten und Knoten hervorheben
const connectedEdges = node.connectedEdges();
const connectedNodes = node.neighborhood('node');
// Info-Panel ausblenden
const nodeInfoPanel = document.getElementById('node-info-panel');
if (nodeInfoPanel) {
nodeInfoPanel.classList.remove('visible');
connectedEdges.style({
'line-color': '#a78bfa',
'target-arrow-color': '#a78bfa',
'source-arrow-color': '#a78bfa',
'line-opacity': 0.8,
'width': 2
});
connectedNodes.style({
'shadow-opacity': 0.7,
'shadow-blur': 10,
'shadow-color': '#a78bfa'
});
// Info-Panel aktualisieren
updateInfoPanel(node);
// Seitenleiste aktualisieren
updateSidebar(node);
});
// Klick auf Hintergrund - Auswahl zurücksetzen
cy.on('tap', function(evt) {
if (evt.target === cy) {
resetSelection(cy);
}
// Standard-Seitenleisten-Panels anzeigen
document.querySelectorAll('[data-sidebar]').forEach(panel => {
if (panel.getAttribute('data-sidebar') === 'node-description') {
panel.classList.add('hidden');
} else {
panel.classList.remove('hidden');
}
});
// Zoom-Controls
document.getElementById('zoomIn')?.addEventListener('click', () => {
cy.zoom({
level: cy.zoom() * 1.2,
renderedPosition: { x: cy.width() / 2, y: cy.height() / 2 }
});
});
document.getElementById('zoomOut')?.addEventListener('click', () => {
cy.zoom({
level: cy.zoom() / 1.2,
renderedPosition: { x: cy.width() / 2, y: cy.height() / 2 }
});
});
document.getElementById('resetView')?.addEventListener('click', () => {
cy.fit();
resetSelection(cy);
});
// Legend-Toggle
document.getElementById('toggleLegend')?.addEventListener('click', () => {
const legend = document.getElementById('categoryLegend');
if (legend) {
isLegendVisible = !isLegendVisible;
legend.style.display = isLegendVisible ? 'block' : 'none';
}
});
// Keyboard-Controls
document.addEventListener('keydown', (e) => {
if (e.key === '+' || e.key === '=') {
cy.zoom({
level: cy.zoom() * 1.2,
renderedPosition: { x: cy.width() / 2, y: cy.height() / 2 }
});
} else if (e.key === '-' || e.key === '_') {
cy.zoom({
level: cy.zoom() / 1.2,
renderedPosition: { x: cy.width() / 2, y: cy.height() / 2 }
});
} else if (e.key === 'Escape') {
resetSelection(cy);
}
});
});
/**
* Aktualisiert das Info-Panel mit Knoteninformationen
* @param {Object} node - Der ausgewählte Knoten
*/
function updateInfoPanel(node) {
const infoPanel = document.getElementById('infoPanel');
if (!infoPanel) return;
const data = node.data();
const connectedNodes = node.neighborhood('node');
let html = `
<h3>${data.label || data.name}</h3>
<p class="category">${data.category || 'Keine Kategorie'}</p>
${data.description ? `<p class="description">${data.description}</p>` : ''}
<div class="connections">
<h4>Verbindungen (${connectedNodes.length})</h4>
<ul>
`;
connectedNodes.forEach(connectedNode => {
const connectedData = connectedNode.data();
html += `
<li style="color: ${connectedData.color || '#60a5fa'}">
${connectedData.label || connectedData.name}
</li>
`;
});
html += `
</ul>
</div>
`;
infoPanel.innerHTML = html;
infoPanel.style.display = 'block';
}
/**
* Gibt die Stile für das neuronale Netzwerk-Design zurück
* @returns {Array} Stildefinitionen für Cytoscape
* Aktualisiert die Seitenleiste mit Knoteninformationen
* @param {Object} node - Der ausgewählte Knoten
*/
function getNeuralNetworkStyles() {
return [
// Neuronen (Knoten)
{
selector: 'node',
style: {
'label': 'data(name)',
'text-valign': 'bottom',
'text-halign': 'center',
'color': '#ffffff',
'text-outline-width': 2,
'text-outline-color': '#0a0e19',
'text-outline-opacity': 0.9,
'font-size': 10,
'text-margin-y': 6,
'width': 'mapData(neuronSize, 3, 10, 15, 40)',
'height': 'mapData(neuronSize, 3, 10, 15, 40)',
'background-color': 'data(color)',
'background-opacity': 0.9,
'border-width': 0,
'shape': 'ellipse',
'shadow-blur': 'mapData(neuronActivity, 0.3, 1, 5, 15)',
'shadow-color': 'data(color)',
'shadow-opacity': 0.6,
'shadow-offset-x': 0,
'shadow-offset-y': 0,
'text-wrap': 'wrap',
'text-max-width': 100,
'transition-property': 'background-color, shadow-color, shadow-opacity, shadow-blur',
'transition-duration': '0.3s'
}
},
// Synapsen (Kanten)
{
selector: 'edge',
style: {
'curve-style': 'bezier',
'line-color': '#8a8aaa',
'width': 'mapData(strength, 0.2, 0.8, 0.8, 2)',
'line-opacity': 'mapData(strength, 0.2, 0.8, 0.4, 0.7)',
'target-arrow-shape': 'none', // Keine Pfeilspitzen bei Neuronen
'target-arrow-color': '#8a8aaa',
'arrow-scale': 0.6,
'transition-property': 'line-color, line-opacity, width',
'transition-duration': '0.3s'
}
},
// Schwache Verbindungen
{
selector: 'edge[strength <= 0.4]',
style: {
'line-style': 'dotted'
}
},
// Mittlere Verbindungen
{
selector: 'edge[strength > 0.4][strength <= 0.6]',
style: {
'line-style': 'dashed'
}
},
// Starke Verbindungen
{
selector: 'edge[strength > 0.6]',
style: {
'line-style': 'solid'
}
},
// Wurzelknoten (speziell gestaltet)
{
selector: 'node[isRoot]',
style: {
'font-size': 12,
'font-weight': 'bold',
'width': 50,
'height': 50,
'background-color': '#6366f1',
'shadow-blur': 20,
'shadow-color': '#6366f1',
'shadow-opacity': 0.8,
'text-margin-y': 8
}
},
// Hover-Effekt für Knoten
{
selector: 'node:hover',
style: {
'shadow-blur': 20,
'shadow-opacity': 0.9,
'transition-property': 'shadow-opacity, shadow-blur',
'transition-duration': '0.2s'
}
},
// Hover-Effekt für Kanten
{
selector: 'edge:hover',
style: {
'line-color': '#a78bfa',
'line-opacity': 0.8,
'width': 2
}
}
];
}
function updateSidebar(node) {
const sidebar = document.getElementById('sidebar');
if (!sidebar) return;
const data = node.data();
const connectedNodes = node.neighborhood('node');
let html = `
<div class="node-details">
<h3>${data.label || data.name}</h3>
<p class="category">${data.category || 'Keine Kategorie'}</p>
${data.description ? `<p class="description">${data.description}</p>` : ''}
<div class="connections">
<h4>Verbindungen (${connectedNodes.length})</h4>
<ul>
`;
connectedNodes.forEach(connectedNode => {
const connectedData = connectedNode.data();
html += `
<li style="color: ${connectedData.color || '#60a5fa'}">
${connectedData.label || connectedData.name}
</li>
`;
});
html += `
</ul>
</div>
</div>
`;
sidebar.innerHTML = html;
}
/**
* Setzt die Auswahl zurück
* @param {Object} cy - Cytoscape-Instanz
*/
function resetSelection(cy) {
window.mindmapInstance.selectedNode = null;
// Alle Hervorhebungen zurücksetzen
cy.nodes().forEach(node => {
node.removeStyle();
node.connectedEdges().removeStyle();
});
// Info-Panel ausblenden
const infoPanel = document.getElementById('infoPanel');
if (infoPanel) {
infoPanel.style.display = 'none';
}
// Seitenleiste leeren
const sidebar = document.getElementById('sidebar');
if (sidebar) {
sidebar.innerHTML = '';
}
}