Update README and enhance application functionality: Add detailed installation instructions, integrate OpenAI GPT for the AI assistant, implement error handling for various HTTP errors, and improve the admin interface with user management features. Refactor mindmap visualization and enhance UI with modern design elements.

This commit is contained in:
2025-04-25 00:30:04 +02:00
parent b0db3398f2
commit 84b492d8d2
28 changed files with 6495 additions and 1776 deletions

View File

@@ -1 +1,67 @@
# MindMap Wissensnetzwerk
Eine interaktive Plattform zum Visualisieren, Erforschen und Teilen von Wissen mit integriertem ChatGPT-Assistenten.
## Features
- Interaktive Mindmap zur Visualisierung von Wissensverbindungen
- Gedanken mit verschiedenen Beziehungstypen verknüpfen
- Suchfunktion für Gedanken und Verbindungen
- Bewertungssystem für Gedanken
- Dark/Light Mode
- **Integrierter KI-Assistent** mit OpenAI GPT-Integration
## Installation
1. Repository klonen:
```
git clone <repository-url>
cd website
```
2. Python-Abhängigkeiten installieren:
```
pip install -r requirements.txt
```
3. Environment-Variablen konfigurieren:
```
cp example.env .env
```
Bearbeite die `.env`-Datei und füge deinen OpenAI API-Schlüssel ein.
4. Datenbank initialisieren:
```
python init_db.py
```
5. Anwendung starten:
```
python run.py
```
## Verwendung des KI-Assistenten
Der KI-Assistent ist über folgende Wege zugänglich:
1. **Schwebende Schaltfläche**: In der unteren rechten Ecke der Webseite ist eine Roboter-Schaltfläche, die den Assistenten öffnet.
2. **Navigation**: In der Hauptnavigation gibt es ebenfalls eine Schaltfläche mit Roboter-Symbol.
3. **Startseite**: Im "KI-Assistent"-Abschnitt auf der Startseite gibt es einen "KI-Chat starten"-Button.
Der Assistent kann bei folgenden Aufgaben helfen:
- Erklärung von Themen und Konzepten
- Suche nach Verbindungen zwischen Gedanken
- Beantwortung von Fragen zur Plattform
- Vorschläge für neue Gedankenverbindungen
## Technologie-Stack
- **Backend**: Flask, SQLAlchemy
- **Frontend**: HTML, CSS, JavaScript, Tailwind CSS, Alpine.js
- **KI**: OpenAI GPT API
- **Datenbank**: SQLite (Standard), kann auf andere Datenbanken umgestellt werden
## Konfiguration
Die Anwendung kann über Umgebungsvariablen konfiguriert werden. Siehe `example.env` für verfügbare Optionen.

1
website/.env Normal file
View File

@@ -0,0 +1 @@
OPENAI_API_KEY=sk-placeholder

View File

@@ -1,3 +1,6 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os import os
from datetime import datetime, timedelta from datetime import datetime, timedelta
from flask import Flask, render_template, request, redirect, url_for, flash, jsonify, session from flask import Flask, render_template, request, redirect, url_for, flash, jsonify, session
@@ -12,6 +15,11 @@ from wtforms.validators import DataRequired, Email, Length, EqualTo, ValidationE
from functools import wraps from functools import wraps
import secrets import secrets
from sqlalchemy.sql import func from sqlalchemy.sql import func
import openai
from dotenv import load_dotenv
# Lade .env-Datei
load_dotenv()
app = Flask(__name__) app = Flask(__name__)
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'default-dev-key') app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'default-dev-key')
@@ -19,6 +27,9 @@ app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///mindmap.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=365) # Langlebige Session für Dark Mode-Einstellung app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=365) # Langlebige Session für Dark Mode-Einstellung
# OpenAI API-Konfiguration
openai.api_key = os.environ.get('OPENAI_API_KEY')
# Context processor für globale Template-Variablen # Context processor für globale Template-Variablen
@app.context_processor @app.context_processor
def inject_globals(): def inject_globals():
@@ -36,6 +47,17 @@ db = SQLAlchemy(app)
login_manager = LoginManager(app) login_manager = LoginManager(app)
login_manager.login_view = 'login' login_manager.login_view = 'login'
# Benutzerdefinierter Decorator für Admin-Zugriff
def admin_required(f):
@wraps(f)
@login_required
def decorated_function(*args, **kwargs):
if not current_user.is_admin:
flash('Zugriff verweigert. Nur Administratoren dürfen diese Seite aufrufen.', 'error')
return redirect(url_for('index'))
return f(*args, **kwargs)
return decorated_function
class RelationType(Enum): class RelationType(Enum):
SUPPORTS = "stützt" SUPPORTS = "stützt"
CONTRADICTS = "widerspricht" CONTRADICTS = "widerspricht"
@@ -197,7 +219,7 @@ def mindmap():
@login_required @login_required
def profile(): def profile():
thoughts = Thought.query.filter_by(user_id=current_user.id).order_by(Thought.timestamp.desc()).all() thoughts = Thought.query.filter_by(user_id=current_user.id).order_by(Thought.timestamp.desc()).all()
return render_template('profile.html', thoughts=thoughts) return render_template('profile.html', thoughts=thoughts, user=current_user)
# Route für Benutzereinstellungen # Route für Benutzereinstellungen
@app.route('/settings', methods=['GET', 'POST']) @app.route('/settings', methods=['GET', 'POST'])
@@ -468,17 +490,56 @@ def add_comment():
# Admin routes # Admin routes
@app.route('/admin') @app.route('/admin')
@login_required @admin_required
def admin(): def admin():
if not current_user.is_admin:
flash('Zugriff verweigert')
return redirect(url_for('index'))
users = User.query.all() users = User.query.all()
nodes = MindMapNode.query.all() nodes = MindMapNode.query.all()
thoughts = Thought.query.all() thoughts = Thought.query.all()
return render_template('admin.html', users=users, nodes=nodes, thoughts=thoughts) # Aktuelles Datum für Logs
now = datetime.now()
return render_template('admin.html', users=users, nodes=nodes, thoughts=thoughts, now=now, active_tab='users')
# Zusätzliche Route für die Admin-Dashboard-Seite
@app.route('/admin/dashboard')
@admin_required
def admin_dashboard():
users = User.query.all()
nodes = MindMapNode.query.all()
thoughts = Thought.query.all()
now = datetime.now()
return render_template('admin.html', users=users, nodes=nodes, thoughts=thoughts, now=now, active_tab='dashboard')
# Zusätzliche Route für die Admin-Benutzer-Seite
@app.route('/admin/users')
@admin_required
def admin_users():
users = User.query.all()
nodes = MindMapNode.query.all()
thoughts = Thought.query.all()
now = datetime.now()
return render_template('admin.html', users=users, nodes=nodes, thoughts=thoughts, now=now, active_tab='users')
# Zusätzliche Route für die Admin-Gedanken-Seite
@app.route('/admin/thoughts')
@admin_required
def admin_thoughts():
users = User.query.all()
nodes = MindMapNode.query.all()
thoughts = Thought.query.all()
now = datetime.now()
return render_template('admin.html', users=users, nodes=nodes, thoughts=thoughts, now=now, active_tab='thoughts')
# Zusätzliche Route für die Admin-Mindmap-Seite
@app.route('/admin/mindmap')
@admin_required
def admin_mindmap():
users = User.query.all()
nodes = MindMapNode.query.all()
thoughts = Thought.query.all()
now = datetime.now()
return render_template('admin.html', users=users, nodes=nodes, thoughts=thoughts, now=now, active_tab='nodes')
@app.route('/api/thoughts/<int:thought_id>/relations', methods=['GET']) @app.route('/api/thoughts/<int:thought_id>/relations', methods=['GET'])
def get_thought_relations(thought_id): def get_thought_relations(thought_id):
@@ -695,6 +756,72 @@ def get_dark_mode():
app.logger.error(f"Fehler beim Abrufen des Dark Mode: {str(e)}") app.logger.error(f"Fehler beim Abrufen des Dark Mode: {str(e)}")
return jsonify({'success': False, 'error': str(e)}) return jsonify({'success': False, 'error': str(e)})
# Fehlerseiten-Handler
@app.errorhandler(404)
def page_not_found(e):
"""Handler für 404 Fehler - Seite nicht gefunden."""
return render_template('errors/404.html'), 404
@app.errorhandler(403)
def forbidden(e):
"""Handler für 403 Fehler - Zugriff verweigert."""
return render_template('errors/403.html'), 403
@app.errorhandler(500)
def internal_server_error(e):
"""Handler für 500 Fehler - Interner Serverfehler."""
app.logger.error(f"500 Fehler: {str(e)}")
return render_template('errors/500.html'), 500
@app.errorhandler(429)
def too_many_requests(e):
"""Handler für 429 Fehler - Zu viele Anfragen."""
return render_template('errors/429.html'), 429
# Route für den KI-Assistenten API-Endpunkt
@app.route('/api/assistant', methods=['POST'])
def chat_with_assistant():
try:
# Daten aus der Anfrage extrahieren
data = request.json
messages = data.get('messages', [])
# Formatiere die Nachrichten für die OpenAI API
formatted_messages = []
for message in messages:
role = message['role']
if role == 'user':
formatted_messages.append({"role": "user", "content": message['content']})
elif role == 'assistant':
formatted_messages.append({"role": "assistant", "content": message['content']})
# Standard-Systemnachricht hinzufügen
formatted_messages.insert(0, {
"role": "system",
"content": "Du bist ein hilfreicher Assistent namens 'MindMap KI', der Benutzer bei ihren Fragen " +
"rund um Wissen, Lernen und dem Finden von Verbindungen zwischen Ideen unterstützt. " +
"Sei präzise, freundlich und hilfsbereit. Versuche, deine Antworten prägnant zu halten, " +
"aber biete dennoch wertvolle Informationen. Wenn du eine Frage nicht beantworten kannst, " +
"sag es ehrlich. Antworte auf Deutsch."
})
# Anfrage an die OpenAI API senden
response = openai.chat.completions.create(
model="gpt-3.5-turbo",
messages=formatted_messages,
max_tokens=500,
temperature=0.7,
)
# Antwort extrahieren
assistant_reply = response.choices[0].message.content
return jsonify({"success": True, "response": assistant_reply})
except Exception as e:
# Log-Fehler für die Serverkonsole
print(f"Fehler bei der KI-Anfrage: {str(e)}")
return jsonify({"success": False, "error": "Fehler bei der Verarbeitung der Anfrage"}), 500
# Flask starten # Flask starten
if __name__ == '__main__': if __name__ == '__main__':
with app.app_context(): with app.app_context():

12
website/example.env Normal file
View File

@@ -0,0 +1,12 @@
# MindMap Umgebungsvariablen
# Kopiere diese Datei zu .env und passe die Werte an
# Flask
SECRET_KEY=dein-geheimer-schluessel-hier
# OpenAI API
OPENAI_API_KEY=sk-dein-openai-api-schluessel-hier
# Datenbank
# Bei Bedarf kann hier eine andere Datenbank-URL angegeben werden
# SQLALCHEMY_DATABASE_URI=sqlite:///mindmap.db

View File

@@ -1,3 +1,6 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from app import app, db, User, Thought, Comment, MindMapNode, ThoughtRelation, ThoughtRating, RelationType from app import app, db, User, Thought, Comment, MindMapNode, ThoughtRelation, ThoughtRating, RelationType
import os import os

Binary file not shown.

View File

@@ -0,0 +1,104 @@
/* ChatGPT Assistent Styles */
#chatgpt-assistant {
font-family: 'Inter', sans-serif;
}
#assistant-chat {
transition: max-height 0.3s ease, opacity 0.3s ease;
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.2);
border-radius: 0.75rem;
overflow: hidden;
}
#assistant-toggle {
transition: transform 0.3s ease;
}
#assistant-toggle:hover {
transform: scale(1.1);
}
#assistant-history {
scrollbar-width: thin;
scrollbar-color: rgba(156, 163, 175, 0.5) transparent;
}
#assistant-history::-webkit-scrollbar {
width: 6px;
}
#assistant-history::-webkit-scrollbar-track {
background: transparent;
}
#assistant-history::-webkit-scrollbar-thumb {
background-color: rgba(156, 163, 175, 0.5);
border-radius: 3px;
}
.dark #assistant-history::-webkit-scrollbar-thumb {
background-color: rgba(156, 163, 175, 0.3);
}
/* Mach Platz für Notifications, damit sie nicht mit dem Assistenten überlappen */
.notification-area {
bottom: 5rem;
}
/* Verbesserter Glassmorphism-Effekt */
.glass-morphism {
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.15);
border: 1px solid rgba(255, 255, 255, 0.18);
}
.dark .glass-morphism {
background: rgba(15, 23, 42, 0.3);
border: 1px solid rgba(255, 255, 255, 0.05);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.4);
}
/* Dunkleres Dark Theme */
.dark {
--tw-bg-opacity: 1;
background-color: rgba(10, 15, 25, var(--tw-bg-opacity)) !important;
min-height: 100vh;
}
.dark .bg-dark-900 {
--tw-bg-opacity: 1;
background-color: rgba(10, 15, 25, var(--tw-bg-opacity)) !important;
}
.dark .bg-dark-800 {
--tw-bg-opacity: 1;
background-color: rgba(15, 23, 42, var(--tw-bg-opacity)) !important;
}
.dark .bg-dark-700 {
--tw-bg-opacity: 1;
background-color: rgba(23, 33, 64, var(--tw-bg-opacity)) !important;
}
/* Footer immer unten */
html, body {
height: 100%;
margin: 0;
min-height: 100vh;
}
body {
display: flex;
flex-direction: column;
min-height: 100vh;
}
main {
flex: 1 0 auto;
}
footer {
flex-shrink: 0;
}

File diff suppressed because it is too large Load Diff

View File

