From e6784b712df0390aec63890c58e6ebedc110da40 Mon Sep 17 00:00:00 2001 From: Till Tomczak Date: Wed, 14 May 2025 13:51:16 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=89=20feat:=20"Add=20Minducture=20mind?= =?UTF-8?q?map=20CSS=20and=20JS=20template=20integration"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/css/src/mindmap.css | 165 ++++++++ static/js/update_mindmap.js | 784 ++++++++++++++++-------------------- templates/mindmap.html | 1 + 3 files changed, 507 insertions(+), 443 deletions(-) create mode 100644 static/css/src/mindmap.css diff --git a/static/css/src/mindmap.css b/static/css/src/mindmap.css new file mode 100644 index 0000000..99bb762 --- /dev/null +++ b/static/css/src/mindmap.css @@ -0,0 +1,165 @@ +/** + * Mindmap Hauptkomponenten Styling + */ + +/* Toast-Container und Benachrichtigungen */ +#toast-container { + position: fixed; + top: 1rem; + right: 1rem; + z-index: 9999; + display: flex; + flex-direction: column; + gap: 0.5rem; + max-width: 350px; +} + +.toast-message { + transition: all 0.3s ease; + overflow: hidden; +} + +.toast-message.translate-x-full { + transform: translateX(100%); +} + +/* Flash-Nachrichten */ +.flash-message { + position: fixed; + top: 1rem; + left: 50%; + transform: translateX(-50%) translateY(-100%); + padding: 0.75rem 1.5rem; + border-radius: 0.5rem; + background-color: rgba(30, 41, 59, 0.95); + color: white; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + z-index: 9999; + transition: all 0.3s ease; +} + +.flash-message.visible { + transform: translateX(-50%) translateY(0); +} + +.flash-message.info { + background-color: rgba(59, 130, 246, 0.95); +} + +.flash-message.success { + background-color: rgba(16, 185, 129, 0.95); +} + +.flash-message.warning { + background-color: rgba(245, 158, 11, 0.95); +} + +.flash-message.error { + background-color: rgba(239, 68, 68, 0.95); +} + +/* Zoom-Steuerungen */ +.control-panel { + position: absolute; + right: 2rem; + top: 50%; + transform: translateY(-50%); + background: rgba(15, 23, 42, 0.9); + border-radius: 1rem; + padding: 1.5rem; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); + z-index: 10; + border: 1px solid rgba(255, 255, 255, 0.1); + backdrop-filter: blur(8px); +} + +.control-button { + display: flex; + align-items: center; + padding: 0.75rem 1rem; + margin: 0.5rem 0; + background: rgba(255, 255, 255, 0.1); + border: none; + border-radius: 0.5rem; + color: #fff; + cursor: pointer; + transition: all 0.3s ease; +} + +.control-button:hover { + background: rgba(255, 255, 255, 0.2); + transform: translateX(-5px); +} + +.control-button i { + margin-right: 0.75rem; +} + +/* Info-Panel */ +.info-panel { + position: absolute; + left: 2rem; + top: 50%; + transform: translateY(-50%) translateX(-20px); + opacity: 0; + background: rgba(15, 23, 42, 0.9); + border-radius: 1rem; + padding: 1.5rem; + width: 300px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); + z-index: 10; + border: 1px solid rgba(255, 255, 255, 0.1); + transition: all 0.3s ease; + backdrop-filter: blur(8px); +} + +.info-panel.visible { + opacity: 1; + transform: translateY(-50%) translateX(0); +} + +.info-title { + font-size: 1.25rem; + font-weight: 600; + color: #fff; + margin-bottom: 1rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); +} + +.info-content { + color: rgba(255, 255, 255, 0.8); + font-size: 0.95rem; + line-height: 1.6; +} + +/* Kategorie-Legende */ +.category-legend { + position: absolute; + bottom: 2rem; + left: 50%; + transform: translateX(-50%); + background: rgba(15, 23, 42, 0.9); + border-radius: 1rem; + padding: 1rem 2rem; + display: flex; + gap: 1.5rem; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); + z-index: 10; + border: 1px solid rgba(255, 255, 255, 0.1); + backdrop-filter: blur(8px); +} + +.category-item { + display: flex; + align-items: center; + color: rgba(255, 255, 255, 0.8); + font-size: 0.9rem; +} + +.category-color { + width: 12px; + height: 12px; + border-radius: 50%; + margin-right: 0.5rem; +} \ No newline at end of file diff --git a/static/js/update_mindmap.js b/static/js/update_mindmap.js index ba36af9..4aafd1c 100644 --- a/static/js/update_mindmap.js +++ b/static/js/update_mindmap.js @@ -309,179 +309,138 @@ async function initializeMindmap() { label: node.name, category: node.category, description: node.description, - hasChildren: node.has_children, - expanded: false, - color: node.color_code, - fontColor: '#ffffff', - fontSize: node.is_center ? 20 : 16 + color: getCategoryColor(node.category) } })), // Kanten ...data.edges.map(edge => ({ data: { + id: edge.id, source: edge.source, target: edge.target, - strength: edge.strength || 0.5 + strength: edge.weight || 1.0 } })) ]; - // Bestehende Cytoscape-Instanz entfernen, falls vorhanden - if (window.cy && typeof window.cy.destroy === 'function') { - window.cy.destroy(); - } - + // Cytoscape-Container const cyContainer = document.getElementById('cy'); if (!cyContainer) { - throw new Error('Mindmap-Container #cy nicht gefunden!'); + throw new Error('Cytoscape-Container nicht gefunden'); } - window.cy = cytoscape({ + // Cytoscape initialisieren + const cy = cytoscape({ container: cyContainer, elements: elements, style: [ + // Knoten-Styling { selector: 'node', style: mindmapStyles.node.base }, - { - selector: 'node[isCenter]', - style: mindmapStyles.node.center - }, - { - selector: 'node:selected', - style: mindmapStyles.node.selected - }, + // Kanten-Styling { selector: 'edge', style: mindmapStyles.edge.base + }, + // Ausgewählte Knoten + { + selector: 'node:selected', + style: mindmapStyles.node.selected } ], 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) { + // Event-Listener für Knoten-Interaktionen + cy.on('tap', 'node', 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); - } + showNodeInfo(node); }); - // Layout ausführen - cy.layout(mindmapStyles.layout.base).run(); - - // Starte neuronale Aktivitätssimulation - startNeuralActivitySimulation(cy); - - // Mindmap mit echten Daten befüllen (Styles, Farben etc.) - updateMindmap(); - - return true; + // UI aktualisieren + document.getElementById('loader').style.display = 'none'; + document.getElementById('statusMessage').style.display = 'none'; + + // Neurales Design anwenden + initializeNeuralDesign(cy); + + // Gebe die Cytoscape-Instanz zurück + return cy; } catch (error) { - console.error('Fehler bei der Mindmap-Initialisierung:', error); - showUINotification({ - error: 'Mindmap konnte nicht initialisiert werden', - details: error.message - }, 'error'); - return false; + console.error('Fehler beim Initialisieren der Mindmap:', error); + document.getElementById('statusMessage').textContent = 'Fehler: ' + error.message; + document.getElementById('statusMessage').style.display = 'block'; + document.getElementById('loader').style.display = 'none'; + + showUINotification('Fehler beim Initialisieren der Mindmap: ' + error.message, 'error'); + throw error; } } -// Warte bis DOM geladen ist -document.addEventListener('DOMContentLoaded', function() { - console.log('DOMContentLoaded Event ausgelöst'); +/** + * Gibt die Farbe für eine Kategorie zurück + * @param {string} category - Die Kategorie + * @returns {string} - Die Farbe als Hex-Code + */ +function getCategoryColor(category) { + const categoryMap = { + 'Philosophie': '#9F7AEA', // Violett + 'Wissenschaft': '#60A5FA', // Blau + 'Technologie': '#10B981', // Grün + 'Künste': '#F59E0B', // Orange + 'Psychologie': '#EF4444' // Rot + }; - // 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; - } - - // Prüfe, ob Cytoscape verfügbar ist - if (typeof cytoscape === 'undefined') { - console.error('Cytoscape ist nicht definiert!'); - return; - } - console.log('Cytoscape ist verfügbar'); + return categoryMap[category] || '#8B5CF6'; // Standardfarbe, wenn Kategorie nicht gefunden +} - // 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'); - }); -}); +/** + * Zeigt Informationen zu einem Knoten an + * @param {Object} node - Der Knoten, dessen Informationen angezeigt werden sollen + */ +function showNodeInfo(node) { + if (!node || !node.isNode()) return; + + const infoPanel = document.getElementById('infoPanel'); + const infoTitle = infoPanel.querySelector('.info-title'); + const infoContent = infoPanel.querySelector('.info-content'); + + if (!infoPanel || !infoTitle || !infoContent) return; + + const data = node.data(); + + infoTitle.textContent = data.label || 'Knotendetails'; + + let contentHTML = ` +

