diff --git a/__pycache__/app.cpython-313.pyc b/__pycache__/app.cpython-313.pyc index 399441f..621e517 100644 Binary files a/__pycache__/app.cpython-313.pyc and b/__pycache__/app.cpython-313.pyc differ diff --git a/app.py b/app.py index 1b9a45b..a0d7e87 100644 --- a/app.py +++ b/app.py @@ -345,23 +345,35 @@ def index(): # Route for the mindmap page @app.route('/mindmap') def mindmap(): - """Zeigt die öffentliche Mindmap an.""" - try: - # Sicherstellen, dass wir Kategorien haben - if Category.query.count() == 0: - create_default_categories() - - # Hole alle Kategorien der obersten Ebene - categories = Category.query.filter_by(parent_id=None).all() - - # Transformiere Kategorien in ein anzeigbares Format für die Vorlage - category_tree = [build_category_tree(cat) for cat in categories] - - return render_template('mindmap.html', categories=category_tree) - except Exception as e: - # Bei Fehler leere Kategorienliste übergeben und Fehler protokollieren - print(f"Fehler beim Laden der Mindmap-Kategorien: {str(e)}") - return render_template('mindmap.html', categories=[], error=str(e)) + """Zeigt die Mindmap-Seite an.""" + + # Benutzer-Mindmaps, falls angemeldet + user_mindmaps = [] + if current_user.is_authenticated: + user_mindmaps = UserMindmap.query.filter_by(user_id=current_user.id).all() + + # Stelle sicher, dass der "Wissen"-Knoten existiert + wissen_node = MindMapNode.query.filter_by(name="Wissen").first() + if not wissen_node: + wissen_node = MindMapNode( + name="Wissen", + description="Zentrale Wissensbasis", + color_code="#4299E1", + is_public=True + ) + db.session.add(wissen_node) + db.session.commit() + print("'Wissen'-Knoten wurde erstellt") + + # Überprüfe, ob es Kategorien gibt, sonst erstelle sie + if Category.query.count() == 0: + create_default_categories() + print("Kategorien wurden erstellt") + + # Stelle sicher, dass die Route für statische Dateien korrekt ist + mindmap_js_path = url_for('static', filename='js/mindmap-init.js') + + return render_template('mindmap.html', user_mindmaps=user_mindmaps, mindmap_js_path=mindmap_js_path) # Route for user profile @app.route('/profile') @@ -1441,14 +1453,42 @@ def refresh_mindmap(): if Category.query.count() == 0: create_default_categories() + # Überprüfe, ob wir bereits einen "Wissen"-Knoten haben + wissen_node = MindMapNode.query.filter_by(name="Wissen").first() + + # Wenn kein "Wissen"-Knoten existiert, erstelle ihn + if not wissen_node: + wissen_node = MindMapNode( + name="Wissen", + description="Zentrale Wissensbasis", + color_code="#4299E1", + is_public=True + ) + db.session.add(wissen_node) + db.session.commit() + # Hole alle Kategorien und Knoten categories = Category.query.filter_by(parent_id=None).all() category_tree = [build_category_tree(cat) for cat in categories] - # Hole alle Mindmap-Knoten - nodes = MindMapNode.query.all() - node_data = [] + # Hole alle Mindmap-Knoten außer dem "Wissen"-Knoten + nodes = MindMapNode.query.filter(MindMapNode.id != wissen_node.id).all() + # Vorbereiten der Node- und Edge-Arrays für die Antwort + node_data = [] + edge_data = [] + + # Zuerst den "Wissen"-Knoten hinzufügen + node_data.append({ + 'id': wissen_node.id, + 'name': wissen_node.name, + 'description': wissen_node.description or '', + 'color_code': wissen_node.color_code or '#4299E1', + 'thought_count': len(wissen_node.thoughts), + 'category_id': wissen_node.category_id + }) + + # Dann die anderen Knoten for node in nodes: node_obj = { 'id': node.id, @@ -1459,15 +1499,28 @@ def refresh_mindmap(): 'category_id': node.category_id } - # Verbindungen hinzufügen - node_obj['connections'] = [{'target': child.id} for child in node.children] + # Verbinde alle Top-Level-Knoten mit dem Wissen-Knoten + if not node.parents.all(): + edge_data.append({ + 'source': wissen_node.id, + 'target': node.id + }) + + # Verbindungen zwischen vorhandenen Knoten hinzufügen + node_children = node.children.all() + for child in node_children: + edge_data.append({ + 'source': node.id, + 'target': child.id + }) node_data.append(node_obj) return jsonify({ 'success': True, 'categories': category_tree, - 'nodes': node_data + 'nodes': node_data, + 'edges': edge_data }) except Exception as e: @@ -1490,4 +1543,9 @@ def mindmap_page(): @app.route('/community_forum') def redirect_to_index(): """Leitet alle Community/Forum-URLs zur Startseite um""" - return redirect(url_for('index')) \ No newline at end of file + return redirect(url_for('index')) + +@app.route('/static/js/mindmap-init.js') +def serve_mindmap_init_js(): + """Bedient die Mindmap-Initialisierungsdatei.""" + return app.send_static_file('js/mindmap-init.js'), 200, {'Content-Type': 'application/javascript'} \ No newline at end of file diff --git a/database/systades.db b/database/systades.db index 6e65762..c9cf76e 100644 Binary files a/database/systades.db and b/database/systades.db differ diff --git a/static/js/mindmap-init.js b/static/js/mindmap-init.js new file mode 100644 index 0000000..f0de75f --- /dev/null +++ b/static/js/mindmap-init.js @@ -0,0 +1,420 @@ +/** + * 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'); + const fitBtn = document.getElementById('fit-btn'); + const resetBtn = document.getElementById('reset-btn'); + const toggleLabelsBtn = document.getElementById('toggle-labels-btn'); + const nodeInfoPanel = document.getElementById('node-info-panel'); + const nodeDescription = document.getElementById('node-description'); + const connectedNodes = document.getElementById('connected-nodes'); + + let labelsVisible = true; + let selectedNode = null; + + // Erstelle Cytoscape-Instanz + const cy = cytoscape({ + container: cyContainer, + style: getDefaultStyles(), + layout: { + name: 'cose', + animate: true, + animationDuration: 800, + nodeDimensionsIncludeLabels: true, + padding: 50, + spacingFactor: 1.2, + randomize: true, + componentSpacing: 100, + nodeRepulsion: 8000, + edgeElasticity: 100, + nestingFactor: 1.2, + gravity: 80 + }, + wheelSensitivity: 0.3, + }); + + // Daten vom Server laden + loadMindmapData(cy); + + // Event-Handler zuweisen + setupEventListeners(cy, fitBtn, resetBtn, toggleLabelsBtn, nodeInfoPanel, nodeDescription, connectedNodes); +} + +/** + * 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 + data.nodes.forEach(node => { + elements.push({ + group: 'nodes', + data: { + id: node.id.toString(), + name: node.name, + description: node.description || '', + color: node.color_code || '#8B5CF6', + isRoot: node.name === 'Wissen' + } + }); + }); + + // 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() + } + }); + }); + } 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() + } + }); + } + }); + } + + // Elemente zu Cytoscape hinzufügen + cy.elements().remove(); + cy.add(elements); + + // Layout anwenden + cy.layout({ + name: 'cose', + animate: true, + animationDuration: 800, + nodeDimensionsIncludeLabels: true, + padding: 50, + spacingFactor: 1.5, + randomize: false, + fit: true + }).run(); + + // 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 + fallbackData.nodes.forEach(node => { + fallbackElements.push({ + group: 'nodes', + data: { + id: node.id.toString(), + name: node.name, + description: node.description || '', + color: node.color_code || '#8B5CF6', + isRoot: node.name === 'Wissen' + } + }); + }); + + // 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() + } + }); + }); + + // Elemente zu Cytoscape hinzufügen + cy.elements().remove(); + cy.add(fallbackElements); + + // Layout anwenden + cy.layout({ name: 'cose', animate: true }).run(); + }); +} + +/** + * Richtet Event-Listener für die Mindmap ein + * @param {Object} cy - Cytoscape-Instanz + * @param {HTMLElement} fitBtn - Fit-Button + * @param {HTMLElement} resetBtn - Reset-Button + * @param {HTMLElement} toggleLabelsBtn - Toggle-Labels-Button + * @param {HTMLElement} nodeInfoPanel - Node-Info-Panel + * @param {HTMLElement} nodeDescription - Node-Description + * @param {HTMLElement} connectedNodes - Connected-Nodes-Container + */ +function setupEventListeners(cy, fitBtn, resetBtn, toggleLabelsBtn, nodeInfoPanel, nodeDescription, connectedNodes) { + let labelsVisible = true; + + // Fit-Button + if (fitBtn) { + fitBtn.addEventListener('click', function() { + cy.fit(); + }); + } + + // Reset-Button + if (resetBtn) { + resetBtn.addEventListener('click', function() { + cy.layout({ name: 'cose', animate: true }).run(); + }); + } + + // Toggle-Labels-Button + if (toggleLabelsBtn) { + toggleLabelsBtn.addEventListener('click', function() { + labelsVisible = !labelsVisible; + cy.style() + .selector('node') + .style({ + 'text-opacity': labelsVisible ? 1 : 0 + }) + .update(); + }); + } + + // Knoten-Klick + cy.on('tap', 'node', function(evt) { + const node = evt.target; + + // Zuvor ausgewählten Knoten zurücksetzen + cy.nodes().removeClass('selected'); + + // Neuen Knoten auswählen + node.addClass('selected'); + + if (nodeInfoPanel && nodeDescription && connectedNodes) { + // Info-Panel aktualisieren + nodeDescription.textContent = node.data('description') || 'Keine Beschreibung verfügbar.'; + + // Verbundene Knoten anzeigen + connectedNodes.innerHTML = ''; + + // Verbundene Knoten sammeln + const connectedNodesList = node.neighborhood('node'); + + if (connectedNodesList.length > 0) { + connectedNodesList.forEach(connectedNode => { + // Nicht den ausgewählten Knoten selbst anzeigen + if (connectedNode.id() !== node.id()) { + const nodeLink = document.createElement('span'); + nodeLink.className = 'node-link'; + nodeLink.textContent = connectedNode.data('name'); + nodeLink.style.backgroundColor = connectedNode.data('color'); + + // Klick-Ereignis, um zu diesem Knoten zu wechseln + nodeLink.addEventListener('click', function() { + connectedNode.select(); + cy.animate({ + center: { eles: connectedNode }, + duration: 500, + easing: 'ease-in-out-cubic' + }); + }); + + connectedNodes.appendChild(nodeLink); + } + }); + } else { + connectedNodes.innerHTML = 'Keine verbundenen Knoten'; + } + + // Panel anzeigen + nodeInfoPanel.classList.add('visible'); + } + }); + + // Hintergrund-Klick + cy.on('tap', function(evt) { + if (evt.target === cy) { + // Klick auf den Hintergrund + cy.nodes().removeClass('selected'); + + // Info-Panel verstecken + if (nodeInfoPanel) { + nodeInfoPanel.classList.remove('visible'); + } + } + }); + + // Dark Mode-Änderungen + document.addEventListener('darkModeToggled', function(event) { + const isDark = event.detail.isDark; + cy.style(getDefaultStyles(isDark)); + }); +} + +/** + * Liefert die Standard-Stile für die Mindmap + * @param {boolean} darkMode - Ob der Dark Mode aktiv ist + * @returns {Array} Array von Cytoscape-Stilen + */ +function getDefaultStyles(darkMode = document.documentElement.classList.contains('dark')) { + return [ + { + selector: 'node', + style: { + 'background-color': 'data(color)', + 'label': 'data(name)', + 'width': 40, + 'height': 40, + 'font-size': 12, + 'text-valign': 'bottom', + 'text-halign': 'center', + 'text-margin-y': 8, + 'color': darkMode ? '#f1f5f9' : '#334155', + 'text-background-color': darkMode ? 'rgba(30, 41, 59, 0.8)' : 'rgba(241, 245, 249, 0.8)', + 'text-background-opacity': 0.8, + 'text-background-padding': '2px', + 'text-background-shape': 'roundrectangle', + 'text-wrap': 'ellipsis', + 'text-max-width': '100px' + } + }, + { + selector: 'node[?isRoot]', + style: { + 'width': 60, + 'height': 60, + 'font-size': 14, + 'font-weight': 'bold', + 'text-background-opacity': 0.9, + 'text-background-color': '#4299E1' + } + }, + { + selector: 'edge', + style: { + 'width': 2, + 'line-color': darkMode ? 'rgba(255, 255, 255, 0.15)' : 'rgba(30, 41, 59, 0.15)', + 'target-arrow-color': darkMode ? 'rgba(255, 255, 255, 0.15)' : 'rgba(30, 41, 59, 0.15)', + 'curve-style': 'bezier', + 'target-arrow-shape': 'triangle' + } + }, + { + selector: 'node.selected', + style: { + 'background-color': 'data(color)', + 'border-width': 3, + 'border-color': '#8b5cf6', + 'width': 50, + 'height': 50, + 'font-size': 14, + 'font-weight': 'bold', + 'text-background-color': '#8b5cf6', + 'text-background-opacity': 0.9 + } + } + ]; +} \ No newline at end of file diff --git a/static/js/modules/mindmap.js b/static/js/modules/mindmap.js index 34148fb..7ae6be2 100644 Binary files a/static/js/modules/mindmap.js and b/static/js/modules/mindmap.js differ diff --git a/templates/mindmap.html b/templates/mindmap.html index 0e7a940..9ea0ff9 100644 --- a/templates/mindmap.html +++ b/templates/mindmap.html @@ -110,6 +110,95 @@ background-color: rgba(0, 0, 0, 0.05); } + /* Info-Panel */ + .mindmap-info-panel { + position: absolute; + right: 1rem; + top: 5rem; + width: 280px; + background-color: rgba(15, 23, 42, 0.85); + border-radius: 8px; + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); + padding: 1rem; + color: #f1f5f9; + opacity: 0; + transform: translateX(20px); + transition: all 0.3s ease; + pointer-events: none; + z-index: 10; + backdrop-filter: blur(4px); + } + + .mindmap-info-panel.visible { + opacity: 1; + transform: translateX(0); + pointer-events: auto; + } + + .info-panel-title { + font-size: 1.1rem; + font-weight: 600; + margin-bottom: 0.75rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + } + + .info-panel-description { + font-size: 0.875rem; + margin-bottom: 1rem; + line-height: 1.5; + } + + .node-navigation-title { + font-size: 0.9rem; + font-weight: 500; + margin-bottom: 0.5rem; + } + + .node-links { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + } + + .node-link { + display: inline-block; + padding: 0.25rem 0.5rem; + font-size: 0.75rem; + border-radius: 4px; + cursor: pointer; + transition: transform 0.2s ease; + color: white; + } + + .node-link:hover { + transform: translateY(-2px); + } + + /* Mindmap-Toolbar-Buttons */ + .mindmap-action-btn { + display: flex; + align-items: center; + gap: 0.35rem; + padding: 0.35rem 0.7rem; + font-size: 0.8rem; + border-radius: 0.25rem; + background-color: rgba(124, 58, 237, 0.1); + color: #8b5cf6; + border: 1px solid rgba(124, 58, 237, 0.2); + cursor: pointer; + transition: all 0.2s ease; + } + + .dark .mindmap-action-btn { + background-color: rgba(124, 58, 237, 0.15); + border-color: rgba(124, 58, 237, 0.3); + } + + .mindmap-action-btn:hover { + background-color: rgba(124, 58, 237, 0.2); + } + /* Zusätzliches Layout */ .mindmap-section { display: grid; @@ -252,14 +341,17 @@ {% endblock %} {% block extra_js %} + + + + + + {% endblock %} \ No newline at end of file