@@ -37,6 +37,14 @@
/* Abstände */ /* Abstände */
--standard-spacing: 2rem; --standard-spacing: 2rem;
--small-spacing: 1rem; --small-spacing: 1rem;
/* High contrast colors for elements - Intensivere Farben für besseren Kontrast */
--primary-bright: #9a7dff;
--secondary-bright: #bb82ff;
--accent-bright: #50eaff;
--success-bright: #50ffe4;
--warning-bright: #ffe050;
--danger-bright: #ff5050;
} }
/* Basiselemente */ /* Basiselemente */
@@ -44,7 +52,7 @@ body {
min-height: 100vh; min-height: 100vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background: linear-gradient(135deg, var(--background-start), var(--background-end)); background: rgb(24 24 28 / var(--tw-bg-opacity, 1));
color: var(--light-color); color: var(--light-color);
font-family: var(--body-font); font-family: var(--body-font);
line-height: 1.6; line-height: 1.6;
@@ -73,22 +81,23 @@ p {
a { a {
text-decoration: none; text-decoration: none;
color: var(--accent-color); color: var(--accent-bright);
transition: all 0.3s ease; transition: all 0.3s ease;
} }
a:hover { a:hover {
color: var(--primary-color); color: var(--primary-bright);
text-shadow: 0 0 8px rgba(154, 125, 255, 0.5);
} }
/* Neumorphische und Glasmorphismus Elemente */ /* Verbesserte Glasmorphismus und Neumorphismus Elemente */
.glass { .glass {
background: var(--glass-bg); background: var(--glass-bg);
backdrop-filter: var(--glass-blur); backdrop-filter: var(--glass-blur);
-webkit-backdrop-filter: var(--glass-blur); -webkit-backdrop-filter: var(--glass-blur);
border: 1px solid var(--glass-border); border: 1px solid var(--glass-border);
border-radius: 16px; border-radius: 16px;
box-shadow: var(--glass-card-shadow); box-shadow: var(--glass-card-shadow), 0 0 15px rgba(108, 93, 211, 0.2);
margin-bottom: var(--standard-spacing); margin-bottom: var(--standard-spacing);
padding: var(--standard-spacing); padding: var(--standard-spacing);
transition: transform 0.3s ease, box-shadow 0.3s ease; transition: transform 0.3s ease, box-shadow 0.3s ease;
@@ -96,7 +105,8 @@ a:hover {
.glass:hover { .glass:hover {
transform: translateY(-5px); transform: translateY(-5px);
box-shadow: 0 15px 30px rgba(0, 0, 0, 0.4); box-shadow: 0 15px 30px rgba(0, 0, 0, 0.4), 0 0 20px rgba(154, 125, 255, 0.3);
border: 1px solid rgba(154, 125, 255, 0.3);
} }
.neumorph { .neumorph {
@@ -109,7 +119,7 @@ a:hover {
} }
.neumorph:hover { .neumorph:hover {
box-shadow: var(--shadow-light), var(--shadow-dark); box-shadow: var(--shadow-light), var(--shadow-dark), 0 0 20px rgba(154, 125, 255, 0.2);
} }
.neumorph-inset { .neumorph-inset {
@@ -121,81 +131,168 @@ a:hover {
/* Stilvolle Farbverläufe für Akzente */ /* Stilvolle Farbverläufe für Akzente */
.gradient-text { .gradient-text {
background: linear-gradient(135deg, var(--primary-color), var(--accent-color)); background: linear-gradient(135deg, var(--primary-bright), var(--accent-bright));
-webkit-background-clip: text; -webkit-background-clip: text;
background-clip: text; background-clip: text;
color: transparent; color: transparent;
text-shadow: 0 0 10px rgba(154, 125, 255, 0.3);
} }
.gradient-bg { .gradient-bg {
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); background: linear-gradient(135deg, var(--primary-bright), var(--secondary-bright));
} }
/* Navbar Design */ /* Navbar Design - Überarbeitet mit verbessertem Glassmorphismus und Responsivität */
.navbar { .navbar {
background: rgba(20, 20, 43, 0.8) !important; background: rgba(20, 20, 43, 0.85); /* Etwas weniger transparent */
backdrop-filter: var(--glass-blur); backdrop-filter: blur(15px); /* Stärkerer Blur */
-webkit-backdrop-filter: var(--glass-blur); -webkit-backdrop-filter: blur(15px);
box-shadow: 0 4px 15px -1px rgba(0, 0, 0, 0.5); box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4); /* Stärkerer Schatten */
border-bottom: 1px solid var(--glass-border); border-bottom: 1px solid rgba(255, 255, 255, 0.15); /* Hellerer Border */
padding: 1rem 0; padding: 0.8rem 0; /* Etwas weniger Padding */
transition: all 0.3s ease;
} }
.navbar-brand { .navbar-brand {
font-family: var(--serif-font); font-family: 'Inter', sans-serif; /* Konsistente Schriftart */
font-weight: 700; font-weight: 800; /* Stärkerer Font */
font-size: 1.5rem; font-size: 1.8rem; /* Größerer Font */
color: var(--light-color) !important; color: white; /* Standardfarbe Weiß */
letter-spacing: 0.05em; letter-spacing: -0.03em; /* Engerer Buchstabenabstand */
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); /* Textschatten */
} }
.navbar-brand i { .navbar-brand i {
margin-right: 0.5rem; margin-right: 0.5rem;
background: linear-gradient(135deg, var(--primary-color), var(--accent-color)); background: linear-gradient(135deg, var(--primary-bright), var(--accent-bright)); /* Hellerer Gradient */
-webkit-background-clip: text; -webkit-background-clip: text;
background-clip: text; background-clip: text;
color: transparent; color: transparent;
filter: drop-shadow(0 0 8px rgba(154, 125, 255, 0.6)); /* Stärkerer Schatten */
} }
.navbar-dark .navbar-nav .nav-link { .navbar-nav .nav-link {
color: rgba(255, 255, 255, 0.8); color: rgba(255, 255, 255, 1); /* Vollständig weißer Text für bessere Lesbarkeit */
font-weight: 500; font-weight: 600; /* Fetterer Text */
padding: 0.5rem 1rem; padding: 0.6rem 1.2rem; /* Angepasstes Padding */
border-radius: 10px; border-radius: 12px; /* Größerer Border-Radius */
transition: all 0.3s ease; transition: all 0.3s ease;
margin: 0 0.2rem; margin: 0 0.3rem; /* Angepasster Margin */
backdrop-filter: blur(10px); /* Leichter Blur für Links */
-webkit-backdrop-filter: blur(10px);
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5); /* Textschatten für bessere Lesbarkeit */
font-size: 1.05rem; /* Etwas größere Schrift */
} }
.navbar-dark .navbar-nav .nav-link:hover, .navbar-nav .nav-link:hover,
.navbar-dark .navbar-nav .nav-link.active { .navbar-nav .nav-link.active {
color: var(--accent-color); color: var(--accent-bright); /* Hellerer Akzent */
background: rgba(108, 93, 211, 0.1); background: rgba(154, 125, 255, 0.15); /* Hellerer Hintergrund bei Hover/Active */
box-shadow: var(--inner-shadow); box-shadow: 0 0 12px rgba(154, 125, 255, 0.2); /* Leichter Schatten */
transform: translateY(-2px); /* Leichter Hover-Effekt */
} }
.navbar-dark .navbar-nav .nav-link i { .navbar-nav .nav-link i {
margin-right: 0.5rem; margin-right: 0.4rem; /* Angepasster Margin */
transition: transform 0.3s ease; transition: transform 0.3s ease;
} }
.navbar-dark .navbar-nav .nav-link:hover i { .navbar-nav .nav-link:hover i {
transform: translateY(-2px); transform: translateY(-2px);
color: var(--accent-color); color: var(--accent-bright);
}
/* Dark Mode Anpassungen für Navbar */
.dark .navbar {
background: rgba(14, 18, 32, 0.9); /* Dunklerer Hintergrund */
border-bottom-color: rgba(255, 255, 255, 0.08); /* Weniger sichtbarer Border */
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.5);
}
.dark .navbar-brand {
color: white;
}
.dark .navbar-nav .nav-link {
color: rgba(255, 255, 255, 0.8);
}
.dark .navbar-nav .nav-link:hover,
.dark .navbar-nav .nav-link.active {
color: var(--accent-bright);
background: rgba(154, 125, 255, 0.1);
box-shadow: 0 0 10px rgba(154, 125, 255, 0.15);
}
/* Light Mode Anpassungen für Navbar */
.light .navbar {
background: rgba(240, 244, 248, 0.9); /* Heller Hintergrund */
border-bottom-color: rgba(0, 0, 0, 0.1); /* Dunklerer Border */
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
}
.light .navbar-brand {
color: #1a202c; /* Dunkler Text */
}
.light .navbar-brand i {
background: linear-gradient(135deg, var(--primary-color), var(--accent-color)); /* Original Gradient */
-webkit-background-clip: text;
background-clip: text;
color: transparent;
filter: none; /* Kein Schatten */
}
.light .navbar-nav .nav-link {
color: #4a5568; /* Dunklerer Text */
backdrop-filter: none; /* Kein Blur */
-webkit-backdrop-filter: none;
}
.light .navbar-nav .nav-link:hover,
.light .navbar-nav .nav-link.active {
color: var(--primary-color); /* Primärfarbe */
background: rgba(108, 93, 211, 0.08); /* Leichter Hintergrund */
box-shadow: none;
}
.light .navbar-nav .nav-link:hover i {
color: var(--primary-color);
}
/* Responsivität für Navbar */
@media (max-width: 768px) {
.navbar {
padding: 0.6rem 0;
}
.navbar-brand {
font-size: 1.4rem;
}
.navbar-nav .nav-link {
padding: 0.5rem 1rem;
margin: 0.2rem 0;
text-align: center;
justify-content: center;
}
} }
/* Buttons */ /* Buttons */
.btn { .btn {
padding: 0.6rem 1.5rem; padding: 0.7rem 1.6rem;
border-radius: 12px; border-radius: 12px;
font-weight: 600; font-weight: 700; /* Fetterer Text */
transition: all 0.3s ease; transition: all 0.3s ease;
border: none; border: none;
letter-spacing: 0.05em; letter-spacing: 0.05em;
text-transform: uppercase; text-transform: uppercase;
font-size: 0.85rem; font-size: 0.9rem; /* Größere Schrift */
position: relative; position: relative;
overflow: hidden; overflow: hidden;
z-index: 1; z-index: 1;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.6); /* Stärkerer Textschatten für bessere Lesbarkeit */
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); /* Stärkerer Schatten für bessere Sichtbarkeit */
} }
.btn::before { .btn::before {
@@ -215,9 +312,11 @@ a:hover {
} }
.btn-primary { .btn-primary {
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); background: var(--light-color);
color: white; color: #000 !important;
box-shadow: 0 4px 15px rgba(108, 93, 211, 0.4); box-shadow: 0 4px 15px rgba(108, 93, 211, 0.6);
font-weight: 700;
border: 2px solid rgba(255, 255, 255, 0.2); /* Sichtbarer Rand */
} }
.btn-primary:hover { .btn-primary:hover {
@@ -238,69 +337,371 @@ a:hover {
border-color: var(--accent-color); border-color: var(--accent-color);
} }
/* Karten-Design */ /* Verbesserte Card-Komponenten mit Glassmorphismus */
.card { .card {
background: var(--glass-bg); background: rgba(30, 30, 46, 0.7);
backdrop-filter: var(--glass-blur); backdrop-filter: blur(10px);
-webkit-backdrop-filter: var(--glass-blur); -webkit-backdrop-filter: blur(10px);
border: 1px solid var(--glass-border);
border-radius: 16px; border-radius: 16px;
box-shadow: var(--glass-card-shadow); border: 1px solid rgba(72, 71, 138, 0.2);
transition: transform 0.3s ease, box-shadow 0.3s ease; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
margin-bottom: 1.5rem;
position: relative;
overflow: hidden; overflow: hidden;
margin-bottom: var(--standard-spacing); transition: all 0.3s ease;
}
.card::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(
135deg,
rgba(255, 255, 255, 0.1) 0%,
rgba(255, 255, 255, 0.05) 20%,
rgba(0, 0, 0, 0) 80%
);
z-index: 0;
pointer-events: none;
} }
.card:hover { .card:hover {
transform: translateY(-5px); transform: translateY(-5px);
box-shadow: 0 15px 30px rgba(0, 0, 0, 0.4); box-shadow: 0 15px 30px rgba(0, 0, 0, 0.4), 0 0 20px rgba(154, 125, 255, 0.2);
border-color: rgba(154, 125, 255, 0.3);
}
.card:hover::before {
opacity: 0.7;
} }
.card-header { .card-header {
background: rgba(20, 20, 43, 0.7); background: rgba(20, 20, 40, 0.5);
border-bottom: 1px solid var(--glass-border); border-bottom: 1px solid rgba(72, 71, 138, 0.2);
padding: 1.25rem 1.5rem; padding: 1rem 1.5rem;
font-family: var(--serif-font); position: relative;
border-radius: 16px 16px 0 0;
font-weight: 600;
}
.card-header::after {
content: '';
position: absolute;
left: 0;
bottom: 0;
height: 1px;
width: 100%;
background: linear-gradient(
to right,
rgba(154, 125, 255, 0.2),
rgba(76, 223, 255, 0.2)
);
} }
.card-header h5 { .card-header h5 {
margin-bottom: 0; margin: 0;
color: var(--primary-color); font-size: 1.25rem;
} font-weight: 600;
color: var(--light-color);
.card-body {
padding: 1.5rem;
}
.card-footer {
background: rgba(0, 0, 0, 0.3);
border-top: 1px solid var(--glass-border);
padding: 1rem 1.5rem;
}
/* Gedanken-Karten */
.thought-card {
height: 100%;
display: flex; display: flex;
flex-direction: column;
border-left: 3px solid var(--primary-color);
}
.thought-card .card-header {
display: flex;
justify-content: space-between;
align-items: center; align-items: center;
} }
.card-body {
position: relative;
z-index: 1;
padding: 1.5rem;
color: rgba(233, 233, 240, 0.9);
}
.card-footer {
background: rgba(20, 20, 40, 0.5);
border-top: 1px solid rgba(72, 71, 138, 0.2);
padding: 1rem 1.5rem;
position: relative;
border-radius: 0 0 16px 16px;
color: rgba(233, 233, 240, 0.7);
}
/* Feature cards auf der Homepage - Verbessertes Design */
.feature-card {
background: rgba(30, 30, 46, 0.5);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-radius: 20px;
border: 1px solid rgba(72, 71, 138, 0.2);
padding: 2rem;
text-align: center;
transition: all 0.3s ease;
height: 100%;
position: relative;
overflow: hidden;
}
.feature-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(
135deg,
rgba(255, 255, 255, 0.05) 0%,
rgba(255, 255, 255, 0.02) 20%,
rgba(0, 0, 0, 0) 80%
);
z-index: 0;
pointer-events: none;
}
.feature-card:hover {
transform: translateY(-8px);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3), 0 0 25px rgba(154, 125, 255, 0.2);
border-color: rgba(154, 125, 255, 0.3);
}
.feature-card .icon {
font-size: 3rem;
margin-bottom: 1.5rem;
display: inline-block;
color: var(--primary-bright);
text-shadow: 0 0 15px rgba(154, 125, 255, 0.5);
}
.feature-card h3 {
font-size: 1.5rem;
margin-bottom: 1rem;
position: relative;
z-index: 1;
color: white;
}
.feature-card p {
color: rgba(233, 233, 240, 0.9);
font-size: 1rem;
line-height: 1.6;
position: relative;
z-index: 1;
}
/* Glass-effect für UI-Komponenten */
.glass-effect {
background: rgba(30, 30, 46, 0.5);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-radius: 16px;
border: 1px solid rgba(72, 71, 138, 0.2);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
}
/* Gradient-Hintergrund, der über gesamte Seite geht */
.full-page-bg {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: var(--dark-color);
z-index: -10;
}
.full-page-bg::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: radial-gradient(
circle at top right,
rgba(118, 69, 217, 0.1),
transparent 40%
),
radial-gradient(
circle at bottom left,
rgba(76, 223, 255, 0.05),
transparent 40%
);
z-index: -5;
}
/* Überarbeitungen für benutzerdefinierte Stile */
.btn-primary {
border-radius: 12px !important;
font-weight: 600 !important;
}
.btn-outline {
border-radius: 12px !important;
border: 1px solid rgba(76, 223, 255, 0.5) !important;
background: transparent !important;
color: var(--accent-bright) !important;
transition: all 0.3s ease !important;
}
.btn-outline:hover {
background: rgba(76, 223, 255, 0.1) !important;
border-color: var(--accent-bright) !important;
color: white !important;
transform: translateY(-2px) !important;
}
/* Bessere Lesbarkeit für Buttons */
button, .btn, a.btn {
font-weight: 700 !important;
letter-spacing: 0.03em !important;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.8) !important;
font-size: 1rem !important;
}
/* Farbschema für Dark/Light Mode strikt trennen */
.dark body {
background: var(--dark-color);
color: white;
}
.light body {
background: #f8f9fa;
color: #333;
}
.light .card {
background: rgba(255, 255, 255, 0.7);
border: 1px solid rgba(230, 230, 250, 0.3);
color: #333;
}
.light .glass-effect {
background: rgba(255, 255, 255, 0.7);
border: 1px solid rgba(230, 230, 250, 0.3);
}
.light .feature-card {
background: rgba(255, 255, 255, 0.7);
border: 1px solid rgba(230, 230, 250, 0.3);
}
.light .feature-card h3 {
color: #333;
}
.light .feature-card p {
color: #555;
}
.light .card-header {
background: rgba(245, 245, 255, 0.7);
border-bottom: 1px solid rgba(230, 230, 250, 0.5);
}
.light .card-footer {
background: rgba(245, 245, 255, 0.7);
border-top: 1px solid rgba(230, 230, 250, 0.5);
}
.light .full-page-bg {
background: #f0f2f5;
}
.light .full-page-bg::before {
background: radial-gradient(
circle at top right,
rgba(118, 69, 217, 0.05),
transparent 40%
),
radial-gradient(
circle at bottom left,
rgba(76, 223, 255, 0.03),
transparent 40%
);
}
/* Verbesserter Kontrast für Text in Light-Mode */
.light h1, .light h2, .light h3, .light h4, .light h5, .light h6 {
color: #222;
}
.light p, .light span, .light div {
color: #444;
}
.light a {
color: var(--primary-color);
}
.light a:hover {
color: var(--primary-hover);
}
/* Spezielle Anpassungen für kleinere Bildschirme */
@media (max-width: 768px) {
.card, .feature-card, .glass-effect {
border-radius: 14px;
}
.card-header {
border-radius: 14px 14px 0 0;
}
.card-footer {
border-radius: 0 0 14px 14px;
}
}
@media (max-width: 576px) {
.card, .feature-card, .glass-effect {
border-radius: 12px;
}
.card-header {
border-radius: 12px 12px 0 0;
}
.card-footer {
border-radius: 0 0 12px 12px;
}
}
/* Gemeinsame Stile für alle Modi */
.thought-card {
background: rgba(26, 26, 46, 0.7);
border-radius: 16px;
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(80, 80, 160, 0.15);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
margin-bottom: 1.5rem;
overflow: hidden;
transition: all 0.3s ease;
}
.thought-card:hover {
transform: translateY(-5px);
border-color: rgba(154, 125, 255, 0.3);
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.4), 0 0 20px rgba(154, 125, 255, 0.15);
}
.thought-card .card-header {
background: rgba(20, 20, 40, 0.7);
padding: 1rem 1.25rem;
}
.thought-card .card-body { .thought-card .card-body {
flex: 1; padding: 1.25rem;
} }
.thought-card .metadata { .thought-card .metadata {
font-size: 0.9rem; display: flex;
color: rgba(255, 255, 255, 0.6); flex-wrap: wrap;
margin-bottom: 1rem; align-items: center;
font-style: italic; font-size: 0.85rem;
color: var(--gray-color);
margin-top: 0.5rem;
gap: 1rem;
} }
.thought-card .keywords { .thought-card .keywords {
@@ -310,56 +711,81 @@ a:hover {
margin-top: 1rem; margin-top: 1rem;
} }
/* Keywords & Tags */
.keyword-tag { .keyword-tag {
background: rgba(108, 93, 211, 0.2); display: inline-flex;
color: var(--accent-color); align-items: center;
padding: 0.25rem 0.75rem; padding: 0.25rem 0.75rem;
border-radius: 2rem; border-radius: 2rem;
font-size: 0.8rem; font-size: 0.75rem;
backdrop-filter: blur(5px); font-weight: 600;
border: 1px solid rgba(108, 93, 211, 0.4); background: linear-gradient(120deg, rgba(154, 125, 255, 0.2), rgba(80, 234, 255, 0.2));
border: 1px solid rgba(154, 125, 255, 0.2);
color: var(--primary-bright);
transition: all 0.2s ease;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 150px;
} }
/* Beziehungs-Badges */ .keyword-tag:hover {
background: linear-gradient(120deg, rgba(154, 125, 255, 0.3), rgba(80, 234, 255, 0.3));
border-color: rgba(154, 125, 255, 0.4);
color: var(--accent-bright);
}
/* Verbesserte Relation-Badges */
.relation-badge { .relation-badge {
padding: 0.4rem 0.8rem; display: inline-flex;
border-radius: 10px; align-items: center;
font-size: 0.85rem; padding: 0.3rem 0.75rem;
border-radius: 2rem;
font-size: 0.75rem;
font-weight: 600; font-weight: 600;
margin: 0.25rem; margin-right: 0.5rem;
display: inline-block; margin-bottom: 0.5rem;
letter-spacing: 0.05em; transition: all 0.2s ease;
backdrop-filter: blur(5px); }
.relation-badge:hover {
transform: translateY(-2px);
} }
.relation-supports { .relation-supports {
background-color: var(--success-color); background: rgba(53, 201, 190, 0.15);
color: #000; color: var(--success-bright);
border: 1px solid rgba(53, 201, 190, 0.3);
} }
.relation-contradicts { .relation-contradicts {
background-color: var(--danger-color); background: rgba(254, 83, 110, 0.15);
color: #fff; color: var(--danger-bright);
border: 1px solid rgba(254, 83, 110, 0.3);
} }
.relation-builds-upon { .relation-builds-upon {
background-color: var(--info-color); background: rgba(62, 127, 255, 0.15);
color: #fff; color: var(--info-color);
border: 1px solid rgba(62, 127, 255, 0.3);
} }
.relation-generalizes { .relation-generalizes {
background-color: var(--warning-color); background: rgba(255, 182, 72, 0.15);
color: #000; color: var(--warning-bright);
border: 1px solid rgba(255, 182, 72, 0.3);
} }
.relation-specifies { .relation-specifies {
background-color: var(--gray-color); background: rgba(154, 125, 255, 0.15);
color: #fff; color: var(--primary-bright);
border: 1px solid rgba(154, 125, 255, 0.3);
} }
.relation-inspires { .relation-inspires {
background-color: var(--accent-color); background: rgba(118, 69, 217, 0.15);
color: #000; color: var(--secondary-bright);
border: 1px solid rgba(118, 69, 217, 0.3);
} }
/* Formulare */ /* Formulare */
@@ -451,16 +877,16 @@ a:hover {
/* Mindmap-Visualisierung */ /* Mindmap-Visualisierung */
.mindmap-container { .mindmap-container {
height: 600px;
width: 100%; width: 100%;
height: 700px;
background: var(--glass-bg);
backdrop-filter: var(--glass-blur);
-webkit-backdrop-filter: var(--glass-blur);
border-radius: 16px; border-radius: 16px;
border: 1px solid var(--glass-border); background: rgba(20, 20, 43, 0.7) !important;
box-shadow: var(--glass-card-shadow); backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(72, 71, 138, 0.2);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
overflow: hidden; overflow: hidden;
margin-bottom: var(--standard-spacing); margin-bottom: 2rem;
} }
/* Filter-Sidebar */ /* Filter-Sidebar */
@@ -721,15 +1147,37 @@ a:hover {
opacity: 0.3; opacity: 0.3;
} }
/* Footer */ /* Footer - Überarbeitet mit verbessertem Glassmorphismus und Responsivität */
footer { footer {
background: rgba(20, 20, 43, 0.8); background: rgba(20, 20, 43, 0.85); /* Etwas weniger transparent */
backdrop-filter: var(--glass-blur); backdrop-filter: blur(15px); /* Stärkerer Blur */
-webkit-backdrop-filter: var(--glass-blur); -webkit-backdrop-filter: blur(15px);
box-shadow: 0 -6px 20px rgba(0, 0, 0, 0.4); /* Schatten nach oben */
border-top: 1px solid rgba(255, 255, 255, 0.15); /* Hellerer Border */
padding: 1.5rem 0; padding: 1.5rem 0;
border-top: 1px solid var(--glass-border); color: rgba(255, 255, 255, 0.9); /* Hellerer Text */
color: rgba(255, 255, 255, 0.8); margin-top: 4rem; /* Konsistenter Abstand */
margin-top: var(--standard-spacing); transition: all 0.3s ease;
}
.dark footer {
background: rgba(14, 18, 32, 0.9); /* Dunklerer Hintergrund */
border-top-color: rgba(255, 255, 255, 0.08); /* Weniger sichtbarer Border */
box-shadow: 0 -6px 20px rgba(0, 0, 0, 0.5);
}
.light footer {
background: rgba(240, 244, 248, 0.9); /* Heller Hintergrund */
border-top-color: rgba(0, 0, 0, 0.1); /* Dunklerer Border */
box-shadow: 0 -4px 15px rgba(0, 0, 0, 0.1);
color: #4a5568; /* Dunklerer Text */
}
/* Responsivität für Footer */
@media (max-width: 768px) {
footer {
padding: 1rem 0;
}
} }
/* Icon-Stilisierung */ /* Icon-Stilisierung */
@@ -852,3 +1300,21 @@ footer {
page-break-inside: avoid; page-break-inside: avoid;
} }
} }
/* Fix for dark background not extending over the entire page */
html, body {
min-height: 100vh;
width: 100%;
margin: 0;
padding: 0;
overflow-x: hidden;
background: linear-gradient(135deg, var(--background-start), var(--background-end));
background-attachment: fixed;
}
/* Sticky navbar */
.navbar.sticky-top {
position: sticky;
top: 0;
z-index: 1000;
}

456
website/static/d3-extensions.js vendored Normal file
View File

@@ -0,0 +1,456 @@
/**
* D3.js Erweiterungen für verbesserte Mindmap-Funktionalität
* Diese Datei enthält zusätzliche Hilfsfunktionen und Erweiterungen für D3.js
*/
class D3Extensions {
/**
* Erstellt einen verbesserten radialen Farbverlauf
* @param {Object} defs - Das D3 defs Element
* @param {string} id - ID für den Gradienten
* @param {string} baseColor - Grundfarbe in hexadezimal oder RGB
* @returns {Object} - Das erstellte Gradient-Element
*/
static createEnhancedRadialGradient(defs, id, baseColor) {
// Farben berechnen
const d3Color = d3.color(baseColor);
const lightColor = d3Color.brighter(0.7);
const darkColor = d3Color.darker(0.3);
const midColor = d3Color;
// Gradient erstellen
const gradient = defs.append('radialGradient')
.attr('id', id)
.attr('cx', '30%')
.attr('cy', '30%')
.attr('r', '70%');
// Farbstops hinzufügen für realistischeren Verlauf
gradient.append('stop')
.attr('offset', '0%')
.attr('stop-color', lightColor.formatHex());
gradient.append('stop')
.attr('offset', '50%')
.attr('stop-color', midColor.formatHex());
gradient.append('stop')
.attr('offset', '100%')
.attr('stop-color', darkColor.formatHex());
return gradient;
}
/**
* Erstellt einen Glüheffekt-Filter
* @param {Object} defs - D3-Referenz auf den defs-Bereich
* @param {String} id - ID des Filters
* @param {String} color - Farbe des Glüheffekts (Hex-Code)
* @param {Number} strength - Stärke des Glüheffekts
* @returns {Object} D3-Referenz auf den erstellten Filter
*/
static createGlowFilter(defs, id, color = '#b38fff', strength = 5) {
const filter = defs.append('filter')
.attr('id', id)
.attr('x', '-50%')
.attr('y', '-50%')
.attr('width', '200%')
.attr('height', '200%');
// Unschärfe-Effekt
filter.append('feGaussianBlur')
.attr('in', 'SourceGraphic')
.attr('stdDeviation', strength)
.attr('result', 'blur');
// Farbverstärkung für den Glüheffekt
filter.append('feColorMatrix')
.attr('in', 'blur')
.attr('type', 'matrix')
.attr('values', '0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 18 -7')
.attr('result', 'glow');
// Farbflut mit der angegebenen Farbe
filter.append('feFlood')
.attr('flood-color', color)
.attr('flood-opacity', '0.7')
.attr('result', 'color');
// Zusammensetzen des Glüheffekts mit der Farbe
filter.append('feComposite')
.attr('in', 'color')
.attr('in2', 'glow')
.attr('operator', 'in')
.attr('result', 'glow-color');
// Zusammenfügen aller Ebenen
const feMerge = filter.append('feMerge');
feMerge.append('feMergeNode')
.attr('in', 'glow-color');
feMerge.append('feMergeNode')
.attr('in', 'SourceGraphic');
return filter;
}
/**
* Berechnet eine konsistente Farbe aus einem String
* @param {string} str - Eingabestring
* @returns {string} - Generierte Farbe als Hex-String
*/
static stringToColor(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash);
}
// Basis-Farbpalette für konsistente Farben
const colorPalette = [
"#4299E1", // Blau
"#9F7AEA", // Lila
"#ED64A6", // Pink
"#48BB78", // Grün
"#ECC94B", // Gelb
"#F56565", // Rot
"#38B2AC", // Türkis
"#ED8936", // Orange
"#667EEA", // Indigo
];
// Farbe aus der Palette wählen basierend auf dem Hash
const colorIndex = Math.abs(hash) % colorPalette.length;
return colorPalette[colorIndex];
}
/**
* Erstellt einen Schatteneffekt-Filter
* @param {Object} defs - D3-Referenz auf den defs-Bereich
* @param {String} id - ID des Filters
* @returns {Object} D3-Referenz auf den erstellten Filter
*/
static createShadowFilter(defs, id) {
const filter = defs.append('filter')
.attr('id', id)
.attr('x', '-50%')
.attr('y', '-50%')
.attr('width', '200%')
.attr('height', '200%');
// Einfacher Schlagschatten
filter.append('feDropShadow')
.attr('dx', 0)
.attr('dy', 4)
.attr('stdDeviation', 4)
.attr('flood-color', 'rgba(0, 0, 0, 0.3)');
return filter;
}
/**
* Erstellt einen Glasmorphismus-Effekt-Filter
* @param {Object} defs - D3-Referenz auf den defs-Bereich
* @param {String} id - ID des Filters
* @returns {Object} D3-Referenz auf den erstellten Filter
*/
static createGlassMorphismFilter(defs, id) {
const filter = defs.append('filter')
.attr('id', id)
.attr('x', '-50%')
.attr('y', '-50%')
.attr('width', '200%')
.attr('height', '200%');
// Hintergrund-Unschärfe für den Glaseffekt
filter.append('feGaussianBlur')
.attr('in', 'SourceGraphic')
.attr('stdDeviation', 8)
.attr('result', 'blur');
// Hellere Farbe für den Glaseffekt
filter.append('feColorMatrix')
.attr('in', 'blur')
.attr('type', 'matrix')
.attr('values', '1 0 0 0 0.1 0 1 0 0 0.1 0 0 1 0 0.1 0 0 0 0.6 0')
.attr('result', 'glass');
// Überlagerung mit dem Original
const feMerge = filter.append('feMerge');
feMerge.append('feMergeNode')
.attr('in', 'glass');
feMerge.append('feMergeNode')
.attr('in', 'SourceGraphic');
return filter;
}
/**
* Erstellt einen verstärkten Glasmorphismus-Effekt mit Farbverlauf
* @param {Object} defs - D3-Referenz auf den defs-Bereich
* @param {String} id - ID des Filters
* @param {String} color1 - Erste Farbe des Verlaufs (Hex-Code)
* @param {String} color2 - Zweite Farbe des Verlaufs (Hex-Code)
* @returns {Object} D3-Referenz auf den erstellten Filter
*/
static createEnhancedGlassMorphismFilter(defs, id, color1 = '#b38fff', color2 = '#58a9ff') {
// Farbverlauf für den Glaseffekt definieren
const gradientId = `gradient-${id}`;
const gradient = defs.append('linearGradient')
.attr('id', gradientId)
.attr('x1', '0%')
.attr('y1', '0%')
.attr('x2', '100%')
.attr('y2', '100%');
gradient.append('stop')
.attr('offset', '0%')
.attr('stop-color', color1)
.attr('stop-opacity', '0.3');
gradient.append('stop')
.attr('offset', '100%')
.attr('stop-color', color2)
.attr('stop-opacity', '0.3');
// Filter erstellen
const filter = defs.append('filter')
.attr('id', id)
.attr('x', '-50%')
.attr('y', '-50%')
.attr('width', '200%')
.attr('height', '200%');
// Hintergrund-Unschärfe
filter.append('feGaussianBlur')
.attr('in', 'SourceGraphic')
.attr('stdDeviation', 6)
.attr('result', 'blur');
// Farbverlauf einfügen
const feImage = filter.append('feImage')
.attr('xlink:href', `#${gradientId}`)
.attr('result', 'gradient')
.attr('x', '0%')
.attr('y', '0%')
.attr('width', '100%')
.attr('height', '100%')
.attr('preserveAspectRatio', 'none');
// Zusammenfügen aller Ebenen
const feMerge = filter.append('feMerge');
feMerge.append('feMergeNode')
.attr('in', 'blur');
feMerge.append('feMergeNode')
.attr('in', 'gradient');
feMerge.append('feMergeNode')
.attr('in', 'SourceGraphic');
return filter;
}
/**
* Erstellt einen 3D-Glaseffekt mit verbesserter Tiefe und Reflexionen
* @param {Object} defs - D3-Referenz auf den defs-Bereich
* @param {String} id - ID des Filters
* @returns {Object} D3-Referenz auf den erstellten Filter
*/
static create3DGlassEffect(defs, id) {
const filter = defs.append('filter')
.attr('id', id)
.attr('x', '-50%')
.attr('y', '-50%')
.attr('width', '200%')
.attr('height', '200%');
// Farbmatrix für Transparenz
filter.append('feColorMatrix')
.attr('type', 'matrix')
.attr('in', 'SourceGraphic')
.attr('values', '1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0.7 0')
.attr('result', 'transparent');
// Hintergrund-Unschärfe für Tiefe
filter.append('feGaussianBlur')
.attr('in', 'transparent')
.attr('stdDeviation', '4')
.attr('result', 'blurred');
// Lichtquelle und Schattierung hinzufügen
const lightSource = filter.append('feSpecularLighting')
.attr('in', 'blurred')
.attr('surfaceScale', '6')
.attr('specularConstant', '1')
.attr('specularExponent', '30')
.attr('lighting-color', '#ffffff')
.attr('result', 'specular');
lightSource.append('fePointLight')
.attr('x', '100')
.attr('y', '100')
.attr('z', '200');
// Lichtreflexion verstärken
filter.append('feComposite')
.attr('in', 'specular')
.attr('in2', 'SourceGraphic')
.attr('operator', 'in')
.attr('result', 'specularHighlight');
// Inneren Schatten erzeugen
const innerShadow = filter.append('feOffset')
.attr('in', 'SourceAlpha')
.attr('dx', '0')
.attr('dy', '1')
.attr('result', 'offsetblur');
innerShadow.append('feGaussianBlur')
.attr('in', 'offsetblur')
.attr('stdDeviation', '2')
.attr('result', 'innerShadow');
filter.append('feComposite')
.attr('in', 'innerShadow')
.attr('in2', 'SourceGraphic')
.attr('operator', 'out')
.attr('result', 'innerShadowEffect');
// Schichten kombinieren
const feMerge = filter.append('feMerge');
feMerge.append('feMergeNode')
.attr('in', 'blurred');
feMerge.append('feMergeNode')
.attr('in', 'innerShadowEffect');
feMerge.append('feMergeNode')
.attr('in', 'specularHighlight');
feMerge.append('feMergeNode')
.attr('in', 'SourceGraphic');
return filter;
}
/**
* Fügt einen Partikelsystem-Effekt für interaktive Knoten hinzu
* @param {Object} parent - Das übergeordnete SVG-Element
* @param {number} x - X-Koordinate des Zentrums
* @param {number} y - Y-Koordinate des Zentrums
* @param {string} color - Partikelfarbe (Hex-Code)
* @param {number} count - Anzahl der Partikel
*/
static createParticleEffect(parent, x, y, color = '#b38fff', count = 5) {
const particles = [];
for (let i = 0; i < count; i++) {
const particle = parent.append('circle')
.attr('cx', x)
.attr('cy', y)
.attr('r', 0)
.attr('fill', color)
.style('opacity', 0.8);
particles.push(particle);
// Partikel animieren
animateParticle(particle);
}
function animateParticle(particle) {
// Zufällige Richtung und Geschwindigkeit
const angle = Math.random() * Math.PI * 2;
const speed = 1 + Math.random() * 2;
const distance = 20 + Math.random() * 30;
// Zielposition berechnen
const targetX = x + Math.cos(angle) * distance;
const targetY = y + Math.sin(angle) * distance;
// Animation mit zufälliger Dauer
const duration = 1000 + Math.random() * 500;
particle
.attr('r', 0)
.style('opacity', 0.8)
.transition()
.duration(duration)
.attr('cx', targetX)
.attr('cy', targetY)
.attr('r', 2 + Math.random() * 3)
.style('opacity', 0)
.on('end', function() {
// Partikel entfernen
particle.remove();
});
}
}
/**
* Führt eine Pulsanimation auf einem Knoten durch
* @param {Object} node - D3-Knoten-Selektion
* @returns {void}
*/
static pulseAnimation(node) {
if (!node) return;
const circle = node.select('circle');
const originalRadius = parseFloat(circle.attr('r'));
const originalFill = circle.attr('fill');
// Pulsanimation
circle
.transition()
.duration(400)
.attr('r', originalRadius * 1.3)
.attr('fill', '#b38fff')
.transition()
.duration(400)
.attr('r', originalRadius)
.attr('fill', originalFill);
}
/**
* Berechnet eine adaptive Schriftgröße basierend auf der Textlänge
* @param {string} text - Der anzuzeigende Text
* @param {number} maxSize - Maximale Schriftgröße in Pixel
* @param {number} minSize - Minimale Schriftgröße in Pixel
* @returns {number} - Die berechnete Schriftgröße
*/
static getAdaptiveFontSize(text, maxSize = 14, minSize = 10) {
if (!text) return maxSize;
// Linear die Schriftgröße basierend auf der Textlänge anpassen
const length = text.length;
if (length <= 6) return maxSize;
if (length >= 20) return minSize;
// Lineare Interpolation
const factor = (length - 6) / (20 - 6);
return maxSize - factor * (maxSize - minSize);
}
/**
* Fügt einen Pulsierenden Effekt zu einer Selektion hinzu
* @param {Object} selection - D3-Selektion
* @param {number} duration - Dauer eines Puls-Zyklus in ms
* @param {number} minOpacity - Minimale Opazität
* @param {number} maxOpacity - Maximale Opazität
*/
static addPulseEffect(selection, duration = 1500, minOpacity = 0.4, maxOpacity = 0.9) {
function pulse() {
selection
.transition()
.duration(duration / 2)
.style('opacity', minOpacity)
.transition()
.duration(duration / 2)
.style('opacity', maxOpacity)
.on('end', pulse);
}
// Initialen Stil setzen
selection.style('opacity', maxOpacity);
// Pulsanimation starten
pulse();
}
}
// Globale Verfügbarkeit sicherstellen
window.D3Extensions = D3Extensions;

