From 59331951961b943827bd3e441010d232d4a8ddc5 Mon Sep 17 00:00:00 2001 From: Till Tomczak Date: Wed, 14 May 2025 13:41:24 +0200 Subject: [PATCH] "Refactor Mindate mindmap UI updates for mindmap controls and components" (feat) --- static/js/mindmap_controls.js | 220 ---- static/js/update_mindmap.js | 1887 ++++++++++++++++++++++++++++++--- templates/mindmap.html | 75 +- 3 files changed, 1731 insertions(+), 451 deletions(-) delete mode 100644 static/js/mindmap_controls.js diff --git a/static/js/mindmap_controls.js b/static/js/mindmap_controls.js deleted file mode 100644 index bf3271a..0000000 --- a/static/js/mindmap_controls.js +++ /dev/null @@ -1,220 +0,0 @@ -/** - * Systades Mindmap Steuerung - * Implementierung der Funktionalitäten für die Mindmap-Kontrollbuttons - */ - -document.addEventListener('DOMContentLoaded', function() { - console.log('Mindmap-Steuerung wird initialisiert...'); - - // Vergrößern-Funktion (Kontrollpanel) - const zoomInBtn = document.querySelector('#zoomIn'); - if (zoomInBtn) { - console.log('Zoom-In Button gefunden'); - zoomInBtn.addEventListener('click', function() { - vergrößern(); - }); - } - - // Verkleinern-Funktion (Kontrollpanel) - const zoomOutBtn = document.querySelector('#zoomOut'); - if (zoomOutBtn) { - console.log('Zoom-Out Button gefunden'); - zoomOutBtn.addEventListener('click', function() { - verkleinern(); - }); - } - - // Zurücksetzen-Funktion (Kontrollpanel) - const resetViewBtn = document.querySelector('#resetView'); - if (resetViewBtn) { - console.log('Reset-View Button gefunden'); - resetViewBtn.addEventListener('click', function() { - zurücksetzen(); - }); - } - - // Legende-Funktion (Kontrollpanel) - const toggleLegendBtn = document.querySelector('#toggleLegend'); - if (toggleLegendBtn) { - console.log('Toggle-Legend Button gefunden'); - toggleLegendBtn.addEventListener('click', function() { - legendeUmschalten(); - }); - } - - // Seitliche Buttons - const vergrößernBtn = document.querySelector('#vergrößernBtn'); - if (vergrößernBtn) { - console.log('Vergrößern Button (Seitenleiste) gefunden'); - vergrößernBtn.addEventListener('click', function() { - vergrößern(); - animateButtonClick(this); - }); - } else { - console.warn('Vergrößern Button nicht gefunden'); - } - - const verkleinernBtn = document.querySelector('#verkleinernBtn'); - if (verkleinernBtn) { - console.log('Verkleinern Button (Seitenleiste) gefunden'); - verkleinernBtn.addEventListener('click', function() { - verkleinern(); - animateButtonClick(this); - }); - } else { - console.warn('Verkleinern Button nicht gefunden'); - } - - const zurücksetzenBtn = document.querySelector('#zurücksetzenBtn'); - if (zurücksetzenBtn) { - console.log('Zurücksetzen Button (Seitenleiste) gefunden'); - zurücksetzenBtn.addEventListener('click', function() { - zurücksetzen(); - animateButtonClick(this); - }); - } else { - console.warn('Zurücksetzen Button nicht gefunden'); - } -}); - -/** - * Vergrößert die aktuelle Mindmap-Ansicht - */ -function vergrößern() { - console.log('Vergrößern-Funktion aufgerufen'); - if (window.cy) { - const aktuellerZoom = window.cy.zoom(); - window.cy.zoom({ - level: aktuellerZoom * 1.2, - renderedPosition: { x: window.innerWidth / 2, y: window.innerHeight / 2 } - }); - console.log('Vergrößerung: Neuer Zoom-Level:', window.cy.zoom()); - } else { - console.error('Cytoscape-Instanz nicht verfügbar'); - } -} - -/** - * Verkleinert die aktuelle Mindmap-Ansicht - */ -function verkleinern() { - console.log('Verkleinern-Funktion aufgerufen'); - if (window.cy) { - const aktuellerZoom = window.cy.zoom(); - window.cy.zoom({ - level: aktuellerZoom * 0.8, - renderedPosition: { x: window.innerWidth / 2, y: window.innerHeight / 2 } - }); - console.log('Verkleinerung: Neuer Zoom-Level:', window.cy.zoom()); - } else { - console.error('Cytoscape-Instanz nicht verfügbar'); - } -} - -/** - * Setzt die Mindmap-Ansicht zurück, sodass alle Elemente sichtbar sind - */ -function zurücksetzen() { - console.log('Zurücksetzen-Funktion aufgerufen'); - if (window.cy) { - window.cy.fit(); - window.cy.center(); - console.log('Ansicht zurückgesetzt'); - - // Zeige kurze Bestätigung an - zeigeFlashNachricht('Ansicht zurückgesetzt', 'info'); - } else { - console.error('Cytoscape-Instanz nicht verfügbar'); - } -} - -/** - * Schaltet die Anzeige der Kategorie-Legende um - */ -function legendeUmschalten() { - console.log('Legende-Umschalten-Funktion aufgerufen'); - const legende = document.getElementById('categoryLegend'); - if (legende) { - const neuerZustand = legende.style.display === 'none' || legende.style.display === '' ? 'flex' : 'none'; - legende.style.display = neuerZustand; - console.log('Legende ist jetzt:', neuerZustand === 'flex' ? 'sichtbar' : 'ausgeblendet'); - - // Zeige kurze Bestätigung an - zeigeFlashNachricht( - neuerZustand === 'flex' ? 'Legende wird angezeigt' : 'Legende ausgeblendet', - 'info' - ); - } else { - console.error('Legende-Element nicht gefunden'); - } -} - -/** - * Zeigt eine kurze Flash-Nachricht auf dem Bildschirm an - * @param {string} nachricht - Die anzuzeigende Nachricht - * @param {string} typ - Der Typ der Nachricht (info, success, warning, error) - */ -function zeigeFlashNachricht(nachricht, typ = 'info') { - console.log(`Flash-Nachricht: ${nachricht} (${typ})`); - - // Prüfe, ob bereits eine Flash-Nachricht existiert - let flashElement = document.getElementById('flash-nachricht'); - - // Falls nicht, erstelle sie - if (!flashElement) { - flashElement = document.createElement('div'); - flashElement.id = 'flash-nachricht'; - document.body.appendChild(flashElement); - - // Styles für das Flash-Element - Object.assign(flashElement.style, { - position: 'fixed', - bottom: '20px', - left: '50%', - transform: 'translateX(-50%)', - padding: '10px 20px', - borderRadius: '5px', - color: 'white', - fontWeight: 'bold', - zIndex: '9999', - opacity: '0', - transition: 'opacity 0.3s ease-in-out' - }); - } - - // Setze Hintergrundfarbe je nach Typ - const hintergrundFarben = { - info: 'rgba(59, 130, 246, 0.9)', - success: 'rgba(16, 185, 129, 0.9)', - warning: 'rgba(245, 158, 11, 0.9)', - error: 'rgba(220, 38, 38, 0.9)' - }; - - flashElement.style.backgroundColor = hintergrundFarben[typ] || hintergrundFarben.info; - flashElement.textContent = nachricht; - - // Animiere das Einblenden - flashElement.style.opacity = '1'; - - // Ausblenden nach 2 Sekunden - setTimeout(() => { - flashElement.style.opacity = '0'; - }, 2000); -} - -/** - * Animiert einen Button beim Klicken - * @param {HTMLElement} button - Das Button-Element, das animiert werden soll - */ -function animateButtonClick(button) { - // Aktuelle Transformation speichern - const originalTransform = button.style.transform; - - // Animation anwenden - button.style.transform = 'scale(0.95)'; - - // Nach kurzer Zeit zurücksetzen - setTimeout(() => { - button.style.transform = originalTransform; - }, 150); -} \ No newline at end of file diff --git a/static/js/update_mindmap.js b/static/js/update_mindmap.js index 26c88a5..20bbb52 100644 --- a/static/js/update_mindmap.js +++ b/static/js/update_mindmap.js @@ -1,58 +1,195 @@ /** - * Mindmap-Funktionalitäten - * Diese Datei enthält die grundlegenden Funktionen für die Mindmap-Visualisierung + * Update Mindmap + * Dieses Skript fügt Knoten zur Mindmap hinzu und stellt sicher, + * dass sie im neuronalen Netzwerk-Design angezeigt werden. + * Implementiert Lazy Loading & Progressive Disclosure */ -// Globale Variable für das Cytoscape-Objekt -window.cy = null; +// Neue zentrale Konfiguration +const mindmapConfig = { + categories: { + 'Philosophie': { + icon: 'fa-solid fa-lightbulb', + color: '#b71c1c', + description: 'Die Lehre vom Denken und der Erkenntnis' + }, + 'Wissenschaft': { + icon: 'fa-solid fa-atom', + color: '#f4b400', + description: 'Systematische Erforschung der Natur und Gesellschaft' + }, + 'Technologie': { + icon: 'fa-solid fa-microchip', + color: '#0d47a1', + description: 'Anwendung wissenschaftlicher Erkenntnisse' + }, + 'Künste': { + icon: 'fa-solid fa-palette', + color: '#c2185b', + description: 'Kreativer Ausdruck und künstlerische Gestaltung' + } + }, + defaultNodeStyle: { + fontSize: 18, + fontColor: '#fff', + neuronSize: 8, + neuronActivity: 0.8 + }, + centerNodeStyle: { + fontSize: 22, + fontColor: '#222', + neuronSize: 12, + neuronActivity: 1.0, + color: '#f5f5f5', + icon: 'fa-solid fa-circle' + } +}; -/** - * Initialisiert die Mindmap mit Standarddaten - */ -async function initializeMindmap() { - console.log('Initialisiere Mindmap...'); - - // Beispieldaten für die Mindmap (falls keine API-Daten verfügbar sind) - const fallbackData = { - nodes: [ - { id: 'wissen', name: 'Wissen', category: 'Zentral', description: 'Zentrum der Wissensdatenbank', has_children: true, is_center: true, color_code: '#f5f5f5' }, - { id: 'philosophie', name: 'Philosophie', category: 'Philosophie', description: 'Philosophisches Denken', has_children: true, is_center: false, color_code: '#9F7AEA' }, - { id: 'wissenschaft', name: 'Wissenschaft', category: 'Wissenschaft', description: 'Wissenschaftliche Erkenntnisse', has_children: true, is_center: false, color_code: '#60A5FA' }, - { id: 'technologie', name: 'Technologie', category: 'Technologie', description: 'Technologische Entwicklungen', has_children: true, is_center: false, color_code: '#10B981' }, - { id: 'kunst', name: 'Künste', category: 'Künste', description: 'Kreative Ausdrucksformen', has_children: true, is_center: false, color_code: '#F59E0B' }, - { id: 'psychologie', name: 'Psychologie', category: 'Psychologie', description: 'Menschliches Verhalten und Geist', has_children: true, is_center: false, color_code: '#EF4444' } - ], - edges: [ - { source: 'wissen', target: 'philosophie', strength: 0.8 }, - { source: 'wissen', target: 'wissenschaft', strength: 0.8 }, - { source: 'wissen', target: 'technologie', strength: 0.7 }, - { source: 'wissen', target: 'kunst', strength: 0.6 }, - { source: 'wissen', target: 'psychologie', strength: 0.7 }, - { source: 'wissenschaft', target: 'technologie', strength: 0.9 }, - { source: 'wissenschaft', target: 'philosophie', strength: 0.5 }, - { source: 'philosophie', target: 'psychologie', strength: 0.6 } - ] - }; - +// Zentrale Styling-Konfiguration +const mindmapStyles = { + node: { + base: { + 'background-color': 'data(color)', + 'label': 'data(label)', + 'color': '#ffffff', + 'text-background-color': 'rgba(0, 0, 0, 0.7)', + 'text-background-opacity': 0.8, + 'text-background-padding': '4px', + 'text-valign': 'center', + 'text-halign': 'center', + 'font-size': 16, + 'width': 40, + 'height': 40, + 'border-width': 2, + 'border-color': '#ffffff', + 'border-opacity': 0.8, + 'shape': 'ellipse', + 'background-opacity': 0.85 + }, + center: { + 'background-color': '#f5f5f5', + 'color': '#222', + 'font-size': 20, + 'border-width': 3, + 'width': 100, + 'height': 100 + }, + selected: { + 'border-color': '#f59e42', + 'border-width': 3, + 'background-opacity': 1 + } + }, + edge: { + base: { + 'width': function(ele) { + return ele.data('strength') ? ele.data('strength') * 2 : 1; + }, + 'line-color': function(ele) { + const sourceColor = ele.source().data('color'); + return sourceColor || '#8a8aaa'; + }, + 'line-opacity': function(ele) { + return ele.data('strength') ? ele.data('strength') * 0.6 : 0.4; + }, + 'curve-style': 'bezier', + 'target-arrow-shape': 'none', + 'control-point-distances': [30, -30], + 'control-point-weights': [0.5, 0.5] + } + }, + layout: { + base: { + name: 'cose', + animate: true, + animationDuration: 500, + refresh: 20, + fit: true, + padding: 30, + nodeRepulsion: 4500, + idealEdgeLength: 50, + edgeElasticity: 0.45, + randomize: true, + componentSpacing: 100, + nodeOverlap: 20, + gravity: 0.25, + initialTemp: 1000, + coolingFactor: 0.95, + minTemp: 1 + } + } +}; + +// Globale Variable für die Mindmap-Daten +let mindmapData = null; + +// Funktion zum Laden der Mindmap-Daten aus der Datenbank +async function loadMindmapData(nodeId = null) { try { - // Versuche, Daten von der API zu laden - const response = await fetch('/api/mindmap/root'); - let data; + const apiUrl = nodeId ? `/api/mindmap/${nodeId}` : '/api/mindmap/root'; + console.log('Lade Mindmap-Daten von:', apiUrl); - if (response.ok) { - data = await response.json(); - - // Prüfe, ob die API korrekte Daten zurückgegeben hat - if (!data.nodes || !data.edges) { - console.warn('API-Antwort hat ungültiges Format, verwende Fallback-Daten'); - data = fallbackData; + const response = await fetch(apiUrl); + console.log('API-Antwort Status:', response.status); + + if (!response.ok) { + let errorData; + try { + errorData = await response.json(); + console.log('API-Fehler Details:', errorData); + } catch (e) { + console.error('Fehler beim Parsen der Fehlerantwort:', e); + errorData = { + error: `HTTP-Fehler ${response.status}: ${response.statusText}` + }; } - } else { - console.warn('API-Anfrage fehlgeschlagen, verwende Fallback-Daten'); - data = fallbackData; + + // Fehlerobjekt für die Benachrichtigung erstellen + const errorMessage = errorData.error || 'Unbekannter Fehler'; + + showUINotification(errorMessage, 'error'); + throw new Error(errorMessage); } - // Cytoscape-Elemente erstellen + const data = await response.json(); + console.log('Geladene Mindmap-Daten:', data); + + if (!data.success) { + const errorMessage = data.error || 'Mindmap-Daten konnten nicht geladen werden'; + showUINotification(errorMessage, 'error'); + throw new Error(errorMessage); + } + + // Überprüfen, ob Nodes und Edges existieren + if (!data.nodes || !data.edges) { + const errorMessage = 'Ungültiges Datenformat: Nodes oder Edges fehlen'; + showUINotification(errorMessage, 'error'); + throw new Error(errorMessage); + } + + // Erfolgreiche Antwort + mindmapData = data; // Speichere die Daten in der globalen Variable + showUINotification('Mindmap-Daten erfolgreich geladen', 'success'); + return data; + } catch (error) { + console.error('Fehler beim Laden der Mindmap-Daten:', error); + + // Stelle sicher, dass wir eine aussagekräftige Fehlermeldung haben + const errorMessage = error.message || 'Unbekannter Fehler beim Laden der Mindmap-Daten'; + + showUINotification(errorMessage, 'error'); + throw error; + } +} + +// Funktion zum Initialisieren der Mindmap +async function initializeMindmap() { + try { + const data = await loadMindmapData(); + if (!data || !data.nodes || !data.edges) { + throw new Error('Ungültiges Datenformat: Mindmap-Daten fehlen oder sind unvollständig'); + } + const elements = [ // Knoten ...data.nodes.map(node => ({ @@ -64,7 +201,8 @@ async function initializeMindmap() { hasChildren: node.has_children, expanded: false, color: node.color_code, - isCenter: node.is_center || false + fontColor: '#ffffff', + fontSize: node.is_center ? 20 : 16 } })), // Kanten @@ -76,134 +214,1279 @@ async function initializeMindmap() { } })) ]; - - // Cytoscape initialisieren - const cyContainer = document.getElementById('cy'); - - if (!cyContainer) { - throw new Error('Container #cy nicht gefunden'); - } - - // Bestehende Cytoscape-Instanz zerstören, wenn vorhanden - if (window.cy) { + + // Bestehende Cytoscape-Instanz entfernen, falls vorhanden + if (window.cy && typeof window.cy.destroy === 'function') { window.cy.destroy(); } - - // Neue Cytoscape-Instanz erstellen + + const cyContainer = document.getElementById('cy'); + if (!cyContainer) { + throw new Error('Mindmap-Container #cy nicht gefunden!'); + } + window.cy = cytoscape({ container: cyContainer, elements: elements, style: [ { selector: 'node', - style: { - 'background-color': 'data(color)', - 'label': 'data(label)', - 'color': '#ffffff', - 'text-outline-color': 'black', - 'text-outline-width': 1, - 'text-valign': 'center', - 'text-halign': 'center', - 'font-size': 18, - 'width': 50, - 'height': 50, - 'border-width': 2, - 'border-color': '#ffffff', - 'border-opacity': 0.6 - } + style: mindmapStyles.node.base }, { selector: 'node[isCenter]', - style: { - 'background-color': '#f5f5f5', - 'color': '#222222', - 'font-size': 24, - 'width': 80, - 'height': 80 - } + style: mindmapStyles.node.center + }, + { + selector: 'node:selected', + style: mindmapStyles.node.selected }, { selector: 'edge', - style: { - 'width': function(ele) { - return ele.data('strength') * 5; - }, - 'line-color': 'rgba(255, 255, 255, 0.5)', - 'opacity': 0.7, - 'curve-style': 'bezier' - } + style: mindmapStyles.edge.base } ], - layout: { - name: 'cose', - idealEdgeLength: 100, - nodeOverlap: 20, - refresh: 20, - fit: true, - padding: 30, - randomize: false, - componentSpacing: 100, - nodeRepulsion: 400000, - edgeElasticity: 100, - nestingFactor: 5, - gravity: 80, - numIter: 1000, - initialTemp: 200, - coolingFactor: 0.95, - minTemp: 1.0 + layout: mindmapStyles.layout.base + }); + + // Füge neuronale Eigenschaften zu allen Knoten hinzu + cy.nodes().forEach(node => { + const data = node.data(); + // Verwende mindmapConfig für Kategorie-Farben oder einen Standardwert + const categoryColor = data.category && mindmapConfig.categories[data.category] + ? mindmapConfig.categories[data.category].color + : '#60a5fa'; + + node.data({ + ...data, + neuronSize: data.neuronSize || 8, + neuronActivity: data.neuronActivity || 0.8, + refractionPeriod: Math.random() * 300 + 700, + threshold: Math.random() * 0.3 + 0.6, + lastFired: 0, + color: data.color || categoryColor + }); + }); + + // Füge synaptische Eigenschaften zu allen Kanten hinzu + cy.edges().forEach(edge => { + const data = edge.data(); + edge.data({ + ...data, + strength: data.strength || 0.5, + conductionVelocity: Math.random() * 0.5 + 0.3, + latency: Math.random() * 100 + 50 + }); + }); + + // Event-Listener für Knoten-Klicks + cy.on('tap', 'node', async function(evt) { + const node = evt.target; + console.log('Node clicked:', node.id(), 'hasChildren:', node.data('hasChildren'), 'expanded:', node.data('expanded')); + + if (node.data('hasChildren') && !node.data('expanded')) { + await loadSubthemes(node); } }); + + // Layout ausführen + cy.layout(mindmapStyles.layout.base).run(); - // Knoten sperren, damit sie nicht verschoben werden können - window.cy.nodes().lock(); + // Starte neuronale Aktivitätssimulation + startNeuralActivitySimulation(cy); - console.log('Mindmap erfolgreich initialisiert'); - return window.cy; + // Mindmap mit echten Daten befüllen (Styles, Farben etc.) + updateMindmap(); + + return true; } catch (error) { - console.error('Fehler bei der Initialisierung der Mindmap:', error); - throw error; + console.error('Fehler bei der Mindmap-Initialisierung:', error); + showUINotification({ + error: 'Mindmap konnte nicht initialisiert werden', + details: error.message + }, 'error'); + return false; } } -/** - * Speichert Änderungen an der Mindmap - * @param {Object} cy - Die Cytoscape-Instanz - */ -function saveMindmapChanges(cy) { - console.log('Speichere Mindmap-Änderungen...'); +// Warte bis DOM geladen ist +document.addEventListener('DOMContentLoaded', function() { + console.log('DOMContentLoaded Event ausgelöst'); - if (!cy) { - console.error('Keine Cytoscape-Instanz übergeben'); + // Prüfe, ob der Container existiert + const cyContainer = document.getElementById('cy'); + console.log('Container gefunden:', cyContainer); + + if (!cyContainer) { + console.error('Mindmap-Container #cy nicht gefunden!'); return; } - // Sammle alle Knoten und Kanten - const nodes = cy.nodes().map(node => ({ - id: node.id(), - name: node.data('label'), - category: node.data('category'), - description: node.data('description'), - has_children: node.data('hasChildren'), - is_center: node.data('isCenter'), - color_code: node.data('color'), - position: node.position() - })); + // Prüfe, ob Cytoscape verfügbar ist + if (typeof cytoscape === 'undefined') { + console.error('Cytoscape ist nicht definiert!'); + return; + } + console.log('Cytoscape ist verfügbar'); + + // Initialisiere die Mindmap + initializeMindmap() + .then(success => { + if (success) { + console.log('Mindmap wurde erfolgreich initialisiert'); + // Event auslösen, damit andere Scripte reagieren können + document.dispatchEvent(new Event('mindmap-loaded')); + console.log('mindmap-loaded Event ausgelöst'); + } else { + console.error('Mindmap-Initialisierung fehlgeschlagen'); + } + }) + .catch(error => { + console.error('Fehler bei der Mindmap-Initialisierung:', error); + showUINotification({ + error: 'Mindmap konnte nicht initialisiert werden', + details: error.message + }, 'error'); + }); +}); + +// Funktion zum Initialisieren des neuronalen Designs +function initializeNeuralDesign(cy) { + // Füge neuronale Eigenschaften zu allen Knoten hinzu + cy.nodes().forEach(node => { + const data = node.data(); + // Verwende mindmapConfig für Kategorie-Farben oder einen Standardwert + const categoryColor = data.category && mindmapConfig.categories[data.category] + ? mindmapConfig.categories[data.category].color + : '#60a5fa'; + + node.data({ + ...data, + neuronSize: data.neuronSize || 8, + neuronActivity: data.neuronActivity || 0.8, + refractionPeriod: Math.random() * 300 + 700, + threshold: Math.random() * 0.3 + 0.6, + lastFired: 0, + color: data.color || categoryColor + }); + }); + + // Füge synaptische Eigenschaften zu allen Kanten hinzu + cy.edges().forEach(edge => { + const data = edge.data(); + edge.data({ + ...data, + strength: data.strength || 0.5, + conductionVelocity: Math.random() * 0.5 + 0.3, + latency: Math.random() * 100 + 50 + }); + }); + + // Wende neuronales Styling an + cy.style() + .selector('node') + .style({ + 'background-color': 'data(color)', + 'label': 'data(label)', + 'color': '#fff', + 'text-background-color': 'rgba(0, 0, 0, 0.7)', + 'text-background-opacity': 0.8, + 'text-background-padding': '4px', + 'text-valign': 'center', + 'text-halign': 'center', + 'font-size': 16, + 'width': 40, + 'height': 40, + 'border-width': 2, + 'border-color': '#fff', + 'border-opacity': 0.8, + 'overlay-padding': 4, + 'z-index': 10, + 'shape': 'ellipse', + 'background-opacity': 0.85, + 'shadow-blur': 15, + 'shadow-color': 'data(color)', + 'shadow-opacity': 0.6, + 'shadow-offset-x': 0, + 'shadow-offset-y': 0 + }) + .selector('edge') + .style({ + 'width': function(ele) { + return ele.data('strength') ? ele.data('strength') * 3 : 1; + }, + 'curve-style': 'bezier', + 'line-color': function(ele) { + const sourceColor = ele.source().data('color'); + return sourceColor || '#8a8aaa'; + }, + 'line-opacity': function(ele) { + return ele.data('strength') ? ele.data('strength') * 0.8 : 0.4; + }, + 'line-style': function(ele) { + const strength = ele.data('strength'); + if (!strength) return 'solid'; + if (strength <= 0.4) return 'dotted'; + if (strength <= 0.6) return 'dashed'; + return 'solid'; + }, + 'target-arrow-shape': 'none', + 'source-endpoint': '0% 50%', + 'target-endpoint': '100% 50%', + 'transition-property': 'line-opacity, width', + 'transition-duration': '0.3s', + 'transition-timing-function': 'ease-in-out' + }) + .update(); + + // Starte neuronale Aktivitätssimulation + startNeuralActivitySimulation(cy); +} + +// Modifiziere die updateMindmap Funktion +function updateMindmap() { + if (!cy) return; - const edges = cy.edges().map(edge => ({ - source: edge.source().id(), - target: edge.target().id(), - strength: edge.data('strength') - })); + // Bestehende Elemente entfernen + cy.elements().remove(); - // Daten zum Speichern vorbereiten - const saveData = { - nodes: nodes, - edges: edges + // Neue Knoten hinzufügen + mindmapData.nodes.forEach(node => { + cy.add({ + group: 'nodes', + data: { + id: node.id, + label: node.name, + category: node.category, + description: node.description, + hasChildren: node.has_children, + expanded: false, + color: node.color_code || mindmapConfig.categories[node.category]?.color || '#60a5fa', + icon: node.icon || mindmapConfig.categories[node.category]?.icon || 'fa-solid fa-circle' + } + }); + }); + + // Neue Kanten hinzufügen + mindmapData.edges.forEach(edge => { + cy.add({ + group: 'edges', + data: { + source: edge.source, + target: edge.target, + strength: edge.strength || 0.5 + } + }); + }); + + // Layout aktualisieren + cy.layout(mindmapStyles.layout.base).run(); +} + +/** + * Erweitert die Mindmap mit dem neuronalen Netzwerk-Design + */ +function enhanceMindmap() { + // Auf die bestehende Cytoscape-Instanz zugreifen + const cy = window.cy; + + if (!cy) { + console.error('Keine Cytoscape-Instanz gefunden.'); + return; + } + + // Aktualisiere das Layout für eine bessere Verteilung + cy.layout({ + name: 'cose', + animate: true, + animationDuration: 2000, + nodeDimensionsIncludeLabels: true, + padding: 100, + spacingFactor: 2, + randomize: true, + fit: true, + componentSpacing: 150, + nodeRepulsion: 10000, + edgeElasticity: 150, + nestingFactor: 1.5, + gravity: 100, + initialTemp: 1000, + coolingFactor: 0.95, + minTemp: 1 + }).run(); + + // Neuronen-Namen mit besserer Lesbarkeit umgestalten + cy.style() + .selector('node') + .style({ + 'text-background-color': 'rgba(10, 14, 25, 0.7)', + 'text-background-opacity': 0.7, + 'text-background-padding': '2px', + 'text-border-opacity': 0.2, + 'text-border-width': 1, + 'text-border-color': '#8b5cf6' + }) + .update(); + + // Sicherstellen, dass alle Knoten Neuronen-Eigenschaften haben + cy.nodes().forEach(node => { + if (!node.data('neuronSize')) { + const neuronSize = Math.floor(Math.random() * 8) + 3; + node.data('neuronSize', neuronSize); + } + + if (!node.data('neuronActivity')) { + const neuronActivity = Math.random() * 0.7 + 0.3; + node.data('neuronActivity', neuronActivity); + } + + // Zusätzliche Neuronale Eigenschaften + node.data('pulseFrequency', Math.random() * 4 + 2); // Pulsfrequenz (2-6 Hz) + node.data('refractionPeriod', Math.random() * 300 + 700); // Refraktionszeit (700-1000ms) + node.data('threshold', Math.random() * 0.3 + 0.6); // Aktivierungsschwelle (0.6-0.9) + }); + + // Sicherstellen, dass alle Kanten Synapse-Eigenschaften haben + cy.edges().forEach(edge => { + if (!edge.data('strength')) { + const strength = Math.random() * 0.6 + 0.2; + edge.data('strength', strength); + } + + // Zusätzliche synaptische Eigenschaften + edge.data('conductionVelocity', Math.random() * 0.5 + 0.3); // Leitungsgeschwindigkeit (0.3-0.8) + edge.data('latency', Math.random() * 100 + 50); // Signalverzögerung (50-150ms) + }); + + // Neuronales Netzwerk-Stil anwenden + applyNeuralNetworkStyle(cy); + + console.log('Mindmap wurde erfolgreich im neuronalen Netzwerk-Stil aktualisiert'); + + // Spezielle Effekte für das neuronale Netzwerk hinzufügen + startNeuralActivitySimulation(cy); +} + +/** + * Wendet detaillierte neuronale Netzwerkstile auf die Mindmap an + * @param {Object} cy - Cytoscape-Instanz + */ +function applyNeuralNetworkStyle(cy) { + cy.style() + .selector('node') + .style({ + 'label': 'data(label)', + 'text-valign': 'center', + 'text-halign': 'center', + 'color': 'data(fontColor)', + 'text-outline-width': 2, + 'text-outline-color': 'rgba(0,0,0,0.8)', + 'text-outline-opacity': 0.9, + 'font-size': 'data(fontSize)', + 'font-weight': '500', + 'text-margin-y': 8, + 'width': function(ele) { + if (ele.data('isCenter')) return 120; + return 80; + }, + 'height': function(ele) { + if (ele.data('isCenter')) return 120; + return 80; + }, + 'background-color': 'data(color)', + 'background-opacity': 0.9, + 'border-width': 2, + 'border-color': '#ffffff', + 'border-opacity': 0.8, + 'shape': 'ellipse', + 'transition-property': 'background-color, background-opacity, border-width', + 'transition-duration': '0.3s', + 'transition-timing-function': 'ease-in-out' + }) + .selector('edge') + .style({ + 'width': function(ele) { + return ele.data('strength') ? ele.data('strength') * 3 : 1; + }, + 'curve-style': 'bezier', + 'line-color': function(ele) { + const sourceColor = ele.source().data('color'); + return sourceColor || '#8a8aaa'; + }, + 'line-opacity': function(ele) { + return ele.data('strength') ? ele.data('strength') * 0.8 : 0.4; + }, + 'line-style': function(ele) { + const strength = ele.data('strength'); + if (!strength) return 'solid'; + if (strength <= 0.4) return 'dotted'; + if (strength <= 0.6) return 'dashed'; + return 'solid'; + }, + 'target-arrow-shape': 'none', + 'source-endpoint': '0% 50%', + 'target-endpoint': '100% 50%', + 'transition-property': 'line-opacity, width', + 'transition-duration': '0.3s', + 'transition-timing-function': 'ease-in-out' + }) + .update(); +} + +// Vereinfachte neuronale Aktivitätssimulation +function startNeuralActivitySimulation(cy) { + if (window.neuralInterval) clearInterval(window.neuralInterval); + + const nodes = cy.nodes(); + let currentTime = Date.now(); + + function simulateNeuralActivity() { + currentTime = Date.now(); + + nodes.forEach(node => { + const data = node.data(); + const lastFired = data.lastFired || 0; + const timeSinceLastFire = currentTime - lastFired; + + if (timeSinceLastFire > data.refractionPeriod) { + if (Math.random() < data.neuronActivity * 0.1) { + fireNeuron(node, true, currentTime); + } + } + }); + } + + function fireNeuron(node, state, currentTime) { + const data = node.data(); + data.lastFired = currentTime; + + node.style({ + 'background-opacity': 1, + 'border-width': 3 + }); + + setTimeout(() => { + node.style({ + 'background-opacity': 0.9, + 'border-width': 2 + }); + }, 200); + + if (state) { + propagateSignal(node, currentTime); + } + } + + function propagateSignal(sourceNode, currentTime) { + const outgoingEdges = sourceNode.connectedEdges(); + + outgoingEdges.forEach(edge => { + const targetNode = edge.target(); + const edgeData = edge.data(); + const latency = edgeData.latency; + + edge.style({ + 'line-opacity': 0.8, + 'width': edgeData.strength * 3 + }); + + setTimeout(() => { + edge.style({ + 'line-opacity': edgeData.strength * 0.6, + 'width': edgeData.strength * 2 + }); + }, 200); + + setTimeout(() => { + const targetData = targetNode.data(); + const timeSinceLastFire = currentTime - (targetData.lastFired || 0); + + if (timeSinceLastFire > targetData.refractionPeriod) { + const signalStrength = edgeData.strength * + edgeData.conductionVelocity * + sourceNode.data('neuronActivity'); + + if (signalStrength > targetData.threshold) { + fireNeuron(targetNode, true, currentTime + latency); + } + } + }, latency); + }); + } + + window.neuralInterval = setInterval(simulateNeuralActivity, 100); +} + +// Hilfe-Funktion zum Hinzufügen eines Flash-Hinweises +function showFlash(message, type = 'info') { + const flashContainer = createFlashContainer(); + const flash = document.createElement('div'); + flash.className = `flash-message ${type}`; + flash.textContent = message; + flashContainer.appendChild(flash); + document.body.appendChild(flashContainer); + + setTimeout(() => { + flash.classList.add('show'); + setTimeout(() => { + flash.classList.remove('show'); + setTimeout(() => { + flashContainer.remove(); + }, 300); + }, 3000); + }, 100); +} + +/** + * Zeigt eine Benachrichtigung in der UI an + * @param {string|object} message - Die anzuzeigende Nachricht oder ein Fehlerobjekt + * @param {string} type - Der Typ der Benachrichtigung ('info', 'success', 'warning', 'error') + * @param {number} duration - Die Anzeigedauer in Millisekunden (Standard: 3000) + */ +function showUINotification(message, type = 'info', duration = 3000) { + // Container erstellen, falls er nicht existiert + let container = document.getElementById('notification-container'); + if (!container) { + container = document.createElement('div'); + container.id = 'notification-container'; + container.style.position = 'fixed'; + container.style.top = '1rem'; + container.style.right = '1rem'; + container.style.zIndex = '1000'; + container.style.maxWidth = '400px'; + document.body.appendChild(container); + } + + // Benachrichtigung erstellen + const notification = document.createElement('div'); + notification.className = `notification notification-${type}`; + notification.style.padding = '1rem'; + notification.style.marginBottom = '0.5rem'; + notification.style.borderRadius = '0.25rem'; + notification.style.boxShadow = '0 2px 5px rgba(0, 0, 0, 0.2)'; + notification.style.position = 'relative'; + notification.style.opacity = '0'; + notification.style.transform = 'translateY(-20px)'; + notification.style.transition = 'all 0.3s ease-in-out'; + + // Farben nach Typ + if (type === 'success') { + notification.style.backgroundColor = '#059669'; + notification.style.color = '#ffffff'; + } else if (type === 'error') { + notification.style.backgroundColor = '#DC2626'; + notification.style.color = '#ffffff'; + } else if (type === 'warning') { + notification.style.backgroundColor = '#F59E0B'; + notification.style.color = '#ffffff'; + } else { + notification.style.backgroundColor = '#3B82F6'; + notification.style.color = '#ffffff'; + } + + // Nachrichteninhalt formatieren + let content = ''; + + if (typeof message === 'object' && message !== null) { + // Wenn es ein Fehler-Objekt ist + if (message.error) { + content = message.error; + if (message.details) { + content += `
${message.details}`; + } + } else { + // Versuche, das Objekt zu stringifizieren + try { + content = JSON.stringify(message); + } catch (e) { + content = 'Objekt konnte nicht angezeigt werden'; + } + } + } else { + // String oder andere primitive Typen + content = message; + } + + notification.innerHTML = content; + + // Schließen-Button + const closeButton = document.createElement('span'); + closeButton.innerHTML = '×'; + closeButton.style.position = 'absolute'; + closeButton.style.top = '0.25rem'; + closeButton.style.right = '0.5rem'; + closeButton.style.fontSize = '1.25rem'; + closeButton.style.cursor = 'pointer'; + closeButton.onclick = () => { + notification.style.opacity = '0'; + notification.style.transform = 'translateY(-20px)'; + setTimeout(() => { + if (notification.parentNode === container) { + container.removeChild(notification); + } + }, 300); + }; + notification.appendChild(closeButton); + + // Zur Seite hinzufügen + container.appendChild(notification); + + // Animation starten + setTimeout(() => { + notification.style.opacity = '1'; + notification.style.transform = 'translateY(0)'; + }, 10); + + // Automatisch ausblenden, wenn keine Dauer von 0 übergeben wurde + if (duration > 0) { + setTimeout(() => { + if (notification.parentNode === container) { + notification.style.opacity = '0'; + notification.style.transform = 'translateY(-20px)'; + setTimeout(() => { + if (notification.parentNode === container) { + container.removeChild(notification); + } + }, 300); + } + }, duration); + } +} + +// Funktion zum Anzeigen der Bearbeitungssteuerungen +function showEditingControls(nodeId) { + const container = nodeId ? + document.getElementById(`cy-${nodeId}`).parentElement : + document.getElementById('cy').parentElement; + + if (!container) return; + + // Erstelle Bearbeitungswerkzeuge, wenn sie noch nicht existieren + let editingControls = container.querySelector('.editing-controls'); + if (!editingControls) { + editingControls = document.createElement('div'); + editingControls.className = 'editing-controls'; + editingControls.innerHTML = ` +
+ + + + + +
+ `; + + container.appendChild(editingControls); + + // Event-Listener für die Bearbeitungswerkzeuge hinzufügen + const toolButtons = editingControls.querySelectorAll('.tool-button'); + toolButtons.forEach(button => { + button.addEventListener('click', function() { + const action = this.getAttribute('data-action'); + handleEditingAction(action, nodeId); + }); + }); + } + + // Zeige die Steuerungen an + editingControls.style.display = 'block'; +} + +// Funktion zum Ausblenden der Bearbeitungssteuerungen +function hideEditingControls(nodeId) { + const container = nodeId ? + document.getElementById(`cy-${nodeId}`).parentElement : + document.getElementById('cy').parentElement; + + if (!container) return; + + const editingControls = container.querySelector('.editing-controls'); + if (editingControls) { + editingControls.style.display = 'none'; + } +} + +// Funktion zum Verarbeiten von Bearbeitungsaktionen +function handleEditingAction(action, nodeId) { + console.log('Bearbeitungsaktion:', action, 'für NodeId:', nodeId); + + // Finde die relevante Cytoscape-Instanz + const cy = nodeId ? + (window.subthemeCyInstances && window.subthemeCyInstances[nodeId]) : + window.cy; + + if (!cy) { + console.error('Keine Cytoscape-Instanz gefunden'); + return; + } + + switch (action) { + case 'add-node': + addNewNode(cy); + break; + case 'add-edge': + enableEdgeCreationMode(cy); + break; + case 'delete': + deleteSelectedElements(cy); + break; + case 'save': + saveMindmapChanges(cy, nodeId); + break; + default: + console.warn('Unbekannte Aktion:', action); + } +} + +// Funktion zum Hinzufügen eines neuen Knotens +function addNewNode(cy) { + // Zentriere den neuen Knoten im sichtbaren Bereich + const extent = cy.extent(); + const centerX = (extent.x1 + extent.x2) / 2; + const centerY = (extent.y1 + extent.y2) / 2; + + // Generiere eine eindeutige ID + const newId = 'new-node-' + Date.now(); + + // Füge den neuen Knoten hinzu + cy.add({ + group: 'nodes', + data: { + id: newId, + label: 'Neuer Knoten', + category: 'Wissenschaft', + description: 'Beschreibung hinzufügen', + hasChildren: false, + expanded: false, + color: mindmapConfig.categories['Wissenschaft'].color, + fontColor: '#ffffff', + fontSize: 16, + neuronSize: 8, + neuronActivity: 0.8 + }, + position: { x: centerX, y: centerY } + }); + + // Wähle den neuen Knoten aus, um ihn zu bearbeiten + const newNode = cy.getElementById(newId); + newNode.select(); + + // Öffne den Bearbeitungsdialog für den neuen Knoten + setTimeout(() => { + editNodeProperties(newNode); + }, 300); +} + +// Funktion zum Bearbeiten von Knoteneigenschaften +function editNodeProperties(node) { + if (!node) return; + + // Erstelle den Modal-Hintergrund + const backdrop = document.createElement('div'); + backdrop.className = 'modal-backdrop'; + document.body.appendChild(backdrop); + + // Erstelle den Bearbeitungsdialog + const dialog = document.createElement('div'); + dialog.className = 'node-edit-dialog'; + + // Hole die aktuellen Daten des Knotens + const data = node.data(); + + // Generiere die Kategorie-Optionen + let categoryOptions = ''; + for (const category in mindmapConfig.categories) { + categoryOptions += ``; + } + + // Erstelle das Formular + dialog.innerHTML = ` +

Knoten bearbeiten

+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ `; + + document.body.appendChild(dialog); + + // Event-Listener für den Abbrechen-Button + dialog.querySelector('button.cancel').addEventListener('click', function() { + document.body.removeChild(dialog); + document.body.removeChild(backdrop); + }); + + // Event-Listener für den Modal-Hintergrund + backdrop.addEventListener('click', function() { + document.body.removeChild(dialog); + document.body.removeChild(backdrop); + }); + + // Event-Listener für das Formular + dialog.querySelector('form').addEventListener('submit', function(e) { + e.preventDefault(); + + // Hole die neuen Werte aus dem Formular + const label = document.getElementById('node-label').value; + const category = document.getElementById('node-category').value; + const description = document.getElementById('node-description').value; + const hasChildren = document.getElementById('node-has-children').value === 'true'; + + // Bestimme die Farbe basierend auf der Kategorie + const color = mindmapConfig.categories[category]?.color || data.color || '#60a5fa'; + + // Aktualisiere die Daten des Knotens + node.data({ + ...data, + label, + category, + description, + hasChildren, + color + }); + + // Schließe den Dialog + document.body.removeChild(dialog); + document.body.removeChild(backdrop); + + // Benachrichtigung anzeigen + showUINotification('Knoten wurde aktualisiert', 'success'); + }); +} + +// Funktion zum Anzeigen des Kontext-Menüs für einen Knoten +function showNodeContextMenu(node, position) { + if (!node) return; + + // Entferne existierende Kontext-Menüs + const existingMenu = document.querySelector('.context-menu'); + if (existingMenu) { + existingMenu.parentNode.removeChild(existingMenu); + } + + // Erstelle das Kontext-Menü + const menu = document.createElement('div'); + menu.className = 'context-menu'; + menu.style.left = `${position.x}px`; + menu.style.top = `${position.y}px`; + + // Menü-Einträge + menu.innerHTML = ` +
+ + + + + Bearbeiten +
+
+ + + + + + Verbindung erstellen +
+
+ + + + + Löschen +
+ `; + + document.body.appendChild(menu); + + // Event-Listener für Menü-Einträge + menu.querySelectorAll('.context-menu-item').forEach(item => { + item.addEventListener('click', function() { + const action = this.getAttribute('data-action'); + + switch (action) { + case 'edit': + editNodeProperties(node); + break; + case 'connect': + startEdgeCreation(node); + break; + case 'delete': + deleteNode(node); + break; + } + + // Entferne das Menü + document.body.removeChild(menu); + }); + }); + + // Klick außerhalb des Menüs schließt es + document.addEventListener('click', function closeMenu(e) { + if (!menu.contains(e.target)) { + if (document.body.contains(menu)) { + document.body.removeChild(menu); + } + document.removeEventListener('click', closeMenu); + } + }); +} + +// Funktion zum Anzeigen des Menüs zum Hinzufügen eines Knotens +function showAddNodeMenu(position) { + // Entferne existierende Kontext-Menüs + const existingMenu = document.querySelector('.context-menu'); + if (existingMenu) { + existingMenu.parentNode.removeChild(existingMenu); + } + + // Erstelle das Kontext-Menü + const menu = document.createElement('div'); + menu.className = 'context-menu'; + menu.style.left = `${position.x}px`; + menu.style.top = `${position.y}px`; + + // Kategorie-Einträge für neue Knoten + let categoryItems = ''; + for (const category in mindmapConfig.categories) { + const categoryConfig = mindmapConfig.categories[category]; + categoryItems += ` +
+ + ${category} +
+ `; + } + + // Menü-Einträge + menu.innerHTML = ` +
Neuen Knoten hinzufügen
+ ${categoryItems} + `; + + document.body.appendChild(menu); + + // Event-Listener für Kategorie-Einträge + menu.querySelectorAll('.context-menu-item').forEach(item => { + item.addEventListener('click', function() { + const category = this.getAttribute('data-category'); + + // Füge einen neuen Knoten an der Position hinzu + addNewNodeAtPosition(cy, position, category); + + // Entferne das Menü + document.body.removeChild(menu); + }); + }); + + // Klick außerhalb des Menüs schließt es + document.addEventListener('click', function closeMenu(e) { + if (!menu.contains(e.target)) { + if (document.body.contains(menu)) { + document.body.removeChild(menu); + } + document.removeEventListener('click', closeMenu); + } + }); +} + +// Funktion zum Hinzufügen eines neuen Knotens an einer bestimmten Position +function addNewNodeAtPosition(cy, position, category) { + if (!cy) return; + + // Berechne die Modellposition (statt der gerenderten Position) + const modelPosition = cy.renderer().projectIntoViewport(position.x, position.y); + + // Generiere eine eindeutige ID + const newId = 'new-node-' + Date.now(); + + // Hole die Kategorie-Konfiguration + const categoryConfig = mindmapConfig.categories[category] || mindmapConfig.categories['Wissenschaft']; + + // Füge den neuen Knoten hinzu + cy.add({ + group: 'nodes', + data: { + id: newId, + label: 'Neuer ' + category + '-Knoten', + category: category, + description: categoryConfig.description || 'Beschreibung hinzufügen', + hasChildren: false, + expanded: false, + color: categoryConfig.color, + fontColor: '#ffffff', + fontSize: 16, + neuronSize: 8, + neuronActivity: 0.8 + }, + position: { + x: modelPosition[0], + y: modelPosition[1] + } + }); + + // Wähle den neuen Knoten aus, um ihn zu bearbeiten + const newNode = cy.getElementById(newId); + newNode.select(); + + // Öffne den Bearbeitungsdialog für den neuen Knoten + setTimeout(() => { + editNodeProperties(newNode); + }, 300); +} + +// Funktion zum Starten der Erstellung einer Kante +function startEdgeCreation(sourceNode) { + if (!sourceNode) return; + + const cy = sourceNode.cy(); + if (!cy) return; + + // Markiere den Quellknoten + sourceNode.addClass('edge-source'); + + // Füge dem Container die Klasse für den Kanten-Erstellungsmodus hinzu + cy.container().classList.add('edge-creation-mode'); + + // Zeige Hinweis an + showUINotification('Wähle einen Zielknoten für die Verbindung', 'info'); + + // Einmaliger Event-Listener für das Auswählen des Zielknotens + cy.once('tap', 'node', function(evt) { + const targetNode = evt.target; + + // Ignoriere, wenn es der gleiche Knoten ist + if (targetNode.id() === sourceNode.id()) { + showUINotification('Quell- und Zielknoten dürfen nicht identisch sein', 'warning'); + finishEdgeCreation(); + return; + } + + // Prüfe, ob bereits eine Kante existiert + const existingEdge = cy.edges(`[source="${sourceNode.id()}"][target="${targetNode.id()}"]`); + if (existingEdge.length > 0) { + showUINotification('Verbindung existiert bereits', 'warning'); + finishEdgeCreation(); + return; + } + + // Füge die neue Kante hinzu + cy.add({ + group: 'edges', + data: { + source: sourceNode.id(), + target: targetNode.id(), + strength: 0.5, + conductionVelocity: Math.random() * 0.5 + 0.3, + latency: Math.random() * 100 + 50 + } + }); + + showUINotification('Verbindung wurde erstellt', 'success'); + finishEdgeCreation(); + }); + + // Event-Listener für Abbruch durch Klick auf leeren Bereich + cy.once('tap', function(evt) { + if (evt.target === cy) { + showUINotification('Kantenerstellung abgebrochen', 'info'); + finishEdgeCreation(); + } + }); + + function finishEdgeCreation() { + sourceNode.removeClass('edge-source'); + cy.container().classList.remove('edge-creation-mode'); + } +} + +// Funktion zum Aktivieren des Kantenerstellungsmodus +function enableEdgeCreationMode(cy) { + if (!cy) return; + + // Füge dem Container die Klasse für den Kanten-Erstellungsmodus hinzu + cy.container().classList.add('edge-creation-mode'); + + // Zeige Hinweis an + showUINotification('Wähle einen Quellknoten für die Verbindung', 'info'); + + // Einmaliger Event-Listener für das Auswählen des Quellknotens + cy.once('tap', 'node', function(evt) { + const sourceNode = evt.target; + startEdgeCreation(sourceNode); + }); + + // Event-Listener für Abbruch durch Rechtsklick oder Escape-Taste + const cancelEdgeCreation = function() { + cy.container().classList.remove('edge-creation-mode'); + cy.removeListener('tap'); + showUINotification('Kantenerstellung abgebrochen', 'info'); }; - console.log('Zu speichernde Daten:', saveData); + cy.once('cxttap', cancelEdgeCreation); + document.addEventListener('keydown', function escKeyHandler(e) { + if (e.key === 'Escape') { + cancelEdgeCreation(); + document.removeEventListener('keydown', escKeyHandler); + } + }); +} + +// Funktion zum Löschen eines Knotens +function deleteNode(node) { + if (!node) return; - // API-Anfrage senden + // Frage den Benutzer, ob er den Knoten wirklich löschen möchte + if (confirm(`Möchtest du den Knoten "${node.data('label')}" wirklich löschen?`)) { + // Entferne den Knoten und alle zugehörigen Kanten + node.remove(); + showUINotification('Knoten wurde gelöscht', 'success'); + } +} + +// Funktion zum Löschen ausgewählter Elemente +function deleteSelectedElements(cy) { + if (!cy) return; + + const selectedElements = cy.elements(':selected'); + if (selectedElements.length === 0) { + showUINotification('Keine Elemente ausgewählt', 'warning'); + return; + } + + // Zähle die ausgewählten Knoten und Kanten + const selectedNodes = selectedElements.filter('node'); + const selectedEdges = selectedElements.filter('edge'); + + let confirmMessage = 'Möchtest du die ausgewählten Elemente wirklich löschen?'; + if (selectedNodes.length > 0 && selectedEdges.length > 0) { + confirmMessage = `Möchtest du ${selectedNodes.length} Knoten und ${selectedEdges.length} Verbindungen wirklich löschen?`; + } else if (selectedNodes.length > 0) { + confirmMessage = `Möchtest du ${selectedNodes.length} Knoten wirklich löschen?`; + } else if (selectedEdges.length > 0) { + confirmMessage = `Möchtest du ${selectedEdges.length} Verbindungen wirklich löschen?`; + } + + // Frage den Benutzer, ob er die ausgewählten Elemente wirklich löschen möchte + if (confirm(confirmMessage)) { + // Entferne die ausgewählten Elemente + selectedElements.remove(); + + let successMessage = 'Elemente wurden gelöscht'; + if (selectedNodes.length > 0 && selectedEdges.length > 0) { + successMessage = `${selectedNodes.length} Knoten und ${selectedEdges.length} Verbindungen wurden gelöscht`; + } else if (selectedNodes.length > 0) { + successMessage = `${selectedNodes.length} Knoten wurden gelöscht`; + } else if (selectedEdges.length > 0) { + successMessage = `${selectedEdges.length} Verbindungen wurden gelöscht`; + } + + showUINotification(successMessage, 'success'); + } +} + +// Funktion zum Speichern der Änderungen an der Mindmap +function saveMindmapChanges(cy, nodeId) { + if (!cy) return; + + // Sammle die Daten aller Knoten und Kanten + const nodes = []; + const edges = []; + + cy.nodes().forEach(node => { + const data = node.data(); + const position = node.position(); + + nodes.push({ + id: data.id, + name: data.label, + category: data.category, + description: data.description, + has_children: data.hasChildren, + color_code: data.color, + position_x: position.x, + position_y: position.y + }); + }); + + cy.edges().forEach(edge => { + const data = edge.data(); + + edges.push({ + source: data.source, + target: data.target, + strength: data.strength + }); + }); + + // Erstelle die Daten für den API-Aufruf + const saveData = { + nodes: nodes, + edges: edges, + parent_id: nodeId || null + }; + + console.log('Speichere Mindmap-Änderungen:', saveData); + + // Zeige eine Speicherbenachrichtigung an + showUINotification('Speichere Mindmap-Änderungen...', 'info'); + + // Sende die Daten an den Server fetch('/api/mindmap/save', { method: 'POST', headers: { @@ -211,24 +1494,314 @@ function saveMindmapChanges(cy) { }, body: JSON.stringify(saveData) }) - .then(response => response.json()) - .then(data => { - if (data.success) { - // Erfolg - console.log('Mindmap erfolgreich gespeichert:', data); - alert('Mindmap wurde erfolgreich gespeichert!'); - } else { - // Fehler - console.error('Fehler beim Speichern der Mindmap:', data.error); - alert('Fehler beim Speichern: ' + data.error); + .then(response => { + if (!response.ok) { + return response.json().then(data => { + throw new Error(data.error || `Fehler ${response.status}: ${response.statusText}`); + }); } + return response.json(); + }) + .then(data => { + console.log('Speichern erfolgreich:', data); + showUINotification('Mindmap wurde erfolgreich gespeichert', 'success'); }) .catch(error => { - console.error('Netzwerkfehler beim Speichern der Mindmap:', error); - alert('Netzwerkfehler beim Speichern: ' + error.message); + console.error('Fehler beim Speichern der Mindmap:', error); + showUINotification({ + error: 'Fehler beim Speichern der Mindmap', + details: error.message + }, 'error'); }); } -// Exportiere die Funktionen -window.initializeMindmap = initializeMindmap; -window.saveMindmapChanges = saveMindmapChanges; \ No newline at end of file +// Füge CSS-Stile für den Bearbeitungsmodus hinzu +const editingStyles = document.createElement('style'); +editingStyles.textContent = ` + /* Bearbeitungsmodus-Cursor */ + .editing-mode { + cursor: grab !important; + } + + .editing-mode:active { + cursor: grabbing !important; + } + + /* Bearbeitungssteuerungen */ + .editing-controls { + position: absolute; + bottom: 20px; + left: 50%; + transform: translateX(-50%); + z-index: 1010; + display: none; + } + + .editing-toolbar { + display: flex; + background: rgba(30, 41, 59, 0.85); + border-radius: 8px; + padding: 8px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + backdrop-filter: blur(8px); + border: 1px solid rgba(255, 255, 255, 0.1); + } + + .tool-button { + background: rgba(255, 255, 255, 0.1); + border: none; + color: white; + width: 40px; + height: 40px; + border-radius: 6px; + margin: 0 4px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; + } + + .tool-button:hover { + background: rgba(255, 255, 255, 0.2); + transform: translateY(-2px); + } + + .tool-button:active { + transform: translateY(0); + } + + .tool-button[data-action="add-node"] { + background: rgba(16, 185, 129, 0.2); + } + + .tool-button[data-action="add-edge"] { + background: rgba(59, 130, 246, 0.2); + } + + .tool-button[data-action="delete"] { + background: rgba(239, 68, 68, 0.2); + } + + .tool-button[data-action="save"] { + background: rgba(245, 158, 11, 0.2); + } + + /* Node-Bearbeitungsdialog */ + .node-edit-dialog { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: rgba(30, 41, 59, 0.95); + border-radius: 12px; + padding: 20px; + width: 400px; + max-width: 90vw; + z-index: 2000; + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2); + backdrop-filter: blur(12px); + border: 1px solid rgba(255, 255, 255, 0.1); + } + + .node-edit-dialog h3 { + margin-top: 0; + color: white; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + padding-bottom: 10px; + } + + .form-group { + margin-bottom: 16px; + } + + .form-group label { + display: block; + margin-bottom: 6px; + color: #e2e8f0; + font-size: 0.9rem; + } + + .form-group input, + .form-group textarea, + .form-group select { + width: 100%; + padding: 8px 12px; + background: rgba(15, 23, 42, 0.5); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 6px; + color: white; + font-size: 0.95rem; + } + + .form-group input:focus, + .form-group textarea:focus, + .form-group select:focus { + outline: none; + border-color: #8b5cf6; + box-shadow: 0 0 0 2px rgba(139, 92, 246, 0.2); + } + + .form-actions { + display: flex; + justify-content: flex-end; + gap: 10px; + margin-top: 20px; + } + + .form-actions button { + padding: 8px 16px; + border-radius: 6px; + border: none; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + } + + .form-actions button.save { + background: #8b5cf6; + color: white; + } + + .form-actions button.cancel { + background: rgba(255, 255, 255, 0.1); + color: white; + } + + .form-actions button:hover { + transform: translateY(-2px); + } + + /* Kontext-Menü */ + .context-menu { + position: absolute; + background: rgba(30, 41, 59, 0.95); + border-radius: 8px; + padding: 8px 0; + min-width: 160px; + z-index: 2000; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); + backdrop-filter: blur(8px); + border: 1px solid rgba(255, 255, 255, 0.1); + } + + .context-menu-item { + padding: 8px 16px; + color: white; + cursor: pointer; + transition: background-color 0.2s ease; + display: flex; + align-items: center; + gap: 8px; + } + + .context-menu-item:hover { + background: rgba(255, 255, 255, 0.1); + } + + .context-menu-item svg { + width: 16px; + height: 16px; + } + + /* Modal-Hintergrund */ + .modal-backdrop { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 1999; + backdrop-filter: blur(2px); + } + + /* Edge-Erstellungsmodus */ + .edge-creation-mode { + cursor: crosshair !important; + } + + /* Erweiterte Mindmap-Seite */ + .mindmap-header { + display: flex; + align-items: center; + justify-content: space-between; + } + + .mindmap-actions { + display: flex; + gap: 10px; + } + + .edit-button { + display: flex; + align-items: center; + gap: 6px; + background: rgba(139, 92, 246, 0.2); + border: none; + color: white; + padding: 6px 12px; + border-radius: 6px; + cursor: pointer; + transition: all 0.2s ease; + } + + .edit-button:hover { + background: rgba(139, 92, 246, 0.3); + transform: translateY(-2px); + } + + .edit-button svg { + opacity: 0.8; + } +`; +document.head.appendChild(editingStyles); + +// Funktion zum Vergrößern der Ansicht +function zoomIn(cy) { + if (cy) { + const currentZoom = cy.zoom(); + cy.zoom({ + level: currentZoom * 1.2, // 20% größer + renderedPosition: { + x: cy.width() / 2, + y: cy.height() / 2 + } + }); + showUINotification('Ansicht vergrößert', 'info', 1000); + } +} + +// Funktion zum Verkleinern der Ansicht +function zoomOut(cy) { + if (cy) { + const currentZoom = cy.zoom(); + cy.zoom({ + level: currentZoom * 0.8, // 20% kleiner + renderedPosition: { + x: cy.width() / 2, + y: cy.height() / 2 + } + }); + showUINotification('Ansicht verkleinert', 'info', 1000); + } +} + +// Funktion zum Zurücksetzen der Ansicht +function resetView(cy) { + if (cy) { + cy.fit(); + cy.center(); + showUINotification('Ansicht zurückgesetzt', 'info', 1000); + } +} + +// Funktion zum Ein- und Ausblenden der Legende +function toggleLegend() { + const legend = document.getElementById('categoryLegend'); + if (legend) { + const isVisible = legend.style.display !== 'none'; + legend.style.display = isVisible ? 'none' : 'flex'; + showUINotification(isVisible ? 'Legende ausgeblendet' : 'Legende eingeblendet', 'info', 1000); + } +} \ No newline at end of file diff --git a/templates/mindmap.html b/templates/mindmap.html index 3134940..84cbf50 100644 --- a/templates/mindmap.html +++ b/templates/mindmap.html @@ -90,7 +90,7 @@ /* Kontrollpanel */ .control-panel { position: absolute; - right: 9rem; + right: 2rem; top: 50%; transform: translateY(-50%); background: rgba(15, 23, 42, 0.9); @@ -123,60 +123,6 @@ margin-right: 0.75rem; } - /* Rechte Seitenleiste */ - .right-sidebar { - position: absolute; - right: 1.5rem; - top: 10rem; - transform: translateY(0); - display: flex; - flex-direction: column; - gap: 1.5rem; - z-index: 100; - } - - .tool-buttons { - display: flex; - flex-direction: column; - gap: 0.75rem; - background: rgba(15, 23, 42, 0.9); - border-radius: 1rem; - padding: 1rem; - backdrop-filter: blur(10px); - border: 1px solid rgba(255, 255, 255, 0.1); - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); - } - - .sidebar-button { - display: flex; - align-items: center; - justify-content: flex-start; - gap: 0.75rem; - padding: 0.75rem; - background: rgba(255, 255, 255, 0.1); - border: 1px solid rgba(255, 255, 255, 0.1); - border-radius: 0.5rem; - color: white; - cursor: pointer; - transition: all 0.3s ease; - font-size: 0.9rem; - width: 100%; - } - - .sidebar-button:hover { - background: rgba(255, 255, 255, 0.2); - transform: translateX(-5px); - } - - .sidebar-button i { - font-size: 1.1rem; - color: #8b5cf6; - } - - .sidebar-button:active { - transform: scale(0.98); - } - /* CRUD Panel */ .crud-panel { position: absolute; @@ -479,24 +425,6 @@ - - -