diff --git a/website/app.py b/website/app.py index d5da608..022963b 100644 --- a/website/app.py +++ b/website/app.py @@ -822,6 +822,12 @@ def chat_with_assistant(): print(f"Fehler bei der KI-Anfrage: {str(e)}") return jsonify({"success": False, "error": "Fehler bei der Verarbeitung der Anfrage"}), 500 +# Route für Benutzer-Merkliste und persönliche Mindmap +@app.route('/my-account') +@login_required +def my_account(): + return render_template('my_account.html', current_year=current_year) + # Flask starten if __name__ == '__main__': with app.app_context(): diff --git a/website/static/js/modules/mindmap.js b/website/static/js/modules/mindmap.js index 41d93cb..69385c8 100644 --- a/website/static/js/modules/mindmap.js +++ b/website/static/js/modules/mindmap.js @@ -32,6 +32,9 @@ class MindMapVisualization { this.tooltipDiv = null; this.isLoading = true; + // Lade die gemerkten Knoten + this.bookmarkedNodes = this.loadBookmarkedNodes(); + // Sicherstellen, dass der Container bereit ist if (this.container.node()) { this.init(); @@ -154,10 +157,13 @@ class MindMapVisualization { this.isLoading = false; this.updateVisualization(); - // API-Aufruf mit längeren Timeout im Hintergrund durchführen + // Status auf bereit setzen - don't wait for API + this.container.attr('data-status', 'ready'); + + // API-Aufruf mit kürzerem Timeout im Hintergrund durchführen try { const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 Sekunden Timeout + const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 Sekunden Timeout - reduced from 10 const response = await fetch('/api/mindmap', { signal: controller.signal, @@ -169,14 +175,15 @@ class MindMapVisualization { clearTimeout(timeoutId); if (!response.ok) { - throw new Error(`HTTP Fehler: ${response.status}`); + console.warn(`HTTP Fehler: ${response.status}, verwende Standarddaten`); + return; // Keep using default data } const data = await response.json(); if (!data || !data.nodes || data.nodes.length === 0) { console.warn('Keine Mindmap-Daten vorhanden, verwende weiterhin Standard-Daten.'); - return; // Behalte Standarddaten bei + return; // Keep using default data } // Flache Liste von Knoten und Verbindungen erstellen @@ -187,14 +194,9 @@ class MindMapVisualization { // Visualisierung aktualisieren mit den tatsächlichen Daten this.updateVisualization(); - // Status auf bereit setzen - this.container.attr('data-status', 'ready'); - } catch (error) { console.warn('Fehler beim Laden der Mindmap-Daten, verwende Standarddaten:', error); - // Fallback zu Standarddaten ist bereits geschehen - // Stellen Sie sicher, dass der Status korrekt gesetzt wird - this.container.attr('data-status', 'ready'); + // Already using default data, no action needed } } catch (error) { @@ -366,11 +368,11 @@ class MindMapVisualization { // Kreis für jeden Knoten group.append('circle') - .attr('class', 'node') + .attr('class', d => `node ${this.isNodeBookmarked(d.id) ? 'bookmarked' : ''}`) .attr('r', this.nodeRadius) .attr('fill', d => d.color || this.generateColorFromString(d.name)) - .attr('stroke', '#ffffff50') - .attr('stroke-width', 2) + .attr('stroke', d => this.isNodeBookmarked(d.id) ? '#FFD700' : '#ffffff50') + .attr('stroke-width', d => this.isNodeBookmarked(d.id) ? 3 : 2) .attr('filter', 'url(#glow)'); // Text-Label mit besserem Kontrast @@ -398,7 +400,10 @@ class MindMapVisualization { update => { // Knoten aktualisieren update.select('.node') - .attr('fill', d => d.color || this.generateColorFromString(d.name)); + .attr('class', d => `node ${this.isNodeBookmarked(d.id) ? 'bookmarked' : ''}`) + .attr('fill', d => d.color || this.generateColorFromString(d.name)) + .attr('stroke', d => this.isNodeBookmarked(d.id) ? '#FFD700' : '#ffffff50') + .attr('stroke-width', d => this.isNodeBookmarked(d.id) ? 3 : 2); // Text aktualisieren update.select('.node-label') @@ -465,6 +470,7 @@ class MindMapVisualization { // Tooltip anzeigen if (this.tooltipEnabled) { + const isBookmarked = this.isNodeBookmarked(d.id); const tooltipContent = `
${d.name} @@ -472,6 +478,12 @@ class MindMapVisualization {
Gedanken: ${d.thought_count}
+
+ +
`; @@ -482,6 +494,20 @@ class MindMapVisualization { .transition() .duration(200) .style('opacity', 1); + + // Event-Listener für den Bookmark-Button hinzufügen + document.getElementById('bookmark-button').addEventListener('click', (e) => { + e.stopPropagation(); + const nodeId = e.currentTarget.getAttribute('data-nodeid'); + const isNowBookmarked = this.toggleBookmark(nodeId); + + // Button-Text aktualisieren + if (isNowBookmarked) { + e.currentTarget.innerHTML = ' Gemerkt'; + } else { + e.currentTarget.innerHTML = ' Merken'; + } + }); } // Knoten visuell hervorheben @@ -489,7 +515,7 @@ class MindMapVisualization { .transition() .duration(200) .attr('r', this.nodeRadius * 1.2) - .attr('stroke', '#ffffff'); + .attr('stroke', this.isNodeBookmarked(d.id) ? '#FFD700' : '#ffffff'); } nodeMouseout(event, d) { @@ -506,11 +532,13 @@ class MindMapVisualization { // Knoten-Stil zurücksetzen, wenn nicht ausgewählt const nodeElement = d3.select(event.currentTarget).select('circle'); if (d !== this.selectedNode) { + const isBookmarked = this.isNodeBookmarked(d.id); nodeElement .transition() .duration(200) .attr('r', this.nodeRadius) - .attr('stroke', '#ffffff50'); + .attr('stroke', isBookmarked ? '#FFD700' : '#ffffff50') + .attr('stroke-width', isBookmarked ? 3 : 2); } } @@ -671,6 +699,78 @@ class MindMapVisualization { return true; } + + // Lädt gemerkete Knoten aus dem LocalStorage + loadBookmarkedNodes() { + try { + const bookmarked = localStorage.getItem('bookmarkedNodes'); + return bookmarked ? JSON.parse(bookmarked) : []; + } catch (error) { + console.error('Fehler beim Laden der gemerkten Knoten:', error); + return []; + } + } + + // Speichert gemerkete Knoten im LocalStorage + saveBookmarkedNodes() { + try { + localStorage.setItem('bookmarkedNodes', JSON.stringify(this.bookmarkedNodes)); + } catch (error) { + console.error('Fehler beim Speichern der gemerkten Knoten:', error); + } + } + + // Prüft, ob ein Knoten gemerkt ist + isNodeBookmarked(nodeId) { + return this.bookmarkedNodes.includes(nodeId); + } + + // Merkt einen Knoten oder hebt die Markierung auf + toggleBookmark(nodeId) { + const index = this.bookmarkedNodes.indexOf(nodeId); + if (index === -1) { + // Node hinzufügen + this.bookmarkedNodes.push(nodeId); + this.updateNodeAppearance(nodeId, true); + } else { + // Node entfernen + this.bookmarkedNodes.splice(index, 1); + this.updateNodeAppearance(nodeId, false); + } + + // Änderungen speichern + this.saveBookmarkedNodes(); + + // Event auslösen für andere Komponenten + const event = new CustomEvent('nodeBookmarkToggled', { + detail: { + nodeId: nodeId, + isBookmarked: index === -1 + } + }); + document.dispatchEvent(event); + + return index === -1; // true wenn jetzt gemerkt, false wenn Markierung aufgehoben + } + + // Aktualisiert das Aussehen eines Knotens basierend auf Bookmark-Status + updateNodeAppearance(nodeId, isBookmarked) { + this.g.selectAll('.node-group') + .filter(d => d.id === nodeId) + .select('.node') + .classed('bookmarked', isBookmarked) + .attr('stroke', isBookmarked ? '#FFD700' : '#ffffff50') + .attr('stroke-width', isBookmarked ? 3 : 2); + } + + // Aktualisiert das Aussehen aller gemerkten Knoten + updateAllBookmarkedNodes() { + this.g.selectAll('.node-group') + .each((d) => { + const isBookmarked = this.isNodeBookmarked(d.id); + this.updateNodeAppearance(d.id, isBookmarked); + }); + } } // Exportiere die Klasse für die Verwendung in anderen Modulen diff --git a/website/templates/base.html b/website/templates/base.html index 7c552ab..4ca2b23 100644 --- a/website/templates/base.html +++ b/website/templates/base.html @@ -601,7 +601,7 @@ - + {% if current_user.is_authenticated %}