View File

@@ -1,4 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os import os
from PIL import Image from PIL import Image
import cairosvg import cairosvg

View File

@@ -2,284 +2,222 @@
* MindMap - Hauptdatei für globale JavaScript-Funktionen * MindMap - Hauptdatei für globale JavaScript-Funktionen
*/ */
// Globales Objekt für App-Funktionen // Import des ChatGPT-Assistenten
import ChatGPTAssistant from './modules/chatgpt-assistant.js';
/**
* Hauptmodul für die MindMap-Anwendung
* Verwaltet die globale Anwendungslogik
*/
document.addEventListener('DOMContentLoaded', function() {
// Initialisiere die Anwendung
MindMap.init();
// Wende Dunkel-/Hellmodus an
const isDarkMode = localStorage.getItem('darkMode') === 'dark';
document.documentElement.classList.toggle('dark', isDarkMode);
});
/**
* Hauptobjekt der MindMap-Anwendung
*/
const MindMap = { const MindMap = {
/** // App-Status
* Initialisiert die Anwendung initialized: false,
*/ darkMode: document.documentElement.classList.contains('dark'),
init: function() { pageInitializers: {},
// Initialisiere alle Komponenten currentPage: document.body.dataset.page,
this.setupDarkMode();
this.setupTooltips();
this.setupUtilityFunctions();
// Prüfe, ob spezifische Seiten-Initialisierer vorhanden sind /**
const currentPage = document.body.dataset.page; * Initialisiert die MindMap-Anwendung
if (currentPage && this.pageInitializers[currentPage]) { */
this.pageInitializers[currentPage](); init() {
if (this.initialized) return;
console.log('MindMap-Anwendung wird initialisiert...');
// Seiten-spezifische Initialisierer aufrufen
if (this.currentPage && this.pageInitializers[this.currentPage]) {
this.pageInitializers[this.currentPage]();
} }
console.log('MindMap App initialisiert'); // Event-Listener einrichten
}, this.setupEventListeners();
/** // Dunkel-/Hellmodus aus LocalStorage wiederherstellen
* Dark Mode Setup if (localStorage.getItem('darkMode') === 'dark') {
*/
setupDarkMode: function() {
// Prüfe, ob Dark Mode bevorzugt wird
const prefersDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
// Prüfe gespeicherte Einstellung
const savedMode = localStorage.getItem('darkMode');
// Setze Dark Mode basierend auf gespeicherter Einstellung oder Systempräferenz
if (savedMode === 'dark' || (savedMode === null && prefersDarkMode)) {
document.documentElement.classList.add('dark'); document.documentElement.classList.add('dark');
} else { this.darkMode = true;
document.documentElement.classList.remove('dark');
} }
// Höre auf System-Präferenzänderungen // Mindmap initialisieren, falls auf der richtigen Seite
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => { this.initializeMindmap();
if (localStorage.getItem('darkMode') === null) {
if (e.matches) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
}
});
// Dark Mode Toggle-Listener (wird über Alpine.js gehandelt) this.initialized = true;
document.addEventListener('darkModeToggled', function(e) {
const isDark = e.detail.isDark;
localStorage.setItem('darkMode', isDark ? 'dark' : 'light');
});
}, },
/** /**
* Tooltips mit Tippy.js einrichten * Initialisiert die D3.js Mindmap-Visualisierung
*/ */
setupTooltips: function() { initializeMindmap() {
// Prüfe, ob Tippy.js geladen ist // Prüfe, ob wir auf der Mindmap-Seite sind
if (typeof tippy !== 'undefined') { const mindmapContainer = document.getElementById('mindmap-container');
// Allgemeine Tooltips if (!mindmapContainer) return;
tippy('[data-tippy-content]', {
theme: 'mindmap', try {
animation: 'scale', console.log('Initialisiere Mindmap...');
arrow: true
// Initialisiere die Mindmap
const mindmap = new MindMapVisualization('#mindmap-container', {
height: mindmapContainer.clientHeight || 600,
nodeRadius: 18,
selectedNodeRadius: 24,
linkDistance: 150,
onNodeClick: this.handleNodeClick.bind(this)
}); });
// Mindmap-Knoten Tooltips // Globale Referenz für andere Module
tippy('.mindmap-node', { window.mindmapInstance = mindmap;
content(reference) {
const title = reference.getAttribute('data-title'); // Event-Listener für Zoom-Buttons
const desc = reference.getAttribute('data-description'); const zoomInBtn = document.getElementById('zoom-in-btn');
return `<div class="node-tooltip"><strong>${title}</strong>${desc ? `<p>${desc}</p>` : ''}</div>`; if (zoomInBtn) {
}, zoomInBtn.addEventListener('click', () => {
allowHTML: true, const svg = d3.select('#mindmap-container svg');
theme: 'mindmap', const currentZoom = d3.zoomTransform(svg.node());
animation: 'scale', const newScale = currentZoom.k * 1.3;
arrow: true, svg.transition().duration(300).call(
placement: 'top' d3.zoom().transform,
d3.zoomIdentity.translate(currentZoom.x, currentZoom.y).scale(newScale)
);
}); });
} }
const zoomOutBtn = document.getElementById('zoom-out-btn');
if (zoomOutBtn) {
zoomOutBtn.addEventListener('click', () => {
const svg = d3.select('#mindmap-container svg');
const currentZoom = d3.zoomTransform(svg.node());
const newScale = currentZoom.k / 1.3;
svg.transition().duration(300).call(
d3.zoom().transform,
d3.zoomIdentity.translate(currentZoom.x, currentZoom.y).scale(newScale)
);
});
}
const centerBtn = document.getElementById('center-btn');
if (centerBtn) {
centerBtn.addEventListener('click', () => {
const svg = d3.select('#mindmap-container svg');
svg.transition().duration(500).call(
d3.zoom().transform,
d3.zoomIdentity.scale(1)
);
});
}
// Event-Listener für Add-Thought-Button
const addThoughtBtn = document.getElementById('add-thought-btn');
if (addThoughtBtn) {
addThoughtBtn.addEventListener('click', () => {
this.showAddThoughtDialog();
});
}
// Event-Listener für Connect-Button
const connectBtn = document.getElementById('connect-btn');
if (connectBtn) {
connectBtn.addEventListener('click', () => {
this.showConnectDialog();
});
}
} catch (error) {
console.error('Fehler bei der Initialisierung der Mindmap:', error);
}
}, },
/** /**
* Hilfsfunktionen einrichten * Handler für Klick auf einen Knoten in der Mindmap
* @param {Object} node - Der angeklickte Knoten
*/ */
setupUtilityFunctions: function() { handleNodeClick(node) {
// Axios-Interceptor für API-Anfragen console.log('Knoten wurde angeklickt:', node);
if (typeof axios !== 'undefined') {
axios.interceptors.request.use(function(config) {
// Hier könnten wir z.B. einen CSRF-Token hinzufügen
return config;
}, function(error) {
return Promise.reject(error);
});
axios.interceptors.response.use(function(response) { // Hier könnte man Logik hinzufügen, um Detailinformationen anzuzeigen
return response; // oder den ausgewählten Knoten hervorzuheben
}, function(error) { const detailsContainer = document.getElementById('node-details');
// Behandle Fehler und zeige Benachrichtigungen if (detailsContainer) {
const message = error.response && error.response.data && error.response.data.error detailsContainer.innerHTML = `
? error.response.data.error <div class="p-4">
: 'Ein Fehler ist aufgetreten.'; <h3 class="text-xl font-bold mb-2">${node.name}</h3>
<p class="text-gray-300 mb-4">${node.description || 'Keine Beschreibung verfügbar.'}</p>
MindMap.showNotification(message, 'error'); <div class="flex items-center justify-between">
return Promise.reject(error); <span class="text-sm">
}); <i class="fas fa-brain mr-1"></i> ${node.thought_count || 0} Gedanken
} </span>
}, <button class="px-3 py-1 bg-purple-600 bg-opacity-30 rounded-lg text-sm">
<i class="fas fa-plus mr-1"></i> Gedanke hinzufügen
/**
* Zeigt eine Benachrichtigung an
* @param {string} message - Nachrichtentext
* @param {string} type - Art der Nachricht: 'success', 'error', 'info'
* @param {number} duration - Anzeigedauer in ms
*/
showNotification: function(message, type = 'info', duration = 5000) {
const notification = document.createElement('div');
notification.className = `notification notification-${type} glass-effect`;
let icon = '';
switch(type) {
case 'success':
icon = '<i class="fa-solid fa-circle-check"></i>';
break;
case 'error':
icon = '<i class="fa-solid fa-circle-exclamation"></i>';
break;
default:
icon = '<i class="fa-solid fa-circle-info"></i>';
}
notification.innerHTML = `
<div class="flex items-start">
<div class="flex-shrink-0">${icon}</div>
<div class="ml-3 flex-1">${message}</div>
<button class="ml-auto" onclick="this.parentNode.parentNode.remove()">
<i class="fa-solid fa-xmark"></i>
</button> </button>
</div> </div>
</div>
`; `;
// Füge zur Notification-Area hinzu // Button zum Hinzufügen eines Gedankens
let notificationArea = document.querySelector('.notification-area'); const addThoughtBtn = detailsContainer.querySelector('button');
if (!notificationArea) { addThoughtBtn.addEventListener('click', () => {
notificationArea = document.createElement('div'); this.showAddThoughtDialog(node);
notificationArea.className = 'notification-area fixed bottom-4 right-4 z-50 flex flex-col space-y-2 max-w-sm'; });
document.body.appendChild(notificationArea);
}
notificationArea.appendChild(notification);
// Animationen
setTimeout(() => {
notification.style.opacity = '1';
notification.style.transform = 'translateY(0)';
}, 10);
// Automatisches Entfernen
if (duration > 0) {
setTimeout(() => {
notification.style.opacity = '0';
notification.style.transform = 'translateY(10px)';
setTimeout(() => {
notification.remove();
}, 300);
}, duration);
} }
}, },
/** /**
* Seitenspezifische Initialisierer * Dialog zum Hinzufügen eines neuen Knotens
*/ */
pageInitializers: { showAddNodeDialog() {
// Startseite // Diese Funktionalität würde in einer vollständigen Implementierung eingebunden werden
'home': function() { alert('Diese Funktion steht bald zur Verfügung!');
console.log('Startseite initialisiert');
// Hier kommen spezifische Funktionen für die Startseite
}, },
// Mindmap-Seite /**
'mindmap': function() { * Dialog zum Hinzufügen eines neuen Gedankens zu einem Knoten
console.log('Mindmap-Seite initialisiert'); */
// Hier werden mindmap-spezifische Funktionen aufgerufen showAddThoughtDialog(node) {
// Die tatsächliche D3.js-Implementierung wird in einer separaten Datei sein // Diese Funktionalität würde in einer vollständigen Implementierung eingebunden werden
alert('Diese Funktion steht bald zur Verfügung!');
}, },
// Profilseite /**
'profile': function() { * Dialog zum Verbinden von Knoten
console.log('Profilseite initialisiert'); */
// Profil-spezifische Funktionen showConnectDialog() {
// Diese Funktionalität würde in einer vollständigen Implementierung eingebunden werden
alert('Diese Funktion steht bald zur Verfügung!');
}, },
// Suchseite /**
'search': function() { * Richtet Event-Listener für die Benutzeroberfläche ein
console.log('Suchseite initialisiert'); */
// Such-spezifische Funktionen setupEventListeners() {
// Event-Listener für Dark Mode-Wechsel
document.addEventListener('darkModeToggled', (event) => {
this.darkMode = event.detail.isDark;
});
// Responsive Anpassungen bei Fenstergröße
window.addEventListener('resize', () => {
if (window.mindmapInstance) {
const container = document.getElementById('mindmap-container');
if (container) {
window.mindmapInstance.width = container.clientWidth;
window.mindmapInstance.height = container.clientHeight;
} }
} }
});
}
}; };
// Initialisiere die App nach dem Laden der Seite // Globale Export für andere Module
document.addEventListener('DOMContentLoaded', () => {
MindMap.init();
// Höre auf Storage-Änderungen, um Dark Mode zwischen Tabs zu synchronisieren
window.addEventListener('storage', (event) => {
if (event.key === 'darkMode') {
const isDarkMode = event.newValue === 'true';
// Aktualisiere das DOM
document.documentElement.classList.toggle('dark', isDarkMode);
// Aktualisiere Alpine.js, falls es bereits geladen wurde
if (window.Alpine) {
const darkModeComponent = Alpine.store('darkMode');
if (darkModeComponent) {
darkModeComponent.enabled = isDarkMode;
}
}
}
});
// Lade die bevorzugte Farbeinstellung des Betriebssystems wenn nichts gespeichert ist
if (localStorage.getItem('darkMode') === null) {
const prefersDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
localStorage.setItem('darkMode', prefersDarkMode);
document.documentElement.classList.toggle('dark', prefersDarkMode);
}
// UI-Verbesserungen
// Füge Schatten zu Karten hinzu, wenn sie sichtbar werden
const cards = document.querySelectorAll('.card');
if ('IntersectionObserver' in window) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
setTimeout(() => {
entry.target.classList.add('shadow-md', 'opacity-100');
entry.target.classList.remove('opacity-0', 'translate-y-4');
}, entry.target.dataset.delay || 0);
}
});
}, { threshold: 0.1 });
cards.forEach((card, index) => {
card.classList.add('transition-all', 'duration-500', 'opacity-0', 'translate-y-4');
card.dataset.delay = index * 100;
observer.observe(card);
});
}
// Verbesserte Formular-Validierung
const forms = document.querySelectorAll('form');
forms.forEach(form => {
const inputs = form.querySelectorAll('input, textarea, select');
inputs.forEach(input => {
// Füge Validierungsklassen hinzu, wenn ein Input fokussiert wurde
input.addEventListener('blur', () => {
if (input.value.trim() !== '') {
input.classList.add('has-content');
} else {
input.classList.remove('has-content');
}
if (input.checkValidity()) {
input.classList.remove('is-invalid');
input.classList.add('is-valid');
} else {
input.classList.remove('is-valid');
input.classList.add('is-invalid');
}
});
});
});
});
// Support für Alpine.js
window.MindMap = MindMap; window.MindMap = MindMap;

View File

@@ -0,0 +1,280 @@
/**
* ChatGPT Assistent Modul
* Verwaltet die Interaktion mit der OpenAI API und die Benutzeroberfläche des Assistenten
*/
class ChatGPTAssistant {
constructor() {
this.messages = [];
this.isOpen = false;
this.isLoading = false;
this.container = null;
this.chatHistory = null;
this.inputField = null;
}
/**
* Initialisiert den Assistenten und fügt die UI zum DOM hinzu
*/
init() {
// Assistent-Container erstellen
this.createAssistantUI();
// Event-Listener hinzufügen
this.setupEventListeners();
// Ersten Willkommensnachricht anzeigen
this.addMessage("assistant", "Wie kann ich dir heute helfen?");
}
/**
* Erstellt die UI-Elemente für den Assistenten
*/
createAssistantUI() {
// Hauptcontainer erstellen
this.container = document.createElement('div');
this.container.id = 'chatgpt-assistant';
this.container.className = 'fixed bottom-4 right-4 z-50 flex flex-col';
// Button zum Öffnen/Schließen des Assistenten
const toggleButton = document.createElement('button');
toggleButton.id = 'assistant-toggle';
toggleButton.className = 'ml-auto bg-primary-600 hover:bg-primary-700 text-white rounded-full p-3 shadow-lg transition-all duration-300 mb-2';
toggleButton.innerHTML = '<i class="fas fa-robot text-xl"></i>';
// Chat-Container
const chatContainer = document.createElement('div');
chatContainer.id = 'assistant-chat';
chatContainer.className = 'bg-white dark:bg-dark-800 rounded-lg shadow-xl overflow-hidden transition-all duration-300 w-80 sm:w-96 max-h-0 opacity-0';
// Chat-Header
const header = document.createElement('div');
header.className = 'bg-primary-600 text-white p-3 flex items-center justify-between';
header.innerHTML = `
<div class="flex items-center">
<i class="fas fa-robot mr-2"></i>
<span>KI-Assistent</span>
</div>
<button id="assistant-close" class="text-white hover:text-gray-200">
<i class="fas fa-times"></i>
</button>
`;
// Chat-Verlauf
this.chatHistory = document.createElement('div');
this.chatHistory.id = 'assistant-history';
this.chatHistory.className = 'p-3 overflow-y-auto max-h-80 space-y-3';
// Chat-Eingabe
const inputContainer = document.createElement('div');
inputContainer.className = 'border-t border-gray-200 dark:border-dark-600 p-3 flex items-center';
this.inputField = document.createElement('input');
this.inputField.type = 'text';
this.inputField.placeholder = 'Frage stellen...';
this.inputField.className = 'flex-1 border border-gray-300 dark:border-dark-600 dark:bg-dark-700 dark:text-white rounded-l-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary-500';
const sendButton = document.createElement('button');
sendButton.id = 'assistant-send';
sendButton.className = 'bg-primary-600 hover:bg-primary-700 text-white px-4 py-2 rounded-r-lg';
sendButton.innerHTML = '<i class="fas fa-paper-plane"></i>';
// Elemente zusammenfügen
inputContainer.appendChild(this.inputField);
inputContainer.appendChild(sendButton);
chatContainer.appendChild(header);
chatContainer.appendChild(this.chatHistory);
chatContainer.appendChild(inputContainer);
this.container.appendChild(toggleButton);
this.container.appendChild(chatContainer);
// Zum DOM hinzufügen
document.body.appendChild(this.container);
}
/**
* Richtet Event-Listener für die Benutzeroberfläche ein
*/
setupEventListeners() {
// Toggle-Button
const toggleButton = document.getElementById('assistant-toggle');
toggleButton.addEventListener('click', () => this.toggleAssistant());
// Schließen-Button
const closeButton = document.getElementById('assistant-close');
closeButton.addEventListener('click', () => this.toggleAssistant(false));
// Senden-Button
const sendButton = document.getElementById('assistant-send');
sendButton.addEventListener('click', () => this.sendMessage());
// Enter-Taste im Eingabefeld
this.inputField.addEventListener('keyup', (e) => {
if (e.key === 'Enter') {
this.sendMessage();
}
});
}
/**
* Öffnet oder schließt den Assistenten
* @param {boolean} state - Optional: erzwingt einen bestimmten Zustand
*/
toggleAssistant(state = null) {
const chatContainer = document.getElementById('assistant-chat');
this.isOpen = state !== null ? state : !this.isOpen;
if (this.isOpen) {
chatContainer.classList.remove('max-h-0', 'opacity-0');
chatContainer.classList.add('max-h-96', 'opacity-100');
this.inputField.focus();
} else {
chatContainer.classList.remove('max-h-96', 'opacity-100');
chatContainer.classList.add('max-h-0', 'opacity-0');
}
}
/**
* Fügt eine Nachricht zum Chat-Verlauf hinzu
* @param {string} sender - 'user' oder 'assistant'
* @param {string} text - Nachrichtentext
*/
addMessage(sender, text) {
// Nachricht zum Verlauf hinzufügen
this.messages.push({ role: sender, content: text });
// DOM-Element erstellen
const messageEl = document.createElement('div');
messageEl.className = `flex ${sender === 'user' ? 'justify-end' : 'justify-start'}`;
const bubble = document.createElement('div');
bubble.className = sender === 'user'
? 'bg-primary-100 dark:bg-primary-900 text-gray-800 dark:text-white rounded-lg py-2 px-3 max-w-[85%]'
: 'bg-gray-100 dark:bg-dark-700 text-gray-800 dark:text-white rounded-lg py-2 px-3 max-w-[85%]';
bubble.textContent = text;
messageEl.appendChild(bubble);
this.chatHistory.appendChild(messageEl);
// Scroll zum Ende des Verlaufs
this.chatHistory.scrollTop = this.chatHistory.scrollHeight;
}
/**
* Sendet die Benutzernachricht an den Server und zeigt die Antwort an
*/
async sendMessage() {
const userInput = this.inputField.value.trim();
if (!userInput || this.isLoading) return;
// Benutzernachricht anzeigen
this.addMessage('user', userInput);
// Eingabefeld zurücksetzen
this.inputField.value = '';
// Ladeindikator anzeigen
this.isLoading = true;
this.showLoadingIndicator();
try {
// Anfrage an den Server senden
const response = await fetch('/api/assistant', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
messages: this.messages
}),
});
if (!response.ok) {
throw new Error('Netzwerkfehler oder Serverproblem');
}
const data = await response.json();
// Ladeindikator entfernen
this.removeLoadingIndicator();
// Antwort anzeigen
this.addMessage('assistant', data.response);
} catch (error) {
console.error('Fehler bei der Kommunikation mit dem Assistenten:', error);
// Ladeindikator entfernen
this.removeLoadingIndicator();
// Fehlermeldung anzeigen
this.addMessage('assistant', 'Es tut mir leid, aber es gab ein Problem bei der Verarbeitung deiner Anfrage. Bitte versuche es später noch einmal.');
} finally {
this.isLoading = false;
}
}
/**
* Zeigt einen Ladeindikator im Chat an
*/
showLoadingIndicator() {
const loadingEl = document.createElement('div');
loadingEl.id = 'assistant-loading';
loadingEl.className = 'flex justify-start';
const bubble = document.createElement('div');
bubble.className = 'bg-gray-100 dark:bg-dark-700 text-gray-800 dark:text-white rounded-lg py-2 px-3';
bubble.innerHTML = '<div class="typing-indicator"><span></span><span></span><span></span></div>';
loadingEl.appendChild(bubble);
this.chatHistory.appendChild(loadingEl);
// Scroll zum Ende des Verlaufs
this.chatHistory.scrollTop = this.chatHistory.scrollHeight;
// Stil für den Typing-Indikator
const style = document.createElement('style');
style.textContent = `
.typing-indicator {
display: flex;
align-items: center;
}
.typing-indicator span {
height: 8px;
width: 8px;
background-color: #888;
border-radius: 50%;
display: inline-block;
margin: 0 2px;
opacity: 0.4;
animation: typing 1.5s infinite ease-in-out;
}
.typing-indicator span:nth-child(2) {
animation-delay: 0.2s;
}
.typing-indicator span:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes typing {
0% { transform: translateY(0); }
50% { transform: translateY(-5px); }
100% { transform: translateY(0); }
}
`;
document.head.appendChild(style);
}
/**
* Entfernt den Ladeindikator aus dem Chat
*/
removeLoadingIndicator() {
const loadingEl = document.getElementById('assistant-loading');
if (loadingEl) {
loadingEl.remove();
}
}
}
// Exportiere die Klasse für die Verwendung in anderen Modulen
export default ChatGPTAssistant;

View File

