/** * Mindmap-Initialisierer * Lädt und initialisiert die Mindmap-Visualisierung */ // 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(); } }); /** * 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 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 = 'Keine verbundenen Knoten'; } // 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, '
') .replace(/\*\*(.*?)\*\*/g, '$1') .replace(/\*(.*?)\*/g, '$1') .replace(/`(.*?)`/g, '$1'); } // Setzt die Auswahl zurück function resetSelection(cy) { // Alle Stile zurücksetzen cy.nodes().forEach(n => { n.removeStyle(); }); cy.edges().forEach(e => { e.removeStyle(); }); // Kein Knoten ausgewählt window.mindmapInstance.selectedNode = null; // Info-Panel ausblenden const nodeInfoPanel = document.getElementById('node-info-panel'); if (nodeInfoPanel) { nodeInfoPanel.classList.remove('visible'); } // 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'); } }); } /** * Gibt die Stile für das neuronale Netzwerk-Design zurück * @returns {Array} Stildefinitionen für Cytoscape */ 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 } } ]; }