Kategorie: ${data.category || 'Nicht kategorisiert'}

+

Beschreibung: ${data.description || 'Keine Beschreibung verfügbar'}

+ `; + + infoContent.innerHTML = contentHTML; + infoPanel.classList.add('visible'); +} // Funktion zum Initialisieren des neuronalen Designs function initializeNeuralDesign(cy) { - // Füge neuronale Eigenschaften zu allen Knoten hinzu + if (!cy) return; + + console.log('Initialisiere neurales Design für Mindmap'); + + // Füge neurale 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 + lastFired: 0 }); }); - + // Füge synaptische Eigenschaften zu allen Kanten hinzu cy.edges().forEach(edge => { const data = edge.data(); @@ -492,66 +451,172 @@ function initializeNeuralDesign(cy) { 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 + + // Wende neurales Netzwerk-Styling an + applyNeuralNetworkStyle(cy); + + // Starte Simulation der neuronalen Aktivität startNeuralActivitySimulation(cy); + + return cy; +} + +// Funktionen für das neurale Netzwerk-Design +function applyNeuralNetworkStyle(cy) { + if (!cy) return; + + // Animation für pulsierende Knoten + const pulseKeyframes = [ + { scale: 1.0, opacity: 0.8, borderOpacity: 0.6 }, + { scale: 1.05, opacity: 1.0, borderOpacity: 0.9 }, + { scale: 1.0, opacity: 0.8, borderOpacity: 0.6 } + ]; + + // Anwenden auf alle Knoten + cy.nodes().forEach(node => { + const randomDelay = Math.random() * 3000; + + // CSS-Animationen auf die Knoten anwenden + node.style({ + 'border-width': 3, + 'border-opacity': 0.6, + 'background-opacity': 0.8, + 'transition-property': 'background-opacity, border-opacity, width, height', + 'transition-duration': '300ms' + }); + + // Pulseffekt mit zufälligem Delay starten + setTimeout(() => { + node.animate({ + style: pulseKeyframes, + duration: 2000 + Math.random() * 1000, + complete: function() { + // Animation wiederholen + this.loopAnimation = true; + if (this.loopAnimation) { + setTimeout(() => { + applyNeuralNetworkStyle(cy); + }, 100); + } + } + }); + }, randomDelay); + }); +} + +// Simulation der neuronalen Aktivität +function startNeuralActivitySimulation(cy) { + if (!cy) return; + + console.log('Starte Simulation der neuronalen Aktivität'); + let isSimulationRunning = true; + + // Funktion zur Simulation der neuronalen Aktivität + function simulateNeuralActivity() { + if (!isSimulationRunning) return; + + const currentTime = Date.now(); + + // Wähle zufällige Knoten zum Feuern aus + const randomNode = cy.nodes()[Math.floor(Math.random() * cy.nodes().length)]; + if (randomNode) { + fireNeuron(randomNode, { isPrimary: true }, currentTime); + } + + // Wiederholung der Simulation + setTimeout(simulateNeuralActivity, 2000 + Math.random() * 3000); + } + + // Funktion zum "Feuern" eines Neurons + function fireNeuron(node, state, currentTime) { + if (!node) return; + + const data = node.data(); + const lastFired = data.lastFired || 0; + const refractionPeriod = data.refractionPeriod || 1000; + + // Prüfe, ob das Neuron feuerbereit ist (Refraktärzeit vorbei) + if (currentTime - lastFired < refractionPeriod && !state.isPrimary) { + return; // Neuron ist noch in Refraktärphase + } + + // Aktualisiere "Letztes Feuern"-Zeitstempel + node.data('lastFired', currentTime); + + // Visuellen Effekt des Feuerns anzeigen + node.animate({ + style: { + 'background-opacity': 1, + 'border-opacity': 1, + 'border-color': '#f59e42', + 'border-width': 4 + }, + duration: 300, + complete: function() { + // Zurück zum Normalzustand + node.animate({ + style: { + 'background-opacity': 0.8, + 'border-opacity': 0.6, + 'border-color': '#ffffff', + 'border-width': 3 + }, + duration: 500 + }); + + // Signal an verbundene Knoten weitergeben + propagateSignal(node, currentTime); + } + }); + } + + // Funktion zur Signalausbreitung zu verbundenen Knoten + function propagateSignal(sourceNode, currentTime) { + // Verbundene Kanten finden + const connectedEdges = sourceNode.outgoers('edge'); + + connectedEdges.forEach(edge => { + const targetNode = edge.target(); + const data = edge.data(); + const strength = data.strength || 0.5; + const conductionVelocity = data.conductionVelocity || 0.5; + const latency = data.latency || 100; + + // Kante hervorheben (Signal fließt durch die Kante) + edge.animate({ + style: { + 'line-opacity': 1, + 'width': 3 + }, + duration: 200, + complete: function() { + // Nach einer Verzögerung (Latenz), das Ziel-Neuron feuern lassen + setTimeout(() => { + edge.animate({ + style: { + 'line-opacity': data.strength ? data.strength * 0.6 : 0.4, + 'width': data.strength ? data.strength * 2 : 1 + }, + duration: 300 + }); + + // Ziel-Neuron feuern lassen mit Wahrscheinlichkeit basierend auf Stärke + if (Math.random() < strength) { + fireNeuron(targetNode, { isPrimary: false }, currentTime + latency); + } + }, latency / conductionVelocity); + } + }); + }); + } + + // Starte die Simulation + simulateNeuralActivity(); + + // Funktion zum Stoppen der Simulation + cy.stopNeuralSimulation = function() { + isSimulationRunning = false; + }; } // Modifiziere die updateMindmap Funktion @@ -678,289 +743,122 @@ function enhanceMindmap() { 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); - + flash.innerHTML = ` +
+ + ${message} +
+ `; + + document.body.appendChild(flash); + + // Animation zum Einblenden setTimeout(() => { - flash.classList.add('show'); + flash.classList.add('visible'); + }, 10); + + // Automatisches Ausblenden nach 3 Sekunden + setTimeout(() => { + flash.classList.remove('visible'); setTimeout(() => { - flash.classList.remove('show'); - setTimeout(() => { - flashContainer.remove(); - }, 300); - }, 3000); - }, 100); + document.body.removeChild(flash); + }, 300); + }, 3000); } /** - * 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) + * Zeigt eine UI-Benachrichtigung + * @param {string} message - Die Nachricht + * @param {string} type - Der Typ der Benachrichtigung (info, success, warning, error) + * @param {number} duration - Die Anzeigedauer in Millisekunden */ 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); + // Prüfe, ob Toast-Container existiert, sonst erstellen + let toastContainer = document.getElementById('toast-container'); + + if (!toastContainer) { + toastContainer = document.createElement('div'); + toastContainer.id = 'toast-container'; + toastContainer.className = 'fixed top-4 right-4 z-50 flex flex-col gap-2'; + document.body.appendChild(toastContainer); } - // 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'; + // Erstelle Toast-Element + const toast = document.createElement('div'); + toast.className = 'toast-message transform transition-all duration-300 ease-out translate-x-full'; - // 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'; + // Setze Styling basierend auf Typ + let iconClass = 'fa-info-circle'; + let bgColorClass = 'bg-blue-500'; + + switch (type) { + case 'success': + iconClass = 'fa-check-circle'; + bgColorClass = 'bg-green-500'; + break; + case 'warning': + iconClass = 'fa-exclamation-circle'; + bgColorClass = 'bg-yellow-500'; + break; + case 'error': + iconClass = 'fa-exclamation-triangle'; + bgColorClass = 'bg-red-500'; + break; } - // Nachrichteninhalt formatieren - let content = ''; + // Inhalt des Toasts + toast.innerHTML = ` +
+ + ${message} + +
+ `; - 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; - } + // Füge Toast zum Container hinzu + toastContainer.appendChild(toast); - 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)'; + // Animation zum Einblenden + setTimeout(() => { + toast.classList.remove('translate-x-full'); }, 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); + // Schließen-Button-Funktionalität + const closeButton = toast.querySelector('button'); + closeButton.addEventListener('click', () => { + removeToast(toast); + }); + + // Automatisches Ausblenden nach der angegebenen Dauer + const timeoutId = setTimeout(() => { + removeToast(toast); + }, duration); + + // Funktion zum Entfernen des Toasts + function removeToast(toastElement) { + // Animation zum Ausblenden + toastElement.classList.add('translate-x-full'); + + // Entfernen nach Abschluss der Animation + setTimeout(() => { + if (toastElement.parentNode === toastContainer) { + toastContainer.removeChild(toastElement); + } + + // Entferne Container, wenn keine Toasts mehr vorhanden sind + if (toastContainer.children.length === 0) { + document.body.removeChild(toastContainer); + } + }, 300); + + // Timeout löschen + clearTimeout(timeoutId); } } diff --git a/templates/mindmap.html b/templates/mindmap.html index 84cbf50..8350fdc 100644 --- a/templates/mindmap.html +++ b/templates/mindmap.html @@ -3,6 +3,7 @@ {% block title %}Interaktive Mindmap{% endblock %} {% block extra_css %} +