@@ -32,8 +32,18 @@ class MindMapVisualization {
this.tooltipDiv = null; this.tooltipDiv = null;
this.isLoading = true; this.isLoading = true;
// Sicherstellen, dass der Container bereit ist
if (this.container.node()) {
this.init(); this.init();
this.setupDefaultNodes(); this.setupDefaultNodes();
// Sofortige Datenladung
window.setTimeout(() => {
this.loadData();
}, 100);
} else {
console.error('Mindmap-Container nicht gefunden:', containerSelector);
}
} }
// Standardknoten als Fallback einrichten, falls die API nicht reagiert // Standardknoten als Fallback einrichten, falls die API nicht reagiert
@@ -62,6 +72,9 @@ class MindMapVisualization {
init() { init() {
// SVG erstellen, wenn noch nicht vorhanden // SVG erstellen, wenn noch nicht vorhanden
if (!this.svg) { if (!this.svg) {
// Container zuerst leeren
this.container.html('');
this.svg = this.container this.svg = this.container
.append('svg') .append('svg')
.attr('width', '100%') .attr('width', '100%')
@@ -80,12 +93,24 @@ class MindMapVisualization {
this.g = this.svg.append('g'); this.g = this.svg.append('g');
// Tooltip initialisieren // Tooltip initialisieren
if (!d3.select('body').select('.node-tooltip').size()) {
this.tooltipDiv = d3.select('body') this.tooltipDiv = d3.select('body')
.append('div') .append('div')
.attr('class', 'node-tooltip') .attr('class', 'node-tooltip')
.style('opacity', 0) .style('opacity', 0)
.style('position', 'absolute') .style('position', 'absolute')
.style('pointer-events', 'none'); .style('pointer-events', 'none')
.style('background', 'rgba(20, 20, 40, 0.9)')
.style('color', '#ffffff')
.style('border', '1px solid rgba(160, 80, 255, 0.2)')
.style('border-radius', '6px')
.style('padding', '8px 12px')
.style('font-size', '14px')
.style('max-width', '250px')
.style('box-shadow', '0 10px 25px rgba(0, 0, 0, 0.5), 0 0 10px rgba(160, 80, 255, 0.2)');
} else {
this.tooltipDiv = d3.select('body').select('.node-tooltip');
}
} }
// Force-Simulation initialisieren // Force-Simulation initialisieren
@@ -94,6 +119,9 @@ class MindMapVisualization {
.force('charge', d3.forceManyBody().strength(this.chargeStrength)) .force('charge', d3.forceManyBody().strength(this.chargeStrength))
.force('center', d3.forceCenter(this.width / 2, this.height / 2).strength(this.centerForce)) .force('center', d3.forceCenter(this.width / 2, this.height / 2).strength(this.centerForce))
.force('collision', d3.forceCollide().radius(this.nodeRadius * 2)); .force('collision', d3.forceCollide().radius(this.nodeRadius * 2));
// Globale Mindmap-Instanz für externe Zugriffe setzen
window.mindmapInstance = this;
} }
handleZoom(transform) { handleZoom(transform) {
@@ -118,13 +146,25 @@ class MindMapVisualization {
// Ladeindikator anzeigen // Ladeindikator anzeigen
this.showLoading(); this.showLoading();
// Verwende sofort die Standarddaten für eine schnelle erste Anzeige
this.nodes = [...this.defaultNodes];
this.links = [...this.defaultLinks];
// Visualisierung sofort aktualisieren
this.isLoading = false;
this.updateVisualization();
// API-Aufruf mit längeren Timeout im Hintergrund durchführen
try { try {
// API-Aufruf mit Timeout versehen
const controller = new AbortController(); const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 Sekunden Timeout const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 Sekunden Timeout
const response = await fetch('/api/mindmap', { const response = await fetch('/api/mindmap', {
signal: controller.signal signal: controller.signal,
headers: {
'Cache-Control': 'no-cache',
'Pragma': 'no-cache'
}
}); });
clearTimeout(timeoutId); clearTimeout(timeoutId);
@@ -135,8 +175,8 @@ class MindMapVisualization {
const data = await response.json(); const data = await response.json();
if (!data || !data.nodes || data.nodes.length === 0) { if (!data || !data.nodes || data.nodes.length === 0) {
console.warn('Keine Mindmap-Daten vorhanden, verwende Standard-Daten.'); console.warn('Keine Mindmap-Daten vorhanden, verwende weiterhin Standard-Daten.');
throw new Error('Keine Daten'); return; // Behalte Standarddaten bei
} }
// Flache Liste von Knoten und Verbindungen erstellen // Flache Liste von Knoten und Verbindungen erstellen
@@ -144,24 +184,29 @@ class MindMapVisualization {
this.links = []; this.links = [];
this.processHierarchicalData(data.nodes); this.processHierarchicalData(data.nodes);
} catch (error) { // Visualisierung aktualisieren mit den tatsächlichen Daten
console.error('Fehler beim Laden der Mindmap-Daten:', error);
// Fallback zu Standarddaten
this.nodes = [...this.defaultNodes];
this.links = [...this.defaultLinks];
}
// Visualisierung aktualisieren
this.isLoading = false;
this.updateVisualization(); 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');
}
} catch (error) { } catch (error) {
console.error('Kritischer Fehler bei der Mindmap-Darstellung:', error); console.error('Kritischer Fehler bei der Mindmap-Darstellung:', error);
this.showError('Fehler beim Laden der Mindmap-Daten. Bitte laden Sie die Seite neu.'); this.showError('Fehler beim Laden der Mindmap-Daten. Bitte laden Sie die Seite neu.');
this.container.attr('data-status', 'error');
} }
} }
showLoading() { showLoading() {
// Element nur leeren, wenn es noch kein SVG enthält
if (!this.container.select('svg').size()) {
this.container.html(` this.container.html(`
<div class="flex justify-center items-center h-full"> <div class="flex justify-center items-center h-full">
<div class="text-center"> <div class="text-center">
@@ -171,6 +216,7 @@ class MindMapVisualization {
</div> </div>
`); `);
} }
}
processHierarchicalData(hierarchicalNodes, parentId = null) { processHierarchicalData(hierarchicalNodes, parentId = null) {
hierarchicalNodes.forEach(node => { hierarchicalNodes.forEach(node => {
@@ -230,6 +276,9 @@ class MindMapVisualization {
this.init(); this.init();
} }
// Performance-Optimierung: Deaktiviere Transition während des Datenladens
const useTransitions = false;
// Links (Edges) erstellen // Links (Edges) erstellen
this.linkElements = this.g.selectAll('.link') this.linkElements = this.g.selectAll('.link')
.data(this.links) .data(this.links)
@@ -263,6 +312,39 @@ class MindMapVisualization {
.attr('fill', '#ffffff50'); .attr('fill', '#ffffff50');
} }
// Simplified Effekte definieren, falls noch nicht vorhanden
if (!this.svg.select('#glow').node()) {
const defs = this.svg.select('defs').size() ? this.svg.select('defs') : this.svg.append('defs');
// Glow-Effekt für Knoten
const filter = defs.append('filter')
.attr('id', 'glow')
.attr('x', '-50%')
.attr('y', '-50%')
.attr('width', '200%')
.attr('height', '200%');
filter.append('feGaussianBlur')
.attr('stdDeviation', '1')
.attr('result', 'blur');
filter.append('feComposite')
.attr('in', 'SourceGraphic')
.attr('in2', 'blur')
.attr('operator', 'over');
// Blur-Effekt für Schatten
const blurFilter = defs.append('filter')
.attr('id', 'blur')
.attr('x', '-50%')
.attr('y', '-50%')
.attr('width', '200%')
.attr('height', '200%');
blurFilter.append('feGaussianBlur')
.attr('stdDeviation', '1');
}
// Knoten-Gruppe erstellen/aktualisieren // Knoten-Gruppe erstellen/aktualisieren
const nodeGroups = this.g.selectAll('.node-group') const nodeGroups = this.g.selectAll('.node-group')
.data(this.nodes) .data(this.nodes)
@@ -303,7 +385,7 @@ class MindMapVisualization {
.style('font-size', '12px') .style('font-size', '12px')
.style('font-weight', '500') .style('font-weight', '500')
.style('pointer-events', 'none') .style('pointer-events', 'none')
.text(d => d.name); .text(d => d.name.length > 12 ? d.name.slice(0, 10) + '...' : d.name);
// Interaktivität hinzufügen // Interaktivität hinzufügen
group group
@@ -320,60 +402,29 @@ class MindMapVisualization {
// Text aktualisieren // Text aktualisieren
update.select('.node-label') update.select('.node-label')
.text(d => d.name); .text(d => d.name.length > 12 ? d.name.slice(0, 10) + '...' : d.name);
return update; return update;
}, },
exit => exit.remove() exit => exit.remove()
); );
// Effekte definieren, falls noch nicht vorhanden
if (!this.svg.select('#glow').node()) {
const defs = this.svg.select('defs').size() ? this.svg.select('defs') : this.svg.append('defs');
// Glow-Effekt für Knoten
const filter = defs.append('filter')
.attr('id', 'glow')
.attr('x', '-50%')
.attr('y', '-50%')
.attr('width', '200%')
.attr('height', '200%');
filter.append('feGaussianBlur')
.attr('stdDeviation', '2')
.attr('result', 'blur');
filter.append('feComposite')
.attr('in', 'SourceGraphic')
.attr('in2', 'blur')
.attr('operator', 'over');
// Blur-Effekt für Schatten
const blurFilter = defs.append('filter')
.attr('id', 'blur')
.attr('x', '-50%')
.attr('y', '-50%')
.attr('width', '200%')
.attr('height', '200%');
blurFilter.append('feGaussianBlur')
.attr('stdDeviation', '2');
}
// Einzelne Elemente für direkten Zugriff speichern // Einzelne Elemente für direkten Zugriff speichern
this.nodeElements = this.g.selectAll('.node'); this.nodeElements = this.g.selectAll('.node');
this.textElements = this.g.selectAll('.node-label'); this.textElements = this.g.selectAll('.node-label');
// Simulation starten // Performance-Optimierung: Weniger Simulationsschritte für schnellere Stabilisierung
this.simulation this.simulation
.nodes(this.nodes) .nodes(this.nodes)
.on('tick', () => this.ticked()); .on('tick', () => this.ticked())
.alpha(0.3) // Reduzierter Wert für schnellere Stabilisierung
.alphaDecay(0.05); // Erhöhter Wert für schnellere Stabilisierung
this.simulation.force('link') this.simulation.force('link')
.links(this.links); .links(this.links);
// Simulation neu starten // Simulation neu starten
this.simulation.alpha(1).restart(); this.simulation.restart();
} }
ticked() { ticked() {

File diff suppressed because it is too large Load Diff

View File

@@ -1,27 +0,0 @@
/* Grundlegendes Styling für die Seite */
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f0f0f0;
}
/* Styling für den Header */
h1 {
text-align: center;
color: #2c3e50;
margin-top: 50px;
}
/* Button für die Navigation */
button {
padding: 10px 20px;
background-color: #3498db;
color: white;
border: none;
cursor: pointer;
}
button:hover {
background-color: #2980b9;
}

View File

@@ -1,45 +1,83 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Admin | Wissenschaftliche Mindmap{% endblock %} {% block title %}Admin-Bereich{% endblock %}
{% block content %} {% block content %}
<div class="container mx-auto px-4 py-8"> <div class="container mx-auto px-4 py-8">
<div class="glass p-8 mb-8"> <h1 class="text-3xl font-bold mb-8 text-gray-800 dark:text-white">Admin-Bereich</h1>
<h1 class="text-3xl font-bold text-white mb-4">Admin Bereich</h1>
<p class="text-white/70">Verwalte Benutzer, Gedanken und die Mindmap-Struktur.</p> <!-- Tabs für verschiedene Bereiche -->
<div x-data="{ activeTab: 'users' }" class="mb-8">
<div class="flex space-x-2 mb-6 overflow-x-auto">
<button
@click="activeTab = 'users'"
:class="activeTab === 'users' ? 'bg-primary-600 text-white' : 'bg-white/10 text-gray-700 dark:text-gray-200'"
class="px-4 py-2 rounded-lg font-medium transition-all">
<i class="fas fa-users mr-2"></i> Benutzer
</button>
<button
@click="activeTab = 'nodes'"
:class="activeTab === 'nodes' ? 'bg-primary-600 text-white' : 'bg-white/10 text-gray-700 dark:text-gray-200'"
class="px-4 py-2 rounded-lg font-medium transition-all">
<i class="fas fa-project-diagram mr-2"></i> Mindmap-Knoten
</button>
<button
@click="activeTab = 'thoughts'"
:class="activeTab === 'thoughts' ? 'bg-primary-600 text-white' : 'bg-white/10 text-gray-700 dark:text-gray-200'"
class="px-4 py-2 rounded-lg font-medium transition-all">
<i class="fas fa-lightbulb mr-2"></i> Gedanken
</button>
<button
@click="activeTab = 'stats'"
:class="activeTab === 'stats' ? 'bg-primary-600 text-white' : 'bg-white/10 text-gray-700 dark:text-gray-200'"
class="px-4 py-2 rounded-lg font-medium transition-all">
<i class="fas fa-chart-bar mr-2"></i> Statistiken
</button>
</div> </div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8"> <!-- Benutzer-Tab -->
<!-- Users Section --> <div x-show="activeTab === 'users'" class="glass-morphism rounded-lg p-6">
<div class="dark-glass p-6" x-data="{ tab: 'users' }"> <div class="flex justify-between items-center mb-6">
<div class="flex items-center justify-between mb-6"> <h2 class="text-xl font-bold text-gray-800 dark:text-white">Benutzerverwaltung</h2>
<h2 class="text-2xl font-bold text-white">Benutzer</h2> <button class="btn-outline">
<span class="bg-indigo-600 text-white text-xs font-medium px-2.5 py-0.5 rounded-full">{{ users|length }}</span> <i class="fas fa-plus mr-2"></i> Neuer Benutzer
</button>
</div> </div>
<div class="overflow-y-auto max-h-[500px]"> <div class="overflow-x-auto">
<table class="w-full text-white/90"> <table class="w-full">
<thead class="text-white/60 text-sm uppercase"> <thead>
<tr> <tr class="text-left border-b border-gray-200 dark:border-gray-700">
<th class="text-left py-3">ID</th> <th class="px-4 py-2 text-gray-700 dark:text-gray-300">ID</th>
<th class="text-left py-3">Benutzername</th> <th class="px-4 py-2 text-gray-700 dark:text-gray-300">Benutzername</th>
<th class="text-left py-3">Email</th> <th class="px-4 py-2 text-gray-700 dark:text-gray-300">E-Mail</th>
<th class="text-left py-3">Rolle</th> <th class="px-4 py-2 text-gray-700 dark:text-gray-300">Admin</th>
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">Gedanken</th>
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">Aktionen</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for user in users %} {% for user in users %}
<tr class="border-t border-white/10"> <tr class="border-b border-gray-100 dark:border-gray-800 hover:bg-gray-50 dark:hover:bg-dark-700/30">
<td class="py-3">{{ user.id }}</td> <td class="px-4 py-3 text-gray-700 dark:text-gray-300">{{ user.id }}</td>
<td class="py-3">{{ user.username }}</td> <td class="px-4 py-3 text-gray-700 dark:text-gray-300 font-medium">{{ user.username }}</td>
<td class="py-3">{{ user.email }}</td> <td class="px-4 py-3 text-gray-700 dark:text-gray-300">{{ user.email }}</td>
<td class="py-3"> <td class="px-4 py-3 text-gray-700 dark:text-gray-300">
{% if user.is_admin %} {% if user.is_admin %}
<span class="bg-purple-600/70 text-white text-xs px-2 py-1 rounded">Admin</span> <span class="bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300 px-2 py-1 rounded text-xs">Admin</span>
{% else %} {% else %}
<span class="bg-blue-600/70 text-white text-xs px-2 py-1 rounded">Benutzer</span> <span class="bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300 px-2 py-1 rounded text-xs">User</span>
{% endif %} {% endif %}
</td> </td>
<td class="px-4 py-3 text-gray-700 dark:text-gray-300">{{ user.thoughts|length }}</td>
<td class="px-4 py-3 flex space-x-2">
<button class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300">
<i class="fas fa-edit"></i>
</button>
<button class="text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300">
<i class="fas fa-trash-alt"></i>
</button>
</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
@@ -47,63 +85,224 @@
</div> </div>
</div> </div>
<!-- Mindmap Nodes Section --> <!-- Mindmap-Knoten-Tab -->
<div class="dark-glass p-6"> <div x-show="activeTab === 'nodes'" class="glass-morphism rounded-lg p-6">
<div class="flex items-center justify-between mb-6"> <div class="flex justify-between items-center mb-6">
<h2 class="text-2xl font-bold text-white">Mindmap Struktur</h2> <h2 class="text-xl font-bold text-gray-800 dark:text-white">Mindmap-Knoten Verwaltung</h2>
<span class="bg-indigo-600 text-white text-xs font-medium px-2.5 py-0.5 rounded-full">{{ nodes|length }}</span> <button class="btn-outline">
<i class="fas fa-plus mr-2"></i> Neuer Knoten
</button>
</div> </div>
<div class="overflow-y-auto max-h-[500px]"> <div class="overflow-x-auto">
<div class="space-y-3"> <table class="w-full">
<thead>
<tr class="text-left border-b border-gray-200 dark:border-gray-700">
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">ID</th>
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">Name</th>
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">Elternknoten</th>
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">Gedanken</th>
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">Aktionen</th>
</tr>
</thead>
<tbody>
{% for node in nodes %} {% for node in nodes %}
<div class="glass p-3"> <tr class="border-b border-gray-100 dark:border-gray-800 hover:bg-gray-50 dark:hover:bg-dark-700/30">
<div class="flex justify-between items-center"> <td class="px-4 py-3 text-gray-700 dark:text-gray-300">{{ node.id }}</td>
<span class="font-medium">{{ node.name }}</span> <td class="px-4 py-3 text-gray-700 dark:text-gray-300 font-medium">{{ node.name }}</td>
<span class="text-xs text-white/60">ID: {{ node.id }}</span> <td class="px-4 py-3 text-gray-700 dark:text-gray-300">
</div>
{% if node.parent %} {% if node.parent %}
<p class="text-sm text-white/60 mt-1">Eltern: {{ node.parent.name }}</p> {{ node.parent.name }}
{% else %} {% else %}
<p class="text-sm text-white/60 mt-1">Hauptknoten</p> <span class="text-gray-400 dark:text-gray-500">Wurzelknoten</span>
{% endif %} {% endif %}
</div> </td>
<td class="px-4 py-3 text-gray-700 dark:text-gray-300">{{ node.thoughts|length }}</td>
<td class="px-4 py-3 flex space-x-2">
<button class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300">
<i class="fas fa-edit"></i>
</button>
<button class="text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300">
<i class="fas fa-trash-alt"></i>
</button>
</td>
</tr>
{% endfor %} {% endfor %}
</tbody>
</table>
</div> </div>
</div> </div>
<div class="mt-6"> <!-- Gedanken-Tab -->
<button class="bg-gradient-to-r from-indigo-500 to-purple-600 hover:from-indigo-600 hover:to-purple-700 text-white font-semibold px-4 py-2 rounded-lg transition-all duration-300 text-sm w-full"> <div x-show="activeTab === 'thoughts'" class="glass-morphism rounded-lg p-6">
Neuen Knoten hinzufügen <div class="flex justify-between items-center mb-6">
<h2 class="text-xl font-bold text-gray-800 dark:text-white">Gedanken-Verwaltung</h2>
<div class="flex space-x-2">
<div class="relative">
<input type="text" placeholder="Suchen..." class="form-input pl-10 pr-4 py-2 rounded-lg bg-white/10 border border-gray-200/20 dark:border-gray-700/20 focus:outline-none focus:ring-2 focus:ring-primary-500 text-gray-700 dark:text-gray-200">
<div class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500">
<i class="fas fa-search"></i>
</div>
</div>
<button class="btn-outline">
<i class="fas fa-filter mr-2"></i> Filter
</button> </button>
</div> </div>
</div> </div>
<!-- Thoughts Section --> <div class="overflow-x-auto">
<div class="dark-glass p-6"> <table class="w-full">
<div class="flex items-center justify-between mb-6"> <thead>
<h2 class="text-2xl font-bold text-white">Gedanken</h2> <tr class="text-left border-b border-gray-200 dark:border-gray-700">
<span class="bg-indigo-600 text-white text-xs font-medium px-2.5 py-0.5 rounded-full">{{ thoughts|length }}</span> <th class="px-4 py-2 text-gray-700 dark:text-gray-300">ID</th>
</div> <th class="px-4 py-2 text-gray-700 dark:text-gray-300">Titel</th>
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">Autor</th>
<div class="overflow-y-auto max-h-[500px]"> <th class="px-4 py-2 text-gray-700 dark:text-gray-300">Datum</th>
<div class="space-y-3"> <th class="px-4 py-2 text-gray-700 dark:text-gray-300">Bewertung</th>
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">Aktionen</th>
</tr>
</thead>
<tbody>
{% for thought in thoughts %} {% for thought in thoughts %}
<div class="glass p-3"> <tr class="border-b border-gray-100 dark:border-gray-800 hover:bg-gray-50 dark:hover:bg-dark-700/30">
<div class="flex justify-between items-start"> <td class="px-4 py-3 text-gray-700 dark:text-gray-300">{{ thought.id }}</td>
<span class="inline-block px-2 py-0.5 text-xs text-white/70 bg-white/10 rounded-full mb-1">{{ thought.branch }}</span> <td class="px-4 py-3 text-gray-700 dark:text-gray-300 font-medium">{{ thought.title }}</td>
<span class="text-xs text-white/50">{{ thought.timestamp.strftime('%d.%m.%Y') }}</span> <td class="px-4 py-3 text-gray-700 dark:text-gray-300">{{ thought.author.username }}</td>
</div> <td class="px-4 py-3 text-gray-700 dark:text-gray-300">{{ thought.timestamp.strftime('%d.%m.%Y') }}</td>
<p class="text-sm text-white mb-1 line-clamp-2">{{ thought.content }}</p> <td class="px-4 py-3 text-gray-700 dark:text-gray-300">
<div class="flex justify-between items-center mt-2 text-xs"> <div class="flex items-center">
<span class="text-white/60">Von: {{ thought.author.username }}</span> <span class="mr-2">{{ "%.1f"|format(thought.average_rating) }}</span>
<span class="text-white/60">{{ thought.comments|length }} Kommentar(e)</span> <div class="flex">
</div> {% for i in range(5) %}
</div> {% if i < thought.average_rating|int %}
<i class="fas fa-star text-yellow-400"></i>
{% elif i < (thought.average_rating|int + 0.5) %}
<i class="fas fa-star-half-alt text-yellow-400"></i>
{% else %}
<i class="far fa-star text-yellow-400"></i>
{% endif %}
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
</td>
<td class="px-4 py-3 flex space-x-2">
<button class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300">
<i class="fas fa-eye"></i>
</button>
<button class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300">
<i class="fas fa-edit"></i>
</button>
<button class="text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300">
<i class="fas fa-trash-alt"></i>
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- Statistiken-Tab -->
<div x-show="activeTab === 'stats'" class="glass-morphism rounded-lg p-6">
<h2 class="text-xl font-bold mb-6 text-gray-800 dark:text-white">Systemstatistiken</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<div class="glass-effect p-4 rounded-lg">
<div class="flex items-center mb-2">
<div class="bg-blue-500/20 p-3 rounded-lg mr-3">
<i class="fas fa-users text-blue-500"></i>
</div>
<div>
<h3 class="text-sm font-medium text-gray-600 dark:text-gray-300">Benutzer</h3>
<p class="text-2xl font-bold text-gray-800 dark:text-white">{{ users|length }}</p>
</div>
</div>
</div>
<div class="glass-effect p-4 rounded-lg">
<div class="flex items-center mb-2">
<div class="bg-purple-500/20 p-3 rounded-lg mr-3">
<i class="fas fa-project-diagram text-purple-500"></i>
</div>
<div>
<h3 class="text-sm font-medium text-gray-600 dark:text-gray-300">Knoten</h3>
<p class="text-2xl font-bold text-gray-800 dark:text-white">{{ nodes|length }}</p>
</div>
</div>
</div>
<div class="glass-effect p-4 rounded-lg">
<div class="flex items-center mb-2">
<div class="bg-green-500/20 p-3 rounded-lg mr-3">
<i class="fas fa-lightbulb text-green-500"></i>
</div>
<div>
<h3 class="text-sm font-medium text-gray-600 dark:text-gray-300">Gedanken</h3>
<p class="text-2xl font-bold text-gray-800 dark:text-white">{{ thoughts|length }}</p>
</div>
</div>
</div>
<div class="glass-effect p-4 rounded-lg">
<div class="flex items-center mb-2">
<div class="bg-red-500/20 p-3 rounded-lg mr-3">
<i class="fas fa-comments text-red-500"></i>
</div>
<div>
<h3 class="text-sm font-medium text-gray-600 dark:text-gray-300">Kommentare</h3>
<p class="text-2xl font-bold text-gray-800 dark:text-white">
{% set comment_count = 0 %}
{% for thought in thoughts %}
{% set comment_count = comment_count + thought.comments|length %}
{% endfor %}
{{ comment_count }}
</p>
</div>
</div>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="glass-effect p-4 rounded-lg">
<h3 class="text-lg font-bold mb-4 text-gray-800 dark:text-white">Aktive Benutzer</h3>
<div class="h-64 flex items-center justify-center bg-gray-100/20 dark:bg-dark-700/20 rounded">
<p class="text-gray-500 dark:text-gray-400">Hier würde ein Aktivitätsdiagramm angezeigt werden</p>
</div>
</div>
<div class="glass-effect p-4 rounded-lg">
<h3 class="text-lg font-bold mb-4 text-gray-800 dark:text-white">Gedanken pro Kategorie</h3>
<div class="h-64 flex items-center justify-center bg-gray-100/20 dark:bg-dark-700/20 rounded">
<p class="text-gray-500 dark:text-gray-400">Hier würde eine Verteilungsstatistik angezeigt werden</p>
</div>
</div>
</div>
</div>
</div>
<!-- System-Log (immer sichtbar) -->
<div class="mt-8">
<h2 class="text-xl font-bold mb-4 text-gray-800 dark:text-white">System-Log</h2>
<div class="glass-morphism rounded-lg p-4 h-32 overflow-y-auto font-mono text-sm text-gray-700 dark:text-gray-300">
<div class="text-green-500">[INFO] [{{ now.strftime('%Y-%m-%d %H:%M:%S') }}] System gestartet</div>
<div class="text-blue-500">[INFO] [{{ now.strftime('%Y-%m-%d %H:%M:%S') }}] Admin-Bereich aufgerufen von {{ current_user.username }}</div>
<div class="text-yellow-500">[WARN] [{{ now.strftime('%Y-%m-%d %H:%M:%S') }}] Hohe Serverauslastung erkannt</div>
<div class="text-gray-500">[INFO] [{{ now.strftime('%Y-%m-%d %H:%M:%S') }}] Backup erfolgreich erstellt</div>
<div class="text-red-500">[ERROR] [{{ now.strftime('%Y-%m-%d %H:%M:%S') }}] API-Zugriffsfehler (Timeout) bei externer Anfrage</div>
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% block extra_js %}
<script>
// Admin-spezifische JavaScript-Funktionen
document.addEventListener('DOMContentLoaded', function() {
console.log('Admin-Bereich geladen');
// Beispiel für AJAX-Ladeverhalten von Daten
// Kann später durch echte API-Calls ersetzt werden
});
</script>
{% endblock %}

View File

@@ -22,14 +22,22 @@
<!-- Icons --> <!-- Icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Assistent CSS -->
<link href="{{ url_for('static', filename='css/assistant.css') }}" rel="stylesheet">
<!-- Basis-Stylesheet -->
<link href="{{ url_for('static', filename='style.css') }}" rel="stylesheet">
<!-- Tailwind CSS --> <!-- Tailwind CSS -->
<link href="{{ url_for('static', filename='css/main.css') }}" rel="stylesheet"> <link href="{{ url_for('static', filename='css/main.css') }}" rel="stylesheet">
<!-- Alpine.js --> <!-- Alpine.js -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.12.3/dist/cdn.min.js"></script> <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.12.3/dist/cdn.min.js"></script>
<!-- Theme Script (run before rendering) --> <!-- Hauptmodul laden (als ES6 Modul) -->
<script> <script type="module">
import MindMap from "{{ url_for('static', filename='js/main.js') }}";
// Alpine.js-Integration
document.addEventListener('alpine:init', () => { document.addEventListener('alpine:init', () => {
Alpine.data('layout', () => ({ Alpine.data('layout', () => ({
darkMode: false, darkMode: false,
@@ -87,379 +95,629 @@
} }
})); }));
}); });
// MindMap global verfügbar machen (für Alpine.js und andere nicht-Module Skripte)
window.MindMap = MindMap;
</script> </script>
<!-- Globale Stile für konsistentes Glasmorphismus-Design -->
<style>
/* Globale Variablen */
:root {
--dark-bg: #0e1220;
--dark-card-bg: rgba(24, 28, 45, 0.8);
--dark-element-bg: rgba(24, 28, 45, 0.8);
--light-bg: #f0f4f8;
--light-card-bg: rgba(255, 255, 255, 0.85);
--accent-color: #b38fff;
--accent-gradient: linear-gradient(135deg, #b38fff, #58a9ff);
--blur-amount: 20px;
--border-radius: 24px;
--button-radius: 16px;
}
/* Dark Mode Einstellungen */
html.dark {
color-scheme: dark;
}
/* Base Styles */
html, body {
background-color: var(--dark-bg) !important;
min-height: 100vh;
width: 100%;
color: #ffffff;
margin: 0;
padding: 0;
font-family: 'Inter', system-ui, -apple-system, sans-serif;
overflow-x: hidden;
}
/* Sicherstellen, dass der dunkle Hintergrund die gesamte Seite abdeckt */
#app-container, .container, main, .mx-auto, .py-12, #content-wrapper {
background-color: var(--dark-bg) !important;
width: 100%;
}
/* Light Mode Einstellungen */
html.light, html.light body {
background-color: var(--light-bg) !important;
color: #1a202c;
}
/* Verbesserte Glasmorphismus-Stile */
.glass-morphism {
background: var(--dark-card-bg);
backdrop-filter: blur(var(--blur-amount));
-webkit-backdrop-filter: blur(var(--blur-amount));
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: var(--border-radius);
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.35);
transition: all 0.3s ease;
}
.glass-morphism:hover {
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.45);
transform: translateY(-2px);
}
.glass-morphism-light {
background: var(--light-card-bg);
backdrop-filter: blur(var(--blur-amount));
-webkit-backdrop-filter: blur(var(--blur-amount));
border: 1px solid rgba(0, 0, 0, 0.08);
border-radius: var(--border-radius);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.12);
transition: all 0.3s ease;
}
.glass-morphism-light:hover {
border: 1px solid rgba(0, 0, 0, 0.15);
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.18);
transform: translateY(-2px);
}
/* Verbesserte Button-Stile mit besserer Lesbarkeit */
.btn, button, .button, [type="button"], [type="submit"] {
transition: all 0.25s ease;
border-radius: var(--button-radius);
padding: 0.75rem 1.5rem;
font-weight: 600;
letter-spacing: 0.4px;
color: rgba(255, 255, 255, 1);
background: rgba(80, 90, 130, 0.8);
border: 1px solid rgba(255, 255, 255, 0.18);
box-shadow: 0 6px 14px rgba(0, 0, 0, 0.2);
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
backdrop-filter: blur(15px);
-webkit-backdrop-filter: blur(15px);
position: relative;
overflow: hidden;
}
.btn:hover, button:hover, .button:hover, [type="button"]:hover, [type="submit"]:hover {
background: rgba(179, 143, 255, 0.65);
transform: translateY(-3px);
border: 1px solid rgba(255, 255, 255, 0.3);
box-shadow: 0 8px 22px rgba(0, 0, 0, 0.25), 0 0 12px rgba(179, 143, 255, 0.35);
color: white;
}
.btn:active, button:active, .button:active, [type="button"]:active, [type="submit"]:active {
transform: translateY(1px);
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.2);
}
/* Navigation Stile */
.nav-link {
transition: all 0.25s ease;
border-radius: var(--border-radius);
padding: 10px 18px;
font-weight: 500;
letter-spacing: 0.3px;
color: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(var(--blur-amount));
-webkit-backdrop-filter: blur(var(--blur-amount));
position: relative;
overflow: visible;
height: 40px !important;
}
.nav-link:hover {
background: rgba(179, 143, 255, 0.2);
color: white;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.nav-link.active {
background: rgba(179, 143, 255, 0.3);
color: white;
font-weight: 600;
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.18);
}
.nav-link.active::after {
content: '';
position: absolute;
bottom: 0;
left: 10%;
width: 80%;
height: 2px;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.7), transparent);
}
/* Light-Mode Navigation Stile */
.nav-link-light {
color: rgba(26, 32, 44, 0.9);
}
.nav-link-light:hover {
background: rgba(179, 143, 255, 0.15);
color: rgba(26, 32, 44, 1);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
}
.nav-link-light.active {
background: rgba(179, 143, 255, 0.2);
color: rgba(26, 32, 44, 1);
font-weight: 600;
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.1);
}
.nav-link-light.active::after {
background: linear-gradient(90deg, transparent, rgba(26, 32, 44, 0.5), transparent);
}
/* Entfernung von Gradient-Hintergrund überall */
.gradient-bg, .purple-gradient, .gradient-purple-bg {
background: var(--dark-bg) !important;
background-image: none !important;
}
/* Verbesserte Light-Mode-Stile für Buttons */
html.light .btn, html.light button, html.light .button,
html.light [type="button"], html.light [type="submit"] {
background: rgba(255, 255, 255, 0.85);
color: #1a202c;
border: 1px solid rgba(0, 0, 0, 0.1);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
text-shadow: none;
}
html.light .btn:hover, html.light button:hover, html.light .button:hover,
html.light [type="button"]:hover, html.light [type="submit"]:hover {
background: rgba(179, 143, 255, 0.85);
color: #ffffff;
border: 1px solid rgba(0, 0, 0, 0.08);
box-shadow: 0 8px 22px rgba(0, 0, 0, 0.12), 0 0 12px rgba(179, 143, 255, 0.2);
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}
/* Verbesserte Buttons mit Glasmorphismus */
.btn-primary {
background: linear-gradient(135deg, rgba(179, 143, 255, 0.8), rgba(88, 169, 255, 0.8));
backdrop-filter: blur(var(--blur-amount));
-webkit-backdrop-filter: blur(var(--blur-amount));
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: var(--button-radius);
color: white !important;
font-weight: 600;
padding: 0.75rem 1.5rem;
transition: all 0.3s ease;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
}
.btn-primary:hover {
transform: translateY(-3px);
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.35);
background: linear-gradient(135deg, rgba(190, 160, 255, 0.9), rgba(100, 180, 255, 0.9));
}
.btn-secondary {
background: rgba(32, 36, 55, 0.8);
backdrop-filter: blur(var(--blur-amount));
-webkit-backdrop-filter: blur(var(--blur-amount));
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: var(--button-radius);
color: white;
font-weight: 500;
padding: 0.75rem 1.5rem;
transition: all 0.3s ease;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.2);
}
.btn-secondary:hover {
transform: translateY(-3px);
box-shadow: 0 10px 28px rgba(0, 0, 0, 0.3);
background: rgba(38, 42, 65, 0.9);
border: 1px solid rgba(255, 255, 255, 0.15);
}
/* Steuerungsbutton-Stil */
.control-btn {
padding: 0.5rem 1rem;
background: rgba(32, 36, 55, 0.8);
color: rgba(255, 255, 255, 0.9);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 12px;
font-size: 0.875rem;
font-weight: 500;
transition: all 0.25s ease;
backdrop-filter: blur(15px);
-webkit-backdrop-filter: blur(15px);
}
.control-btn:hover {
background: rgba(38, 42, 65, 0.9);
border: 1px solid rgba(255, 255, 255, 0.2);
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.25);
}
/* Verbesserter Farbverlauf-Text */
.gradient-text {
background: var(--accent-gradient);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
font-weight: 700;
letter-spacing: -0.02em;
}
/* Kartenstil für Feature-Cards */
.feature-card {
background: rgba(24, 28, 45, 0.8);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 24px;
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.35);
transition: all 0.3s ease;
padding: 1.75rem;
height: 100%;
display: flex;
flex-direction: column;
}
.feature-card:hover {
transform: translateY(-5px);
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 20px 48px rgba(0, 0, 0, 0.45);
background: rgba(32, 36, 55, 0.9);
}
.feature-card .icon {
font-size: 2.75rem;
margin-bottom: 1.25rem;
background: linear-gradient(135deg, #b38fff, #58a9ff);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
filter: drop-shadow(0 0 10px rgba(179, 143, 255, 0.6));
}
.feature-card h3 {
font-size: 1.75rem;
font-weight: 700;
margin-bottom: 0.9rem;
color: #ffffff;
text-shadow: 0 2px 3px rgba(0, 0, 0, 0.3);
letter-spacing: 0.2px;
}
.feature-card p {
color: rgba(255, 255, 255, 0.95);
font-size: 1.1rem;
line-height: 1.6;
font-weight: 400;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.25);
letter-spacing: 0.3px;
}
/* Light Mode Anpassungen für Feature-Cards */
html.light .feature-card {
background: rgba(255, 255, 255, 0.85);
border: 1px solid rgba(0, 0, 0, 0.08);
color: #1a202c;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.12);
}
html.light .feature-card:hover {
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.15);
}
html.light .feature-card h3 {
color: #1a202c;
text-shadow: none;
}
html.light .feature-card p {
color: rgba(26, 32, 44, 0.9);
text-shadow: none;
}
/* Globaler Hintergrund */
.full-page-bg {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: var(--dark-bg);
z-index: -10;
}
html.light .full-page-bg {
background-color: var(--light-bg);
}
/* Animationen für Hintergrundeffekte */
@keyframes float {
0% { transform: translateY(0); }
50% { transform: translateY(-12px); }
100% { transform: translateY(0); }
}
@keyframes pulse {
0% { opacity: 0.7; transform: scale(1); }
50% { opacity: 1; transform: scale(1.05); }
100% { opacity: 0.7; transform: scale(1); }
}
.animate-float {
animation: float 6s ease-in-out infinite;
}
.animate-pulse {
animation: pulse 3s ease-in-out infinite;
}
/* Verbesserter Container für konsistente Layouts */
.page-container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem 1rem;
}
.dot {
transform: translateX(0);
transition: transform 0.3s ease-in-out, background-color 0.3s ease;
}
input:checked ~ .dot {
transform: translateX(100%);
background-color: #b38fff; /* Angepasst an Farbschema */
}
input:checked ~ .block {
background-color: rgba(179, 143, 255, 0.4); /* Angepasst an Farbschema */
}
</style>
<!-- Seitenspezifische Styles -->
{% block extra_css %}{% endblock %} {% block extra_css %}{% endblock %}
</head> </head>
<body x-data="{ <body data-page="{{ request.endpoint }}" class="relative overflow-x-hidden">
darkMode: true, <!-- Globaler Hintergrund -->
mobileMenuOpen: false, <div class="full-page-bg"></div>
userMenuOpen: false,
searchOpen: false,
showSettingsModal: false,
toggleDarkMode() { <!-- App-Container -->
this.darkMode = !this.darkMode; <div id="app-container" class="flex flex-col min-h-screen" x-data="layout">
document.querySelector('html').classList.toggle('dark', this.darkMode); <!-- Hauptnavigation -->
<nav style="position: sticky; top: 0; left: 0; right: 0; backdrop-filter: blur(15px); -webkit-backdrop-filter: blur(15px); background: rgba(14, 18, 32, 0.75); box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2); border-bottom: 1px solid rgba(255, 255, 255, 0.1); padding: 1rem 2rem; z-index: 100; transition: all 0.3s ease; display: flex; justify-content: space-between; align-items: center;" x-bind:style="darkMode ? 'background: rgba(14, 18, 32, 0.75);' : 'background: rgba(255, 255, 255, 0.75); border-bottom: 1px solid rgba(0, 0, 0, 0.05);'">
// Speichere den Dark Mode-Status auf dem Server <!-- Logo -->
fetch('/set_dark_mode', { <a href="{{ url_for('index') }}" style="display: flex; align-items: center; text-decoration: none;">
method: 'POST', <span style="font-size: 1.5rem; font-weight: 700; background: linear-gradient(135deg, #b38fff, #58a9ff); -webkit-background-clip: text; background-clip: text; color: transparent;">MindMap</span>
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ darkMode: this.darkMode })
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Zusätzlich im localStorage speichern für sofortige Reaktion bei Seitenwechsel
localStorage.setItem('darkMode', this.darkMode ? 'dark' : 'light');
// Event auslösen für andere Komponenten
document.dispatchEvent(new CustomEvent('darkModeToggled', {
detail: { isDark: this.darkMode }
}));
} else {
console.error('Fehler beim Speichern der Dark Mode-Einstellung:', data.error);
}
})
.catch(error => {
console.error('Fehler beim Speichern der Dark Mode-Einstellung:', error);
});
}
}"
:class="{ 'dark': darkMode }"
class="font-sans antialiased relative min-h-screen bg-gray-50 text-gray-900"
x-init="fetch('/get_dark_mode')
.then(response => response.json())
.then(data => {
if (data.success) {
darkMode = data.darkMode === 'true';
document.querySelector('html').classList.toggle('dark', darkMode);
} else {
// Fallback zu localStorage wenn Server-Abfrage fehlschlägt
const savedTheme = localStorage.getItem('darkMode');
if (savedTheme) {
darkMode = savedTheme === 'dark';
document.querySelector('html').classList.toggle('dark', darkMode);
}
}
})
.catch(error => {
console.error('Fehler beim Laden der Dark Mode-Einstellung:', error);
// Fallback zu localStorage wenn Fetch fehlschlägt
const savedTheme = localStorage.getItem('darkMode');
if (savedTheme) {
darkMode = savedTheme === 'dark';
document.querySelector('html').classList.toggle('dark', darkMode);
}
})">
<!-- CSRF Token für Fetch-Requests -->
<!-- Hintergrund -->
<div class="fixed inset-0 -z-10 bg-gray-50 dark:bg-dark-900">
<div class="absolute inset-0 bg-gradient-to-br from-primary-100/30 via-white dark:from-primary-900/30 dark:via-dark-900 to-secondary-100/30 dark:to-secondary-900/30"></div>
<div class="absolute inset-0 opacity-30">
<div class="h-full w-full" style="background-image: radial-gradient(rgba(0, 0, 0, 0.1) 1px, transparent 1px); background-size: 30px 30px;"></div>
</div>
</div>
<!-- Header -->
<header class="relative z-10">
<nav class="glass-effect border-b border-gray-200 dark:border-white/10 backdrop-blur-lg" style="border-radius: 20px !important;">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<!-- Logo und Desktop Navigation -->
<div class="flex">
<div class="flex-shrink-0 flex items-center">
<a href="{{ url_for('index') }}" class="flex items-center">
<img class="h-8 w-auto mr-2" src="{{ url_for('static', filename='img/favicon.svg') }}" alt="MindMap Logo">
<span class="text-gray-800 dark:text-white font-bold text-xl">Mind<span class="gradient-text">Map</span></span>
</a> </a>
</div>
<div class="hidden sm:ml-6 sm:flex sm:space-x-8"> <!-- Hauptnavigation - Desktop -->
<a href="{{ url_for('mindmap') }}" class="text-gray-700 dark:text-gray-200 hover:text-primary-600 dark:hover:text-white inline-flex items-center px-1 pt-1 border-b-2 {% if request.path == url_for('mindmap') %}border-primary-400{% else %}border-transparent{% endif %}"> <div style="display: none; align-items: center; gap: 1rem;" x-bind:style="window.innerWidth >= 768 ? 'display: flex;' : 'display: none;'">
<i class="fa-solid fa-diagram-project mr-2"></i> Mindmap <a href="{{ url_for('index') }}"
style="display: flex; align-items: center; padding: 0.5rem 1rem; border-radius: 0.75rem; font-weight: 500; transition: all 0.25s ease; backdrop-filter: blur(15px); -webkit-backdrop-filter: blur(15px);"
x-bind:style="darkMode
? '{{ request.endpoint == 'index' ? 'background: rgba(179, 143, 255, 0.3); color: white; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);' : 'background: transparent; color: rgba(255, 255, 255, 0.9);' }}'
: '{{ request.endpoint == 'index' ? 'background: rgba(179, 143, 255, 0.2); color: rgba(26, 32, 44, 1); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);' : 'background: transparent; color: rgba(26, 32, 44, 0.9);' }}'">
<i class="fa-solid fa-home" style="margin-right: 0.5rem;"></i>Start
</a> </a>
<a href="{{ url_for('search_thoughts_page') }}" class="text-gray-700 dark:text-gray-200 hover:text-primary-600 dark:hover:text-white inline-flex items-center px-1 pt-1 border-b-2 {% if request.path == url_for('search_thoughts_page') %}border-primary-400{% else %}border-transparent{% endif %}"> <a href="{{ url_for('mindmap') }}"
<i class="fa-solid fa-magnifying-glass mr-2"></i> Suche style="display: flex; align-items: center; padding: 0.5rem 1rem; border-radius: 0.75rem; font-weight: 500; transition: all 0.25s ease; backdrop-filter: blur(15px); -webkit-backdrop-filter: blur(15px);"
x-bind:style="darkMode
? '{{ request.endpoint == 'mindmap' ? 'background: rgba(179, 143, 255, 0.3); color: white; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);' : 'background: transparent; color: rgba(255, 255, 255, 0.9);' }}'
: '{{ request.endpoint == 'mindmap' ? 'background: rgba(179, 143, 255, 0.2); color: rgba(26, 32, 44, 1); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);' : 'background: transparent; color: rgba(26, 32, 44, 0.9);' }}'">
<i class="fa-solid fa-diagram-project" style="margin-right: 0.5rem;"></i>Mindmap
</a> </a>
{% if current_user.is_authenticated %} {% if current_user.is_authenticated %}
<a href="{{ url_for('profile') }}" class="text-gray-700 dark:text-gray-200 hover:text-primary-600 dark:hover:text-white inline-flex items-center px-1 pt-1 border-b-2 {% if request.path == url_for('profile') %}border-primary-400{% else %}border-transparent{% endif %}"> <a href="{{ url_for('profile') }}"
<i class="fa-solid fa-user mr-2"></i> Profil style="display: flex; align-items: center; padding: 0.5rem 1rem; border-radius: 0.75rem; font-weight: 500; transition: all 0.25s ease; backdrop-filter: blur(15px); -webkit-backdrop-filter: blur(15px);"
</a> x-bind:style="darkMode
{% if current_user.is_admin %} ? '{{ request.endpoint == 'profile' ? 'background: rgba(179, 143, 255, 0.3); color: white; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);' : 'background: transparent; color: rgba(255, 255, 255, 0.9);' }}'
<a href="{{ url_for('admin') }}" class="text-gray-700 dark:text-gray-200 hover:text-primary-600 dark:hover:text-white inline-flex items-center px-1 pt-1 border-b-2 {% if request.path == url_for('admin') %}border-primary-400{% else %}border-transparent{% endif %}"> : '{{ request.endpoint == 'profile' ? 'background: rgba(179, 143, 255, 0.2); color: rgba(26, 32, 44, 1); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);' : 'background: transparent; color: rgba(26, 32, 44, 0.9);' }}'">
<i class="fa-solid fa-screwdriver-wrench mr-2"></i> Admin <i class="fa-solid fa-user" style="margin-right: 0.5rem;"></i>Profil
</a> </a>
{% endif %} {% endif %}
{% endif %} </div>
<!-- Rechte Seite -->
<div style="display: flex; align-items: center; gap: 1rem;">
<!-- Dark Mode Toggle Switch -->
<div style="display: flex; align-items: center; cursor: pointer;" @click="toggleDarkMode">
<div style="position: relative; width: 2.5rem; height: 1.5rem;">
<input type="checkbox" id="darkModeToggle" style="position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border-width: 0;" x-model="darkMode">
<div x-bind:style="darkMode ? 'background: rgba(88, 169, 255, 0.5);' : 'background: rgba(100, 100, 100, 0.5);'" style="width: 2.5rem; height: 1.5rem; border-radius: 9999px; transition: background-color 0.3s ease;"></div>
<div x-bind:style="darkMode ? 'transform: translateX(1rem); background-color: #58a9ff;' : 'transform: translateX(0); background-color: #ffffff;'" style="position: absolute; left: 0.25rem; top: 0.25rem; width: 1rem; height: 1rem; border-radius: 9999px; transition: transform 0.3s ease, background-color 0.3s ease; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);"></div>
</div>
<div x-bind:style="darkMode ? 'color: rgba(255, 255, 255, 0.9);' : 'color: rgba(26, 32, 44, 0.9);'" style="margin-left: 0.75rem; display: none;" x-bind:style="window.innerWidth >= 640 ? 'display: block;' : 'display: none;'">
<span x-text="darkMode ? 'Hell' : 'Dunkel'"></span>
</div>
<div x-bind:style="darkMode ? 'color: rgba(255, 255, 255, 0.9);' : 'color: rgba(26, 32, 44, 0.9);'" style="margin-left: 0.75rem;" x-bind:style="window.innerWidth < 640 ? 'display: block;' : 'display: none;'">
<i class="fa-solid" :class="darkMode ? 'fa-sun' : 'fa-moon'"></i>
</div> </div>
</div> </div>
<!-- Rechte Aktionsleiste (Desktop) --> <!-- Mobilmenü-Button -->
<div class="hidden sm:ml-6 sm:flex sm:items-center"> <button @click="mobileMenuOpen = !mobileMenuOpen" style="display: none; padding: 0.5rem; border-radius: 0.5rem; border: none; background: transparent; cursor: pointer;" x-bind:style="window.innerWidth < 768 ? 'display: block;' : 'display: none;'">
<!-- Dark Mode Toggle --> <i class="fa-solid fa-bars" x-bind:style="darkMode ? 'color: rgba(255, 255, 255, 0.9);' : 'color: rgba(26, 32, 44, 0.9);'" style="font-size: 1.25rem;"></i>
<button
type="button"
class="rounded-full p-2 text-gray-700 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-secondary-500 dark:text-gray-200 dark:hover:bg-dark-700 z-20"
@click="toggleDarkMode()"
aria-label="Toggle dark mode">
<!-- Sun icon -->
<svg
x-show="!darkMode"
class="h-5 w-5 inline-block"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
<!-- Moon icon -->
<svg
x-show="darkMode"
class="h-5 w-5 inline-block"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
</svg>
</button> </button>
<!-- Nutzer-Menü --> <!-- Benutzermenü oder Login -->
{% if current_user.is_authenticated %} {% if current_user.is_authenticated %}
<div class="ml-3 relative"> <div style="position: relative;" x-data="{ open: false }">
<div> <button @click="open = !open" style="display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem; border-radius: 9999px; border: none; transition: all 0.25s ease; cursor: pointer; outline: none;"
<button @click="userMenuOpen = !userMenuOpen" class="flex rounded-full focus:outline-none"> x-bind:style="darkMode
<span class="sr-only">User Menü öffnen</span> ? 'background: rgba(32, 36, 55, 0.8); color: rgba(255, 255, 255, 0.9);'
<div class="h-8 w-8 rounded-full bg-gradient-to-r from-primary-400 to-primary-600 flex items-center justify-center text-white font-bold"> : 'background: rgba(240, 240, 240, 0.8); color: rgba(26, 32, 44, 0.9);'">
{{ current_user.username[0] | upper }} <div style="width: 2rem; height: 2rem; border-radius: 9999px; display: flex; align-items: center; justify-content: center; color: white; font-weight: 500; font-size: 0.875rem; overflow: hidden; background-color: #6366f1;">
</div> {% if current_user.avatar %}
</button> <img src="{{ current_user.avatar }}" alt="{{ current_user.username }}" style="width: 100%; height: 100%; object-fit: cover;">
</div>
<div x-show="userMenuOpen"
@click.away="userMenuOpen = false"
class="origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white dark:bg-dark-800 ring-1 ring-black ring-opacity-5 focus:outline-none z-50"
x-transition:enter="transition ease-out duration-100"
x-transition:enter-start="transform opacity-0 scale-95"
x-transition:enter-end="transform opacity-100 scale-100"
x-transition:leave="transition ease-in duration-75"
x-transition:leave-start="transform opacity-100 scale-100"
x-transition:leave-end="transform opacity-0 scale-95">
<a href="{{ url_for('settings') }}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-white/10">
<i class="fa-solid fa-gear mr-2"></i> Einstellungen
</a>
<a href="{{ url_for('logout') }}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-white/10">
<i class="fa-solid fa-sign-out-alt mr-2"></i> Abmelden
</a>
</div>
</div>
{% else %} {% else %}
<div class="ml-3 flex items-center space-x-4">
<a href="{{ url_for('login') }}" class="text-gray-700 hover:text-primary-600 dark:text-gray-200 dark:hover:text-white">Anmelden</a>
<a href="{{ url_for('register') }}" class="btn-primary">Registrieren</a>
</div>
{% endif %}
</div>
<!-- Mobile menu button -->
<div class="absolute inset-y-0 left-0 flex items-center sm:hidden">
<button @click="mobileMenuOpen = !mobileMenuOpen"
class="inline-flex items-center justify-center p-2 rounded-md text-gray-600 hover:text-gray-900 hover:bg-gray-100 dark:text-gray-300 dark:hover:text-white dark:hover:bg-white/10 focus:outline-none"
aria-expanded="false">
<span class="sr-only">Menü öffnen</span>
<svg x-show="!mobileMenuOpen" class="h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
</svg>
<!-- Mobile Menu Button -->
<div class="flex items-center sm:hidden">
<button @click="mobileMenuOpen = !mobileMenuOpen" type="button" class="inline-flex items-center justify-center p-2 rounded-md text-gray-600 hover:text-gray-900 hover:bg-gray-100 dark:text-gray-200 dark:hover:text-white dark:hover:bg-white/10 focus:outline-none">
<span class="sr-only">Menü öffnen</span>
<i x-show="!mobileMenuOpen" class="fa-solid fa-bars text-xl"></i>
<i x-show="mobileMenuOpen" class="fa-solid fa-xmark text-xl"></i>
</button>
</div>
</div>
</div>
<!-- Mobile Menu -->
<div x-show="mobileMenuOpen" class="sm:hidden glass-effect" id="mobile-menu">
<div class="pt-2 pb-3 space-y-1">
<a href="{{ url_for('mindmap') }}" class="text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:text-white dark:hover:bg-white/10 block pl-3 pr-4 py-2 text-base font-medium {% if request.path == url_for('mindmap') %}bg-gray-100 dark:bg-white/10 border-l-4 border-primary-400{% endif %}">
<i class="fa-solid fa-diagram-project mr-2"></i> Mindmap
</a>
<a href="{{ url_for('search_thoughts_page') }}" class="text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:text-white dark:hover:bg-white/10 block pl-3 pr-4 py-2 text-base font-medium {% if request.path == url_for('search_thoughts_page') %}bg-gray-100 dark:bg-white/10 border-l-4 border-primary-400{% endif %}">
<i class="fa-solid fa-magnifying-glass mr-2"></i> Suche
</a>
{% if current_user.is_authenticated %}
<a href="{{ url_for('profile') }}" class="text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:text-white dark:hover:bg-white/10 block pl-3 pr-4 py-2 text-base font-medium {% if request.path == url_for('profile') %}bg-gray-100 dark:bg-white/10 border-l-4 border-primary-400{% endif %}">
<i class="fa-solid fa-user mr-2"></i> Profil
</a>
{% if current_user.is_admin %}
<a href="{{ url_for('admin') }}" class="text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:text-white dark:hover:bg-white/10 block pl-3 pr-4 py-2 text-base font-medium {% if request.path == url_for('admin') %}bg-gray-100 dark:bg-white/10 border-l-4 border-primary-400{% endif %}">
<i class="fa-solid fa-screwdriver-wrench mr-2"></i> Admin
</a>
{% endif %}
{% endif %}
</div>
<!-- Mobile Benutzer-Menü -->
<div class="pt-4 pb-3 border-t border-gray-200 dark:border-white/10">
{% if current_user.is_authenticated %}
<div class="flex items-center px-4">
<div class="flex-shrink-0">
<div class="h-10 w-10 rounded-full bg-primary-500 flex items-center justify-center text-white font-bold">
{{ current_user.username[0].upper() }} {{ current_user.username[0].upper() }}
</div>
</div>
<div class="ml-3">
<div class="text-base font-medium text-gray-800 dark:text-white">{{ current_user.username }}</div>
<div class="text-sm font-medium text-gray-600 dark:text-gray-300">{{ current_user.email }}</div>
</div>
<button @click="toggleDarkMode()" class="ml-auto p-1 text-gray-600 hover:text-gray-900 dark:text-gray-200 dark:hover:text-white focus:outline-none">
<svg
x-show="!darkMode"
class="h-5 w-5"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
<svg
x-show="darkMode"
class="h-5 w-5"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
</svg>
</button>
</div>
<div class="mt-3 space-y-1 px-2">
<a href="{{ url_for('profile') }}" class="block px-3 py-2 rounded-md text-base font-medium text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:text-white dark:hover:bg-white/10">Dein Profil</a>
<a href="{{ url_for('settings') }}" class="block px-3 py-2 rounded-md text-base font-medium text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:text-white dark:hover:bg-white/10">Einstellungen</a>
<a href="{{ url_for('logout') }}" class="block px-3 py-2 rounded-md text-base font-medium text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:text-white dark:hover:bg-white/10">Abmelden</a>
</div>
{% else %}
<div class="mt-3 space-y-1 px-2">
<a href="{{ url_for('login') }}" class="block px-3 py-2 rounded-md text-base font-medium text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:text-white dark:hover:bg-white/10">Anmelden</a>
<a href="{{ url_for('register') }}" class="block px-3 py-2 rounded-md text-base font-medium text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:text-white dark:hover:bg-white/10">Registrieren</a>
</div>
{% endif %} {% endif %}
</div> </div>
<span style="font-size: 0.875rem; display: none;" x-bind:style="window.innerWidth >= 1024 ? 'display: block;' : 'display: none;'">{{ current_user.username }}</span>
<i class="fa-solid fa-chevron-down" style="font-size: 0.75rem; transform: rotate(0deg); transition: transform 0.2s ease; display: none;" x-bind:style="open ? 'transform: rotate(180deg);' : 'transform: rotate(0deg);'" x-bind:style="window.innerWidth >= 1024 ? 'display: block;' : 'display: none;'"></i>
</button>
<!-- Dropdown-Menü -->
<div x-show="open"
@click.away="open = false"
x-transition:enter="transition ease-out duration-100"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-75"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
style="position: absolute; right: 0; margin-top: 0.5rem; width: 12rem; border-radius: 0.75rem; overflow: hidden; z-index: 50; box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2); transform-origin: top right; transition: all 0.3s ease;"
x-bind:style="darkMode
? 'background: rgba(24, 28, 45, 0.95); backdrop-filter: blur(15px); -webkit-backdrop-filter: blur(15px); border: 1px solid rgba(255, 255, 255, 0.1);'
: 'background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(15px); -webkit-backdrop-filter: blur(15px); border: 1px solid rgba(0, 0, 0, 0.05);'">
<a href="{{ url_for('profile') }}" style="display: block; padding: 0.75rem 1rem; text-decoration: none; transition: all 0.25s ease;"
x-bind:style="darkMode
? 'color: rgba(255, 255, 255, 0.9);'
: 'color: rgba(26, 32, 44, 0.9);'"
onmouseover="this.style.backgroundColor = this.getAttribute('data-hover-bg')"
onmouseout="this.style.backgroundColor = 'transparent'"
x-bind:data-hover-bg="darkMode ? 'rgba(179, 143, 255, 0.2)' : 'rgba(179, 143, 255, 0.1)'">
<i class="fa-solid fa-user" style="margin-right: 0.5rem; color: #b38fff;"></i>Profil
</a>
<a href="{{ url_for('settings') }}" style="display: block; padding: 0.75rem 1rem; text-decoration: none; transition: all 0.25s ease;"
x-bind:style="darkMode
? 'color: rgba(255, 255, 255, 0.9);'
: 'color: rgba(26, 32, 44, 0.9);'"
onmouseover="this.style.backgroundColor = this.getAttribute('data-hover-bg')"
onmouseout="this.style.backgroundColor = 'transparent'"
x-bind:data-hover-bg="darkMode ? 'rgba(179, 143, 255, 0.2)' : 'rgba(179, 143, 255, 0.1)'">
<i class="fa-solid fa-gear" style="margin-right: 0.5rem; color: #b38fff;"></i>Einstellungen
</a>
<div style="margin: 0.5rem 0; height: 1px;" x-bind:style="darkMode ? 'background: rgba(255, 255, 255, 0.1);' : 'background: rgba(0, 0, 0, 0.05);'"></div>
<a href="{{ url_for('logout') }}" style="display: block; padding: 0.75rem 1rem; text-decoration: none; transition: all 0.25s ease;"
x-bind:style="darkMode
? 'color: rgba(255, 255, 255, 0.9);'
: 'color: rgba(26, 32, 44, 0.9);'"
onmouseover="this.style.backgroundColor = this.getAttribute('data-hover-bg')"
onmouseout="this.style.backgroundColor = 'transparent'"
x-bind:data-hover-bg="darkMode ? 'rgba(239, 68, 68, 0.2)' : 'rgba(239, 68, 68, 0.1)'">
<i class="fa-solid fa-right-from-bracket" style="margin-right: 0.5rem; color: #ef4444;"></i>Abmelden
</a>
</div>
</div>
{% else %}
<a href="{{ url_for('login') }}" style="display: flex; align-items: center; padding: 0.5rem 1rem; border-radius: 0.75rem; font-weight: 500; text-decoration: none; transition: all 0.25s ease; backdrop-filter: blur(15px); -webkit-backdrop-filter: blur(15px);"
x-bind:style="darkMode
? 'background: rgba(179, 143, 255, 0.3); color: white; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);'
: 'background: rgba(179, 143, 255, 0.2); color: rgba(26, 32, 44, 1); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);'">
<i class="fa-solid fa-right-to-bracket" style="margin-right: 0.5rem;"></i>Anmelden
</a>
{% endif %}
</div> </div>
</nav> </nav>
</header>
<!-- Flash Messages --> <!-- Mobile Menü (erscheint, wenn mobileMenuOpen true ist) -->
<div class="container max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-4 z-20"> <div x-show="mobileMenuOpen"
{% with messages = get_flashed_messages(with_categories=true) %} x-transition:enter="transition ease-out duration-200"
{% if messages %} x-transition:enter-start="opacity-0 -translate-y-10"
{% for category, message in messages %} x-transition:enter-end="opacity-100 translate-y-0"
<div x-data="{ show: true }" x-show="show" x-transition class="mb-4 glass-effect border {% if category == 'error' %}border-red-500/50{% elif category == 'success' %}border-green-500/50{% else %}border-blue-500/50{% endif %} rounded-lg p-4"> x-transition:leave="transition ease-in duration-150"
<div class="flex items-start"> x-transition:leave-start="opacity-100 translate-y-0"
<div class="flex-shrink-0"> x-transition:leave-end="opacity-0 -translate-y-10"
{% if category == 'error' %} style="width: 100%; overflow: hidden; z-index: 90;"
<i class="fa-solid fa-circle-exclamation text-red-500"></i> x-bind:style="darkMode
{% elif category == 'success' %} ? 'background: rgba(14, 18, 32, 0.95); backdrop-filter: blur(15px); -webkit-backdrop-filter: blur(15px); border-bottom: 1px solid rgba(255, 255, 255, 0.1);'
<i class="fa-solid fa-circle-check text-green-500"></i> : 'background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(15px); -webkit-backdrop-filter: blur(15px); border-bottom: 1px solid rgba(0, 0, 0, 0.05);'">
{% else %} <div style="display: flex; flex-direction: column; padding: 1rem;">
<i class="fa-solid fa-circle-info text-blue-500"></i> <a href="{{ url_for('index') }}"
style="display: flex; align-items: center; padding: 0.75rem 1rem; margin-bottom: 0.5rem; border-radius: 0.75rem; font-weight: 500; text-decoration: none; transition: all 0.25s ease;"
x-bind:style="darkMode
? '{{ request.endpoint == 'index' ? 'background: rgba(179, 143, 255, 0.3); color: white;' : 'background: transparent; color: rgba(255, 255, 255, 0.9);' }}'
: '{{ request.endpoint == 'index' ? 'background: rgba(179, 143, 255, 0.2); color: rgba(26, 32, 44, 1);' : 'background: transparent; color: rgba(26, 32, 44, 0.9);' }}'">
<i class="fa-solid fa-home" style="margin-right: 0.5rem; width: 1.25rem;"></i>Start
</a>
<a href="{{ url_for('mindmap') }}"
style="display: flex; align-items: center; padding: 0.75rem 1rem; margin-bottom: 0.5rem; border-radius: 0.75rem; font-weight: 500; text-decoration: none; transition: all 0.25s ease;"
x-bind:style="darkMode
? '{{ request.endpoint == 'mindmap' ? 'background: rgba(179, 143, 255, 0.3); color: white;' : 'background: transparent; color: rgba(255, 255, 255, 0.9);' }}'
: '{{ request.endpoint == 'mindmap' ? 'background: rgba(179, 143, 255, 0.2); color: rgba(26, 32, 44, 1);' : 'background: transparent; color: rgba(26, 32, 44, 0.9);' }}'">
<i class="fa-solid fa-diagram-project" style="margin-right: 0.5rem; width: 1.25rem;"></i>Mindmap
</a>
{% if current_user.is_authenticated %}
<a href="{{ url_for('profile') }}"
style="display: flex; align-items: center; padding: 0.75rem 1rem; margin-bottom: 0.5rem; border-radius: 0.75rem; font-weight: 500; text-decoration: none; transition: all 0.25s ease;"
x-bind:style="darkMode
? '{{ request.endpoint == 'profile' ? 'background: rgba(179, 143, 255, 0.3); color: white;' : 'background: transparent; color: rgba(255, 255, 255, 0.9);' }}'
: '{{ request.endpoint == 'profile' ? 'background: rgba(179, 143, 255, 0.2); color: rgba(26, 32, 44, 1);' : 'background: transparent; color: rgba(26, 32, 44, 0.9);' }}'">
<i class="fa-solid fa-user" style="margin-right: 0.5rem; width: 1.25rem;"></i>Profil
</a>
{% endif %} {% endif %}
</div> </div>
<div class="ml-3 flex-1 pt-0.5">
<p class="text-sm text-gray-700 dark:text-gray-200">{{ message }}</p>
</div>
<div class="ml-4 flex-shrink-0 flex">
<button @click="show = false" type="button" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 focus:outline-none">
<span class="sr-only">Schließen</span>
<i class="fa-solid fa-xmark"></i>
</button>
</div>
</div>
</div>
{% endfor %}
{% endif %}
{% endwith %}
</div> </div>
<!-- Hauptinhalt --> <!-- Hauptinhalt -->
<main class="flex-grow container max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 my-6 relative z-10"> <main class="flex-grow pt-4">
{% block content %}{% endblock %} {% block content %}{% endblock %}
</main> </main>
<!-- Footer --> <!-- Footer -->
<footer class="mt-auto py-6 glass-effect border-t border-gray-200 dark:border-white/10 relative z-10"> <footer class="mt-12 py-8 transition-colors duration-300" {# mt-16 changed to mt-12, py-10 changed to py-8 #}
<div class="container max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> :class="darkMode ? 'glass-morphism' : 'glass-morphism-light'">
<div class="md:flex md:justify-between"> <div class="container mx-auto px-4">
<div class="mb-6 md:mb-0"> <div class="flex flex-col md:flex-row justify-between items-center md:items-start">
<a href="{{ url_for('index') }}" class="flex items-center"> <div class="mb-8 md:mb-0 text-center md:text-left">
<img class="h-8 w-auto mr-2" src="{{ url_for('static', filename='img/favicon.svg') }}" alt="MindMap Logo"> <a href="{{ url_for('index') }}" class="text-2xl font-bold gradient-text">MindMap</a>
<span class="text-gray-800 dark:text-white font-bold text-xl">Mind<span class="gradient-text">Map</span></span> <p class="mt-2 text-sm max-w-sm"
</a> :class="darkMode ? 'text-gray-400' : 'text-gray-600'">Eine interaktive Plattform zum Visualisieren, Erforschen und Teilen von Wissen.</p>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-300 max-w-sm">
Eine interaktive Plattform zum Visualisieren, Erforschen und Teilen von Wissen.
</p>
</div> </div>
<div class="grid grid-cols-1 gap-8 sm:grid-cols-2 md:grid-cols-3"> <div class="flex flex-wrap justify-center md:justify-start space-x-6 mb-8 md:mb-0">
<div>
<h3 class="text-sm font-semibold text-gray-800 dark:text-white tracking-wider uppercase">Navigieren</h3> <a href="{{ url_for('impressum') }}"
<ul class="mt-4 space-y-2"> class="transition-colors text-sm px-3 py-2 rounded-lg"
<li><a href="{{ url_for('index') }}" class="text-gray-600 hover:text-primary-600 dark:text-gray-300 dark:hover:text-white">Startseite</a></li> :class="darkMode ? 'text-gray-400 hover:text-white hover:bg-gray-800/50' : 'text-gray-700 hover:text-gray-900 hover:bg-gray-200/50'">Impressum</a>
<li><a href="{{ url_for('mindmap') }}" class="text-gray-600 hover:text-primary-600 dark:text-gray-300 dark:hover:text-white">Mindmap</a></li> <a href="{{ url_for('datenschutz') }}"
<li><a href="{{ url_for('search_thoughts_page') }}" class="text-gray-600 hover:text-primary-600 dark:text-gray-300 dark:hover:text-white">Suche</a></li> class="transition-colors text-sm px-3 py-2 rounded-lg"
</ul> :class="darkMode ? 'text-gray-400 hover:text-white hover:bg-gray-800/50' : 'text-gray-700 hover:text-gray-900 hover:bg-gray-200/50'">Datenschutz</a>
<a href="{{ url_for('agb') }}"
class="transition-colors text-sm px-3 py-2 rounded-lg"
:class="darkMode ? 'text-gray-400 hover:text-white hover:bg-gray-800/50' : 'text-gray-700 hover:text-gray-900 hover:bg-gray-200/50'">AGB</a>
</div> </div>
<div> <!-- Optional: Social Media Links oder andere Elemente -->
<h3 class="text-sm font-semibold text-gray-800 dark:text-white tracking-wider uppercase">Rechtliches</h3> {#
<ul class="mt-4 space-y-2"> <div class="flex space-x-4">
<li><a href="{{ url_for('impressum') }}" class="text-gray-600 hover:text-primary-600 dark:text-gray-300 dark:hover:text-white">Impressum</a></li> <a href="#" class="text-gray-400 hover:text-white transition-colors"><i class="fab fa-twitter"></i></a>
<li><a href="{{ url_for('datenschutz') }}" class="text-gray-600 hover:text-primary-600 dark:text-gray-300 dark:hover:text-white">Datenschutz</a></li> <a href="#" class="text-gray-400 hover:text-white transition-colors"><i class="fab fa-linkedin"></i></a>
<li><a href="{{ url_for('agb') }}" class="text-gray-600 hover:text-primary-600 dark:text-gray-300 dark:hover:text-white">AGB</a></li> <a href="#" class="text-gray-400 hover:text-white transition-colors"><i class="fab fa-github"></i></a>
</ul> </div>
#}
</div> </div>
<div> <div class="mt-8 pt-6 text-center text-xs" {# mt-10 changed to mt-8 #}
<h3 class="text-sm font-semibold text-gray-800 dark:text-white tracking-wider uppercase">Folgen</h3> :class="darkMode ? 'border-t border-gray-800/50 text-gray-500' : 'border-t border-gray-300/50 text-gray-600'">
<div class="mt-4 flex space-x-4"> &copy; {{ current_year }} MindMap. Alle Rechte vorbehalten.
<a href="https://github.com" target="_blank" rel="noopener noreferrer" class="text-gray-600 hover:text-primary-600 dark:text-gray-300 dark:hover:text-white">
<i class="fa-brands fa-github text-xl"></i>
<span class="sr-only">GitHub</span>
</a>
<a href="https://twitter.com" target="_blank" rel="noopener noreferrer" class="text-gray-600 hover:text-primary-600 dark:text-gray-300 dark:hover:text-white">
<i class="fa-brands fa-twitter text-xl"></i>
<span class="sr-only">Twitter</span>
</a>
<a href="mailto:info@mindmap.example.com" class="text-gray-600 hover:text-primary-600 dark:text-gray-300 dark:hover:text-white">
<i class="fa-solid fa-envelope text-xl"></i>
<span class="sr-only">E-Mail</span>
</a>
</div>
</div>
</div>
</div>
<div class="mt-8 border-t border-gray-200 dark:border-white/10 pt-4 md:flex md:items-center md:justify-between">
<p class="text-xs text-gray-600 dark:text-gray-300">© {{ current_year }} Mind<span class="gradient-text">Map</span>. Alle Rechte vorbehalten.</p>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-2 md:mt-0">
<span class="inline-flex items-center">
Mit <i class="fa-solid fa-heart text-red-500 mx-1"></i> und
<i class="fa-solid fa-code text-primary-500 mx-1"></i> erstellt
</span>
</p>
</div> </div>
</div> </div>
</footer> </footer>
</div>
<!-- Scripts --> <!-- Hilfsscripts -->
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script> {% block scripts %}{% endblock %}
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
{% block extra_js %}{% endblock %}
</body> </body>
</html> </html>

View File

@@ -0,0 +1,23 @@
{% extends "base.html" %}
{% block title %}403 - Zugriff verweigert{% endblock %}
{% block content %}
<div class="min-h-[65vh] flex flex-col items-center justify-center px-4 py-12">
<div class="glass-effect max-w-2xl w-full p-8 rounded-lg border border-gray-300/20 dark:border-gray-700/30 shadow-lg">
<div class="text-center">
<h1 class="text-6xl font-bold text-primary-600 dark:text-primary-400 mb-4">403</h1>
<h2 class="text-2xl font-semibold mb-4">Zugriff verweigert</h2>
<p class="text-gray-600 dark:text-gray-300 mb-8">Sie haben nicht die erforderlichen Berechtigungen, um auf diese Seite zuzugreifen. Bitte melden Sie sich an oder nutzen Sie ein Konto mit entsprechenden Rechten.</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<a href="{{ url_for('index') }}" class="btn-primary">
<i class="fa-solid fa-home mr-2"></i>Zur Startseite
</a>
<a href="javascript:history.back()" class="btn-secondary">
<i class="fa-solid fa-arrow-left mr-2"></i>Zurück
</a>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,23 @@
{% extends "base.html" %}
{% block title %}404 - Seite nicht gefunden{% endblock %}
{% block content %}
<div class="min-h-[65vh] flex flex-col items-center justify-center px-4 py-12">
<div class="glass-effect max-w-2xl w-full p-8 rounded-lg border border-gray-300/20 dark:border-gray-700/30 shadow-lg">
<div class="text-center">
<h1 class="text-6xl font-bold text-primary-600 dark:text-primary-400 mb-4">404</h1>
<h2 class="text-2xl font-semibold mb-4">Seite nicht gefunden</h2>
<p class="text-gray-600 dark:text-gray-300 mb-8">Die gesuchte Seite existiert nicht oder wurde verschoben. Bitte prüfen Sie die URL oder nutzen Sie die Navigation.</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<a href="{{ url_for('index') }}" class="btn-primary">
<i class="fa-solid fa-home mr-2"></i>Zur Startseite
</a>
<a href="javascript:history.back()" class="btn-secondary">
<i class="fa-solid fa-arrow-left mr-2"></i>Zurück
</a>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,23 @@
{% extends "base.html" %}
{% block title %}429 - Zu viele Anfragen{% endblock %}
{% block content %}
<div class="min-h-[65vh] flex flex-col items-center justify-center px-4 py-12">
<div class="glass-effect max-w-2xl w-full p-8 rounded-lg border border-gray-300/20 dark:border-gray-700/30 shadow-lg">
<div class="text-center">
<h1 class="text-6xl font-bold text-primary-600 dark:text-primary-400 mb-4">429</h1>
<h2 class="text-2xl font-semibold mb-4">Zu viele Anfragen</h2>
<p class="text-gray-600 dark:text-gray-300 mb-8">Sie haben zu viele Anfragen in kurzer Zeit gestellt. Bitte warten Sie einen Moment und versuchen Sie es dann erneut.</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<a href="{{ url_for('index') }}" class="btn-primary">
<i class="fa-solid fa-home mr-2"></i>Zur Startseite
</a>
<a href="javascript:history.back()" class="btn-secondary">
<i class="fa-solid fa-arrow-left mr-2"></i>Zurück
</a>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,23 @@
{% extends "base.html" %}
{% block title %}500 - Serverfehler{% endblock %}
{% block content %}
<div class="min-h-[65vh] flex flex-col items-center justify-center px-4 py-12">
<div class="glass-effect max-w-2xl w-full p-8 rounded-lg border border-gray-300/20 dark:border-gray-700/30 shadow-lg">
<div class="text-center">
<h1 class="text-6xl font-bold text-primary-600 dark:text-primary-400 mb-4">500</h1>
<h2 class="text-2xl font-semibold mb-4">Interner Serverfehler</h2>
<p class="text-gray-600 dark:text-gray-300 mb-8">Es ist ein Fehler auf unserem Server aufgetreten. Unser Team wurde informiert und arbeitet bereits an einer Lösung. Bitte versuchen Sie es später erneut.</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<a href="{{ url_for('index') }}" class="btn-primary">
<i class="fa-solid fa-home mr-2"></i>Zur Startseite
</a>
<a href="javascript:history.back()" class="btn-secondary">
<i class="fa-solid fa-arrow-left mr-2"></i>Zurück
</a>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -4,9 +4,19 @@
{% block extra_css %} {% block extra_css %}
<style> <style>
.hero-gradient { /* Hintergrund über die gesamte Seite erstrecken */
background: linear-gradient(125deg, rgba(32, 92, 245, 0.8), rgba(128, 32, 245, 0.8)); html, body {
clip-path: polygon(0 0, 100% 0, 100% 85%, 0 100%); min-height: 100vh;
width: 100%;
margin: 0;
padding: 0;
overflow-x: hidden;
}
/* Entferne den Gradient-Hintergrund vollständig */
.hero-gradient, .bg-fade {
background: none !important;
clip-path: none !important;
} }
.tech-line { .tech-line {
@@ -30,48 +40,6 @@
background-color: rgba(255, 255, 255, 0.3); background-color: rgba(255, 255, 255, 0.3);
} }
.animate-float {
animation: float 6s ease-in-out infinite;
}
@keyframes float {
0% { transform: translateY(0); }
50% { transform: translateY(-12px); }
100% { transform: translateY(0); }
}
.gradient-btn {
background-size: 200% auto;
transition: 0.5s;
background-image: linear-gradient(to right, #205cf5 0%, #8020f5 50%, #205cf5 100%);
transform: translateY(0);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
.gradient-btn:hover {
background-position: right center;
transform: translateY(-2px);
box-shadow: 0 10px 15px -3px rgba(128, 32, 245, 0.2), 0 4px 6px -2px rgba(128, 32, 245, 0.1);
}
.card-hover {
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.card-hover:hover {
transform: translateY(-5px);
box-shadow: 0 10px 25px -5px rgba(128, 32, 245, 0.2);
}
.ascii-art {
font-family: 'JetBrains Mono', monospace;
font-size: 10px;
line-height: 1;
white-space: pre;
user-select: none;
opacity: 0.4;
}
@keyframes pulse { @keyframes pulse {
0% { r: 10; opacity: 0.7; } 0% { r: 10; opacity: 0.7; }
50% { r: 12; opacity: 1; } 50% { r: 12; opacity: 1; }
@@ -82,15 +50,6 @@
animation: pulse 3s ease-in-out infinite; animation: pulse 3s ease-in-out infinite;
} }
.bg-fade {
background: linear-gradient(to bottom, rgba(240, 240, 245, 0.4) 0%, rgba(240, 240, 245, 0.8) 100%);
}
.dark .bg-fade {
background: linear-gradient(to bottom, rgba(10, 15, 30, 0.2) 0%, rgba(10, 15, 30, 0.6) 100%);
backdrop-filter: blur(2px);
}
@keyframes iconPulse { @keyframes iconPulse {
0% { transform: scale(1); } 0% { transform: scale(1); }
50% { transform: scale(1.1); } 50% { transform: scale(1.1); }
@@ -101,26 +60,33 @@
animation: iconPulse 3s ease-in-out infinite; animation: iconPulse 3s ease-in-out infinite;
display: inline-block; display: inline-block;
} }
/* Volle Seitenbreite für Container */
#app-container, .container, main, .mx-auto, .py-12 {
width: 100%;
}
/* Sicherstellen dass der Hintergrund die ganze Seite abdeckt */
.full-page-bg {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: -1;
}
</style> </style>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<!-- Hintergrund für die gesamte Seite -->
<div class="full-page-bg gradient-background"></div>
<!-- Hero Section --> <!-- Hero Section -->
<section class="relative pt-16 pb-32" style="border-radius: 20px !important;" style="border-radius: 20px !important;"> <section class="relative pt-16 pb-32">
<!-- Background gradient effect -->
<div class="absolute inset-0 bg-fade"></div>
<!-- Tech dots background -->
<div class="absolute inset-0 overflow-hidden opacity-30" style="border-radius: 20px !important;">
{% for i in range(15) %}
<div class="tech-dot" style="top: {{ 5 + (i * 6) }}%; left: {{ (i * 7) % 90 }}%;"></div>
{% endfor %}
</div>
<!-- Hero Content --> <!-- Hero Content -->
<div class="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10" style="border-radius: 20px !important;"> <div class="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
<div class="text-center mb-16" style="border-radius: 20px !important;"> <div class="text-center mb-16">
<h1 class="text-5xl md:text-7xl lg:text-8xl font-bold tracking-tight mb-8 text-gray-900 dark:text-white"> <h1 class="text-5xl md:text-7xl lg:text-8xl font-bold tracking-tight mb-8 text-gray-900 dark:text-white">
<span class="gradient-text">Wissen</span> neu <span class="gradient-text">Wissen</span> neu
<div class="mt-2">vernetzen</div> <div class="mt-2">vernetzen</div>
@@ -130,14 +96,14 @@
in einem interaktiven Wissensnetzwerk. in einem interaktiven Wissensnetzwerk.
</p> </p>
<div class="flex flex-col sm:flex-row gap-4 justify-center"> <div class="flex flex-col sm:flex-row gap-4 justify-center">
<a href="{{ url_for('mindmap') }}" class="btn-primary text-lg px-8 py-4"> <a href="{{ url_for('mindmap') }}" class="btn-primary text-lg px-8 py-4 rounded-lg">
<span class="flex items-center"> <span class="flex items-center">
<i class="fa-solid fa-diagram-project mr-3"></i> <i class="fa-solid fa-diagram-project mr-3"></i>
Mindmap erkunden Mindmap erkunden
</span> </span>
</a> </a>
{% if not current_user.is_authenticated %} {% if not current_user.is_authenticated %}
<a href="{{ url_for('register') }}" class="gradient-btn text-lg px-8 py-4 text-white rounded-lg shadow-lg"> <a href="{{ url_for('register') }}" class="gradient-btn text-lg px-8 py-4 rounded-lg shadow-lg">
<span class="flex items-center"> <span class="flex items-center">
<i class="fa-solid fa-user-plus mr-3 icon-pulse"></i> <i class="fa-solid fa-user-plus mr-3 icon-pulse"></i>
Konto erstellen Konto erstellen
@@ -194,12 +160,12 @@
<!-- Network Nodes --> <!-- Network Nodes -->
<g class="nodes"> <g class="nodes">
<circle cx="400" cy="150" r="15" fill="url(#nodeGradient)" filter="url(#glow)" class="animate-pulse" /> <circle cx="400" cy="150" r="15" fill="url(#nodeGradient)" filter="url(#glow)" class="animate-pulse float-animation" />
<circle cx="200" cy="250" r="10" fill="url(#nodeGradient)" /> <circle cx="200" cy="250" r="10" fill="url(#nodeGradient)" class="float-animation" />
<circle cx="600" cy="250" r="10" fill="url(#nodeGradient)" /> <circle cx="600" cy="250" r="10" fill="url(#nodeGradient)" class="float-animation" />
<circle cx="400" cy="350" r="15" fill="url(#nodeGradient)" filter="url(#glow)" class="animate-pulse" /> <circle cx="400" cy="350" r="15" fill="url(#nodeGradient)" filter="url(#glow)" class="animate-pulse float-animation" />
<circle cx="300" cy="200" r="8" fill="url(#nodeGradient)" /> <circle cx="300" cy="200" r="8" fill="url(#nodeGradient)" class="float-animation" />
<circle cx="500" cy="300" r="8" fill="url(#nodeGradient)" /> <circle cx="500" cy="300" r="8" fill="url(#nodeGradient)" class="float-animation" />
</g> </g>
</svg> </svg>
</div> </div>
@@ -222,72 +188,72 @@
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
<!-- Feature Card 1 --> <!-- Feature Card 1 -->
<div class="card card-hover p-6"> <div class="card">
<div class="text-primary-400 text-3xl mb-4"> <div class="icon">
<i class="fa-solid fa-brain icon-pulse"></i> <i class="fa-solid fa-brain icon-pulse"></i>
</div> </div>
<h3 class="text-xl font-bold mb-3 text-gray-800 dark:text-white">Visualisiere Wissen</h3> <h3>Visualisiere Wissen</h3>
<p class="text-gray-600 dark:text-gray-300"> <p>
Sieh Wissen als vernetztes System, entdecke Zusammenhänge und erkenne überraschende Sieh Wissen als vernetztes System, entdecke Zusammenhänge und erkenne überraschende
Verbindungen zwischen verschiedenen Themengebieten. Verbindungen zwischen verschiedenen Themengebieten.
</p> </p>
</div> </div>
<!-- Feature Card 2 --> <!-- Feature Card 2 -->
<div class="card card-hover p-6"> <div class="card">
<div class="text-secondary-400 text-3xl mb-4"> <div class="icon">
<i class="fa-solid fa-lightbulb icon-pulse"></i> <i class="fa-solid fa-lightbulb icon-pulse"></i>
</div> </div>
<h3 class="text-xl font-bold mb-3 text-gray-800 dark:text-white">Teile Gedanken</h3> <h3>Teile Gedanken</h3>
<p class="text-gray-600 dark:text-gray-300"> <p>
Füge deine eigenen Ideen und Perspektiven hinzu. Erstelle Verbindungen zu Füge deine eigenen Ideen und Perspektiven hinzu. Erstelle Verbindungen zu
vorhandenen Gedanken und bereichere die wachsende Wissensbasis. vorhandenen Gedanken und bereichere die wachsende Wissensbasis.
</p> </p>
</div> </div>
<!-- Feature Card 3 --> <!-- Feature Card 3 -->
<div class="card card-hover p-6"> <div class="card">
<div class="text-green-400 text-3xl mb-4"> <div class="icon">
<i class="fa-solid fa-users icon-pulse"></i> <i class="fa-solid fa-users icon-pulse"></i>
</div> </div>
<h3 class="text-xl font-bold mb-3 text-gray-800 dark:text-white">Community</h3> <h3>Community</h3>
<p class="text-gray-600 dark:text-gray-300"> <p>
Sei Teil einer Gemeinschaft, die gemeinsam ein verteiltes Wissensarchiv aufbaut Sei Teil einer Gemeinschaft, die gemeinsam ein verteiltes Wissensarchiv aufbaut
und sich in thematisch fokussierten Bereichen austauscht. und sich in thematisch fokussierten Bereichen austauscht.
</p> </p>
</div> </div>
<!-- Feature Card 4 --> <!-- Feature Card 4 -->
<div class="card card-hover p-6"> <div class="card">
<div class="text-blue-400 text-3xl mb-4"> <div class="icon">
<i class="fa-solid fa-robot icon-pulse"></i> <i class="fa-solid fa-robot icon-pulse"></i>
</div> </div>
<h3 class="text-xl font-bold mb-3 text-gray-800 dark:text-white">KI-Assistenz</h3> <h3>KI-Assistenz</h3>
<p class="text-gray-600 dark:text-gray-300"> <p>
Lass dir von künstlicher Intelligenz helfen, neue Zusammenhänge zu entdecken, Lass dir von künstlicher Intelligenz helfen, neue Zusammenhänge zu entdecken,
Inhalte zusammenzufassen und Fragen zu beantworten. Inhalte zusammenzufassen und Fragen zu beantworten.
</p> </p>
</div> </div>
<!-- Feature Card 5 --> <!-- Feature Card 5 -->
<div class="card card-hover p-6"> <div class="card">
<div class="text-purple-400 text-3xl mb-4"> <div class="icon">
<i class="fa-solid fa-search icon-pulse"></i> <i class="fa-solid fa-search icon-pulse"></i>
</div> </div>
<h3 class="text-xl font-bold mb-3 text-gray-800 dark:text-white">Intelligente Suche</h3> <h3>Intelligente Suche</h3>
<p class="text-gray-600 dark:text-gray-300"> <p>
Finde genau die Informationen, die du suchst, mit fortschrittlichen Such- und Finde genau die Informationen, die du suchst, mit fortschrittlichen Such- und
Filterfunktionen für eine präzise Navigation durch das Wissen. Filterfunktionen für eine präzise Navigation durch das Wissen.
</p> </p>
</div> </div>
<!-- Feature Card 6 --> <!-- Feature Card 6 -->
<div class="card card-hover p-6"> <div class="card">
<div class="text-indigo-400 text-3xl mb-4"> <div class="icon">
<i class="fa-solid fa-route icon-pulse"></i> <i class="fa-solid fa-route icon-pulse"></i>
</div> </div>
<h3 class="text-xl font-bold mb-3 text-gray-800 dark:text-white">Geführte Pfade</h3> <h3>Geführte Pfade</h3>
<p class="text-gray-600 dark:text-gray-300"> <p>
Folge kuratierten Lernpfaden durch komplexe Themen oder erschaffe selbst Folge kuratierten Lernpfaden durch komplexe Themen oder erschaffe selbst
Routen für andere, die deinen Gedankengängen folgen möchten. Routen für andere, die deinen Gedankengängen folgen möchten.
</p> </p>
@@ -298,10 +264,8 @@
<!-- Call to Action Section --> <!-- Call to Action Section -->
<section class="py-16 relative overflow-hidden"> <section class="py-16 relative overflow-hidden">
<div class="absolute inset-0 bg-gradient-to-r from-primary-100/50 to-secondary-100/50 dark:from-primary-900/50 dark:to-secondary-900/50 z-0"></div>
<div class="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10"> <div class="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
<div class="glass-effect rounded-xl p-8 md:p-12 border border-gray-200 dark:border-gray-700"> <div class="glass-effect p-8 md:p-12 rounded-lg">
<div class="md:flex md:items-center md:justify-between"> <div class="md:flex md:items-center md:justify-between">
<div class="md:w-2/3"> <div class="md:w-2/3">
<h2 class="text-3xl font-bold mb-4 text-gray-900 dark:text-white">Bereit, Wissen neu zu entdecken?</h2> <h2 class="text-3xl font-bold mb-4 text-gray-900 dark:text-white">Bereit, Wissen neu zu entdecken?</h2>
@@ -310,7 +274,7 @@
</p> </p>
</div> </div>
<div class="md:w-1/3 text-center md:text-right"> <div class="md:w-1/3 text-center md:text-right">
<a href="{{ url_for('mindmap') }}" class="inline-block gradient-btn text-white font-bold py-3 px-8 rounded-lg shadow-md hover:shadow-lg transition-all duration-300 transform hover:-translate-y-1"> <a href="{{ url_for('mindmap') }}" class="inline-block btn-primary font-bold py-3 px-8 rounded-lg shadow-md hover:shadow-lg transition-all duration-300 transform hover:-translate-y-1">
<span class="flex items-center justify-center"> <span class="flex items-center justify-center">
<i class="fa-solid fa-arrow-right mr-2"></i> <i class="fa-solid fa-arrow-right mr-2"></i>
Zur Mindmap Zur Mindmap
@@ -327,7 +291,7 @@
<div class="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8"> <div class="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6"> <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- KI-Chat --> <!-- KI-Chat -->
<div class="card p-6"> <div class="glass-effect p-6 rounded-lg">
<h3 class="text-xl font-bold mb-4 flex items-center text-gray-800 dark:text-white"> <h3 class="text-xl font-bold mb-4 flex items-center text-gray-800 dark:text-white">
<i class="fa-solid fa-robot text-primary-400 mr-2"></i> <i class="fa-solid fa-robot text-primary-400 mr-2"></i>
KI-Assistent KI-Assistent
@@ -336,7 +300,7 @@
Stelle Fragen, lasse dir Themen erklären oder finde neue Verbindungen mit Hilfe Stelle Fragen, lasse dir Themen erklären oder finde neue Verbindungen mit Hilfe
unseres KI-Assistenten. unseres KI-Assistenten.
</p> </p>
<div class="glass-effect p-4 rounded-lg mb-4 border border-gray-200 dark:border-gray-700"> <div class="glass-effect p-4 rounded-lg mb-4">
<div class="flex items-start"> <div class="flex items-start">
<div class="w-8 h-8 rounded-full bg-primary-500 flex items-center justify-center flex-shrink-0 mr-3"> <div class="w-8 h-8 rounded-full bg-primary-500 flex items-center justify-center flex-shrink-0 mr-3">
<i class="fa-solid fa-robot text-white text-sm"></i> <i class="fa-solid fa-robot text-white text-sm"></i>
@@ -357,11 +321,11 @@
</div> </div>
</div> </div>
</div> </div>
<a href="#" class="btn-outline w-full text-center">KI-Chat starten</a> <button onclick="window.MindMap.assistant.toggleAssistant(true)" class="btn-outline w-full text-center rounded-lg">KI-Chat starten</button>
</div> </div>
<!-- Themen-Übersicht --> <!-- Themen-Übersicht -->
<div class="card p-6"> <div class="glass-effect p-6 rounded-lg">
<h3 class="text-xl font-bold mb-4 flex items-center text-gray-800 dark:text-white"> <h3 class="text-xl font-bold mb-4 flex items-center text-gray-800 dark:text-white">
<i class="fa-solid fa-fire text-secondary-400 mr-2"></i> <i class="fa-solid fa-fire text-secondary-400 mr-2"></i>
Themen-Übersicht Themen-Übersicht
@@ -392,7 +356,7 @@
<i class="fa-solid fa-chevron-right text-gray-500"></i> <i class="fa-solid fa-chevron-right text-gray-500"></i>
</a> </a>
</div> </div>
<a href="{{ url_for('search_thoughts_page') }}" class="btn-outline w-full text-center">Alle Themen entdecken</a> <a href="{{ url_for('search_thoughts_page') }}" class="btn-outline w-full text-center rounded-lg">Alle Themen entdecken</a>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,11 @@
<!-- Navigation -->
<header class="w-full">
<nav class="fixed top-0 z-50 w-full bg-dark-900 border-b border-gray-700">
<!-- ... existing code ... -->
</nav>
</header>
<!-- Main Content Container -->
<div class="container mx-auto px-4 pt-20 pb-10">
<!-- ... existing code ... -->
</div>

View File

@@ -5,87 +5,212 @@
{% block extra_css %} {% block extra_css %}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tippy.js@6.3.7/dist/tippy.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tippy.js@6.3.7/dist/tippy.css">
<style> <style>
/* Modernes Dark-Mode Design mit Purple-Blue Gradient */ /* Globaler Stil - Hintergrund über die gesamte Seite */
.mindmap-header { html, body {
background: linear-gradient(120deg, rgba(32, 32, 64, 0.7), rgba(24, 24, 48, 0.7)); background-color: var(--dark-bg) !important;
border-radius: 0.75rem; min-height: 100vh;
backdrop-filter: blur(10px); width: 100%;
border: 1px solid rgba(255, 255, 255, 0.05); color: #ffffff;
margin: 0;
padding: 0;
overflow-x: hidden;
} }
.gradient-text { /* Sicherstellen, dass der Hintergrund die gesamte Seite abdeckt */
background: linear-gradient(to right, #a67fff, #5096ff); #app-container, .container, main, .mx-auto, .py-12, body > div, #content-wrapper, #mindmap-container {
background-color: var(--dark-bg) !important;
width: 100%;
}
/* Verbesserte Glasmorphismus-Stile für Karten */
.glass-card, .mindmap-card {
background: rgba(24, 28, 45, 0.75);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 24px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.35);
transition: all 0.3s ease;
}
.glass-card:hover, .mindmap-card:hover {
transform: translateY(-5px);
box-shadow: 0 20px 48px rgba(0, 0, 0, 0.45);
border: 1px solid rgba(255, 255, 255, 0.2);
}
/* Feature-Cards-Stil mit besserem Glasmorphismus */
.feature-card {
background: rgba(24, 28, 45, 0.75);
border-radius: 24px;
overflow: hidden;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.12);
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.35);
transition: all 0.3s ease;
height: 100%;
display: flex;
flex-direction: column;
padding: 1.75rem;
}
.feature-card:hover {
transform: translateY(-5px);
border: 1px solid rgba(255, 255, 255, 0.25);
box-shadow: 0 20px 48px rgba(0, 0, 0, 0.45);
background: rgba(28, 32, 52, 0.9);
}
.feature-card-icon {
font-size: 2.75rem;
margin-bottom: 1.25rem;
background: linear-gradient(135deg, #b38fff, #14b8a6);
-webkit-background-clip: text; -webkit-background-clip: text;
background-clip: text; background-clip: text;
color: transparent; color: transparent;
text-shadow: 0 0 20px rgba(160, 80, 255, 0.3); filter: drop-shadow(0 0 10px rgba(179, 143, 255, 0.6));
}
/* Feature-Card-Text besser lesbar machen */
.feature-card h3 {
font-size: 1.75rem;
font-weight: 700;
margin-bottom: 0.9rem;
color: #ffffff;
text-shadow: 0 2px 3px rgba(0, 0, 0, 0.3);
letter-spacing: 0.2px;
}
.feature-card p {
color: rgba(255, 255, 255, 0.95);
font-size: 1.1rem;
line-height: 1.6;
font-weight: 400;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.25);
letter-spacing: 0.3px;
}
/* Mindmap-Header */
.mindmap-header {
background: rgba(20, 24, 42, 0.85);
border-radius: 24px;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.12);
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.35);
}
.gradient-text {
background: linear-gradient(to right, #b38fff, #58a9ff);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
text-shadow: 0 0 25px rgba(179, 143, 255, 0.25);
font-weight: 800;
} }
/* D3.js Mindmap spezifische Stile */ /* D3.js Mindmap spezifische Stile */
.mindmap-svg { .mindmap-svg {
background: radial-gradient(circle at center, rgba(40, 30, 60, 0.4) 0%, rgba(20, 20, 40, 0.2) 70%); background: rgba(14, 18, 32, 0.3);
width: 100%; width: 100%;
height: 100%; height: 100%;
border-radius: 0.5rem; border-radius: 24px;
} }
/* Verbesserte Mindmap-Knoten-Stile */
.node { .node {
cursor: pointer; cursor: pointer;
transition: all 0.3s ease; transition: all 0.3s ease;
} }
.node circle {
stroke: rgba(255, 255, 255, 0.12);
stroke-width: 2px;
fill: rgba(24, 28, 45, 0.85);
filter: url(#glass-effect);
}
.node:hover circle {
filter: url(#hover-glow);
stroke: rgba(255, 255, 255, 0.25);
}
.node.selected circle {
filter: url(#selected-glow);
stroke: rgba(179, 143, 255, 0.6);
stroke-width: 3px;
}
.node-label { .node-label {
font-family: 'Inter', 'SF Pro Display', system-ui, sans-serif; font-family: 'Inter', 'SF Pro Display', system-ui, sans-serif;
font-weight: 500; font-weight: 600;
pointer-events: none; pointer-events: none;
user-select: none; user-select: none;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5); text-shadow: 0 2px 4px rgba(0, 0, 0, 0.7);
font-size: 16px;
letter-spacing: 0.3px;
fill: #ffffff;
} }
.link { .link {
transition: stroke 0.3s ease, opacity 0.3s ease; transition: stroke 0.3s ease, opacity 0.3s ease;
stroke: rgba(255, 255, 255, 0.3);
stroke-width: 2;
opacity: 0.7;
} }
/* Control Bar */ .link:hover, .link.highlighted {
stroke: rgba(179, 143, 255, 0.7);
opacity: 0.9;
stroke-width: 3;
}
/* Control Bar mit verbesserten Glasmorphismus und Lesbarkeit */
.controls-bar { .controls-bar {
background: rgba(20, 20, 40, 0.7); background: rgba(24, 28, 45, 0.85);
backdrop-filter: blur(10px); backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.05); -webkit-backdrop-filter: blur(20px);
border-radius: 0.5rem 0.5rem 0 0; border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 20px 20px 0 0;
} }
/* Tooltip Stile */ /* Tooltip Stile */
.tippy-box[data-theme~='mindmap'] { .tippy-box[data-theme~='mindmap'] {
background-color: rgba(20, 20, 40, 0.9); background-color: rgba(24, 28, 45, 0.95);
color: #ffffff; color: #ffffff;
border: 1px solid rgba(160, 80, 255, 0.2); border: 1px solid rgba(179, 143, 255, 0.25);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5), 0 0 10px rgba(160, 80, 255, 0.2); box-shadow: 0 12px 30px rgba(0, 0, 0, 0.5), 0 0 15px rgba(179, 143, 255, 0.25);
backdrop-filter: blur(10px); backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-radius: 16px;
} }
.tippy-box[data-theme~='mindmap'] .tippy-arrow { .tippy-box[data-theme~='mindmap'] .tippy-arrow {
color: rgba(20, 20, 40, 0.9); color: rgba(24, 28, 45, 0.95);
} }
.node-tooltip { .node-tooltip {
font-family: 'Inter', 'SF Pro Display', system-ui, sans-serif; font-family: 'Inter', 'SF Pro Display', system-ui, sans-serif;
font-size: 14px; font-size: 14px;
line-height: 1.5; line-height: 1.6;
padding: 8px 12px; padding: 12px 16px;
letter-spacing: 0.3px;
} }
.node-tooltip strong { .node-tooltip strong {
font-weight: 600; font-weight: 600;
color: #a67fff; color: #b38fff;
} }
/* Gedanken-Container */ /* Gedanken-Container */
.thought-container { .thought-container {
border: 1px solid rgba(255, 255, 255, 0.05); border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 0.5rem; border-radius: 24px;
backdrop-filter: blur(10px); backdrop-filter: blur(20px);
background: rgba(20, 20, 40, 0.7); -webkit-backdrop-filter: blur(20px);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2); background: rgba(24, 28, 45, 0.85);
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.3);
} }
/* Angepasste Scrollbar für den Gedanken-Container */ /* Angepasste Scrollbar für den Gedanken-Container */
@@ -94,17 +219,17 @@
} }
.custom-scrollbar::-webkit-scrollbar-track { .custom-scrollbar::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05); background: rgba(255, 255, 255, 0.08);
border-radius: 3px; border-radius: 3px;
} }
.custom-scrollbar::-webkit-scrollbar-thumb { .custom-scrollbar::-webkit-scrollbar-thumb {
background: rgba(160, 80, 255, 0.3); background: rgba(179, 143, 255, 0.5);
border-radius: 3px; border-radius: 3px;
} }
.custom-scrollbar::-webkit-scrollbar-thumb:hover { .custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: rgba(160, 80, 255, 0.5); background: rgba(179, 143, 255, 0.7);
} }
/* Pulse-Animation für leere Gedanken */ /* Pulse-Animation für leere Gedanken */
@@ -121,208 +246,268 @@
} }
} }
/* ASCII-Style Tech Decoration */ /* Button-Effekte mit verbesserter Lesbarkeit */
.ascii-decoration {
font-family: monospace;
color: rgba(160, 80, 255, 0.15);
font-size: 12px;
white-space: pre;
line-height: 1;
user-select: none;
position: absolute;
z-index: -1;
}
.top-right-deco {
top: 20px;
right: 20px;
}
.bottom-left-deco {
bottom: 20px;
left: 20px;
}
/* Button-Effekte */
.control-btn { .control-btn {
transition: all 0.2s ease; background: rgba(32, 36, 55, 0.85);
position: relative; color: #ffffff;
overflow: hidden; border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 16px;
padding: 0.75rem 1.5rem;
font-weight: 600;
transition: all 0.3s ease;
backdrop-filter: blur(15px);
-webkit-backdrop-filter: blur(15px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.25);
font-size: 1rem;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
letter-spacing: 0.3px;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
} }
.control-btn:hover { .control-btn:hover {
background: rgba(160, 80, 255, 0.2); background: rgba(179, 143, 255, 0.35);
transform: translateY(-1px); transform: translateY(-3px);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.35), 0 0 15px rgba(179, 143, 255, 0.25);
border: 1px solid rgba(255, 255, 255, 0.25);
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
} }
.control-btn:active { .control-btn:active {
transform: translateY(1px); transform: translateY(1px);
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.2);
} }
.control-btn::after { .control-btn.active {
content: ""; background: rgba(179, 143, 255, 0.4);
display: block; border: 1px solid rgba(179, 143, 255, 0.5);
position: absolute; box-shadow: 0 0 15px rgba(179, 143, 255, 0.3);
width: 100%;
height: 100%;
top: 0;
left: 0;
pointer-events: none;
background-image: radial-gradient(circle, rgba(255, 255, 255, 0.3) 10%, transparent 10.01%);
background-repeat: no-repeat;
background-position: 50%;
transform: scale(10, 10);
opacity: 0;
transition: transform 0.5s, opacity 0.5s;
} }
.control-btn:active::after { /* Glow Effect für Buttons */
transform: scale(0, 0); .btn-glow:hover {
opacity: 0.3; box-shadow: 0 0 15px rgba(179, 143, 255, 0.5);
transition: 0s; }
/* Light Mode Anpassungen */
html.light .mindmap-svg {
background: rgba(240, 244, 248, 0.3);
}
html.light .node circle {
fill: rgba(255, 255, 255, 0.9);
stroke: rgba(0, 0, 0, 0.1);
}
html.light .node-label {
fill: #1a202c;
text-shadow: 0 1px 2px rgba(255, 255, 255, 0.7);
}
html.light .link {
stroke: rgba(0, 0, 0, 0.2);
}
html.light .glass-card,
html.light .mindmap-card,
html.light .feature-card,
html.light .thought-container,
html.light .mindmap-header,
html.light .controls-bar {
background: rgba(255, 255, 255, 0.85);
border: 1px solid rgba(0, 0, 0, 0.08);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
}
html.light .control-btn {
background: rgba(255, 255, 255, 0.9);
color: #1a202c;
border: 1px solid rgba(0, 0, 0, 0.08);
text-shadow: none;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
font-weight: 600;
}
html.light .control-btn:hover {
background: rgba(179, 143, 255, 0.15);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.12);
color: #7e3ff2;
font-weight: 700;
}
html.light .control-btn.active {
background: rgba(179, 143, 255, 0.2);
border: 1px solid rgba(126, 63, 242, 0.3);
color: #7e3ff2;
font-weight: 700;
}
html.light .feature-card h3 {
color: #1a202c;
text-shadow: none;
}
html.light .feature-card p {
color: #4a5568;
text-shadow: none;
}
html.light .node-tooltip strong {
color: #7e3ff2;
}
/* Karten in der Mindmap mit verbesserten Styles */
.mindmap-card {
background: rgba(24, 28, 45, 0.75);
border-radius: 24px;
overflow: hidden;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.12);
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.35);
transition: all 0.3s ease;
}
.mindmap-card:hover {
transform: translateY(-5px);
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.45);
background: rgba(28, 32, 52, 0.8);
}
.mindmap-card-header {
padding: 1.25rem 1.5rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
display: flex;
align-items: center;
justify-content: space-between;
}
.mindmap-card-body {
padding: 1.5rem;
}
.mindmap-card-footer {
padding: 1.25rem 1.5rem;
border-top: 1px solid rgba(255, 255, 255, 0.06);
display: flex;
align-items: center;
justify-content: space-between;
}
html.light .mindmap-card-header,
html.light .mindmap-card-footer {
border-color: rgba(0, 0, 0, 0.06);
} }
</style> </style>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="relative mb-6" data-page="mindmap"> <div class="container mx-auto px-4 py-12">
<!-- ASCII Dekorationen --> <!-- Feature-Karten-Container -->
<div class="ascii-decoration top-right-deco"> <div class="grid grid-cols-1 md:grid-cols-3 gap-8 mb-12">
┌─┐ ┌─┐ ┌─┐ ┌─┐ <!-- Feature-Karte 1: Visualisiere Wissen -->
│ │ │ │ │ │ │ │ <div class="feature-card">
└─┘ └─┘ └─┘ └─┘ <div class="feature-card-icon">
<i class="fas fa-brain"></i>
</div> </div>
<div class="ascii-decoration bottom-left-deco"> <h3>Visualisiere Wissen</h3>
╔══╗ ╔═══╗ ╔══╗ <p>Sieh Wissen als vernetztes System, entdecke Zusammenhänge und erkenne überraschende Verbindungen zwischen verschiedenen Themengebieten.</p>
║ ║ ║ ║ ║ ║
╚══╝ ╚═══╝ ╚══╝
</div> </div>
<!-- Header Bereich --> <!-- Feature-Karte 2: Teile Gedanken -->
<div class="mb-8 p-6 mindmap-header"> <div class="feature-card">
<h1 class="text-5xl md:text-6xl font-bold mb-3"> <div class="feature-card-icon">
<span class="gradient-text">Mindmap</span> <i class="fas fa-lightbulb"></i>
</h1> </div>
<p class="text-lg text-gray-200 max-w-3xl leading-relaxed"> <h3>Teile Gedanken</h3>
Erkunden Sie interaktiv verknüpfte Wissensgebiete und ihre Verbindungen. Fügen Sie eigene Gedanken hinzu und erstellen Sie ein kollaboratives Wissensnetz. <p>Füge deine eigenen Ideen und Perspektiven hinzu. Erstelle Verbindungen zu vorhandenen Gedanken und bereichere die wachsende Wissensbasis.</p>
</p> </div>
<div class="mt-3 flex flex-wrap gap-3">
<span class="px-3 py-1 text-xs rounded-full bg-purple-900/50 text-purple-200 border border-purple-700/30"> <!-- Feature-Karte 3: Community -->
<i class="fa-solid fa-diagram-project mr-1"></i> Interaktiv <div class="feature-card">
</span> <div class="feature-card-icon">
<span class="px-3 py-1 text-xs rounded-full bg-blue-900/50 text-blue-200 border border-blue-700/30"> <i class="fas fa-users"></i>
<i class="fa-solid fa-sitemap mr-1"></i> Wissensvernetzung </div>
</span> <h3>Community</h3>
<span class="px-3 py-1 text-xs rounded-full bg-indigo-900/50 text-indigo-200 border border-indigo-700/30"> <p>Sei Teil einer Gemeinschaft, die gemeinsam ein verteiltes Wissensarchiv aufbaut und sich in thematisch fokussierten Bereichen austauscht.</p>
<i class="fa-solid fa-lightbulb mr-1"></i> Kollaborativ
</span>
</div> </div>
</div> </div>
<!-- Haupt-Grid-Layout --> <!-- Mindmap-Visualisierung Header -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6"> <div class="mindmap-header p-6 mb-6">
<!-- Visualisierungsbereich --> <h2 class="text-3xl font-bold mb-2 gradient-text">Wissenslandschaft erkunden</h2>
<div class="lg:col-span-2"> <p class="text-gray-300 mb-0">Interagiere mit der Mindmap, um Verbindungen zu entdecken und neue Ideen hinzuzufügen</p>
<div class="rounded-lg overflow-hidden bg-gradient-to-br from-slate-900/60 to-slate-800/40 border border-slate-700/20 shadow-xl">
<!-- Mindmap Controls Bar -->
<div class="flex items-center justify-between p-4 controls-bar">
<div class="relative flex-grow max-w-md">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fa-solid fa-magnifying-glass text-gray-400"></i>
</div>
<input type="text" id="mindmap-search"
class="bg-slate-800/80 border border-slate-700/50 text-white rounded-md block w-full pl-10 p-2.5 focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-all"
placeholder="Knoten suchen...">
</div> </div>
<div class="flex gap-2"> <!-- Mindmap-Container -->
<button id="zoom-in" class="control-btn p-2 rounded-md text-gray-200" title="Vergrößern"> <div class="glass-card overflow-hidden mb-12">
<i class="fa-solid fa-plus"></i> <div id="mindmap-container" class="relative" style="height: 70vh; min-height: 500px;">
<!-- Lade-Overlay -->
<div class="mindmap-loading absolute inset-0 flex items-center justify-center z-10" style="background: rgba(14, 18, 32, 0.7); backdrop-filter: blur(5px);">
<div class="text-center">
<div class="inline-block animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-purple-500 mb-4"></div>
<p class="text-white text-lg">Wissenslandschaft wird geladen...</p>
<div class="w-64 h-2 bg-gray-700 rounded-full mt-4 overflow-hidden">
<div class="loading-progress h-full bg-gradient-to-r from-purple-500 to-blue-500 rounded-full" style="width: 0%"></div>
</div>
</div>
</div>
</div>
<!-- Steuerungsleiste -->
<div class="controls-bar p-4 flex flex-wrap items-center justify-between gap-3">
<div class="flex flex-wrap gap-2">
<button class="control-btn" id="zoom-in-btn">
<i class="fas fa-search-plus mr-1"></i> Vergrößern
</button> </button>
<button id="zoom-out" class="control-btn p-2 rounded-md text-gray-200" title="Verkleinern"> <button class="control-btn" id="zoom-out-btn">
<i class="fa-solid fa-minus"></i> <i class="fas fa-search-minus mr-1"></i> Verkleinern
</button> </button>
<button id="zoom-reset" class="control-btn p-2 rounded-md text-gray-200" title="Ansicht zurücksetzen"> <button class="control-btn" id="center-btn">
<i class="fa-solid fa-house"></i> <i class="fas fa-bullseye mr-1"></i> Zentrieren
</button> </button>
<button id="toggle-guide" class="control-btn p-2 rounded-md text-gray-200" title="Anleitung anzeigen"> </div>
<i class="fa-solid fa-circle-question"></i> <div class="flex flex-wrap gap-2">
<button class="control-btn" id="add-thought-btn">
<i class="fas fa-plus-circle mr-1"></i> Gedanke hinzufügen
</button>
<button class="control-btn" id="connect-btn">
<i class="fas fa-link mr-1"></i> Verbinden
</button> </button>
</div> </div>
</div> </div>
<!-- Mindmap Container -->
<div id="mindmap-container" class="relative w-full h-[600px] bg-gradient-to-br from-slate-900/30 to-indigo-900/10">
<!-- Wird durch D3.js befüllt -->
</div> </div>
<!-- Legende --> <!-- Unterer Bereich: KI-Assistenz, Suche und Lernpfade -->
<div class="p-4 border-t border-white/5 bg-slate-900/70 backdrop-blur"> <div class="grid grid-cols-1 md:grid-cols-3 gap-8">
<div class="text-xs text-gray-400 mb-2">Legende</div> <!-- KI-Assistenz -->
<div class="flex flex-wrap gap-4"> <div class="feature-card">
<div class="flex items-center"> <div class="feature-card-icon">
<div class="w-3 h-3 rounded-full bg-purple-500 mr-2"></div> <i class="fas fa-robot"></i>
<span class="text-sm text-gray-300">Hauptkategorien</span>
</div>
<div class="flex items-center">
<div class="w-3 h-3 rounded-full bg-blue-500 mr-2"></div>
<span class="text-sm text-gray-300">Unterkategorien</span>
</div>
<div class="flex items-center">
<div class="w-3 h-3 rounded-full bg-green-500 mr-2"></div>
<span class="text-sm text-gray-300">Konzepte</span>
</div>
</div>
</div> </div>
<h3>KI-Assistenz</h3>
<p>Lass dir von künstlicher Intelligenz helfen, neue Zusammenhänge zu entdecken, Inhalte zusammenzufassen und Fragen zu beantworten.</p>
</div> </div>
<!-- Anleitung --> <!-- Intelligente Suche -->
<div id="guide-panel" class="mt-4 rounded-lg overflow-hidden bg-gradient-to-br from-slate-900/60 to-slate-800/40 border border-slate-700/20 shadow-xl p-4 hidden"> <div class="feature-card">
<h3 class="text-lg font-semibold mb-2 text-white">Navigation der Mindmap</h3> <div class="feature-card-icon">
<ul class="space-y-1 text-gray-300 text-sm"> <i class="fas fa-search"></i>
<li class="flex items-start">
<i class="fa-solid fa-circle-dot text-purple-400 mt-1 mr-2"></i>
<span><strong>Knoten ansehen:</strong> Bewegen Sie die Maus über einen Knoten, um Details zu sehen.</span>
</li>
<li class="flex items-start">
<i class="fa-solid fa-circle-dot text-purple-400 mt-1 mr-2"></i>
<span><strong>Gedanken anzeigen:</strong> Klicken Sie auf einen Knoten, um zugehörige Gedanken anzuzeigen.</span>
</li>
<li class="flex items-start">
<i class="fa-solid fa-circle-dot text-purple-400 mt-1 mr-2"></i>
<span><strong>Zoomen:</strong> Nutzen Sie das Mausrad oder die Zoom-Schaltflächen oben.</span>
</li>
<li class="flex items-start">
<i class="fa-solid fa-circle-dot text-purple-400 mt-1 mr-2"></i>
<span><strong>Verschieben:</strong> Klicken und ziehen Sie den Hintergrund, um die Ansicht zu verschieben.</span>
</li>
<li class="flex items-start">
<i class="fa-solid fa-circle-dot text-purple-400 mt-1 mr-2"></i>
<span><strong>Knoten bewegen:</strong> Ziehen Sie einen Knoten, um seine Position anzupassen.</span>
</li>
<li class="flex items-start">
<i class="fa-solid fa-circle-dot text-purple-400 mt-1 mr-2"></i>
<span><strong>Suche:</strong> Nutzen Sie die Suchleiste, um bestimmte Knoten zu finden.</span>
</li>
</ul>
</div> </div>
<h3>Intelligente Suche</h3>
<p>Finde genau die Informationen, die du suchst, mit fortschrittlichen Such- und Filterfunktionen für eine präzise Navigation durch das Wissen.</p>
</div> </div>
<!-- Gedanken-Bereich --> <!-- Geführte Pfade -->
<div class="lg:col-span-1"> <div class="feature-card">
<div id="thoughts-container" class="thought-container p-6 h-[600px] overflow-y-auto custom-scrollbar"> <div class="feature-card-icon">
<div class="text-center p-6"> <i class="fas fa-map-signs"></i>
<div class="mb-6 pulse-animation">
<i class="fa-solid fa-diagram-project text-6xl text-indigo-400/50"></i>
</div>
<h3 class="text-xl font-semibold mb-2 text-white">Mindmap erkunden</h3>
<p class="text-gray-300 mb-4">Wählen Sie einen Knoten in der Mindmap aus, um zugehörige Gedanken anzuzeigen.</p>
<div class="p-4 rounded-lg inline-block bg-gradient-to-r from-purple-900/20 to-indigo-900/20 border border-indigo-500/20">
<i class="fa-solid fa-arrow-left text-purple-400 mr-2"></i>
<span class="text-gray-200">Klicken Sie auf einen Knoten</span>
</div>
</div>
</div> </div>
<h3>Geführte Pfade</h3>
<p>Folge kuratierten Lernpfaden durch komplexe Themen oder erschaffe selbst Routen für andere, die deinen Gedankengängen folgen möchten.</p>
</div> </div>
</div> </div>
</div> </div>
@@ -330,55 +515,148 @@
{% block extra_js %} {% block extra_js %}
<!-- D3.js für die Mindmap-Visualisierung --> <!-- D3.js für die Mindmap-Visualisierung -->
<script src="https://cdn.jsdelivr.net/npm/d3@7.8.5/dist/d3.min.js"></script> <script src="https://d3js.org/d3.v7.min.js"></script>
<!-- Tippy.js für erweiterte Tooltips --> <!-- Tippy.js für verbesserte Tooltips -->
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.6/dist/umd/popper.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/tippy.js@6.3.7/dist/tippy.umd.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/tippy.js@6.3.7/dist/tippy.umd.min.js"></script>
<!-- Mindmap-Module --> <!-- D3-Erweiterungen für spezifische Effekte -->
<script src="{{ url_for('static', filename='js/modules/mindmap.js') }}"></script> <script src="{{ url_for('static', filename='d3-extensions.js') }}"></script>
<script src="{{ url_for('static', filename='js/modules/mindmap-page.js') }}"></script> <!-- Mindmap JS -->
<script src="{{ url_for('static', filename='mindmap.js') }}"></script>
<script> <script>
// Zusätzliche Seiten-spezifische Skripte
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Setze Seiten-Identifier für den richtigen Initialisierer // Initialisiere die Mindmap-Visualisierung
document.body.dataset.page = 'mindmap'; const mindmapContainer = document.getElementById('mindmap-container');
const containerWidth = mindmapContainer.clientWidth;
const containerHeight = mindmapContainer.clientHeight;
// Toggle für die Anleitung const mindmap = new MindMapVisualization('#mindmap-container', {
const guidePanel = document.getElementById('guide-panel'); width: containerWidth,
const toggleGuideBtn = document.getElementById('toggle-guide'); height: containerHeight,
nodeRadius: 22,
toggleGuideBtn.addEventListener('click', function() { selectedNodeRadius: 28,
guidePanel.classList.toggle('hidden'); linkDistance: 160,
chargeStrength: -1200,
centerForce: 0.1,
tooltipEnabled: true,
onNodeClick: function(node) {
console.log('Node clicked:', node);
// Hier können spezifische Aktionen für Knotenklicks definiert werden
}
}); });
// Zoom-Buttons mit der Mindmap verbinden // Event-Listener für Steuerungsbuttons
const zoomIn = document.getElementById('zoom-in'); document.getElementById('zoom-in-btn').addEventListener('click', function() {
const zoomOut = document.getElementById('zoom-out'); // Zoom-In-Funktionalität
const zoomReset = document.getElementById('zoom-reset'); const svg = d3.select('#mindmap-container svg');
const currentZoom = d3.zoomTransform(svg.node());
if (zoomIn && zoomOut && zoomReset && window.mindmapInstance) { const newScale = currentZoom.k * 1.3;
zoomIn.addEventListener('click', function() { svg.transition().duration(300).call(
const currentZoom = d3.zoomTransform(window.mindmapInstance.svg.node()).k;
window.mindmapInstance.svg.transition().duration(300).call(
d3.zoom().transform, d3.zoom().transform,
d3.zoomIdentity.scale(currentZoom * 1.3) d3.zoomIdentity.translate(currentZoom.x, currentZoom.y).scale(newScale)
); );
}); });
zoomOut.addEventListener('click', function() { document.getElementById('zoom-out-btn').addEventListener('click', function() {
const currentZoom = d3.zoomTransform(window.mindmapInstance.svg.node()).k; // Zoom-Out-Funktionalität
window.mindmapInstance.svg.transition().duration(300).call( const svg = d3.select('#mindmap-container svg');
const currentZoom = d3.zoomTransform(svg.node());
const newScale = currentZoom.k / 1.3;
svg.transition().duration(300).call(
d3.zoom().transform, d3.zoom().transform,
d3.zoomIdentity.scale(currentZoom / 1.3) d3.zoomIdentity.translate(currentZoom.x, currentZoom.y).scale(newScale)
); );
}); });
zoomReset.addEventListener('click', function() { document.getElementById('center-btn').addEventListener('click', function() {
window.mindmapInstance.svg.transition().duration(300).call( // Zentrieren-Funktionalität
const svg = d3.select('#mindmap-container svg');
svg.transition().duration(500).call(
d3.zoom().transform, d3.zoom().transform,
d3.zoomIdentity.scale(1) d3.zoomIdentity.scale(1)
); );
}); });
// Add Thought Button
document.getElementById('add-thought-btn').addEventListener('click', function() {
// Implementierung für das Hinzufügen eines neuen Gedankens
if (mindmap.selectedNode) {
const newNodeName = prompt('Gedanke eingeben:');
if (newNodeName && newNodeName.trim() !== '') {
const newNodeId = 'node_' + Date.now();
const newNode = {
id: newNodeId,
name: newNodeName,
description: 'Neuer Gedanke',
thought_count: 0
};
// Node zur Mindmap hinzufügen
mindmap.nodes.push(newNode);
// Link zum ausgewählten Knoten erstellen
mindmap.links.push({
source: mindmap.selectedNode.id,
target: newNodeId
});
// Mindmap aktualisieren
mindmap.updateVisualization();
} }
} else {
alert('Bitte zuerst einen Knoten auswählen, um einen Gedanken hinzuzufügen.');
}
});
// Connect Button
document.getElementById('connect-btn').addEventListener('click', function() {
// Implementierung für das Verbinden von Knoten
if (mindmap.selectedNode && mindmap.mouseoverNode && mindmap.selectedNode !== mindmap.mouseoverNode) {
// Prüfen, ob Verbindung bereits existiert
const existingLink = mindmap.links.find(link =>
(link.source.id === mindmap.selectedNode.id && link.target.id === mindmap.mouseoverNode.id) ||
(link.source.id === mindmap.mouseoverNode.id && link.target.id === mindmap.selectedNode.id)
);
if (!existingLink) {
// Link erstellen
mindmap.links.push({
source: mindmap.selectedNode.id,
target: mindmap.mouseoverNode.id
});
// Mindmap aktualisieren
mindmap.updateVisualization();
} else {
alert('Diese Verbindung existiert bereits.');
}
} else {
alert('Bitte wähle zwei verschiedene Knoten aus, um sie zu verbinden.');
}
});
// Responsive Anpassung bei Fenstergrößenänderung
window.addEventListener('resize', function() {
const newWidth = mindmapContainer.clientWidth;
const newHeight = mindmapContainer.clientHeight;
if (mindmap.svg) {
mindmap.svg
.attr('width', newWidth)
.attr('height', newHeight);
mindmap.width = newWidth;
mindmap.height = newHeight;
// Force-Simulation aktualisieren
if (mindmap.simulation) {
mindmap.simulation
.force('center', d3.forceCenter(newWidth / 2, newHeight / 2))
.restart();
}
}
});
}); });
</script> </script>
{% endblock %} {% endblock %}

File diff suppressed because it is too large Load Diff