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
from datetime import datetime, timedelta
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
import secrets
from sqlalchemy.sql import func
import openai
from dotenv import load_dotenv
# Lade .env-Datei
load_dotenv()
app = Flask(__name__)
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['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
@app.context_processor
def inject_globals():
@@ -36,6 +47,17 @@ db = SQLAlchemy(app)
login_manager = LoginManager(app)
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):
SUPPORTS = "stützt"
CONTRADICTS = "widerspricht"
@@ -197,7 +219,7 @@ def mindmap():
@login_required
def profile():
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
@app.route('/settings', methods=['GET', 'POST'])
@@ -468,17 +490,56 @@ def add_comment():
# Admin routes
@app.route('/admin')
@login_required
@admin_required
def admin():
if not current_user.is_admin:
flash('Zugriff verweigert')
return redirect(url_for('index'))
users = User.query.all()
nodes = MindMapNode.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'])
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)}")
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
if __name__ == '__main__':
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
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 */
--standard-spacing: 2rem;
--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 */
@@ -44,7 +52,7 @@ body {
min-height: 100vh;
display: flex;
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);
font-family: var(--body-font);
line-height: 1.6;
@@ -73,22 +81,23 @@ p {
a {
text-decoration: none;
color: var(--accent-color);
color: var(--accent-bright);
transition: all 0.3s ease;
}
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 {
background: var(--glass-bg);
backdrop-filter: var(--glass-blur);
-webkit-backdrop-filter: var(--glass-blur);
border: 1px solid var(--glass-border);
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);
padding: var(--standard-spacing);
transition: transform 0.3s ease, box-shadow 0.3s ease;
@@ -96,7 +105,8 @@ a:hover {
.glass:hover {
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 {
@@ -109,7 +119,7 @@ a: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 {
@@ -121,81 +131,168 @@ a:hover {
/* Stilvolle Farbverläufe für Akzente */
.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;
background-clip: text;
color: transparent;
text-shadow: 0 0 10px rgba(154, 125, 255, 0.3);
}
.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 {
background: rgba(20, 20, 43, 0.8) !important;
backdrop-filter: var(--glass-blur);
-webkit-backdrop-filter: var(--glass-blur);
box-shadow: 0 4px 15px -1px rgba(0, 0, 0, 0.5);
border-bottom: 1px solid var(--glass-border);
padding: 1rem 0;
background: rgba(20, 20, 43, 0.85); /* Etwas weniger transparent */
backdrop-filter: blur(15px); /* Stärkerer Blur */
-webkit-backdrop-filter: blur(15px);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4); /* Stärkerer Schatten */
border-bottom: 1px solid rgba(255, 255, 255, 0.15); /* Hellerer Border */
padding: 0.8rem 0; /* Etwas weniger Padding */
transition: all 0.3s ease;
}
.navbar-brand {
font-family: var(--serif-font);
font-weight: 700;
font-size: 1.5rem;
color: var(--light-color) !important;
letter-spacing: 0.05em;
font-family: 'Inter', sans-serif; /* Konsistente Schriftart */
font-weight: 800; /* Stärkerer Font */
font-size: 1.8rem; /* Größerer Font */
color: white; /* Standardfarbe Weiß */
letter-spacing: -0.03em; /* Engerer Buchstabenabstand */
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); /* Textschatten */
}
.navbar-brand i {
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;
background-clip: text;
color: transparent;
filter: drop-shadow(0 0 8px rgba(154, 125, 255, 0.6)); /* Stärkerer Schatten */
}
.navbar-dark .navbar-nav .nav-link {
color: rgba(255, 255, 255, 0.8);
font-weight: 500;
padding: 0.5rem 1rem;
border-radius: 10px;
.navbar-nav .nav-link {
color: rgba(255, 255, 255, 1); /* Vollständig weißer Text für bessere Lesbarkeit */
font-weight: 600; /* Fetterer Text */
padding: 0.6rem 1.2rem; /* Angepasstes Padding */
border-radius: 12px; /* Größerer Border-Radius */
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-dark .navbar-nav .nav-link.active {
color: var(--accent-color);
background: rgba(108, 93, 211, 0.1);
box-shadow: var(--inner-shadow);
.navbar-nav .nav-link:hover,
.navbar-nav .nav-link.active {
color: var(--accent-bright); /* Hellerer Akzent */
background: rgba(154, 125, 255, 0.15); /* Hellerer Hintergrund bei Hover/Active */
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 {
margin-right: 0.5rem;
.navbar-nav .nav-link i {
margin-right: 0.4rem; /* Angepasster Margin */
transition: transform 0.3s ease;
}
.navbar-dark .navbar-nav .nav-link:hover i {
.navbar-nav .nav-link:hover i {
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 */
.btn {
padding: 0.6rem 1.5rem;
padding: 0.7rem 1.6rem;
border-radius: 12px;
font-weight: 600;
font-weight: 700; /* Fetterer Text */
transition: all 0.3s ease;
border: none;
letter-spacing: 0.05em;
text-transform: uppercase;
font-size: 0.85rem;
font-size: 0.9rem; /* Größere Schrift */
position: relative;
overflow: hidden;
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 {
@@ -215,9 +312,11 @@ a:hover {
}
.btn-primary {
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
color: white;
box-shadow: 0 4px 15px rgba(108, 93, 211, 0.4);
background: var(--light-color);
color: #000 !important;
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 {
@@ -238,69 +337,371 @@ a:hover {
border-color: var(--accent-color);
}
/* Karten-Design */
/* Verbesserte Card-Komponenten mit Glassmorphismus */
.card {
background: var(--glass-bg);
backdrop-filter: var(--glass-blur);
-webkit-backdrop-filter: var(--glass-blur);
border: 1px solid var(--glass-border);
background: rgba(30, 30, 46, 0.7);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-radius: 16px;
box-shadow: var(--glass-card-shadow);
transition: transform 0.3s ease, box-shadow 0.3s ease;
border: 1px solid rgba(72, 71, 138, 0.2);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
margin-bottom: 1.5rem;
position: relative;
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 {
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 {
background: rgba(20, 20, 43, 0.7);
border-bottom: 1px solid var(--glass-border);
padding: 1.25rem 1.5rem;
font-family: var(--serif-font);
background: rgba(20, 20, 40, 0.5);
border-bottom: 1px solid rgba(72, 71, 138, 0.2);
padding: 1rem 1.5rem;
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 {
margin-bottom: 0;
color: var(--primary-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%;
margin: 0;
font-size: 1.25rem;
font-weight: 600;
color: var(--light-color);
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;
}
.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 {
flex: 1;
padding: 1.25rem;
}
.thought-card .metadata {
font-size: 0.9rem;
color: rgba(255, 255, 255, 0.6);
margin-bottom: 1rem;
font-style: italic;
display: flex;
flex-wrap: wrap;
align-items: center;
font-size: 0.85rem;
color: var(--gray-color);
margin-top: 0.5rem;
gap: 1rem;
}
.thought-card .keywords {
@@ -310,56 +711,81 @@ a:hover {
margin-top: 1rem;
}
/* Keywords & Tags */
.keyword-tag {
background: rgba(108, 93, 211, 0.2);
color: var(--accent-color);
display: inline-flex;
align-items: center;
padding: 0.25rem 0.75rem;
border-radius: 2rem;
font-size: 0.8rem;
backdrop-filter: blur(5px);
border: 1px solid rgba(108, 93, 211, 0.4);
font-size: 0.75rem;
font-weight: 600;
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 {
padding: 0.4rem 0.8rem;
border-radius: 10px;
font-size: 0.85rem;
display: inline-flex;
align-items: center;
padding: 0.3rem 0.75rem;
border-radius: 2rem;
font-size: 0.75rem;
font-weight: 600;
margin: 0.25rem;
display: inline-block;
letter-spacing: 0.05em;
backdrop-filter: blur(5px);
margin-right: 0.5rem;
margin-bottom: 0.5rem;
transition: all 0.2s ease;
}
.relation-badge:hover {
transform: translateY(-2px);
}
.relation-supports {
background-color: var(--success-color);
color: #000;
background: rgba(53, 201, 190, 0.15);
color: var(--success-bright);
border: 1px solid rgba(53, 201, 190, 0.3);
}
.relation-contradicts {
background-color: var(--danger-color);
color: #fff;
background: rgba(254, 83, 110, 0.15);
color: var(--danger-bright);
border: 1px solid rgba(254, 83, 110, 0.3);
}
.relation-builds-upon {
background-color: var(--info-color);
color: #fff;
background: rgba(62, 127, 255, 0.15);
color: var(--info-color);
border: 1px solid rgba(62, 127, 255, 0.3);
}
.relation-generalizes {
background-color: var(--warning-color);
color: #000;
background: rgba(255, 182, 72, 0.15);
color: var(--warning-bright);
border: 1px solid rgba(255, 182, 72, 0.3);
}
.relation-specifies {
background-color: var(--gray-color);
color: #fff;
background: rgba(154, 125, 255, 0.15);
color: var(--primary-bright);
border: 1px solid rgba(154, 125, 255, 0.3);
}
.relation-inspires {
background-color: var(--accent-color);
color: #000;
background: rgba(118, 69, 217, 0.15);
color: var(--secondary-bright);
border: 1px solid rgba(118, 69, 217, 0.3);
}
/* Formulare */
@@ -451,16 +877,16 @@ a:hover {
/* Mindmap-Visualisierung */
.mindmap-container {
height: 600px;
width: 100%;
height: 700px;
background: var(--glass-bg);
backdrop-filter: var(--glass-blur);
-webkit-backdrop-filter: var(--glass-blur);
border-radius: 16px;
border: 1px solid var(--glass-border);
box-shadow: var(--glass-card-shadow);
background: rgba(20, 20, 43, 0.7) !important;
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;
margin-bottom: var(--standard-spacing);
margin-bottom: 2rem;
}
/* Filter-Sidebar */
@@ -721,15 +1147,37 @@ a:hover {
opacity: 0.3;
}
/* Footer */
/* Footer - Überarbeitet mit verbessertem Glassmorphismus und Responsivität */
footer {
background: rgba(20, 20, 43, 0.8);
backdrop-filter: var(--glass-blur);
-webkit-backdrop-filter: var(--glass-blur);
background: rgba(20, 20, 43, 0.85); /* Etwas weniger transparent */
backdrop-filter: blur(15px); /* Stärkerer 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;
border-top: 1px solid var(--glass-border);
color: rgba(255, 255, 255, 0.8);
margin-top: var(--standard-spacing);
color: rgba(255, 255, 255, 0.9); /* Hellerer Text */
margin-top: 4rem; /* Konsistenter Abstand */
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 */
@@ -852,3 +1300,21 @@ footer {
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
# -*- coding: utf-8 -*-
import os
from PIL import Image
import cairosvg

View File

@@ -2,284 +2,222 @@
* 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 = {
/**
* Initialisiert die Anwendung
*/
init: function() {
// Initialisiere alle Komponenten
this.setupDarkMode();
this.setupTooltips();
this.setupUtilityFunctions();
// App-Status
initialized: false,
darkMode: document.documentElement.classList.contains('dark'),
pageInitializers: {},
currentPage: document.body.dataset.page,
// Prüfe, ob spezifische Seiten-Initialisierer vorhanden sind
const currentPage = document.body.dataset.page;
if (currentPage && this.pageInitializers[currentPage]) {
this.pageInitializers[currentPage]();
/**
* Initialisiert die MindMap-Anwendung
*/
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();
/**
* Dark Mode Setup
*/
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)) {
// Dunkel-/Hellmodus aus LocalStorage wiederherstellen
if (localStorage.getItem('darkMode') === 'dark') {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
this.darkMode = true;
}
// Höre auf System-Präferenzänderungen
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
if (localStorage.getItem('darkMode') === null) {
if (e.matches) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
}
});
// Mindmap initialisieren, falls auf der richtigen Seite
this.initializeMindmap();
// Dark Mode Toggle-Listener (wird über Alpine.js gehandelt)
document.addEventListener('darkModeToggled', function(e) {
const isDark = e.detail.isDark;
localStorage.setItem('darkMode', isDark ? 'dark' : 'light');
});
this.initialized = true;
},
/**
* Tooltips mit Tippy.js einrichten
* Initialisiert die D3.js Mindmap-Visualisierung
*/
setupTooltips: function() {
// Prüfe, ob Tippy.js geladen ist
if (typeof tippy !== 'undefined') {
// Allgemeine Tooltips
tippy('[data-tippy-content]', {
theme: 'mindmap',
animation: 'scale',
arrow: true
initializeMindmap() {
// Prüfe, ob wir auf der Mindmap-Seite sind
const mindmapContainer = document.getElementById('mindmap-container');
if (!mindmapContainer) return;
try {
console.log('Initialisiere Mindmap...');
// 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
tippy('.mindmap-node', {
content(reference) {
const title = reference.getAttribute('data-title');
const desc = reference.getAttribute('data-description');
return `<div class="node-tooltip"><strong>${title}</strong>${desc ? `<p>${desc}</p>` : ''}</div>`;
},
allowHTML: true,
theme: 'mindmap',
animation: 'scale',
arrow: true,
placement: 'top'
// Globale Referenz für andere Module
window.mindmapInstance = mindmap;
// Event-Listener für Zoom-Buttons
const zoomInBtn = document.getElementById('zoom-in-btn');
if (zoomInBtn) {
zoomInBtn.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 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() {
// Axios-Interceptor für API-Anfragen
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);
});
handleNodeClick(node) {
console.log('Knoten wurde angeklickt:', node);
axios.interceptors.response.use(function(response) {
return response;
}, function(error) {
// Behandle Fehler und zeige Benachrichtigungen
const message = error.response && error.response.data && error.response.data.error
? error.response.data.error
: 'Ein Fehler ist aufgetreten.';
// Hier könnte man Logik hinzufügen, um Detailinformationen anzuzeigen
// oder den ausgewählten Knoten hervorzuheben
const detailsContainer = document.getElementById('node-details');
if (detailsContainer) {
detailsContainer.innerHTML = `
<div class="p-4">
<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');
return Promise.reject(error);
});
}
},
/**
* 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>
<div class="flex items-center justify-between">
<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
</button>
</div>
</div>
`;
// Füge zur Notification-Area hinzu
let notificationArea = document.querySelector('.notification-area');
if (!notificationArea) {
notificationArea = document.createElement('div');
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);
// Button zum Hinzufügen eines Gedankens
const addThoughtBtn = detailsContainer.querySelector('button');
addThoughtBtn.addEventListener('click', () => {
this.showAddThoughtDialog(node);
});
}
},
/**
* Seitenspezifische Initialisierer
* Dialog zum Hinzufügen eines neuen Knotens
*/
pageInitializers: {
// Startseite
'home': function() {
console.log('Startseite initialisiert');
// Hier kommen spezifische Funktionen für die Startseite
showAddNodeDialog() {
// Diese Funktionalität würde in einer vollständigen Implementierung eingebunden werden
alert('Diese Funktion steht bald zur Verfügung!');
},
// Mindmap-Seite
'mindmap': function() {
console.log('Mindmap-Seite initialisiert');
// Hier werden mindmap-spezifische Funktionen aufgerufen
// Die tatsächliche D3.js-Implementierung wird in einer separaten Datei sein
/**
* Dialog zum Hinzufügen eines neuen Gedankens zu einem Knoten
*/
showAddThoughtDialog(node) {
// Diese Funktionalität würde in einer vollständigen Implementierung eingebunden werden
alert('Diese Funktion steht bald zur Verfügung!');
},
// Profilseite
'profile': function() {
console.log('Profilseite initialisiert');
// Profil-spezifische Funktionen
/**
* Dialog zum Verbinden von Knoten
*/
showConnectDialog() {
// Diese Funktionalität würde in einer vollständigen Implementierung eingebunden werden
alert('Diese Funktion steht bald zur Verfügung!');
},
// Suchseite
'search': function() {
console.log('Suchseite initialisiert');
// Such-spezifische Funktionen
/**
* Richtet Event-Listener für die Benutzeroberfläche ein
*/
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
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
// Globale Export für andere Module
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.isLoading = true;
// Sicherstellen, dass der Container bereit ist
if (this.container.node()) {
this.init();
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
@@ -62,6 +72,9 @@ class MindMapVisualization {
init() {
// SVG erstellen, wenn noch nicht vorhanden
if (!this.svg) {
// Container zuerst leeren
this.container.html('');
this.svg = this.container
.append('svg')
.attr('width', '100%')
@@ -80,12 +93,24 @@ class MindMapVisualization {
this.g = this.svg.append('g');
// Tooltip initialisieren
if (!d3.select('body').select('.node-tooltip').size()) {
this.tooltipDiv = d3.select('body')
.append('div')
.attr('class', 'node-tooltip')
.style('opacity', 0)
.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
@@ -94,6 +119,9 @@ class MindMapVisualization {
.force('charge', d3.forceManyBody().strength(this.chargeStrength))
.force('center', d3.forceCenter(this.width / 2, this.height / 2).strength(this.centerForce))
.force('collision', d3.forceCollide().radius(this.nodeRadius * 2));
// Globale Mindmap-Instanz für externe Zugriffe setzen
window.mindmapInstance = this;
}
handleZoom(transform) {
@@ -118,13 +146,25 @@ class MindMapVisualization {
// Ladeindikator anzeigen
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 {
// API-Aufruf mit Timeout versehen
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', {
signal: controller.signal
signal: controller.signal,
headers: {
'Cache-Control': 'no-cache',
'Pragma': 'no-cache'
}
});
clearTimeout(timeoutId);
@@ -135,8 +175,8 @@ class MindMapVisualization {
const data = await response.json();
if (!data || !data.nodes || data.nodes.length === 0) {
console.warn('Keine Mindmap-Daten vorhanden, verwende Standard-Daten.');
throw new Error('Keine Daten');
console.warn('Keine Mindmap-Daten vorhanden, verwende weiterhin Standard-Daten.');
return; // Behalte Standarddaten bei
}
// Flache Liste von Knoten und Verbindungen erstellen
@@ -144,24 +184,29 @@ class MindMapVisualization {
this.links = [];
this.processHierarchicalData(data.nodes);
} catch (error) {
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;
// Visualisierung aktualisieren mit den tatsächlichen Daten
this.updateVisualization();
// Status auf bereit setzen
this.container.attr('data-status', 'ready');
} catch (error) {
console.warn('Fehler beim Laden der Mindmap-Daten, verwende Standarddaten:', error);
// Fallback zu Standarddaten ist bereits geschehen
// Stellen Sie sicher, dass der Status korrekt gesetzt wird
this.container.attr('data-status', 'ready');
}
} catch (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.container.attr('data-status', 'error');
}
}
showLoading() {
// Element nur leeren, wenn es noch kein SVG enthält
if (!this.container.select('svg').size()) {
this.container.html(`
<div class="flex justify-center items-center h-full">
<div class="text-center">
@@ -171,6 +216,7 @@ class MindMapVisualization {
</div>
`);
}
}
processHierarchicalData(hierarchicalNodes, parentId = null) {
hierarchicalNodes.forEach(node => {
@@ -230,6 +276,9 @@ class MindMapVisualization {
this.init();
}
// Performance-Optimierung: Deaktiviere Transition während des Datenladens
const useTransitions = false;
// Links (Edges) erstellen
this.linkElements = this.g.selectAll('.link')
.data(this.links)
@@ -263,6 +312,39 @@ class MindMapVisualization {
.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
const nodeGroups = this.g.selectAll('.node-group')
.data(this.nodes)
@@ -303,7 +385,7 @@ class MindMapVisualization {
.style('font-size', '12px')
.style('font-weight', '500')
.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
group
@@ -320,60 +402,29 @@ class MindMapVisualization {
// Text aktualisieren
update.select('.node-label')
.text(d => d.name);
.text(d => d.name.length > 12 ? d.name.slice(0, 10) + '...' : d.name);
return update;
},
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
this.nodeElements = this.g.selectAll('.node');
this.textElements = this.g.selectAll('.node-label');
// Simulation starten
// Performance-Optimierung: Weniger Simulationsschritte für schnellere Stabilisierung
this.simulation
.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')
.links(this.links);
// Simulation neu starten
this.simulation.alpha(1).restart();
this.simulation.restart();
}
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" %}
{% block title %}Admin | Wissenschaftliche Mindmap{% endblock %}
{% block title %}Admin-Bereich{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-8">
<div class="glass p-8 mb-8">
<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>
<h1 class="text-3xl font-bold mb-8 text-gray-800 dark:text-white">Admin-Bereich</h1>
<!-- 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 class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<!-- Users Section -->
<div class="dark-glass p-6" x-data="{ tab: 'users' }">
<div class="flex items-center justify-between mb-6">
<h2 class="text-2xl font-bold text-white">Benutzer</h2>
<span class="bg-indigo-600 text-white text-xs font-medium px-2.5 py-0.5 rounded-full">{{ users|length }}</span>
<!-- Benutzer-Tab -->
<div x-show="activeTab === 'users'" class="glass-morphism rounded-lg p-6">
<div class="flex justify-between items-center mb-6">
<h2 class="text-xl font-bold text-gray-800 dark:text-white">Benutzerverwaltung</h2>
<button class="btn-outline">
<i class="fas fa-plus mr-2"></i> Neuer Benutzer
</button>
</div>
<div class="overflow-y-auto max-h-[500px]">
<table class="w-full text-white/90">
<thead class="text-white/60 text-sm uppercase">
<tr>
<th class="text-left py-3">ID</th>
<th class="text-left py-3">Benutzername</th>
<th class="text-left py-3">Email</th>
<th class="text-left py-3">Rolle</th>
<div class="overflow-x-auto">
<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">Benutzername</th>
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">E-Mail</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>
</thead>
<tbody>
{% for user in users %}
<tr class="border-t border-white/10">
<td class="py-3">{{ user.id }}</td>
<td class="py-3">{{ user.username }}</td>
<td class="py-3">{{ user.email }}</td>
<td class="py-3">
<tr class="border-b border-gray-100 dark:border-gray-800 hover:bg-gray-50 dark:hover:bg-dark-700/30">
<td class="px-4 py-3 text-gray-700 dark:text-gray-300">{{ user.id }}</td>
<td class="px-4 py-3 text-gray-700 dark:text-gray-300 font-medium">{{ user.username }}</td>
<td class="px-4 py-3 text-gray-700 dark:text-gray-300">{{ user.email }}</td>
<td class="px-4 py-3 text-gray-700 dark:text-gray-300">
{% 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 %}
<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 %}
</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>
{% endfor %}
</tbody>
@@ -47,63 +85,224 @@
</div>
</div>
<!-- Mindmap Nodes Section -->
<div class="dark-glass p-6">
<div class="flex items-center justify-between mb-6">
<h2 class="text-2xl font-bold text-white">Mindmap Struktur</h2>
<span class="bg-indigo-600 text-white text-xs font-medium px-2.5 py-0.5 rounded-full">{{ nodes|length }}</span>
<!-- Mindmap-Knoten-Tab -->
<div x-show="activeTab === 'nodes'" class="glass-morphism rounded-lg p-6">
<div class="flex justify-between items-center mb-6">
<h2 class="text-xl font-bold text-gray-800 dark:text-white">Mindmap-Knoten Verwaltung</h2>
<button class="btn-outline">
<i class="fas fa-plus mr-2"></i> Neuer Knoten
</button>
</div>
<div class="overflow-y-auto max-h-[500px]">
<div class="space-y-3">
<div class="overflow-x-auto">
<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 %}
<div class="glass p-3">
<div class="flex justify-between items-center">
<span class="font-medium">{{ node.name }}</span>
<span class="text-xs text-white/60">ID: {{ node.id }}</span>
</div>
<tr class="border-b border-gray-100 dark:border-gray-800 hover:bg-gray-50 dark:hover:bg-dark-700/30">
<td class="px-4 py-3 text-gray-700 dark:text-gray-300">{{ node.id }}</td>
<td class="px-4 py-3 text-gray-700 dark:text-gray-300 font-medium">{{ node.name }}</td>
<td class="px-4 py-3 text-gray-700 dark:text-gray-300">
{% if node.parent %}
<p class="text-sm text-white/60 mt-1">Eltern: {{ node.parent.name }}</p>
{{ node.parent.name }}
{% else %}
<p class="text-sm text-white/60 mt-1">Hauptknoten</p>
<span class="text-gray-400 dark:text-gray-500">Wurzelknoten</span>
{% 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 %}
</tbody>
</table>
</div>
</div>
<div class="mt-6">
<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">
Neuen Knoten hinzufügen
<!-- Gedanken-Tab -->
<div x-show="activeTab === 'thoughts'" class="glass-morphism rounded-lg p-6">
<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>
</div>
</div>
<!-- Thoughts Section -->
<div class="dark-glass p-6">
<div class="flex items-center justify-between mb-6">
<h2 class="text-2xl font-bold text-white">Gedanken</h2>
<span class="bg-indigo-600 text-white text-xs font-medium px-2.5 py-0.5 rounded-full">{{ thoughts|length }}</span>
</div>
<div class="overflow-y-auto max-h-[500px]">
<div class="space-y-3">
<div class="overflow-x-auto">
<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">Titel</th>
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">Autor</th>
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">Datum</th>
<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 %}
<div class="glass p-3">
<div class="flex justify-between items-start">
<span class="inline-block px-2 py-0.5 text-xs text-white/70 bg-white/10 rounded-full mb-1">{{ thought.branch }}</span>
<span class="text-xs text-white/50">{{ thought.timestamp.strftime('%d.%m.%Y') }}</span>
</div>
<p class="text-sm text-white mb-1 line-clamp-2">{{ thought.content }}</p>
<div class="flex justify-between items-center mt-2 text-xs">
<span class="text-white/60">Von: {{ thought.author.username }}</span>
<span class="text-white/60">{{ thought.comments|length }} Kommentar(e)</span>
</div>
</div>
<tr class="border-b border-gray-100 dark:border-gray-800 hover:bg-gray-50 dark:hover:bg-dark-700/30">
<td class="px-4 py-3 text-gray-700 dark:text-gray-300">{{ thought.id }}</td>
<td class="px-4 py-3 text-gray-700 dark:text-gray-300 font-medium">{{ thought.title }}</td>
<td class="px-4 py-3 text-gray-700 dark:text-gray-300">{{ thought.author.username }}</td>
<td class="px-4 py-3 text-gray-700 dark:text-gray-300">{{ thought.timestamp.strftime('%d.%m.%Y') }}</td>
<td class="px-4 py-3 text-gray-700 dark:text-gray-300">
<div class="flex items-center">
<span class="mr-2">{{ "%.1f"|format(thought.average_rating) }}</span>
<div class="flex">
{% for i in range(5) %}
{% 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 %}
</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>
{% 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 -->
<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 -->
<link href="{{ url_for('static', filename='css/main.css') }}" rel="stylesheet">
<!-- Alpine.js -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.12.3/dist/cdn.min.js"></script>
<!-- Theme Script (run before rendering) -->
<script>
<!-- Hauptmodul laden (als ES6 Modul) -->
<script type="module">
import MindMap from "{{ url_for('static', filename='js/main.js') }}";
// Alpine.js-Integration
document.addEventListener('alpine:init', () => {
Alpine.data('layout', () => ({
darkMode: false,
@@ -87,379 +95,629 @@
}
}));
});
// MindMap global verfügbar machen (für Alpine.js und andere nicht-Module Skripte)
window.MindMap = MindMap;
</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 %}
</head>
<body x-data="{
darkMode: true,
mobileMenuOpen: false,
userMenuOpen: false,
searchOpen: false,
showSettingsModal: false,
<body data-page="{{ request.endpoint }}" class="relative overflow-x-hidden">
<!-- Globaler Hintergrund -->
<div class="full-page-bg"></div>
toggleDarkMode() {
this.darkMode = !this.darkMode;
document.querySelector('html').classList.toggle('dark', this.darkMode);
// Speichere den Dark Mode-Status auf dem Server
fetch('/set_dark_mode', {
method: 'POST',
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>
<!-- App-Container -->
<div id="app-container" class="flex flex-col min-h-screen" x-data="layout">
<!-- 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);'">
<!-- Logo -->
<a href="{{ url_for('index') }}" style="display: flex; align-items: center; text-decoration: none;">
<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>
</a>
</div>
<div class="hidden sm:ml-6 sm:flex sm:space-x-8">
<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 %}">
<i class="fa-solid fa-diagram-project mr-2"></i> Mindmap
<!-- Hauptnavigation - Desktop -->
<div style="display: none; align-items: center; gap: 1rem;" x-bind:style="window.innerWidth >= 768 ? 'display: flex;' : 'display: none;'">
<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 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 %}">
<i class="fa-solid fa-magnifying-glass mr-2"></i> Suche
<a href="{{ url_for('mindmap') }}"
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>
{% 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 %}">
<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 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 %}">
<i class="fa-solid fa-screwdriver-wrench mr-2"></i> Admin
<a href="{{ url_for('profile') }}"
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 == '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);' }}'
: '{{ 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-user" style="margin-right: 0.5rem;"></i>Profil
</a>
{% 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>
<!-- Rechte Aktionsleiste (Desktop) -->
<div class="hidden sm:ml-6 sm:flex sm:items-center">
<!-- Dark Mode Toggle -->
<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>
<!-- Mobilmenü-Button -->
<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;'">
<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>
<!-- Nutzer-Menü -->
<!-- Benutzermenü oder Login -->
{% if current_user.is_authenticated %}
<div class="ml-3 relative">
<div>
<button @click="userMenuOpen = !userMenuOpen" class="flex rounded-full focus:outline-none">
<span class="sr-only">User Menü öffnen</span>
<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">
{{ current_user.username[0] | upper }}
</div>
</button>
</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>
<div style="position: relative;" x-data="{ open: false }">
<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;"
x-bind:style="darkMode
? 'background: rgba(32, 36, 55, 0.8); color: rgba(255, 255, 255, 0.9);'
: 'background: rgba(240, 240, 240, 0.8); color: rgba(26, 32, 44, 0.9);'">
<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;">
{% if current_user.avatar %}
<img src="{{ current_user.avatar }}" alt="{{ current_user.username }}" style="width: 100%; height: 100%; object-fit: cover;">
{% 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() }}
</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 %}
</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>
</nav>
</header>
<!-- Flash Messages -->
<div class="container max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-4 z-20">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<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">
<div class="flex items-start">
<div class="flex-shrink-0">
{% if category == 'error' %}
<i class="fa-solid fa-circle-exclamation text-red-500"></i>
{% elif category == 'success' %}
<i class="fa-solid fa-circle-check text-green-500"></i>
{% else %}
<i class="fa-solid fa-circle-info text-blue-500"></i>
<!-- Mobile Menü (erscheint, wenn mobileMenuOpen true ist) -->
<div x-show="mobileMenuOpen"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 -translate-y-10"
x-transition:enter-end="opacity-100 translate-y-0"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100 translate-y-0"
x-transition:leave-end="opacity-0 -translate-y-10"
style="width: 100%; overflow: hidden; z-index: 90;"
x-bind:style="darkMode
? '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);'
: '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);'">
<div style="display: flex; flex-direction: column; padding: 1rem;">
<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 %}
</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>
<!-- 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 %}
</main>
<!-- Footer -->
<footer class="mt-auto py-6 glass-effect border-t border-gray-200 dark:border-white/10 relative z-10">
<div class="container max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="md:flex md:justify-between">
<div class="mb-6 md:mb-0">
<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>
<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>
<footer class="mt-12 py-8 transition-colors duration-300" {# mt-16 changed to mt-12, py-10 changed to py-8 #}
:class="darkMode ? 'glass-morphism' : 'glass-morphism-light'">
<div class="container mx-auto px-4">
<div class="flex flex-col md:flex-row justify-between items-center md:items-start">
<div class="mb-8 md:mb-0 text-center md:text-left">
<a href="{{ url_for('index') }}" class="text-2xl font-bold gradient-text">MindMap</a>
<p class="mt-2 text-sm max-w-sm"
:class="darkMode ? 'text-gray-400' : 'text-gray-600'">Eine interaktive Plattform zum Visualisieren, Erforschen und Teilen von Wissen.</p>
</div>
<div class="grid grid-cols-1 gap-8 sm:grid-cols-2 md:grid-cols-3">
<div>
<h3 class="text-sm font-semibold text-gray-800 dark:text-white tracking-wider uppercase">Navigieren</h3>
<ul class="mt-4 space-y-2">
<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>
<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>
<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>
</ul>
<div class="flex flex-wrap justify-center md:justify-start space-x-6 mb-8 md:mb-0">
<a href="{{ url_for('impressum') }}"
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'">Impressum</a>
<a href="{{ url_for('datenschutz') }}"
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'">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>
<h3 class="text-sm font-semibold text-gray-800 dark:text-white tracking-wider uppercase">Rechtliches</h3>
<ul class="mt-4 space-y-2">
<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>
<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>
<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>
</ul>
<!-- Optional: Social Media Links oder andere Elemente -->
{#
<div class="flex space-x-4">
<a href="#" class="text-gray-400 hover:text-white transition-colors"><i class="fab fa-twitter"></i></a>
<a href="#" class="text-gray-400 hover:text-white transition-colors"><i class="fab fa-linkedin"></i></a>
<a href="#" class="text-gray-400 hover:text-white transition-colors"><i class="fab fa-github"></i></a>
</div>
#}
</div>
<div>
<h3 class="text-sm font-semibold text-gray-800 dark:text-white tracking-wider uppercase">Folgen</h3>
<div class="mt-4 flex space-x-4">
<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 class="mt-8 pt-6 text-center text-xs" {# mt-10 changed to mt-8 #}
:class="darkMode ? 'border-t border-gray-800/50 text-gray-500' : 'border-t border-gray-300/50 text-gray-600'">
&copy; {{ current_year }} MindMap. Alle Rechte vorbehalten.
</div>
</div>
</footer>
</div>
<!-- Scripts -->
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
{% block extra_js %}{% endblock %}
<!-- Hilfsscripts -->
{% block scripts %}{% endblock %}
</body>
</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 %}
<style>
.hero-gradient {
background: linear-gradient(125deg, rgba(32, 92, 245, 0.8), rgba(128, 32, 245, 0.8));
clip-path: polygon(0 0, 100% 0, 100% 85%, 0 100%);
/* Hintergrund über die gesamte Seite erstrecken */
html, body {
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 {
@@ -30,48 +40,6 @@
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 {
0% { r: 10; opacity: 0.7; }
50% { r: 12; opacity: 1; }
@@ -82,15 +50,6 @@
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 {
0% { transform: scale(1); }
50% { transform: scale(1.1); }
@@ -101,26 +60,33 @@
animation: iconPulse 3s ease-in-out infinite;
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>
{% endblock %}
{% block content %}
<!-- Hintergrund für die gesamte Seite -->
<div class="full-page-bg gradient-background"></div>
<!-- Hero Section -->
<section class="relative pt-16 pb-32" style="border-radius: 20px !important;" style="border-radius: 20px !important;">
<!-- 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>
<section class="relative pt-16 pb-32">
<!-- 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="text-center mb-16" 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">
<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
<div class="mt-2">vernetzen</div>
@@ -130,14 +96,14 @@
in einem interaktiven Wissensnetzwerk.
</p>
<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">
<i class="fa-solid fa-diagram-project mr-3"></i>
Mindmap erkunden
</span>
</a>
{% 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">
<i class="fa-solid fa-user-plus mr-3 icon-pulse"></i>
Konto erstellen
@@ -194,12 +160,12 @@
<!-- Network Nodes -->
<g class="nodes">
<circle cx="400" cy="150" r="15" fill="url(#nodeGradient)" filter="url(#glow)" class="animate-pulse" />
<circle cx="200" cy="250" r="10" fill="url(#nodeGradient)" />
<circle cx="600" cy="250" r="10" fill="url(#nodeGradient)" />
<circle cx="400" cy="350" r="15" fill="url(#nodeGradient)" filter="url(#glow)" class="animate-pulse" />
<circle cx="300" cy="200" r="8" fill="url(#nodeGradient)" />
<circle cx="500" cy="300" r="8" fill="url(#nodeGradient)" />
<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)" class="float-animation" />
<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 float-animation" />
<circle cx="300" cy="200" r="8" fill="url(#nodeGradient)" class="float-animation" />
<circle cx="500" cy="300" r="8" fill="url(#nodeGradient)" class="float-animation" />
</g>
</svg>
</div>
@@ -222,72 +188,72 @@
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
<!-- Feature Card 1 -->
<div class="card card-hover p-6">
<div class="text-primary-400 text-3xl mb-4">
<div class="card">
<div class="icon">
<i class="fa-solid fa-brain icon-pulse"></i>
</div>
<h3 class="text-xl font-bold mb-3 text-gray-800 dark:text-white">Visualisiere Wissen</h3>
<p class="text-gray-600 dark:text-gray-300">
<h3>Visualisiere Wissen</h3>
<p>
Sieh Wissen als vernetztes System, entdecke Zusammenhänge und erkenne überraschende
Verbindungen zwischen verschiedenen Themengebieten.
</p>
</div>
<!-- Feature Card 2 -->
<div class="card card-hover p-6">
<div class="text-secondary-400 text-3xl mb-4">
<div class="card">
<div class="icon">
<i class="fa-solid fa-lightbulb icon-pulse"></i>
</div>
<h3 class="text-xl font-bold mb-3 text-gray-800 dark:text-white">Teile Gedanken</h3>
<p class="text-gray-600 dark:text-gray-300">
<h3>Teile Gedanken</h3>
<p>
Füge deine eigenen Ideen und Perspektiven hinzu. Erstelle Verbindungen zu
vorhandenen Gedanken und bereichere die wachsende Wissensbasis.
</p>
</div>
<!-- Feature Card 3 -->
<div class="card card-hover p-6">
<div class="text-green-400 text-3xl mb-4">
<div class="card">
<div class="icon">
<i class="fa-solid fa-users icon-pulse"></i>
</div>
<h3 class="text-xl font-bold mb-3 text-gray-800 dark:text-white">Community</h3>
<p class="text-gray-600 dark:text-gray-300">
<h3>Community</h3>
<p>
Sei Teil einer Gemeinschaft, die gemeinsam ein verteiltes Wissensarchiv aufbaut
und sich in thematisch fokussierten Bereichen austauscht.
</p>
</div>
<!-- Feature Card 4 -->
<div class="card card-hover p-6">
<div class="text-blue-400 text-3xl mb-4">
<div class="card">
<div class="icon">
<i class="fa-solid fa-robot icon-pulse"></i>
</div>
<h3 class="text-xl font-bold mb-3 text-gray-800 dark:text-white">KI-Assistenz</h3>
<p class="text-gray-600 dark:text-gray-300">
<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>
<!-- Feature Card 5 -->
<div class="card card-hover p-6">
<div class="text-purple-400 text-3xl mb-4">
<div class="card">
<div class="icon">
<i class="fa-solid fa-search icon-pulse"></i>
</div>
<h3 class="text-xl font-bold mb-3 text-gray-800 dark:text-white">Intelligente Suche</h3>
<p class="text-gray-600 dark:text-gray-300">
<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>
<!-- Feature Card 6 -->
<div class="card card-hover p-6">
<div class="text-indigo-400 text-3xl mb-4">
<div class="card">
<div class="icon">
<i class="fa-solid fa-route icon-pulse"></i>
</div>
<h3 class="text-xl font-bold mb-3 text-gray-800 dark:text-white">Geführte Pfade</h3>
<p class="text-gray-600 dark:text-gray-300">
<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>
@@ -298,10 +264,8 @@
<!-- Call to Action Section -->
<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="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:w-2/3">
<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>
</div>
<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">
<i class="fa-solid fa-arrow-right mr-2"></i>
Zur Mindmap
@@ -327,7 +291,7 @@
<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">
<!-- 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">
<i class="fa-solid fa-robot text-primary-400 mr-2"></i>
KI-Assistent
@@ -336,7 +300,7 @@
Stelle Fragen, lasse dir Themen erklären oder finde neue Verbindungen mit Hilfe
unseres KI-Assistenten.
</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="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>
@@ -357,11 +321,11 @@
</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>
<!-- 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">
<i class="fa-solid fa-fire text-secondary-400 mr-2"></i>
Themen-Übersicht
@@ -392,7 +356,7 @@
<i class="fa-solid fa-chevron-right text-gray-500"></i>
</a>
</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>

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 %}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tippy.js@6.3.7/dist/tippy.css">
<style>
/* Modernes Dark-Mode Design mit Purple-Blue Gradient */
.mindmap-header {
background: linear-gradient(120deg, rgba(32, 32, 64, 0.7), rgba(24, 24, 48, 0.7));
border-radius: 0.75rem;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.05);
/* Globaler Stil - Hintergrund über die gesamte Seite */
html, body {
background-color: var(--dark-bg) !important;
min-height: 100vh;
width: 100%;
color: #ffffff;
margin: 0;
padding: 0;
overflow-x: hidden;
}
.gradient-text {
background: linear-gradient(to right, #a67fff, #5096ff);
/* Sicherstellen, dass der Hintergrund die gesamte Seite abdeckt */
#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;
background-clip: text;
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 */
.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%;
height: 100%;
border-radius: 0.5rem;
border-radius: 24px;
}
/* Verbesserte Mindmap-Knoten-Stile */
.node {
cursor: pointer;
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 {
font-family: 'Inter', 'SF Pro Display', system-ui, sans-serif;
font-weight: 500;
font-weight: 600;
pointer-events: 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 {
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 {
background: rgba(20, 20, 40, 0.7);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 0.5rem 0.5rem 0 0;
background: rgba(24, 28, 45, 0.85);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 20px 20px 0 0;
}
/* Tooltip Stile */
.tippy-box[data-theme~='mindmap'] {
background-color: rgba(20, 20, 40, 0.9);
background-color: rgba(24, 28, 45, 0.95);
color: #ffffff;
border: 1px solid rgba(160, 80, 255, 0.2);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5), 0 0 10px rgba(160, 80, 255, 0.2);
backdrop-filter: blur(10px);
border: 1px solid rgba(179, 143, 255, 0.25);
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.5), 0 0 15px rgba(179, 143, 255, 0.25);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-radius: 16px;
}
.tippy-box[data-theme~='mindmap'] .tippy-arrow {
color: rgba(20, 20, 40, 0.9);
color: rgba(24, 28, 45, 0.95);
}
.node-tooltip {
font-family: 'Inter', 'SF Pro Display', system-ui, sans-serif;
font-size: 14px;
line-height: 1.5;
padding: 8px 12px;
line-height: 1.6;
padding: 12px 16px;
letter-spacing: 0.3px;
}
.node-tooltip strong {
font-weight: 600;
color: #a67fff;
color: #b38fff;
}
/* Gedanken-Container */
.thought-container {
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 0.5rem;
backdrop-filter: blur(10px);
background: rgba(20, 20, 40, 0.7);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 24px;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
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 */
@@ -94,17 +219,17 @@
}
.custom-scrollbar::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
background: rgba(255, 255, 255, 0.08);
border-radius: 3px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: rgba(160, 80, 255, 0.3);
background: rgba(179, 143, 255, 0.5);
border-radius: 3px;
}
.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 */
@@ -121,208 +246,268 @@
}
}
/* ASCII-Style Tech Decoration */
.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 */
/* Button-Effekte mit verbesserter Lesbarkeit */
.control-btn {
transition: all 0.2s ease;
position: relative;
overflow: hidden;
background: rgba(32, 36, 55, 0.85);
color: #ffffff;
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 {
background: rgba(160, 80, 255, 0.2);
transform: translateY(-1px);
background: rgba(179, 143, 255, 0.35);
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 {
transform: translateY(1px);
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.2);
}
.control-btn::after {
content: "";
display: block;
position: absolute;
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 {
background: rgba(179, 143, 255, 0.4);
border: 1px solid rgba(179, 143, 255, 0.5);
box-shadow: 0 0 15px rgba(179, 143, 255, 0.3);
}
.control-btn:active::after {
transform: scale(0, 0);
opacity: 0.3;
transition: 0s;
/* Glow Effect für Buttons */
.btn-glow:hover {
box-shadow: 0 0 15px rgba(179, 143, 255, 0.5);
}
/* 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>
{% endblock %}
{% block content %}
<div class="relative mb-6" data-page="mindmap">
<!-- ASCII Dekorationen -->
<div class="ascii-decoration top-right-deco">
┌─┐ ┌─┐ ┌─┐ ┌─┐
│ │ │ │ │ │ │ │
└─┘ └─┘ └─┘ └─┘
<div class="container mx-auto px-4 py-12">
<!-- Feature-Karten-Container -->
<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 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>
<!-- Header Bereich -->
<div class="mb-8 p-6 mindmap-header">
<h1 class="text-5xl md:text-6xl font-bold mb-3">
<span class="gradient-text">Mindmap</span>
</h1>
<p class="text-lg text-gray-200 max-w-3xl leading-relaxed">
Erkunden Sie interaktiv verknüpfte Wissensgebiete und ihre Verbindungen. Fügen Sie eigene Gedanken hinzu und erstellen Sie ein kollaboratives Wissensnetz.
</p>
<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">
<i class="fa-solid fa-diagram-project mr-1"></i> Interaktiv
</span>
<span class="px-3 py-1 text-xs rounded-full bg-blue-900/50 text-blue-200 border border-blue-700/30">
<i class="fa-solid fa-sitemap mr-1"></i> Wissensvernetzung
</span>
<span class="px-3 py-1 text-xs rounded-full bg-indigo-900/50 text-indigo-200 border border-indigo-700/30">
<i class="fa-solid fa-lightbulb mr-1"></i> Kollaborativ
</span>
<!-- Feature-Karte 2: Teile Gedanken -->
<div class="feature-card">
<div class="feature-card-icon">
<i class="fas fa-lightbulb"></i>
</div>
<h3>Teile Gedanken</h3>
<p>Füge deine eigenen Ideen und Perspektiven hinzu. Erstelle Verbindungen zu vorhandenen Gedanken und bereichere die wachsende Wissensbasis.</p>
</div>
<!-- Feature-Karte 3: Community -->
<div class="feature-card">
<div class="feature-card-icon">
<i class="fas fa-users"></i>
</div>
<h3>Community</h3>
<p>Sei Teil einer Gemeinschaft, die gemeinsam ein verteiltes Wissensarchiv aufbaut und sich in thematisch fokussierten Bereichen austauscht.</p>
</div>
</div>
<!-- Haupt-Grid-Layout -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- Visualisierungsbereich -->
<div class="lg:col-span-2">
<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...">
<!-- Mindmap-Visualisierung Header -->
<div class="mindmap-header p-6 mb-6">
<h2 class="text-3xl font-bold mb-2 gradient-text">Wissenslandschaft erkunden</h2>
<p class="text-gray-300 mb-0">Interagiere mit der Mindmap, um Verbindungen zu entdecken und neue Ideen hinzuzufügen</p>
</div>
<div class="flex gap-2">
<button id="zoom-in" class="control-btn p-2 rounded-md text-gray-200" title="Vergrößern">
<i class="fa-solid fa-plus"></i>
<!-- Mindmap-Container -->
<div class="glass-card overflow-hidden mb-12">
<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 id="zoom-out" class="control-btn p-2 rounded-md text-gray-200" title="Verkleinern">
<i class="fa-solid fa-minus"></i>
<button class="control-btn" id="zoom-out-btn">
<i class="fas fa-search-minus mr-1"></i> Verkleinern
</button>
<button id="zoom-reset" class="control-btn p-2 rounded-md text-gray-200" title="Ansicht zurücksetzen">
<i class="fa-solid fa-house"></i>
<button class="control-btn" id="center-btn">
<i class="fas fa-bullseye mr-1"></i> Zentrieren
</button>
<button id="toggle-guide" class="control-btn p-2 rounded-md text-gray-200" title="Anleitung anzeigen">
<i class="fa-solid fa-circle-question"></i>
</div>
<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>
</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>
<!-- Legende -->
<div class="p-4 border-t border-white/5 bg-slate-900/70 backdrop-blur">
<div class="text-xs text-gray-400 mb-2">Legende</div>
<div class="flex flex-wrap gap-4">
<div class="flex items-center">
<div class="w-3 h-3 rounded-full bg-purple-500 mr-2"></div>
<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>
<!-- Unterer Bereich: KI-Assistenz, Suche und Lernpfade -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
<!-- KI-Assistenz -->
<div class="feature-card">
<div class="feature-card-icon">
<i class="fas fa-robot"></i>
</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>
<!-- Anleitung -->
<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">
<h3 class="text-lg font-semibold mb-2 text-white">Navigation der Mindmap</h3>
<ul class="space-y-1 text-gray-300 text-sm">
<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>
<!-- Intelligente Suche -->
<div class="feature-card">
<div class="feature-card-icon">
<i class="fas fa-search"></i>
</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>
<!-- Gedanken-Bereich -->
<div class="lg:col-span-1">
<div id="thoughts-container" class="thought-container p-6 h-[600px] overflow-y-auto custom-scrollbar">
<div class="text-center p-6">
<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>
<!-- Geführte Pfade -->
<div class="feature-card">
<div class="feature-card-icon">
<i class="fas fa-map-signs"></i>
</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>
@@ -330,55 +515,148 @@
{% block extra_js %}
<!-- D3.js für die Mindmap-Visualisierung -->
<script src="https://cdn.jsdelivr.net/npm/d3@7.8.5/dist/d3.min.js"></script>
<!-- Tippy.js für erweiterte Tooltips -->
<script src="https://d3js.org/d3.v7.min.js"></script>
<!-- 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>
<!-- Mindmap-Module -->
<script src="{{ url_for('static', filename='js/modules/mindmap.js') }}"></script>
<script src="{{ url_for('static', filename='js/modules/mindmap-page.js') }}"></script>
<!-- D3-Erweiterungen für spezifische Effekte -->
<script src="{{ url_for('static', filename='d3-extensions.js') }}"></script>
<!-- Mindmap JS -->
<script src="{{ url_for('static', filename='mindmap.js') }}"></script>
<script>
// Zusätzliche Seiten-spezifische Skripte
document.addEventListener('DOMContentLoaded', function() {
// Setze Seiten-Identifier für den richtigen Initialisierer
document.body.dataset.page = 'mindmap';
// Initialisiere die Mindmap-Visualisierung
const mindmapContainer = document.getElementById('mindmap-container');
const containerWidth = mindmapContainer.clientWidth;
const containerHeight = mindmapContainer.clientHeight;
// Toggle für die Anleitung
const guidePanel = document.getElementById('guide-panel');
const toggleGuideBtn = document.getElementById('toggle-guide');
toggleGuideBtn.addEventListener('click', function() {
guidePanel.classList.toggle('hidden');
const mindmap = new MindMapVisualization('#mindmap-container', {
width: containerWidth,
height: containerHeight,
nodeRadius: 22,
selectedNodeRadius: 28,
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
const zoomIn = document.getElementById('zoom-in');
const zoomOut = document.getElementById('zoom-out');
const zoomReset = document.getElementById('zoom-reset');
if (zoomIn && zoomOut && zoomReset && window.mindmapInstance) {
zoomIn.addEventListener('click', function() {
const currentZoom = d3.zoomTransform(window.mindmapInstance.svg.node()).k;
window.mindmapInstance.svg.transition().duration(300).call(
// Event-Listener für Steuerungsbuttons
document.getElementById('zoom-in-btn').addEventListener('click', function() {
// Zoom-In-Funktionalität
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.scale(currentZoom * 1.3)
d3.zoomIdentity.translate(currentZoom.x, currentZoom.y).scale(newScale)
);
});
zoomOut.addEventListener('click', function() {
const currentZoom = d3.zoomTransform(window.mindmapInstance.svg.node()).k;
window.mindmapInstance.svg.transition().duration(300).call(
document.getElementById('zoom-out-btn').addEventListener('click', function() {
// Zoom-Out-Funktionalität
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.scale(currentZoom / 1.3)
d3.zoomIdentity.translate(currentZoom.x, currentZoom.y).scale(newScale)
);
});
zoomReset.addEventListener('click', function() {
window.mindmapInstance.svg.transition().duration(300).call(
document.getElementById('center-btn').addEventListener('click', function() {
// Zentrieren-Funktionalität
const svg = d3.select('#mindmap-container svg');
svg.transition().duration(500).call(
d3.zoom().transform,
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>
{% endblock %}

File diff suppressed because it is too large Load Diff