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

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

View File

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

1
website/.env Normal file
View File

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

View File

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

12
website/example.env Normal file
View File

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

View File

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

Binary file not shown.

View File

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

File diff suppressed because it is too large Load Diff

View File

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

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

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

View File

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

View File

@@ -2,284 +2,222 @@
* MindMap - Hauptdatei für globale JavaScript-Funktionen * MindMap - Hauptdatei für globale JavaScript-Funktionen
*/ */
// Globales Objekt für App-Funktionen // Import des ChatGPT-Assistenten
import ChatGPTAssistant from './modules/chatgpt-assistant.js';
/**
* Hauptmodul für die MindMap-Anwendung
* Verwaltet die globale Anwendungslogik
*/
document.addEventListener('DOMContentLoaded', function() {
// Initialisiere die Anwendung
MindMap.init();
// Wende Dunkel-/Hellmodus an
const isDarkMode = localStorage.getItem('darkMode') === 'dark';
document.documentElement.classList.toggle('dark', isDarkMode);
});
/**
* Hauptobjekt der MindMap-Anwendung
*/
const MindMap = { const MindMap = {
// App-Status
initialized: false,
darkMode: document.documentElement.classList.contains('dark'),
pageInitializers: {},
currentPage: document.body.dataset.page,
/** /**
* Initialisiert die Anwendung * Initialisiert die MindMap-Anwendung
*/ */
init: function() { init() {
// Initialisiere alle Komponenten if (this.initialized) return;
this.setupDarkMode();
this.setupTooltips();
this.setupUtilityFunctions();
// Prüfe, ob spezifische Seiten-Initialisierer vorhanden sind console.log('MindMap-Anwendung wird initialisiert...');
const currentPage = document.body.dataset.page;
if (currentPage && this.pageInitializers[currentPage]) { // Seiten-spezifische Initialisierer aufrufen
this.pageInitializers[currentPage](); 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() { setupEventListeners() {
// Prüfe, ob Dark Mode bevorzugt wird // Event-Listener für Dark Mode-Wechsel
const prefersDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; document.addEventListener('darkModeToggled', (event) => {
this.darkMode = event.detail.isDark;
});
// Prüfe gespeicherte Einstellung // Responsive Anpassungen bei Fenstergröße
const savedMode = localStorage.getItem('darkMode'); window.addEventListener('resize', () => {
if (window.mindmapInstance) {
// Setze Dark Mode basierend auf gespeicherter Einstellung oder Systempräferenz const container = document.getElementById('mindmap-container');
if (savedMode === 'dark' || (savedMode === null && prefersDarkMode)) { if (container) {
document.documentElement.classList.add('dark'); window.mindmapInstance.width = container.clientWidth;
} else { window.mindmapInstance.height = container.clientHeight;
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');
} }
} }
}); });
// 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 // Globale Export für andere Module
document.addEventListener('DOMContentLoaded', () => {
MindMap.init();
// Höre auf Storage-Änderungen, um Dark Mode zwischen Tabs zu synchronisieren
window.addEventListener('storage', (event) => {
if (event.key === 'darkMode') {
const isDarkMode = event.newValue === 'true';
// Aktualisiere das DOM
document.documentElement.classList.toggle('dark', isDarkMode);
// Aktualisiere Alpine.js, falls es bereits geladen wurde
if (window.Alpine) {
const darkModeComponent = Alpine.store('darkMode');
if (darkModeComponent) {
darkModeComponent.enabled = isDarkMode;
}
}
}
});
// Lade die bevorzugte Farbeinstellung des Betriebssystems wenn nichts gespeichert ist
if (localStorage.getItem('darkMode') === null) {
const prefersDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
localStorage.setItem('darkMode', prefersDarkMode);
document.documentElement.classList.toggle('dark', prefersDarkMode);
}
// UI-Verbesserungen
// Füge Schatten zu Karten hinzu, wenn sie sichtbar werden
const cards = document.querySelectorAll('.card');
if ('IntersectionObserver' in window) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
setTimeout(() => {
entry.target.classList.add('shadow-md', 'opacity-100');
entry.target.classList.remove('opacity-0', 'translate-y-4');
}, entry.target.dataset.delay || 0);
}
});
}, { threshold: 0.1 });
cards.forEach((card, index) => {
card.classList.add('transition-all', 'duration-500', 'opacity-0', 'translate-y-4');
card.dataset.delay = index * 100;
observer.observe(card);
});
}
// Verbesserte Formular-Validierung
const forms = document.querySelectorAll('form');
forms.forEach(form => {
const inputs = form.querySelectorAll('input, textarea, select');
inputs.forEach(input => {
// Füge Validierungsklassen hinzu, wenn ein Input fokussiert wurde
input.addEventListener('blur', () => {
if (input.value.trim() !== '') {
input.classList.add('has-content');
} else {
input.classList.remove('has-content');
}
if (input.checkValidity()) {
input.classList.remove('is-invalid');
input.classList.add('is-valid');
} else {
input.classList.remove('is-valid');
input.classList.add('is-invalid');
}
});
});
});
});
// Support für Alpine.js
window.MindMap = MindMap; window.MindMap = MindMap;

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,45 +1,83 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Admin | Wissenschaftliche Mindmap{% endblock %} {% block title %}Admin-Bereich{% endblock %}
{% block content %} {% block content %}
<div class="container mx-auto px-4 py-8"> <div class="container mx-auto px-4 py-8">
<div class="glass p-8 mb-8"> <h1 class="text-3xl font-bold mb-8 text-gray-800 dark:text-white">Admin-Bereich</h1>
<h1 class="text-3xl font-bold text-white mb-4">Admin Bereich</h1>
<p class="text-white/70">Verwalte Benutzer, Gedanken und die Mindmap-Struktur.</p>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8"> <!-- Tabs für verschiedene Bereiche -->
<!-- Users Section --> <div x-data="{ activeTab: 'users' }" class="mb-8">
<div class="dark-glass p-6" x-data="{ tab: 'users' }"> <div class="flex space-x-2 mb-6 overflow-x-auto">
<div class="flex items-center justify-between mb-6"> <button
<h2 class="text-2xl font-bold text-white">Benutzer</h2> @click="activeTab = 'users'"
<span class="bg-indigo-600 text-white text-xs font-medium px-2.5 py-0.5 rounded-full">{{ users|length }}</span> :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>
<div class="overflow-y-auto max-h-[500px]"> <div class="overflow-x-auto">
<table class="w-full text-white/90"> <table class="w-full">
<thead class="text-white/60 text-sm uppercase"> <thead>
<tr> <tr class="text-left border-b border-gray-200 dark:border-gray-700">
<th class="text-left py-3">ID</th> <th class="px-4 py-2 text-gray-700 dark:text-gray-300">ID</th>
<th class="text-left py-3">Benutzername</th> <th class="px-4 py-2 text-gray-700 dark:text-gray-300">Benutzername</th>
<th class="text-left py-3">Email</th> <th class="px-4 py-2 text-gray-700 dark:text-gray-300">E-Mail</th>
<th class="text-left py-3">Rolle</th> <th class="px-4 py-2 text-gray-700 dark:text-gray-300">Admin</th>
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">Gedanken</th>
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">Aktionen</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for user in users %} {% for user in users %}
<tr class="border-t border-white/10"> <tr class="border-b border-gray-100 dark:border-gray-800 hover:bg-gray-50 dark:hover:bg-dark-700/30">
<td class="py-3">{{ user.id }}</td> <td class="px-4 py-3 text-gray-700 dark:text-gray-300">{{ user.id }}</td>
<td class="py-3">{{ user.username }}</td> <td class="px-4 py-3 text-gray-700 dark:text-gray-300 font-medium">{{ user.username }}</td>
<td class="py-3">{{ user.email }}</td> <td class="px-4 py-3 text-gray-700 dark:text-gray-300">{{ user.email }}</td>
<td class="py-3"> <td class="px-4 py-3 text-gray-700 dark:text-gray-300">
{% if user.is_admin %} {% if user.is_admin %}
<span class="bg-purple-600/70 text-white text-xs px-2 py-1 rounded">Admin</span> <span class="bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300 px-2 py-1 rounded text-xs">Admin</span>
{% else %} {% else %}
<span class="bg-blue-600/70 text-white text-xs px-2 py-1 rounded">Benutzer</span> <span class="bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300 px-2 py-1 rounded text-xs">User</span>
{% endif %} {% endif %}
</td> </td>
<td class="px-4 py-3 text-gray-700 dark:text-gray-300">{{ user.thoughts|length }}</td>
<td class="px-4 py-3 flex space-x-2">
<button class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300">
<i class="fas fa-edit"></i>
</button>
<button class="text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300">
<i class="fas fa-trash-alt"></i>
</button>
</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
@@ -47,63 +85,224 @@
</div> </div>
</div> </div>
<!-- Mindmap Nodes Section --> <!-- Mindmap-Knoten-Tab -->
<div class="dark-glass p-6"> <div x-show="activeTab === 'nodes'" class="glass-morphism rounded-lg p-6">
<div class="flex items-center justify-between mb-6"> <div class="flex justify-between items-center mb-6">
<h2 class="text-2xl font-bold text-white">Mindmap Struktur</h2> <h2 class="text-xl font-bold text-gray-800 dark:text-white">Mindmap-Knoten Verwaltung</h2>
<span class="bg-indigo-600 text-white text-xs font-medium px-2.5 py-0.5 rounded-full">{{ nodes|length }}</span> <button class="btn-outline">
</div> <i class="fas fa-plus mr-2"></i> Neuer Knoten
<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
</button> </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">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> </div>
<!-- Thoughts Section --> <!-- Gedanken-Tab -->
<div class="dark-glass p-6"> <div x-show="activeTab === 'thoughts'" class="glass-morphism rounded-lg p-6">
<div class="flex items-center justify-between mb-6"> <div class="flex justify-between items-center mb-6">
<h2 class="text-2xl font-bold text-white">Gedanken</h2> <h2 class="text-xl font-bold text-gray-800 dark:text-white">Gedanken-Verwaltung</h2>
<span class="bg-indigo-600 text-white text-xs font-medium px-2.5 py-0.5 rounded-full">{{ thoughts|length }}</span> <div class="flex space-x-2">
</div> <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="overflow-y-auto max-h-[500px]"> <div class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500">
<div class="space-y-3"> <i class="fas fa-search"></i>
{% for thought in thoughts %}
<div class="glass p-3">
<div class="flex justify-between items-start">
<span class="inline-block px-2 py-0.5 text-xs text-white/70 bg-white/10 rounded-full mb-1">{{ thought.branch }}</span>
<span class="text-xs text-white/50">{{ thought.timestamp.strftime('%d.%m.%Y') }}</span>
</div>
<p class="text-sm text-white mb-1 line-clamp-2">{{ thought.content }}</p>
<div class="flex justify-between items-center mt-2 text-xs">
<span class="text-white/60">Von: {{ thought.author.username }}</span>
<span class="text-white/60">{{ thought.comments|length }} Kommentar(e)</span>
</div> </div>
</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>
</div> </div>
</div> </div>
<!-- System-Log (immer sichtbar) -->
<div class="mt-8">
<h2 class="text-xl font-bold mb-4 text-gray-800 dark:text-white">System-Log</h2>
<div class="glass-morphism rounded-lg p-4 h-32 overflow-y-auto font-mono text-sm text-gray-700 dark:text-gray-300">
<div class="text-green-500">[INFO] [{{ now.strftime('%Y-%m-%d %H:%M:%S') }}] System gestartet</div>
<div class="text-blue-500">[INFO] [{{ now.strftime('%Y-%m-%d %H:%M:%S') }}] Admin-Bereich aufgerufen von {{ current_user.username }}</div>
<div class="text-yellow-500">[WARN] [{{ now.strftime('%Y-%m-%d %H:%M:%S') }}] Hohe Serverauslastung erkannt</div>
<div class="text-gray-500">[INFO] [{{ now.strftime('%Y-%m-%d %H:%M:%S') }}] Backup erfolgreich erstellt</div>
<div class="text-red-500">[ERROR] [{{ now.strftime('%Y-%m-%d %H:%M:%S') }}] API-Zugriffsfehler (Timeout) bei externer Anfrage</div>
</div>
</div>
</div> </div>
{% 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 %} {% endblock %}

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,87 +5,212 @@
{% block extra_css %} {% block extra_css %}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tippy.js@6.3.7/dist/tippy.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tippy.js@6.3.7/dist/tippy.css">
<style> <style>
/* Modernes Dark-Mode Design mit Purple-Blue Gradient */ /* Globaler Stil - Hintergrund über die gesamte Seite */
.mindmap-header { html, body {
background: linear-gradient(120deg, rgba(32, 32, 64, 0.7), rgba(24, 24, 48, 0.7)); background-color: var(--dark-bg) !important;
border-radius: 0.75rem; min-height: 100vh;
backdrop-filter: blur(10px); width: 100%;
border: 1px solid rgba(255, 255, 255, 0.05); color: #ffffff;
margin: 0;
padding: 0;
overflow-x: hidden;
} }
.gradient-text { /* Sicherstellen, dass der Hintergrund die gesamte Seite abdeckt */
background: linear-gradient(to right, #a67fff, #5096ff); #app-container, .container, main, .mx-auto, .py-12, body > div, #content-wrapper, #mindmap-container {
background-color: var(--dark-bg) !important;
width: 100%;
}
/* Verbesserte Glasmorphismus-Stile für Karten */
.glass-card, .mindmap-card {
background: rgba(24, 28, 45, 0.75);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 24px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.35);
transition: all 0.3s ease;
}
.glass-card:hover, .mindmap-card:hover {
transform: translateY(-5px);
box-shadow: 0 20px 48px rgba(0, 0, 0, 0.45);
border: 1px solid rgba(255, 255, 255, 0.2);
}
/* Feature-Cards-Stil mit besserem Glasmorphismus */
.feature-card {
background: rgba(24, 28, 45, 0.75);
border-radius: 24px;
overflow: hidden;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.12);
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.35);
transition: all 0.3s ease;
height: 100%;
display: flex;
flex-direction: column;
padding: 1.75rem;
}
.feature-card:hover {
transform: translateY(-5px);
border: 1px solid rgba(255, 255, 255, 0.25);
box-shadow: 0 20px 48px rgba(0, 0, 0, 0.45);
background: rgba(28, 32, 52, 0.9);
}
.feature-card-icon {
font-size: 2.75rem;
margin-bottom: 1.25rem;
background: linear-gradient(135deg, #b38fff, #14b8a6);
-webkit-background-clip: text; -webkit-background-clip: text;
background-clip: text; background-clip: text;
color: transparent; color: transparent;
text-shadow: 0 0 20px rgba(160, 80, 255, 0.3); filter: drop-shadow(0 0 10px rgba(179, 143, 255, 0.6));
}
/* Feature-Card-Text besser lesbar machen */
.feature-card h3 {
font-size: 1.75rem;
font-weight: 700;
margin-bottom: 0.9rem;
color: #ffffff;
text-shadow: 0 2px 3px rgba(0, 0, 0, 0.3);
letter-spacing: 0.2px;
}
.feature-card p {
color: rgba(255, 255, 255, 0.95);
font-size: 1.1rem;
line-height: 1.6;
font-weight: 400;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.25);
letter-spacing: 0.3px;
}
/* Mindmap-Header */
.mindmap-header {
background: rgba(20, 24, 42, 0.85);
border-radius: 24px;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.12);
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.35);
}
.gradient-text {
background: linear-gradient(to right, #b38fff, #58a9ff);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
text-shadow: 0 0 25px rgba(179, 143, 255, 0.25);
font-weight: 800;
} }
/* D3.js Mindmap spezifische Stile */ /* D3.js Mindmap spezifische Stile */
.mindmap-svg { .mindmap-svg {
background: radial-gradient(circle at center, rgba(40, 30, 60, 0.4) 0%, rgba(20, 20, 40, 0.2) 70%); background: rgba(14, 18, 32, 0.3);
width: 100%; width: 100%;
height: 100%; height: 100%;
border-radius: 0.5rem; border-radius: 24px;
} }
/* Verbesserte Mindmap-Knoten-Stile */
.node { .node {
cursor: pointer; cursor: pointer;
transition: all 0.3s ease; transition: all 0.3s ease;
} }
.node circle {
stroke: rgba(255, 255, 255, 0.12);
stroke-width: 2px;
fill: rgba(24, 28, 45, 0.85);
filter: url(#glass-effect);
}
.node:hover circle {
filter: url(#hover-glow);
stroke: rgba(255, 255, 255, 0.25);
}
.node.selected circle {
filter: url(#selected-glow);
stroke: rgba(179, 143, 255, 0.6);
stroke-width: 3px;
}
.node-label { .node-label {
font-family: 'Inter', 'SF Pro Display', system-ui, sans-serif; font-family: 'Inter', 'SF Pro Display', system-ui, sans-serif;
font-weight: 500; font-weight: 600;
pointer-events: none; pointer-events: none;
user-select: none; user-select: none;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5); text-shadow: 0 2px 4px rgba(0, 0, 0, 0.7);
font-size: 16px;
letter-spacing: 0.3px;
fill: #ffffff;
} }
.link { .link {
transition: stroke 0.3s ease, opacity 0.3s ease; transition: stroke 0.3s ease, opacity 0.3s ease;
stroke: rgba(255, 255, 255, 0.3);
stroke-width: 2;
opacity: 0.7;
} }
/* Control Bar */ .link:hover, .link.highlighted {
stroke: rgba(179, 143, 255, 0.7);
opacity: 0.9;
stroke-width: 3;
}
/* Control Bar mit verbesserten Glasmorphismus und Lesbarkeit */
.controls-bar { .controls-bar {
background: rgba(20, 20, 40, 0.7); background: rgba(24, 28, 45, 0.85);
backdrop-filter: blur(10px); backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.05); -webkit-backdrop-filter: blur(20px);
border-radius: 0.5rem 0.5rem 0 0; border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 20px 20px 0 0;
} }
/* Tooltip Stile */ /* Tooltip Stile */
.tippy-box[data-theme~='mindmap'] { .tippy-box[data-theme~='mindmap'] {
background-color: rgba(20, 20, 40, 0.9); background-color: rgba(24, 28, 45, 0.95);
color: #ffffff; color: #ffffff;
border: 1px solid rgba(160, 80, 255, 0.2); border: 1px solid rgba(179, 143, 255, 0.25);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5), 0 0 10px rgba(160, 80, 255, 0.2); box-shadow: 0 12px 30px rgba(0, 0, 0, 0.5), 0 0 15px rgba(179, 143, 255, 0.25);
backdrop-filter: blur(10px); backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-radius: 16px;
} }
.tippy-box[data-theme~='mindmap'] .tippy-arrow { .tippy-box[data-theme~='mindmap'] .tippy-arrow {
color: rgba(20, 20, 40, 0.9); color: rgba(24, 28, 45, 0.95);
} }
.node-tooltip { .node-tooltip {
font-family: 'Inter', 'SF Pro Display', system-ui, sans-serif; font-family: 'Inter', 'SF Pro Display', system-ui, sans-serif;
font-size: 14px; font-size: 14px;
line-height: 1.5; line-height: 1.6;
padding: 8px 12px; padding: 12px 16px;
letter-spacing: 0.3px;
} }
.node-tooltip strong { .node-tooltip strong {
font-weight: 600; font-weight: 600;
color: #a67fff; color: #b38fff;
} }
/* Gedanken-Container */ /* Gedanken-Container */
.thought-container { .thought-container {
border: 1px solid rgba(255, 255, 255, 0.05); border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 0.5rem; border-radius: 24px;
backdrop-filter: blur(10px); backdrop-filter: blur(20px);
background: rgba(20, 20, 40, 0.7); -webkit-backdrop-filter: blur(20px);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2); background: rgba(24, 28, 45, 0.85);
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.3);
} }
/* Angepasste Scrollbar für den Gedanken-Container */ /* Angepasste Scrollbar für den Gedanken-Container */
@@ -94,17 +219,17 @@
} }
.custom-scrollbar::-webkit-scrollbar-track { .custom-scrollbar::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05); background: rgba(255, 255, 255, 0.08);
border-radius: 3px; border-radius: 3px;
} }
.custom-scrollbar::-webkit-scrollbar-thumb { .custom-scrollbar::-webkit-scrollbar-thumb {
background: rgba(160, 80, 255, 0.3); background: rgba(179, 143, 255, 0.5);
border-radius: 3px; border-radius: 3px;
} }
.custom-scrollbar::-webkit-scrollbar-thumb:hover { .custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: rgba(160, 80, 255, 0.5); background: rgba(179, 143, 255, 0.7);
} }
/* Pulse-Animation für leere Gedanken */ /* Pulse-Animation für leere Gedanken */
@@ -121,208 +246,268 @@
} }
} }
/* ASCII-Style Tech Decoration */ /* Button-Effekte mit verbesserter Lesbarkeit */
.ascii-decoration {
font-family: monospace;
color: rgba(160, 80, 255, 0.15);
font-size: 12px;
white-space: pre;
line-height: 1;
user-select: none;
position: absolute;
z-index: -1;
}
.top-right-deco {
top: 20px;
right: 20px;
}
.bottom-left-deco {
bottom: 20px;
left: 20px;
}
/* Button-Effekte */
.control-btn { .control-btn {
transition: all 0.2s ease; background: rgba(32, 36, 55, 0.85);
position: relative; color: #ffffff;
overflow: hidden; border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 16px;
padding: 0.75rem 1.5rem;
font-weight: 600;
transition: all 0.3s ease;
backdrop-filter: blur(15px);
-webkit-backdrop-filter: blur(15px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.25);
font-size: 1rem;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
letter-spacing: 0.3px;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
} }
.control-btn:hover { .control-btn:hover {
background: rgba(160, 80, 255, 0.2); background: rgba(179, 143, 255, 0.35);
transform: translateY(-1px); transform: translateY(-3px);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.35), 0 0 15px rgba(179, 143, 255, 0.25);
border: 1px solid rgba(255, 255, 255, 0.25);
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
} }
.control-btn:active { .control-btn:active {
transform: translateY(1px); transform: translateY(1px);
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.2);
} }
.control-btn::after { .control-btn.active {
content: ""; background: rgba(179, 143, 255, 0.4);
display: block; border: 1px solid rgba(179, 143, 255, 0.5);
position: absolute; box-shadow: 0 0 15px rgba(179, 143, 255, 0.3);
width: 100%;
height: 100%;
top: 0;
left: 0;
pointer-events: none;
background-image: radial-gradient(circle, rgba(255, 255, 255, 0.3) 10%, transparent 10.01%);
background-repeat: no-repeat;
background-position: 50%;
transform: scale(10, 10);
opacity: 0;
transition: transform 0.5s, opacity 0.5s;
} }
.control-btn:active::after { /* Glow Effect für Buttons */
transform: scale(0, 0); .btn-glow:hover {
opacity: 0.3; box-shadow: 0 0 15px rgba(179, 143, 255, 0.5);
transition: 0s; }
/* Light Mode Anpassungen */
html.light .mindmap-svg {
background: rgba(240, 244, 248, 0.3);
}
html.light .node circle {
fill: rgba(255, 255, 255, 0.9);
stroke: rgba(0, 0, 0, 0.1);
}
html.light .node-label {
fill: #1a202c;
text-shadow: 0 1px 2px rgba(255, 255, 255, 0.7);
}
html.light .link {
stroke: rgba(0, 0, 0, 0.2);
}
html.light .glass-card,
html.light .mindmap-card,
html.light .feature-card,
html.light .thought-container,
html.light .mindmap-header,
html.light .controls-bar {
background: rgba(255, 255, 255, 0.85);
border: 1px solid rgba(0, 0, 0, 0.08);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
}
html.light .control-btn {
background: rgba(255, 255, 255, 0.9);
color: #1a202c;
border: 1px solid rgba(0, 0, 0, 0.08);
text-shadow: none;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
font-weight: 600;
}
html.light .control-btn:hover {
background: rgba(179, 143, 255, 0.15);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.12);
color: #7e3ff2;
font-weight: 700;
}
html.light .control-btn.active {
background: rgba(179, 143, 255, 0.2);
border: 1px solid rgba(126, 63, 242, 0.3);
color: #7e3ff2;
font-weight: 700;
}
html.light .feature-card h3 {
color: #1a202c;
text-shadow: none;
}
html.light .feature-card p {
color: #4a5568;
text-shadow: none;
}
html.light .node-tooltip strong {
color: #7e3ff2;
}
/* Karten in der Mindmap mit verbesserten Styles */
.mindmap-card {
background: rgba(24, 28, 45, 0.75);
border-radius: 24px;
overflow: hidden;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.12);
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.35);
transition: all 0.3s ease;
}
.mindmap-card:hover {
transform: translateY(-5px);
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.45);
background: rgba(28, 32, 52, 0.8);
}
.mindmap-card-header {
padding: 1.25rem 1.5rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
display: flex;
align-items: center;
justify-content: space-between;
}
.mindmap-card-body {
padding: 1.5rem;
}
.mindmap-card-footer {
padding: 1.25rem 1.5rem;
border-top: 1px solid rgba(255, 255, 255, 0.06);
display: flex;
align-items: center;
justify-content: space-between;
}
html.light .mindmap-card-header,
html.light .mindmap-card-footer {
border-color: rgba(0, 0, 0, 0.06);
} }
</style> </style>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="relative mb-6" data-page="mindmap"> <div class="container mx-auto px-4 py-12">
<!-- ASCII Dekorationen --> <!-- Feature-Karten-Container -->
<div class="ascii-decoration top-right-deco"> <div class="grid grid-cols-1 md:grid-cols-3 gap-8 mb-12">
┌─┐ ┌─┐ ┌─┐ ┌─┐ <!-- Feature-Karte 1: Visualisiere Wissen -->
│ │ │ │ │ │ │ │ <div class="feature-card">
└─┘ └─┘ └─┘ └─┘ <div class="feature-card-icon">
</div> <i class="fas fa-brain"></i>
<div class="ascii-decoration bottom-left-deco"> </div>
╔══╗ ╔═══╗ ╔══╗ <h3>Visualisiere Wissen</h3>
║ ║ ║ ║ ║ ║ <p>Sieh Wissen als vernetztes System, entdecke Zusammenhänge und erkenne überraschende Verbindungen zwischen verschiedenen Themengebieten.</p>
╚══╝ ╚═══╝ ╚══╝ </div>
</div>
<!-- Feature-Karte 2: Teile Gedanken -->
<!-- Header Bereich --> <div class="feature-card">
<div class="mb-8 p-6 mindmap-header"> <div class="feature-card-icon">
<h1 class="text-5xl md:text-6xl font-bold mb-3"> <i class="fas fa-lightbulb"></i>
<span class="gradient-text">Mindmap</span> </div>
</h1> <h3>Teile Gedanken</h3>
<p class="text-lg text-gray-200 max-w-3xl leading-relaxed"> <p>Füge deine eigenen Ideen und Perspektiven hinzu. Erstelle Verbindungen zu vorhandenen Gedanken und bereichere die wachsende Wissensbasis.</p>
Erkunden Sie interaktiv verknüpfte Wissensgebiete und ihre Verbindungen. Fügen Sie eigene Gedanken hinzu und erstellen Sie ein kollaboratives Wissensnetz. </div>
</p>
<div class="mt-3 flex flex-wrap gap-3"> <!-- Feature-Karte 3: Community -->
<span class="px-3 py-1 text-xs rounded-full bg-purple-900/50 text-purple-200 border border-purple-700/30"> <div class="feature-card">
<i class="fa-solid fa-diagram-project mr-1"></i> Interaktiv <div class="feature-card-icon">
</span> <i class="fas fa-users"></i>
<span class="px-3 py-1 text-xs rounded-full bg-blue-900/50 text-blue-200 border border-blue-700/30"> </div>
<i class="fa-solid fa-sitemap mr-1"></i> Wissensvernetzung <h3>Community</h3>
</span> <p>Sei Teil einer Gemeinschaft, die gemeinsam ein verteiltes Wissensarchiv aufbaut und sich in thematisch fokussierten Bereichen austauscht.</p>
<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> </div>
</div> </div>
<!-- Haupt-Grid-Layout --> <!-- Mindmap-Visualisierung Header -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6"> <div class="mindmap-header p-6 mb-6">
<!-- Visualisierungsbereich --> <h2 class="text-3xl font-bold mb-2 gradient-text">Wissenslandschaft erkunden</h2>
<div class="lg:col-span-2"> <p class="text-gray-300 mb-0">Interagiere mit der Mindmap, um Verbindungen zu entdecken und neue Ideen hinzuzufügen</p>
<div class="rounded-lg overflow-hidden bg-gradient-to-br from-slate-900/60 to-slate-800/40 border border-slate-700/20 shadow-xl"> </div>
<!-- Mindmap Controls Bar -->
<div class="flex items-center justify-between p-4 controls-bar"> <!-- Mindmap-Container -->
<div class="relative flex-grow max-w-md"> <div class="glass-card overflow-hidden mb-12">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"> <div id="mindmap-container" class="relative" style="height: 70vh; min-height: 500px;">
<i class="fa-solid fa-magnifying-glass text-gray-400"></i> <!-- Lade-Overlay -->
</div> <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);">
<input type="text" id="mindmap-search" <div class="text-center">
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" <div class="inline-block animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-purple-500 mb-4"></div>
placeholder="Knoten suchen..."> <p class="text-white text-lg">Wissenslandschaft wird geladen...</p>
</div> <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 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>
</div> </div>
</div> </div>
<!-- Mindmap Container -->
<div id="mindmap-container" class="relative w-full h-[600px] bg-gradient-to-br from-slate-900/30 to-indigo-900/10">
<!-- Wird durch D3.js befüllt -->
</div>
<!-- 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>
</div> </div>
<!-- Gedanken-Bereich --> <!-- Steuerungsleiste -->
<div class="lg:col-span-1"> <div class="controls-bar p-4 flex flex-wrap items-center justify-between gap-3">
<div id="thoughts-container" class="thought-container p-6 h-[600px] overflow-y-auto custom-scrollbar"> <div class="flex flex-wrap gap-2">
<div class="text-center p-6"> <button class="control-btn" id="zoom-in-btn">
<div class="mb-6 pulse-animation"> <i class="fas fa-search-plus mr-1"></i> Vergrößern
<i class="fa-solid fa-diagram-project text-6xl text-indigo-400/50"></i> </button>
</div> <button class="control-btn" id="zoom-out-btn">
<h3 class="text-xl font-semibold mb-2 text-white">Mindmap erkunden</h3> <i class="fas fa-search-minus mr-1"></i> Verkleinern
<p class="text-gray-300 mb-4">Wählen Sie einen Knoten in der Mindmap aus, um zugehörige Gedanken anzuzeigen.</p> </button>
<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"> <button class="control-btn" id="center-btn">
<i class="fa-solid fa-arrow-left text-purple-400 mr-2"></i> <i class="fas fa-bullseye mr-1"></i> Zentrieren
<span class="text-gray-200">Klicken Sie auf einen Knoten</span> </button>
</div>
</div>
</div> </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> </div>
</div> </div>
@@ -330,55 +515,148 @@
{% block extra_js %} {% block extra_js %}
<!-- D3.js für die Mindmap-Visualisierung --> <!-- D3.js für die Mindmap-Visualisierung -->
<script src="https://cdn.jsdelivr.net/npm/d3@7.8.5/dist/d3.min.js"></script> <script src="https://d3js.org/d3.v7.min.js"></script>
<!-- Tippy.js für erweiterte Tooltips --> <!-- Tippy.js für verbesserte Tooltips -->
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.6/dist/umd/popper.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/tippy.js@6.3.7/dist/tippy.umd.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/tippy.js@6.3.7/dist/tippy.umd.min.js"></script>
<!-- Mindmap-Module --> <!-- D3-Erweiterungen für spezifische Effekte -->
<script src="{{ url_for('static', filename='js/modules/mindmap.js') }}"></script> <script src="{{ url_for('static', filename='d3-extensions.js') }}"></script>
<script src="{{ url_for('static', filename='js/modules/mindmap-page.js') }}"></script> <!-- Mindmap JS -->
<script src="{{ url_for('static', filename='mindmap.js') }}"></script>
<script> <script>
// Zusätzliche Seiten-spezifische Skripte
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Setze Seiten-Identifier für den richtigen Initialisierer // Initialisiere die Mindmap-Visualisierung
document.body.dataset.page = 'mindmap'; const mindmapContainer = document.getElementById('mindmap-container');
const containerWidth = mindmapContainer.clientWidth;
const containerHeight = mindmapContainer.clientHeight;
// Toggle für die Anleitung const mindmap = new MindMapVisualization('#mindmap-container', {
const guidePanel = document.getElementById('guide-panel'); width: containerWidth,
const toggleGuideBtn = document.getElementById('toggle-guide'); height: containerHeight,
nodeRadius: 22,
toggleGuideBtn.addEventListener('click', function() { selectedNodeRadius: 28,
guidePanel.classList.toggle('hidden'); linkDistance: 160,
chargeStrength: -1200,
centerForce: 0.1,
tooltipEnabled: true,
onNodeClick: function(node) {
console.log('Node clicked:', node);
// Hier können spezifische Aktionen für Knotenklicks definiert werden
}
}); });
// Zoom-Buttons mit der Mindmap verbinden // Event-Listener für Steuerungsbuttons
const zoomIn = document.getElementById('zoom-in'); document.getElementById('zoom-in-btn').addEventListener('click', function() {
const zoomOut = document.getElementById('zoom-out'); // Zoom-In-Funktionalität
const zoomReset = document.getElementById('zoom-reset'); const svg = d3.select('#mindmap-container svg');
const currentZoom = d3.zoomTransform(svg.node());
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) { document.getElementById('zoom-out-btn').addEventListener('click', function() {
zoomIn.addEventListener('click', function() { // Zoom-Out-Funktionalität
const currentZoom = d3.zoomTransform(window.mindmapInstance.svg.node()).k; const svg = d3.select('#mindmap-container svg');
window.mindmapInstance.svg.transition().duration(300).call( const currentZoom = d3.zoomTransform(svg.node());
d3.zoom().transform, const newScale = currentZoom.k / 1.3;
d3.zoomIdentity.scale(currentZoom * 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() { if (mindmap.svg) {
const currentZoom = d3.zoomTransform(window.mindmapInstance.svg.node()).k; mindmap.svg
window.mindmapInstance.svg.transition().duration(300).call( .attr('width', newWidth)
d3.zoom().transform, .attr('height', newHeight);
d3.zoomIdentity.scale(currentZoom / 1.3)
); mindmap.width = newWidth;
}); mindmap.height = newHeight;
zoomReset.addEventListener('click', function() { // Force-Simulation aktualisieren
window.mindmapInstance.svg.transition().duration(300).call( if (mindmap.simulation) {
d3.zoom().transform, mindmap.simulation
d3.zoomIdentity.scale(1) .force('center', d3.forceCenter(newWidth / 2, newHeight / 2))
); .restart();
}); }
} }
});
}); });
</script> </script>
{% endblock %} {% endblock %}

File diff suppressed because it is too large Load Diff