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:
68
README.md
68
README.md
@@ -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
1
website/.env
Normal file
@@ -0,0 +1 @@
|
||||
OPENAI_API_KEY=sk-placeholder
|
||||
Binary file not shown.
Binary file not shown.
141
website/app.py
141
website/app.py
@@ -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
12
website/example.env
Normal 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
|
||||
@@ -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.
104
website/static/css/assistant.css
Normal file
104
website/static/css/assistant.css
Normal 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
@@ -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 */
|
||||
@@ -851,4 +1299,22 @@ footer {
|
||||
break-inside: avoid;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
}
|
||||
|
||||
/* Fix for dark background not extending over the entire page */
|
||||
html, body {
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow-x: hidden;
|
||||
background: linear-gradient(135deg, var(--background-start), var(--background-end));
|
||||
background-attachment: fixed;
|
||||
}
|
||||
|
||||
/* Sticky navbar */
|
||||
.navbar.sticky-top {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
}
|
||||
456
website/static/d3-extensions.js
vendored
Normal file
456
website/static/d3-extensions.js
vendored
Normal 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;
|
||||
@@ -1,4 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
from PIL import Image
|
||||
import cairosvg
|
||||
|
||||
@@ -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 = {
|
||||
// App-Status
|
||||
initialized: false,
|
||||
darkMode: document.documentElement.classList.contains('dark'),
|
||||
pageInitializers: {},
|
||||
currentPage: document.body.dataset.page,
|
||||
|
||||
/**
|
||||
* Initialisiert die Anwendung
|
||||
* Initialisiert die MindMap-Anwendung
|
||||
*/
|
||||
init: function() {
|
||||
// Initialisiere alle Komponenten
|
||||
this.setupDarkMode();
|
||||
this.setupTooltips();
|
||||
this.setupUtilityFunctions();
|
||||
init() {
|
||||
if (this.initialized) return;
|
||||
|
||||
// Prüfe, ob spezifische Seiten-Initialisierer vorhanden sind
|
||||
const currentPage = document.body.dataset.page;
|
||||
if (currentPage && this.pageInitializers[currentPage]) {
|
||||
this.pageInitializers[currentPage]();
|
||||
console.log('MindMap-Anwendung wird initialisiert...');
|
||||
|
||||
// Seiten-spezifische Initialisierer aufrufen
|
||||
if (this.currentPage && this.pageInitializers[this.currentPage]) {
|
||||
this.pageInitializers[this.currentPage]();
|
||||
}
|
||||
|
||||
console.log('MindMap App initialisiert');
|
||||
// Event-Listener einrichten
|
||||
this.setupEventListeners();
|
||||
|
||||
// Dunkel-/Hellmodus aus LocalStorage wiederherstellen
|
||||
if (localStorage.getItem('darkMode') === 'dark') {
|
||||
document.documentElement.classList.add('dark');
|
||||
this.darkMode = true;
|
||||
}
|
||||
|
||||
// Mindmap initialisieren, falls auf der richtigen Seite
|
||||
this.initializeMindmap();
|
||||
|
||||
this.initialized = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialisiert die D3.js Mindmap-Visualisierung
|
||||
*/
|
||||
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)
|
||||
});
|
||||
|
||||
// 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);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handler für Klick auf einen Knoten in der Mindmap
|
||||
* @param {Object} node - Der angeklickte Knoten
|
||||
*/
|
||||
handleNodeClick(node) {
|
||||
console.log('Knoten wurde angeklickt:', node);
|
||||
|
||||
// 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>
|
||||
|
||||
<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>
|
||||
`;
|
||||
|
||||
// Button zum Hinzufügen eines Gedankens
|
||||
const addThoughtBtn = detailsContainer.querySelector('button');
|
||||
addThoughtBtn.addEventListener('click', () => {
|
||||
this.showAddThoughtDialog(node);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Dialog zum Hinzufügen eines neuen Knotens
|
||||
*/
|
||||
showAddNodeDialog() {
|
||||
// Diese Funktionalität würde in einer vollständigen Implementierung eingebunden werden
|
||||
alert('Diese Funktion steht bald zur Verfügung!');
|
||||
},
|
||||
|
||||
/**
|
||||
* 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!');
|
||||
},
|
||||
|
||||
/**
|
||||
* 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!');
|
||||
},
|
||||
|
||||
/**
|
||||
* Dark Mode Setup
|
||||
* Richtet Event-Listener für die Benutzeroberfläche ein
|
||||
*/
|
||||
setupDarkMode: function() {
|
||||
// Prüfe, ob Dark Mode bevorzugt wird
|
||||
const prefersDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
setupEventListeners() {
|
||||
// Event-Listener für Dark Mode-Wechsel
|
||||
document.addEventListener('darkModeToggled', (event) => {
|
||||
this.darkMode = event.detail.isDark;
|
||||
});
|
||||
|
||||
// Prüfe gespeicherte Einstellung
|
||||
const savedMode = localStorage.getItem('darkMode');
|
||||
|
||||
// Setze Dark Mode basierend auf gespeicherter Einstellung oder Systempräferenz
|
||||
if (savedMode === 'dark' || (savedMode === null && prefersDarkMode)) {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
|
||||
// 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');
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 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');
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Tooltips mit Tippy.js einrichten
|
||||
*/
|
||||
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
|
||||
});
|
||||
|
||||
// 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'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Hilfsfunktionen einrichten
|
||||
*/
|
||||
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);
|
||||
});
|
||||
|
||||
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.';
|
||||
|
||||
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>
|
||||
</button>
|
||||
</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);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Seitenspezifische Initialisierer
|
||||
*/
|
||||
pageInitializers: {
|
||||
// Startseite
|
||||
'home': function() {
|
||||
console.log('Startseite initialisiert');
|
||||
// Hier kommen spezifische Funktionen für die Startseite
|
||||
},
|
||||
|
||||
// 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
|
||||
},
|
||||
|
||||
// Profilseite
|
||||
'profile': function() {
|
||||
console.log('Profilseite initialisiert');
|
||||
// Profil-spezifische Funktionen
|
||||
},
|
||||
|
||||
// Suchseite
|
||||
'search': function() {
|
||||
console.log('Suchseite initialisiert');
|
||||
// Such-spezifische Funktionen
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 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;
|
||||
280
website/static/js/modules/chatgpt-assistant.js
Normal file
280
website/static/js/modules/chatgpt-assistant.js
Normal 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;
|
||||
@@ -32,8 +32,18 @@ class MindMapVisualization {
|
||||
this.tooltipDiv = null;
|
||||
this.isLoading = true;
|
||||
|
||||
this.init();
|
||||
this.setupDefaultNodes();
|
||||
// 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
|
||||
this.tooltipDiv = d3.select('body')
|
||||
.append('div')
|
||||
.attr('class', 'node-tooltip')
|
||||
.style('opacity', 0)
|
||||
.style('position', 'absolute')
|
||||
.style('pointer-events', 'none');
|
||||
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('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,32 +184,38 @@ class MindMapVisualization {
|
||||
this.links = [];
|
||||
this.processHierarchicalData(data.nodes);
|
||||
|
||||
// Visualisierung aktualisieren mit den tatsächlichen Daten
|
||||
this.updateVisualization();
|
||||
|
||||
// Status auf bereit setzen
|
||||
this.container.attr('data-status', 'ready');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Mindmap-Daten:', error);
|
||||
// Fallback zu Standarddaten
|
||||
this.nodes = [...this.defaultNodes];
|
||||
this.links = [...this.defaultLinks];
|
||||
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');
|
||||
}
|
||||
|
||||
// Visualisierung aktualisieren
|
||||
this.isLoading = false;
|
||||
this.updateVisualization();
|
||||
|
||||
} 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() {
|
||||
this.container.html(`
|
||||
<div class="flex justify-center items-center h-full">
|
||||
<div class="text-center">
|
||||
<div class="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-primary-400 mx-auto mb-4"></div>
|
||||
<p class="text-lg text-white">Mindmap wird geladen...</p>
|
||||
// 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">
|
||||
<div class="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-primary-400 mx-auto mb-4"></div>
|
||||
<p class="text-lg text-white">Mindmap wird geladen...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
processHierarchicalData(hierarchicalNodes, parentId = null) {
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
</div>
|
||||
<h1 class="text-3xl font-bold mb-8 text-gray-800 dark:text-white">Admin-Bereich</h1>
|
||||
|
||||
<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>
|
||||
<!-- 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>
|
||||
|
||||
<!-- 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>
|
||||
</div>
|
||||
|
||||
<div class="overflow-y-auto max-h-[500px]">
|
||||
<div class="space-y-3">
|
||||
{% 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>
|
||||
{% if node.parent %}
|
||||
<p class="text-sm text-white/60 mt-1">Eltern: {{ node.parent.name }}</p>
|
||||
{% else %}
|
||||
<p class="text-sm text-white/60 mt-1">Hauptknoten</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</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
|
||||
<!-- 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-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 %}
|
||||
<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 %}
|
||||
{{ node.parent.name }}
|
||||
{% else %}
|
||||
<span class="text-gray-400 dark:text-gray-500">Wurzelknoten</span>
|
||||
{% endif %}
|
||||
</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>
|
||||
|
||||
<!-- 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">
|
||||
{% 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>
|
||||
<!-- 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>
|
||||
{% endfor %}
|
||||
<button class="btn-outline">
|
||||
<i class="fas fa-filter mr-2"></i> Filter
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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 %}
|
||||
<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 %}
|
||||
File diff suppressed because it is too large
Load Diff
23
website/templates/errors/403.html
Normal file
23
website/templates/errors/403.html
Normal 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 %}
|
||||
23
website/templates/errors/404.html
Normal file
23
website/templates/errors/404.html
Normal 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 %}
|
||||
23
website/templates/errors/429.html
Normal file
23
website/templates/errors/429.html
Normal 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 %}
|
||||
23
website/templates/errors/500.html
Normal file
23
website/templates/errors/500.html
Normal 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 %}
|
||||
@@ -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">
|
||||
Sieh Wissen als vernetztes System, entdecke Zusammenhänge und erkenne überraschende
|
||||
<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">
|
||||
Füge deine eigenen Ideen und Perspektiven hinzu. Erstelle Verbindungen zu
|
||||
<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">
|
||||
Finde genau die Informationen, die du suchst, mit fortschrittlichen Such- und
|
||||
<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>
|
||||
|
||||
11
website/templates/layout.html
Normal file
11
website/templates/layout.html
Normal 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>
|
||||
@@ -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>
|
||||
<div class="ascii-decoration bottom-left-deco">
|
||||
╔══╗ ╔═══╗ ╔══╗
|
||||
║ ║ ║ ║ ║ ║
|
||||
╚══╝ ╚═══╝ ╚══╝
|
||||
</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>
|
||||
<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>
|
||||
<h3>Visualisiere Wissen</h3>
|
||||
<p>Sieh Wissen als vernetztes System, entdecke Zusammenhänge und erkenne überraschende Verbindungen zwischen verschiedenen Themengebieten.</p>
|
||||
</div>
|
||||
|
||||
<!-- 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...">
|
||||
</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>
|
||||
</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>
|
||||
<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>
|
||||
<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>
|
||||
</button>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<!-- 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>
|
||||
</div>
|
||||
</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>
|
||||
</div>
|
||||
</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>
|
||||
<!-- 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 class="control-btn" id="zoom-out-btn">
|
||||
<i class="fas fa-search-minus mr-1"></i> Verkleinern
|
||||
</button>
|
||||
<button class="control-btn" id="center-btn">
|
||||
<i class="fas fa-bullseye mr-1"></i> Zentrieren
|
||||
</button>
|
||||
</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>
|
||||
</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>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<!-- 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');
|
||||
// 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.translate(currentZoom.x, currentZoom.y).scale(newScale)
|
||||
);
|
||||
});
|
||||
|
||||
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(
|
||||
d3.zoom().transform,
|
||||
d3.zoomIdentity.scale(currentZoom * 1.3)
|
||||
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.translate(currentZoom.x, currentZoom.y).scale(newScale)
|
||||
);
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
zoomOut.addEventListener('click', function() {
|
||||
const currentZoom = d3.zoomTransform(window.mindmapInstance.svg.node()).k;
|
||||
window.mindmapInstance.svg.transition().duration(300).call(
|
||||
d3.zoom().transform,
|
||||
d3.zoomIdentity.scale(currentZoom / 1.3)
|
||||
);
|
||||
});
|
||||
|
||||
zoomReset.addEventListener('click', function() {
|
||||
window.mindmapInstance.svg.transition().duration(300).call(
|
||||
d3.zoom().transform,
|
||||
d3.zoomIdentity.scale(1)
|
||||
);
|
||||
});
|
||||
}
|
||||
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
Reference in New Issue
Block a user