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

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

View File

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

1
website/.env Normal file
View File

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

View File

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

12
website/example.env Normal file
View File

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

View File

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

Binary file not shown.

View File

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

File diff suppressed because it is too large Load Diff

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff