diff --git a/README.md b/README.md index 0519ecb..62df75e 100644 --- a/README.md +++ b/README.md @@ -1 +1,67 @@ - \ No newline at end of file +# 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 + 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. \ No newline at end of file diff --git a/website/.env b/website/.env new file mode 100644 index 0000000..a44a6ea --- /dev/null +++ b/website/.env @@ -0,0 +1 @@ +OPENAI_API_KEY=sk-placeholder diff --git a/website/__pycache__/app.cpython-311.pyc b/website/__pycache__/app.cpython-311.pyc index 5534b6c..896763e 100644 Binary files a/website/__pycache__/app.cpython-311.pyc and b/website/__pycache__/app.cpython-311.pyc differ diff --git a/website/__pycache__/init_db.cpython-311.pyc b/website/__pycache__/init_db.cpython-311.pyc index f0e320c..b660905 100644 Binary files a/website/__pycache__/init_db.cpython-311.pyc and b/website/__pycache__/init_db.cpython-311.pyc differ diff --git a/website/app.py b/website/app.py index d896fa9..d5da608 100644 --- a/website/app.py +++ b/website/app.py @@ -1,3 +1,6 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + import os from datetime import datetime, timedelta from flask import Flask, render_template, request, redirect, url_for, flash, jsonify, session @@ -12,6 +15,11 @@ from wtforms.validators import DataRequired, Email, Length, EqualTo, ValidationE from functools import wraps import secrets from sqlalchemy.sql import func +import openai +from dotenv import load_dotenv + +# Lade .env-Datei +load_dotenv() app = Flask(__name__) app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'default-dev-key') @@ -19,6 +27,9 @@ app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///mindmap.db' app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=365) # Langlebige Session für Dark Mode-Einstellung +# OpenAI API-Konfiguration +openai.api_key = os.environ.get('OPENAI_API_KEY') + # Context processor für globale Template-Variablen @app.context_processor def inject_globals(): @@ -36,6 +47,17 @@ db = SQLAlchemy(app) login_manager = LoginManager(app) login_manager.login_view = 'login' +# Benutzerdefinierter Decorator für Admin-Zugriff +def admin_required(f): + @wraps(f) + @login_required + def decorated_function(*args, **kwargs): + if not current_user.is_admin: + flash('Zugriff verweigert. Nur Administratoren dürfen diese Seite aufrufen.', 'error') + return redirect(url_for('index')) + return f(*args, **kwargs) + return decorated_function + class RelationType(Enum): SUPPORTS = "stützt" CONTRADICTS = "widerspricht" @@ -197,7 +219,7 @@ def mindmap(): @login_required def profile(): thoughts = Thought.query.filter_by(user_id=current_user.id).order_by(Thought.timestamp.desc()).all() - return render_template('profile.html', thoughts=thoughts) + return render_template('profile.html', thoughts=thoughts, user=current_user) # Route für Benutzereinstellungen @app.route('/settings', methods=['GET', 'POST']) @@ -468,17 +490,56 @@ def add_comment(): # Admin routes @app.route('/admin') -@login_required +@admin_required def admin(): - if not current_user.is_admin: - flash('Zugriff verweigert') - return redirect(url_for('index')) - users = User.query.all() nodes = MindMapNode.query.all() thoughts = Thought.query.all() - return render_template('admin.html', users=users, nodes=nodes, thoughts=thoughts) + # Aktuelles Datum für Logs + now = datetime.now() + + return render_template('admin.html', users=users, nodes=nodes, thoughts=thoughts, now=now, active_tab='users') + +# Zusätzliche Route für die Admin-Dashboard-Seite +@app.route('/admin/dashboard') +@admin_required +def admin_dashboard(): + users = User.query.all() + nodes = MindMapNode.query.all() + thoughts = Thought.query.all() + now = datetime.now() + return render_template('admin.html', users=users, nodes=nodes, thoughts=thoughts, now=now, active_tab='dashboard') + +# Zusätzliche Route für die Admin-Benutzer-Seite +@app.route('/admin/users') +@admin_required +def admin_users(): + users = User.query.all() + nodes = MindMapNode.query.all() + thoughts = Thought.query.all() + now = datetime.now() + return render_template('admin.html', users=users, nodes=nodes, thoughts=thoughts, now=now, active_tab='users') + +# Zusätzliche Route für die Admin-Gedanken-Seite +@app.route('/admin/thoughts') +@admin_required +def admin_thoughts(): + users = User.query.all() + nodes = MindMapNode.query.all() + thoughts = Thought.query.all() + now = datetime.now() + return render_template('admin.html', users=users, nodes=nodes, thoughts=thoughts, now=now, active_tab='thoughts') + +# Zusätzliche Route für die Admin-Mindmap-Seite +@app.route('/admin/mindmap') +@admin_required +def admin_mindmap(): + users = User.query.all() + nodes = MindMapNode.query.all() + thoughts = Thought.query.all() + now = datetime.now() + return render_template('admin.html', users=users, nodes=nodes, thoughts=thoughts, now=now, active_tab='nodes') @app.route('/api/thoughts//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(): diff --git a/website/example.env b/website/example.env new file mode 100644 index 0000000..035e4a2 --- /dev/null +++ b/website/example.env @@ -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 \ No newline at end of file diff --git a/website/init_db.py b/website/init_db.py index 90a843c..bd94e6b 100644 --- a/website/init_db.py +++ b/website/init_db.py @@ -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 diff --git a/website/instance/mindmap.db b/website/instance/mindmap.db index fecf879..1db9c4d 100644 Binary files a/website/instance/mindmap.db and b/website/instance/mindmap.db differ diff --git a/website/static/css/assistant.css b/website/static/css/assistant.css new file mode 100644 index 0000000..9c2b3d3 --- /dev/null +++ b/website/static/css/assistant.css @@ -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; +} \ No newline at end of file diff --git a/website/static/css/main.css b/website/static/css/main.css index 65645e9..94cea3b 100644 --- a/website/static/css/main.css +++ b/website/static/css/main.css @@ -1504,6 +1504,34 @@ label:is(.dark *) { border-color: #2563eb; } +.form-input::-moz-placeholder, .form-textarea::-moz-placeholder { + color: #6b7280; + opacity: 1; +} + +.form-input::placeholder,.form-textarea::placeholder { + color: #6b7280; + opacity: 1; +} + +.form-input::-webkit-datetime-edit-fields-wrapper { + padding: 0; +} + +.form-input::-webkit-date-and-time-value { + min-height: 1.5em; + text-align: inherit; +} + +.form-input::-webkit-datetime-edit { + display: inline-flex; +} + +.form-input::-webkit-datetime-edit,.form-input::-webkit-datetime-edit-year-field,.form-input::-webkit-datetime-edit-month-field,.form-input::-webkit-datetime-edit-day-field,.form-input::-webkit-datetime-edit-hour-field,.form-input::-webkit-datetime-edit-minute-field,.form-input::-webkit-datetime-edit-second-field,.form-input::-webkit-datetime-edit-millisecond-field,.form-input::-webkit-datetime-edit-meridiem-field { + padding-top: 0; + padding-bottom: 0; +} + .form-select { background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e"); background-position: right 0.5rem center; @@ -1617,6 +1645,70 @@ label:is(.dark *) { background-color: rgb(22 84 246 / var(--tw-bg-opacity, 1)); } +.btn-secondary { + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 0.375rem; + padding-left: 1rem; + padding-right: 1rem; + padding-top: 0.5rem; + padding-bottom: 0.5rem; + font-size: 1rem; + line-height: 1.5rem; + font-weight: 500; + --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); + transition-property: all; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +.btn-secondary:focus { + outline: 2px solid transparent; + outline-offset: 2px; + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); + --tw-ring-offset-width: 2px; +} + +.btn-secondary { + --tw-bg-opacity: 1; + background-color: rgb(120 51 248 / var(--tw-bg-opacity, 1)); + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity, 1)); +} + +.btn-secondary:hover { + --tw-bg-opacity: 1; + background-color: rgb(105 36 226 / var(--tw-bg-opacity, 1)); + --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.btn-secondary:focus { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(139 85 255 / var(--tw-ring-opacity, 1)); +} + +.btn-secondary:active { + --tw-translate-y: 0.125rem; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.btn-secondary:is(.dark *) { + --tw-bg-opacity: 1; + background-color: rgb(105 36 226 / var(--tw-bg-opacity, 1)); +} + +.btn-secondary:hover:is(.dark *) { + --tw-bg-opacity: 1; + background-color: rgb(120 51 248 / var(--tw-bg-opacity, 1)); +} + .btn-outline { display: inline-flex; align-items: center; @@ -1768,27 +1860,6 @@ label:is(.dark *) { color: rgb(255 255 255 / var(--tw-text-opacity, 1)); } -.mindmap-node { - cursor: pointer; - border-width: 2px; - --tw-border-opacity: 1; - border-color: rgb(229 231 235 / var(--tw-border-opacity, 1)); - transition-property: all; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-duration: 200ms; -} - -.mindmap-node:hover { - --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); - --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color); - box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); -} - -.mindmap-node:is(.dark *) { - --tw-border-opacity: 1; - border-color: rgb(91 91 105 / var(--tw-border-opacity, 1)); -} - /* Mindmap-spezifische Stile */ .mindmap-container { @@ -1808,28 +1879,6 @@ label:is(.dark *) { width: 100%; } -.ascii-art { - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; - white-space: pre; - font-family: JetBrains Mono, ui-monospace, SFMono-Regular, monospace; - font-size: 0.75rem; - line-height: 1rem; - font-weight: 700; - line-height: 1; - letter-spacing: -0.025em; - --tw-text-opacity: 1; - color: rgb(17 66 226 / var(--tw-text-opacity, 1)); - opacity: 0.8; -} - -.ascii-art:is(.dark *) { - --tw-text-opacity: 1; - color: rgb(90 147 255 / var(--tw-text-opacity, 1)); - opacity: 0.6; -} - /* Verbesserte Formulareingabefelder */ .form-input, @@ -1885,20 +1934,8 @@ label:is(.dark *) { --tw-ring-color: rgb(90 147 255 / var(--tw-ring-opacity, 1)); } -.sr-only { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - white-space: nowrap; - border-width: 0; -} - -.pointer-events-none { - pointer-events: none; +.visible { + visibility: visible; } .static { @@ -1917,13 +1954,12 @@ label:is(.dark *) { position: relative; } -.inset-0 { - inset: 0px; +.sticky { + position: sticky; } -.inset-y-0 { - top: 0px; - bottom: 0px; +.inset-0 { + inset: 0px; } .bottom-4 { @@ -1938,8 +1974,12 @@ label:is(.dark *) { left: 50%; } -.right-0 { - right: 0px; +.left-3 { + left: 0.75rem; +} + +.right-3 { + right: 0.75rem; } .right-4 { @@ -1950,29 +1990,28 @@ label:is(.dark *) { top: 0px; } -.-z-10 { - z-index: -10; +.top-1\/2 { + top: 50%; } -.z-0 { - z-index: 0; +.top-6 { + top: 1.5rem; } .z-10 { z-index: 10; } -.z-20 { - z-index: 20; -} - .z-50 { z-index: 50; } -.mx-1 { - margin-left: 0.25rem; - margin-right: 0.25rem; +.order-1 { + order: 1; +} + +.order-2 { + order: 2; } .mx-auto { @@ -1980,15 +2019,18 @@ label:is(.dark *) { margin-right: auto; } -.my-6 { - margin-top: 1.5rem; - margin-bottom: 1.5rem; +.mb-0 { + margin-bottom: 0px; } .mb-1 { margin-bottom: 0.25rem; } +.mb-1\.5 { + margin-bottom: 0.375rem; +} + .mb-12 { margin-bottom: 3rem; } @@ -2021,6 +2063,10 @@ label:is(.dark *) { margin-inline-end: 0.5rem; } +.ml-1 { + margin-left: 0.25rem; +} + .ml-3 { margin-left: 0.75rem; } @@ -2049,6 +2095,18 @@ label:is(.dark *) { margin-top: 0.25rem; } +.mt-10 { + margin-top: 2.5rem; +} + +.mt-12 { + margin-top: 3rem; +} + +.mt-16 { + margin-top: 4rem; +} + .mt-2 { margin-top: 0.5rem; } @@ -2065,25 +2123,10 @@ label:is(.dark *) { margin-top: 1.25rem; } -.mt-6 { - margin-top: 1.5rem; -} - .mt-8 { margin-top: 2rem; } -.mt-auto { - margin-top: auto; -} - -.line-clamp-2 { - overflow: hidden; - display: -webkit-box; - -webkit-box-orient: vertical; - -webkit-line-clamp: 2; -} - .block { display: block; } @@ -2132,12 +2175,20 @@ label:is(.dark *) { height: 0.75rem; } +.h-32 { + height: 8rem; +} + .h-5 { height: 1.25rem; } -.h-6 { - height: 1.5rem; +.h-64 { + height: 16rem; +} + +.h-7 { + height: 1.75rem; } .h-8 { @@ -2148,26 +2199,34 @@ label:is(.dark *) { height: 20rem; } -.h-\[600px\] { - height: 600px; -} - .h-full { height: 100%; } -.max-h-\[500px\] { - max-height: 500px; +.max-h-0 { + max-height: 0px; } -.max-h-\[80vh\] { - max-height: 80vh; +.max-h-48 { + max-height: 12rem; +} + +.max-h-80 { + max-height: 20rem; +} + +.max-h-96 { + max-height: 24rem; } .max-h-\[90vh\] { max-height: 90vh; } +.min-h-\[65vh\] { + min-height: 65vh; +} + .min-h-screen { min-height: 100vh; } @@ -2184,6 +2243,14 @@ label:is(.dark *) { width: 3rem; } +.w-14 { + width: 3.5rem; +} + +.w-16 { + width: 4rem; +} + .w-2 { width: 0.5rem; } @@ -2192,24 +2259,24 @@ label:is(.dark *) { width: 0.75rem; } -.w-48 { - width: 12rem; -} - .w-5 { width: 1.25rem; } -.w-6 { - width: 1.5rem; +.w-60 { + width: 15rem; +} + +.w-64 { + width: 16rem; } .w-8 { width: 2rem; } -.w-auto { - width: auto; +.w-80 { + width: 20rem; } .w-full { @@ -2228,12 +2295,8 @@ label:is(.dark *) { max-width: 56rem; } -.max-w-7xl { - max-width: 80rem; -} - -.max-w-md { - max-width: 28rem; +.max-w-\[85\%\] { + max-width: 85%; } .max-w-screen-xl { @@ -2248,6 +2311,10 @@ label:is(.dark *) { flex: 1 1 0%; } +.flex-shrink { + flex-shrink: 1; +} + .flex-shrink-0 { flex-shrink: 0; } @@ -2256,15 +2323,36 @@ label:is(.dark *) { flex-grow: 1; } -.origin-top-right { - transform-origin: top right; -} - .-translate-x-1\/2 { --tw-translate-x: -50%; transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); } +.-translate-y-1\/2 { + --tw-translate-y: -50%; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.-translate-y-10 { + --tw-translate-y: -2.5rem; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.translate-x-0 { + --tw-translate-x: 0px; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.translate-x-7 { + --tw-translate-x: 1.75rem; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.translate-y-0 { + --tw-translate-y: 0px; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + .translate-y-4 { --tw-translate-y: 1rem; transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); @@ -2348,6 +2436,10 @@ label:is(.dark *) { align-items: center; } +.justify-start { + justify-content: flex-start; +} + .justify-end { justify-content: flex-end; } @@ -2368,6 +2460,10 @@ label:is(.dark *) { gap: 0.5rem; } +.gap-3 { + gap: 0.75rem; +} + .gap-4 { gap: 1rem; } @@ -2398,6 +2494,12 @@ label:is(.dark *) { margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse))); } +.space-x-6 > :not([hidden]) ~ :not([hidden]) { + --tw-space-x-reverse: 0; + margin-right: calc(1.5rem * var(--tw-space-x-reverse)); + margin-left: calc(1.5rem * calc(1 - var(--tw-space-x-reverse))); +} + .space-y-1 > :not([hidden]) ~ :not([hidden]) { --tw-space-y-reverse: 0; margin-top: calc(0.25rem * calc(1 - var(--tw-space-y-reverse))); @@ -2432,10 +2534,18 @@ label:is(.dark *) { overflow: hidden; } +.overflow-x-auto { + overflow-x: auto; +} + .overflow-y-auto { overflow-y: auto; } +.overflow-x-hidden { + overflow-x: hidden; +} + .rounded { border-radius: 0.25rem; } @@ -2452,8 +2562,14 @@ label:is(.dark *) { border-radius: 0.375rem; } -.rounded-xl { - border-radius: 0.75rem; +.rounded-l-lg { + border-top-left-radius: 0.5rem; + border-bottom-left-radius: 0.5rem; +} + +.rounded-r-lg { + border-top-right-radius: 0.5rem; + border-bottom-right-radius: 0.5rem; } .border { @@ -2468,6 +2584,10 @@ label:is(.dark *) { border-bottom-width: 2px; } +.border-l-2 { + border-left-width: 2px; +} + .border-l-4 { border-left-width: 4px; } @@ -2480,10 +2600,6 @@ label:is(.dark *) { border-top-width: 2px; } -.border-blue-500\/50 { - border-color: rgb(59 130 246 / 0.5); -} - .border-dark-500 { --tw-border-opacity: 1; border-color: rgb(111 111 126 / var(--tw-border-opacity, 1)); @@ -2494,13 +2610,40 @@ label:is(.dark *) { border-color: rgb(91 91 105 / var(--tw-border-opacity, 1)); } +.border-gray-100 { + --tw-border-opacity: 1; + border-color: rgb(243 244 246 / var(--tw-border-opacity, 1)); +} + .border-gray-200 { --tw-border-opacity: 1; border-color: rgb(229 231 235 / var(--tw-border-opacity, 1)); } -.border-green-500\/50 { - border-color: rgb(34 197 94 / 0.5); +.border-gray-200\/20 { + border-color: rgb(229 231 235 / 0.2); +} + +.border-gray-300 { + --tw-border-opacity: 1; + border-color: rgb(209 213 219 / var(--tw-border-opacity, 1)); +} + +.border-gray-300\/20 { + border-color: rgb(209 213 219 / 0.2); +} + +.border-gray-300\/50 { + border-color: rgb(209 213 219 / 0.5); +} + +.border-gray-700 { + --tw-border-opacity: 1; + border-color: rgb(55 65 81 / var(--tw-border-opacity, 1)); +} + +.border-gray-800\/50 { + border-color: rgb(31 41 55 / 0.5); } .border-primary-400 { @@ -2508,24 +2651,31 @@ label:is(.dark *) { border-color: rgb(90 147 255 / var(--tw-border-opacity, 1)); } -.border-red-500\/50 { - border-color: rgb(239 68 68 / 0.5); +.border-purple-500 { + --tw-border-opacity: 1; + border-color: rgb(168 85 247 / var(--tw-border-opacity, 1)); } -.border-transparent { - border-color: transparent; +.border-slate-700 { + --tw-border-opacity: 1; + border-color: rgb(51 65 85 / var(--tw-border-opacity, 1)); } -.border-white\/10 { - border-color: rgb(255 255 255 / 0.1); +.border-slate-700\/20 { + border-color: rgb(51 65 85 / 0.2); } .bg-black\/50 { background-color: rgb(0 0 0 / 0.5); } -.bg-blue-600\/70 { - background-color: rgb(37 99 235 / 0.7); +.bg-blue-100 { + --tw-bg-opacity: 1; + background-color: rgb(219 234 254 / var(--tw-bg-opacity, 1)); +} + +.bg-blue-500\/20 { + background-color: rgb(59 130 246 / 0.2); } .bg-dark-700 { @@ -2533,14 +2683,33 @@ label:is(.dark *) { background-color: rgb(73 73 79 / var(--tw-bg-opacity, 1)); } +.bg-dark-800 { + --tw-bg-opacity: 1; + background-color: rgb(44 44 51 / var(--tw-bg-opacity, 1)); +} + +.bg-dark-900 { + --tw-bg-opacity: 1; + background-color: rgb(24 24 28 / var(--tw-bg-opacity, 1)); +} + .bg-gray-100 { --tw-bg-opacity: 1; background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1)); } -.bg-gray-50 { +.bg-gray-100\/20 { + background-color: rgb(243 244 246 / 0.2); +} + +.bg-gray-700 { --tw-bg-opacity: 1; - background-color: rgb(249 250 251 / var(--tw-bg-opacity, 1)); + background-color: rgb(55 65 81 / var(--tw-bg-opacity, 1)); +} + +.bg-green-100 { + --tw-bg-opacity: 1; + background-color: rgb(220 252 231 / var(--tw-bg-opacity, 1)); } .bg-green-400 { @@ -2548,9 +2717,13 @@ label:is(.dark *) { background-color: rgb(74 222 128 / var(--tw-bg-opacity, 1)); } -.bg-indigo-600 { +.bg-green-500\/20 { + background-color: rgb(34 197 94 / 0.2); +} + +.bg-primary-100 { --tw-bg-opacity: 1; - background-color: rgb(79 70 229 / var(--tw-bg-opacity, 1)); + background-color: rgb(217 231 255 / var(--tw-bg-opacity, 1)); } .bg-primary-400 { @@ -2568,8 +2741,30 @@ label:is(.dark *) { background-color: rgb(22 84 246 / var(--tw-bg-opacity, 1)); } -.bg-purple-600\/70 { - background-color: rgb(147 51 234 / 0.7); +.bg-purple-500 { + --tw-bg-opacity: 1; + background-color: rgb(168 85 247 / var(--tw-bg-opacity, 1)); +} + +.bg-purple-500\/20 { + background-color: rgb(168 85 247 / 0.2); +} + +.bg-purple-600 { + --tw-bg-opacity: 1; + background-color: rgb(147 51 234 / var(--tw-bg-opacity, 1)); +} + +.bg-purple-900\/20 { + background-color: rgb(88 28 135 / 0.2); +} + +.bg-purple-900\/30 { + background-color: rgb(88 28 135 / 0.3); +} + +.bg-red-500\/20 { + background-color: rgb(239 68 68 / 0.2); } .bg-secondary-400 { @@ -2582,6 +2777,19 @@ label:is(.dark *) { background-color: rgb(105 36 226 / var(--tw-bg-opacity, 1)); } +.bg-slate-700 { + --tw-bg-opacity: 1; + background-color: rgb(51 65 85 / var(--tw-bg-opacity, 1)); +} + +.bg-slate-800\/80 { + background-color: rgb(30 41 59 / 0.8); +} + +.bg-slate-900\/50 { + background-color: rgb(15 23 42 / 0.5); +} + .bg-white { --tw-bg-opacity: 1; background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1)); @@ -2591,8 +2799,8 @@ label:is(.dark *) { background-color: rgb(255 255 255 / 0.1); } -.bg-white\/5 { - background-color: rgb(255 255 255 / 0.05); +.bg-opacity-30 { + --tw-bg-opacity: 0.3; } .bg-gradient-to-br { @@ -2603,53 +2811,34 @@ label:is(.dark *) { background-image: linear-gradient(to right, var(--tw-gradient-stops)); } -.from-indigo-500 { - --tw-gradient-from: #6366f1 var(--tw-gradient-from-position); - --tw-gradient-to: rgb(99 102 241 / 0) var(--tw-gradient-to-position); +.from-purple-500 { + --tw-gradient-from: #a855f7 var(--tw-gradient-from-position); + --tw-gradient-to: rgb(168 85 247 / 0) var(--tw-gradient-to-position); --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); } -.from-primary-100\/30 { - --tw-gradient-from: rgb(217 231 255 / 0.3) var(--tw-gradient-from-position); - --tw-gradient-to: rgb(217 231 255 / 0) var(--tw-gradient-to-position); +.from-purple-600 { + --tw-gradient-from: #9333ea var(--tw-gradient-from-position); + --tw-gradient-to: rgb(147 51 234 / 0) var(--tw-gradient-to-position); --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); } -.from-primary-100\/50 { - --tw-gradient-from: rgb(217 231 255 / 0.5) var(--tw-gradient-from-position); - --tw-gradient-to: rgb(217 231 255 / 0) var(--tw-gradient-to-position); +.from-slate-900\/80 { + --tw-gradient-from: rgb(15 23 42 / 0.8) var(--tw-gradient-from-position); + --tw-gradient-to: rgb(15 23 42 / 0) var(--tw-gradient-to-position); --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); } -.from-primary-400 { - --tw-gradient-from: #5a93ff var(--tw-gradient-from-position); - --tw-gradient-to: rgb(90 147 255 / 0) var(--tw-gradient-to-position); - --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); +.to-blue-500 { + --tw-gradient-to: #3b82f6 var(--tw-gradient-to-position); } -.via-white { - --tw-gradient-to: rgb(255 255 255 / 0) var(--tw-gradient-to-position); - --tw-gradient-stops: var(--tw-gradient-from), #fff var(--tw-gradient-via-position), var(--tw-gradient-to); +.to-blue-600 { + --tw-gradient-to: #2563eb var(--tw-gradient-to-position); } -.to-primary-600 { - --tw-gradient-to: #1654f6 var(--tw-gradient-to-position); -} - -.to-purple-600 { - --tw-gradient-to: #9333ea var(--tw-gradient-to-position); -} - -.to-secondary-100\/30 { - --tw-gradient-to: rgb(236 232 255 / 0.3) var(--tw-gradient-to-position); -} - -.to-secondary-100\/50 { - --tw-gradient-to: rgb(236 232 255 / 0.5) var(--tw-gradient-to-position); -} - -.p-1 { - padding: 0.25rem; +.to-slate-800\/60 { + --tw-gradient-to: rgb(30 41 59 / 0.6) var(--tw-gradient-to-position); } .p-12 { @@ -2690,11 +2879,6 @@ label:is(.dark *) { padding-right: 0.5rem; } -.px-2\.5 { - padding-left: 0.625rem; - padding-right: 0.625rem; -} - .px-3 { padding-left: 0.75rem; padding-right: 0.75rem; @@ -2705,26 +2889,21 @@ label:is(.dark *) { padding-right: 1rem; } -.px-6 { - padding-left: 1.5rem; - padding-right: 1.5rem; -} - .px-8 { padding-left: 2rem; padding-right: 2rem; } -.py-0\.5 { - padding-top: 0.125rem; - padding-bottom: 0.125rem; -} - .py-1 { padding-top: 0.25rem; padding-bottom: 0.25rem; } +.py-10 { + padding-top: 2.5rem; + padding-bottom: 2.5rem; +} + .py-12 { padding-top: 3rem; padding-bottom: 3rem; @@ -2760,18 +2939,13 @@ label:is(.dark *) { padding-bottom: 1.25rem; } -.py-6 { - padding-top: 1.5rem; - padding-bottom: 1.5rem; -} - .py-8 { padding-top: 2rem; padding-bottom: 2rem; } -.pb-3 { - padding-bottom: 0.75rem; +.pb-10 { + padding-bottom: 2.5rem; } .pb-32 { @@ -2794,12 +2968,8 @@ label:is(.dark *) { padding-right: 1rem; } -.pt-0\.5 { - padding-top: 0.125rem; -} - -.pt-1 { - padding-top: 0.25rem; +.pr-9 { + padding-right: 2.25rem; } .pt-16 { @@ -2810,10 +2980,18 @@ label:is(.dark *) { padding-top: 0.5rem; } +.pt-20 { + padding-top: 5rem; +} + .pt-4 { padding-top: 1rem; } +.pt-6 { + padding-top: 1.5rem; +} + .text-left { text-align: left; } @@ -2822,8 +3000,8 @@ label:is(.dark *) { text-align: center; } -.font-sans { - font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, sans-serif; +.font-mono { + font-family: JetBrains Mono, ui-monospace, SFMono-Regular, monospace; } .text-2xl { @@ -2851,11 +3029,6 @@ label:is(.dark *) { line-height: 1; } -.text-base { - font-size: 1rem; - line-height: 1.5rem; -} - .text-lg { font-size: 1.125rem; line-height: 1.75rem; @@ -2892,10 +3065,6 @@ label:is(.dark *) { text-transform: uppercase; } -.leading-relaxed { - line-height: 1.625; -} - .tracking-tight { letter-spacing: -0.025em; } @@ -2904,6 +3073,11 @@ label:is(.dark *) { letter-spacing: 0.05em; } +.text-amber-400 { + --tw-text-opacity: 1; + color: rgb(251 191 36 / var(--tw-text-opacity, 1)); +} + .text-blue-400 { --tw-text-opacity: 1; color: rgb(96 165 250 / var(--tw-text-opacity, 1)); @@ -2919,6 +3093,11 @@ label:is(.dark *) { color: rgb(37 99 235 / var(--tw-text-opacity, 1)); } +.text-blue-800 { + --tw-text-opacity: 1; + color: rgb(30 64 175 / var(--tw-text-opacity, 1)); +} + .text-gray-200 { --tw-text-opacity: 1; color: rgb(229 231 235 / var(--tw-text-opacity, 1)); @@ -2969,14 +3148,9 @@ label:is(.dark *) { color: rgb(34 197 94 / var(--tw-text-opacity, 1)); } -.text-indigo-300 { +.text-green-800 { --tw-text-opacity: 1; - color: rgb(165 180 252 / var(--tw-text-opacity, 1)); -} - -.text-indigo-400 { - --tw-text-opacity: 1; - color: rgb(129 140 248 / var(--tw-text-opacity, 1)); + color: rgb(22 101 52 / var(--tw-text-opacity, 1)); } .text-primary-400 { @@ -2984,9 +3158,14 @@ label:is(.dark *) { color: rgb(90 147 255 / var(--tw-text-opacity, 1)); } -.text-primary-500 { +.text-primary-600 { --tw-text-opacity: 1; - color: rgb(41 112 255 / var(--tw-text-opacity, 1)); + color: rgb(22 84 246 / var(--tw-text-opacity, 1)); +} + +.text-purple-300 { + --tw-text-opacity: 1; + color: rgb(216 180 254 / var(--tw-text-opacity, 1)); } .text-purple-400 { @@ -2994,11 +3173,26 @@ label:is(.dark *) { color: rgb(192 132 252 / var(--tw-text-opacity, 1)); } +.text-purple-500 { + --tw-text-opacity: 1; + color: rgb(168 85 247 / var(--tw-text-opacity, 1)); +} + +.text-red-400 { + --tw-text-opacity: 1; + color: rgb(248 113 113 / var(--tw-text-opacity, 1)); +} + .text-red-500 { --tw-text-opacity: 1; color: rgb(239 68 68 / var(--tw-text-opacity, 1)); } +.text-red-600 { + --tw-text-opacity: 1; + color: rgb(220 38 38 / var(--tw-text-opacity, 1)); +} + .text-secondary-400 { --tw-text-opacity: 1; color: rgb(166 133 255 / var(--tw-text-opacity, 1)); @@ -3009,25 +3203,14 @@ label:is(.dark *) { color: rgb(255 255 255 / var(--tw-text-opacity, 1)); } -.text-white\/50 { - color: rgb(255 255 255 / 0.5); +.text-yellow-400 { + --tw-text-opacity: 1; + color: rgb(250 204 21 / var(--tw-text-opacity, 1)); } -.text-white\/60 { - color: rgb(255 255 255 / 0.6); -} - -.text-white\/70 { - color: rgb(255 255 255 / 0.7); -} - -.text-white\/90 { - color: rgb(255 255 255 / 0.9); -} - -.antialiased { - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; +.text-yellow-500 { + --tw-text-opacity: 1; + color: rgb(234 179 8 / var(--tw-text-opacity, 1)); } .opacity-0 { @@ -3038,10 +3221,6 @@ label:is(.dark *) { opacity: 1; } -.opacity-30 { - opacity: 0.3; -} - .opacity-75 { opacity: 0.75; } @@ -3064,6 +3243,10 @@ label:is(.dark *) { box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); } +.outline { + outline-style: solid; +} + .ring-1 { --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color); @@ -3084,14 +3267,13 @@ label:is(.dark *) { filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); } -.filter { +.drop-shadow { + --tw-drop-shadow: drop-shadow(0 1px 2px rgb(0 0 0 / 0.1)) drop-shadow(0 1px 1px rgb(0 0 0 / 0.06)); filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); } -.backdrop-blur-lg { - --tw-backdrop-blur: blur(16px); - -webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); - backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); +.filter { + filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); } .backdrop-blur-sm { @@ -3125,16 +3307,26 @@ label:is(.dark *) { transition-duration: 150ms; } +.transition-transform { + transition-property: transform; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + .duration-100 { transition-duration: 100ms; } -.duration-300 { - transition-duration: 300ms; +.duration-150 { + transition-duration: 150ms; } -.duration-500 { - transition-duration: 500ms; +.duration-200 { + transition-duration: 200ms; +} + +.duration-300 { + transition-duration: 300ms; } .duration-75 { @@ -3291,11 +3483,21 @@ label:is(.dark *) { --tw-prose-td-borders: var(--tw-prose-invert-td-borders); } +.hover\:-translate-y-0\.5:hover { + --tw-translate-y: -0.125rem; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + .hover\:-translate-y-1:hover { --tw-translate-y: -0.25rem; transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); } +.hover\:bg-dark-600:hover { + --tw-bg-opacity: 1; + background-color: rgb(91 91 105 / var(--tw-bg-opacity, 1)); +} + .hover\:bg-gray-100:hover { --tw-bg-opacity: 1; background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1)); @@ -3306,18 +3508,40 @@ label:is(.dark *) { background-color: rgb(229 231 235 / var(--tw-bg-opacity, 1)); } +.hover\:bg-gray-200\/50:hover { + background-color: rgb(229 231 235 / 0.5); +} + +.hover\:bg-gray-50:hover { + --tw-bg-opacity: 1; + background-color: rgb(249 250 251 / var(--tw-bg-opacity, 1)); +} + +.hover\:bg-gray-800\/50:hover { + background-color: rgb(31 41 55 / 0.5); +} + +.hover\:bg-primary-700:hover { + --tw-bg-opacity: 1; + background-color: rgb(17 66 226 / var(--tw-bg-opacity, 1)); +} + .hover\:bg-white\/10:hover { background-color: rgb(255 255 255 / 0.1); } -.hover\:from-indigo-600:hover { - --tw-gradient-from: #4f46e5 var(--tw-gradient-from-position); - --tw-gradient-to: rgb(79 70 229 / 0) var(--tw-gradient-to-position); +.hover\:bg-white\/5:hover { + background-color: rgb(255 255 255 / 0.05); +} + +.hover\:from-purple-700:hover { + --tw-gradient-from: #7e22ce var(--tw-gradient-from-position); + --tw-gradient-to: rgb(126 34 206 / 0) var(--tw-gradient-to-position); --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); } -.hover\:to-purple-700:hover { - --tw-gradient-to: #7e22ce var(--tw-gradient-to-position); +.hover\:to-blue-700:hover { + --tw-gradient-to: #1d4ed8 var(--tw-gradient-to-position); } .hover\:text-blue-800:hover { @@ -3325,9 +3549,9 @@ label:is(.dark *) { color: rgb(30 64 175 / var(--tw-text-opacity, 1)); } -.hover\:text-gray-600:hover { +.hover\:text-gray-200:hover { --tw-text-opacity: 1; - color: rgb(75 85 99 / var(--tw-text-opacity, 1)); + color: rgb(229 231 235 / var(--tw-text-opacity, 1)); } .hover\:text-gray-900:hover { @@ -3335,9 +3559,14 @@ label:is(.dark *) { color: rgb(17 24 39 / var(--tw-text-opacity, 1)); } -.hover\:text-primary-600:hover { +.hover\:text-purple-300:hover { --tw-text-opacity: 1; - color: rgb(22 84 246 / var(--tw-text-opacity, 1)); + color: rgb(216 180 254 / var(--tw-text-opacity, 1)); +} + +.hover\:text-red-800:hover { + --tw-text-opacity: 1; + color: rgb(153 27 27 / var(--tw-text-opacity, 1)); } .hover\:text-white:hover { @@ -3351,20 +3580,35 @@ label:is(.dark *) { box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); } +.focus\:border-transparent:focus { + border-color: transparent; +} + .focus\:outline-none:focus { outline: 2px solid transparent; outline-offset: 2px; } +.focus\:ring-1:focus { + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); +} + .focus\:ring-2:focus { --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); } -.focus\:ring-secondary-500:focus { +.focus\:ring-primary-500:focus { --tw-ring-opacity: 1; - --tw-ring-color: rgb(139 85 255 / var(--tw-ring-opacity, 1)); + --tw-ring-color: rgb(41 112 255 / var(--tw-ring-opacity, 1)); +} + +.focus\:ring-purple-500:focus { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(168 85 247 / var(--tw-ring-opacity, 1)); } .dark\:inline:is(.dark *) { @@ -3375,13 +3619,31 @@ label:is(.dark *) { display: none; } +.dark\:border-dark-600:is(.dark *) { + --tw-border-opacity: 1; + border-color: rgb(91 91 105 / var(--tw-border-opacity, 1)); +} + .dark\:border-gray-700:is(.dark *) { --tw-border-opacity: 1; border-color: rgb(55 65 81 / var(--tw-border-opacity, 1)); } -.dark\:border-white\/10:is(.dark *) { - border-color: rgb(255 255 255 / 0.1); +.dark\:border-gray-700\/20:is(.dark *) { + border-color: rgb(55 65 81 / 0.2); +} + +.dark\:border-gray-700\/30:is(.dark *) { + border-color: rgb(55 65 81 / 0.3); +} + +.dark\:border-gray-800:is(.dark *) { + --tw-border-opacity: 1; + border-color: rgb(31 41 55 / var(--tw-border-opacity, 1)); +} + +.dark\:bg-blue-900\/30:is(.dark *) { + background-color: rgb(30 58 138 / 0.3); } .dark\:bg-dark-700:is(.dark *) { @@ -3389,43 +3651,32 @@ label:is(.dark *) { background-color: rgb(73 73 79 / var(--tw-bg-opacity, 1)); } +.dark\:bg-dark-700\/20:is(.dark *) { + background-color: rgb(73 73 79 / 0.2); +} + .dark\:bg-dark-800:is(.dark *) { --tw-bg-opacity: 1; background-color: rgb(44 44 51 / var(--tw-bg-opacity, 1)); } -.dark\:bg-dark-900:is(.dark *) { +.dark\:bg-green-900\/30:is(.dark *) { + background-color: rgb(20 83 45 / 0.3); +} + +.dark\:bg-primary-900:is(.dark *) { --tw-bg-opacity: 1; - background-color: rgb(24 24 28 / var(--tw-bg-opacity, 1)); + background-color: rgb(21 51 144 / var(--tw-bg-opacity, 1)); } -.dark\:bg-white\/10:is(.dark *) { - background-color: rgb(255 255 255 / 0.1); +.dark\:text-blue-300:is(.dark *) { + --tw-text-opacity: 1; + color: rgb(147 197 253 / var(--tw-text-opacity, 1)); } -.dark\:from-primary-900\/30:is(.dark *) { - --tw-gradient-from: rgb(21 51 144 / 0.3) var(--tw-gradient-from-position); - --tw-gradient-to: rgb(21 51 144 / 0) var(--tw-gradient-to-position); - --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); -} - -.dark\:from-primary-900\/50:is(.dark *) { - --tw-gradient-from: rgb(21 51 144 / 0.5) var(--tw-gradient-from-position); - --tw-gradient-to: rgb(21 51 144 / 0) var(--tw-gradient-to-position); - --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); -} - -.dark\:via-dark-900:is(.dark *) { - --tw-gradient-to: rgb(24 24 28 / 0) var(--tw-gradient-to-position); - --tw-gradient-stops: var(--tw-gradient-from), #18181c var(--tw-gradient-via-position), var(--tw-gradient-to); -} - -.dark\:to-secondary-900\/30:is(.dark *) { - --tw-gradient-to: rgb(72 28 150 / 0.3) var(--tw-gradient-to-position); -} - -.dark\:to-secondary-900\/50:is(.dark *) { - --tw-gradient-to: rgb(72 28 150 / 0.5) var(--tw-gradient-to-position); +.dark\:text-blue-400:is(.dark *) { + --tw-text-opacity: 1; + color: rgb(96 165 250 / var(--tw-text-opacity, 1)); } .dark\:text-gray-200:is(.dark *) { @@ -3443,6 +3694,26 @@ label:is(.dark *) { color: rgb(156 163 175 / var(--tw-text-opacity, 1)); } +.dark\:text-gray-500:is(.dark *) { + --tw-text-opacity: 1; + color: rgb(107 114 128 / var(--tw-text-opacity, 1)); +} + +.dark\:text-green-300:is(.dark *) { + --tw-text-opacity: 1; + color: rgb(134 239 172 / var(--tw-text-opacity, 1)); +} + +.dark\:text-primary-400:is(.dark *) { + --tw-text-opacity: 1; + color: rgb(90 147 255 / var(--tw-text-opacity, 1)); +} + +.dark\:text-red-400:is(.dark *) { + --tw-text-opacity: 1; + color: rgb(248 113 113 / var(--tw-text-opacity, 1)); +} + .dark\:text-white:is(.dark *) { --tw-text-opacity: 1; color: rgb(255 255 255 / var(--tw-text-opacity, 1)); @@ -3453,64 +3724,37 @@ label:is(.dark *) { background-color: rgb(91 91 105 / var(--tw-bg-opacity, 1)); } -.dark\:hover\:bg-dark-700:hover:is(.dark *) { - --tw-bg-opacity: 1; - background-color: rgb(73 73 79 / var(--tw-bg-opacity, 1)); -} - -.dark\:hover\:bg-white\/10:hover:is(.dark *) { - background-color: rgb(255 255 255 / 0.1); +.dark\:hover\:bg-dark-700\/30:hover:is(.dark *) { + background-color: rgb(73 73 79 / 0.3); } .dark\:hover\:bg-white\/5:hover:is(.dark *) { background-color: rgb(255 255 255 / 0.05); } -.dark\:hover\:text-gray-200:hover:is(.dark *) { +.dark\:hover\:text-blue-300:hover:is(.dark *) { --tw-text-opacity: 1; - color: rgb(229 231 235 / var(--tw-text-opacity, 1)); + color: rgb(147 197 253 / var(--tw-text-opacity, 1)); } -.dark\:hover\:text-white:hover:is(.dark *) { +.dark\:hover\:text-red-300:hover:is(.dark *) { --tw-text-opacity: 1; - color: rgb(255 255 255 / var(--tw-text-opacity, 1)); + color: rgb(252 165 165 / var(--tw-text-opacity, 1)); } @media (min-width: 640px) { - .sm\:ml-6 { - margin-left: 1.5rem; - } - - .sm\:flex { - display: flex; - } - - .sm\:hidden { - display: none; - } - .sm\:h-96 { height: 24rem; } - .sm\:grid-cols-2 { - grid-template-columns: repeat(2, minmax(0, 1fr)); + .sm\:w-96 { + width: 24rem; } .sm\:flex-row { flex-direction: row; } - .sm\:items-center { - align-items: center; - } - - .sm\:space-x-8 > :not([hidden]) ~ :not([hidden]) { - --tw-space-x-reverse: 0; - margin-right: calc(2rem * var(--tw-space-x-reverse)); - margin-left: calc(2rem * calc(1 - var(--tw-space-x-reverse))); - } - .sm\:px-6 { padding-left: 1.5rem; padding-right: 1.5rem; @@ -3522,10 +3766,6 @@ label:is(.dark *) { margin-bottom: 0px; } - .md\:mt-0 { - margin-top: 0px; - } - .md\:block { display: block; } @@ -3554,10 +3794,18 @@ label:is(.dark *) { flex-direction: row; } + .md\:items-start { + align-items: flex-start; + } + .md\:items-center { align-items: center; } + .md\:justify-start { + justify-content: flex-start; + } + .md\:justify-between { justify-content: space-between; } @@ -3570,6 +3818,10 @@ label:is(.dark *) { padding: 2rem; } + .md\:text-left { + text-align: left; + } + .md\:text-right { text-align: right; } @@ -3596,6 +3848,14 @@ label:is(.dark *) { } @media (min-width: 1024px) { + .lg\:order-1 { + order: 1; + } + + .lg\:order-2 { + order: 2; + } + .lg\:col-span-1 { grid-column: span 1 / span 1; } @@ -3608,6 +3868,10 @@ label:is(.dark *) { grid-template-columns: repeat(3, minmax(0, 1fr)); } + .lg\:grid-cols-4 { + grid-template-columns: repeat(4, minmax(0, 1fr)); + } + .lg\:px-8 { padding-left: 2rem; padding-right: 2rem; diff --git a/website/static/css/style.css b/website/static/css/style.css index f84a7be..1731bf3 100644 --- a/website/static/css/style.css +++ b/website/static/css/style.css @@ -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; } \ No newline at end of file diff --git a/website/static/d3-extensions.js b/website/static/d3-extensions.js new file mode 100644 index 0000000..da9c1f3 --- /dev/null +++ b/website/static/d3-extensions.js @@ -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; diff --git a/website/static/img/favicon-gen.py b/website/static/img/favicon-gen.py index 9ff7f79..ba6faeb 100644 --- a/website/static/img/favicon-gen.py +++ b/website/static/img/favicon-gen.py @@ -1,4 +1,6 @@ #!/usr/bin/env python3 +# -*- coding: utf-8 -*- + import os from PIL import Image import cairosvg diff --git a/website/static/js/main.js b/website/static/js/main.js index 4ce27cf..8ce511b 100644 --- a/website/static/js/main.js +++ b/website/static/js/main.js @@ -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 = ` +
+

${node.name}

+

${node.description || 'Keine Beschreibung verfügbar.'}

+ +
+ + ${node.thought_count || 0} Gedanken + + +
+
+ `; + + // 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 `
${title}${desc ? `

${desc}

` : ''}
`; - }, - 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 = ''; - break; - case 'error': - icon = ''; - break; - default: - icon = ''; - } - - notification.innerHTML = ` -
-
${icon}
-
${message}
- -
- `; - - // 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; \ No newline at end of file diff --git a/website/static/js/modules/chatgpt-assistant.js b/website/static/js/modules/chatgpt-assistant.js new file mode 100644 index 0000000..4083b15 --- /dev/null +++ b/website/static/js/modules/chatgpt-assistant.js @@ -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 = ''; + + // 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 = ` +
+ + KI-Assistent +
+ + `; + + // 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 = ''; + + // 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 = '
'; + + 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; \ No newline at end of file diff --git a/website/static/js/modules/mindmap.js b/website/static/js/modules/mindmap.js index ed394a2..b6a2e04 100644 --- a/website/static/js/modules/mindmap.js +++ b/website/static/js/modules/mindmap.js @@ -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(` -
-
-
-

Mindmap wird geladen...

+ // Element nur leeren, wenn es noch kein SVG enthält + if (!this.container.select('svg').size()) { + this.container.html(` +
+
+
+

Mindmap wird geladen...

+
-
- `); + `); + } } 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() { diff --git a/website/static/mindmap.js b/website/static/mindmap.js index 15d2732..26a6c84 100644 --- a/website/static/mindmap.js +++ b/website/static/mindmap.js @@ -1,49 +1,1401 @@ -// Erstelle eine einfache Mindmap-Struktur mit D3.js -const data = { - name: "Wissenschaftliche Mindmap", - children: [ - { name: "Forschung", children: [{ name: "Theorie" }, { name: "Experimente" }] }, - { name: "Technologie", children: [{ name: "Datenbanken" }, { name: "Cloud Computing" }] } - ] -}; +/** + * MindMap D3.js Modul + * Visualisiert die Mindmap mit D3.js + */ -// D3.js-Setup für die Darstellung der Mindmap -const width = 800; -const height = 600; -const margin = 50; +class MindMapVisualization { + constructor(containerSelector, options = {}) { + this.containerSelector = containerSelector; + this.container = d3.select(containerSelector); + this.width = options.width || this.container.node().clientWidth || 800; + this.height = options.height || 600; + this.nodeRadius = options.nodeRadius || 22; + this.selectedNodeRadius = options.selectedNodeRadius || 28; + this.linkDistance = options.linkDistance || 150; + this.chargeStrength = options.chargeStrength || -1000; + this.centerForce = options.centerForce || 0.15; + this.onNodeClick = options.onNodeClick || ((node) => console.log('Node clicked:', node)); -const svg = d3.select("#mindmap") - .append("svg") - .attr("width", width) - .attr("height", height); + this.nodes = []; + this.links = []; + this.simulation = null; + this.svg = null; + this.linkElements = null; + this.nodeElements = null; + this.textElements = null; + this.tooltipEnabled = options.tooltipEnabled !== undefined ? options.tooltipEnabled : true; + + this.mouseoverNode = null; + this.selectedNode = null; + + this.zoomFactor = 1; + this.tooltipDiv = null; + this.isLoading = true; + + // Erweiterte Farbpalette für Knotentypen + this.colorPalette = { + 'default': '#b38fff', + 'root': '#7e3ff2', + 'philosophy': '#58a9ff', + 'science': '#38b2ac', + 'technology': '#6366f1', + 'arts': '#ec4899', + 'ai': '#8b5cf6', + 'ethics': '#f59e0b', + 'math': '#06b6d4', + 'psychology': '#10b981', + 'biology': '#84cc16', + 'literature': '#f43f5e', + 'history': '#fb7185', + 'economics': '#fbbf24', + 'sociology': '#a78bfa', + 'design': '#f472b6', + 'languages': '#4ade80' + }; + + // 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 + setupDefaultNodes() { + // Basis-Mindmap mit Hauptthemen + const defaultNodes = [ + { id: "root", name: "Wissen", description: "Zentrale Wissensbasis", thought_count: 3 }, + { id: "philosophy", name: "Philosophie", description: "Philosophisches Denken", thought_count: 2 }, + { id: "science", name: "Wissenschaft", description: "Wissenschaftliche Erkenntnisse", thought_count: 5 }, + { id: "technology", name: "Technologie", description: "Technologische Entwicklungen", thought_count: 7 }, + { id: "arts", name: "Künste", description: "Künstlerische Ausdrucksformen", thought_count: 4 }, + { id: "ai", name: "Künstliche Intelligenz", description: "KI-Forschung und Anwendungen", thought_count: 6 }, + { id: "ethics", name: "Ethik", description: "Moralische Grundsätze", thought_count: 2 }, + { id: "math", name: "Mathematik", description: "Mathematische Konzepte", thought_count: 3 }, + { id: "psychology", name: "Psychologie", description: "Menschliches Verhalten und Kognition", thought_count: 4 }, + { id: "biology", name: "Biologie", description: "Lebenswissenschaften", thought_count: 3 }, + { id: "literature", name: "Literatur", description: "Literarische Werke und Analysen", thought_count: 2 } + ]; + + const defaultLinks = [ + { source: "root", target: "philosophy" }, + { source: "root", target: "science" }, + { source: "root", target: "technology" }, + { source: "root", target: "arts" }, + { source: "science", target: "math" }, + { source: "science", target: "biology" }, + { source: "technology", target: "ai" }, + { source: "philosophy", target: "ethics" }, + { source: "philosophy", target: "psychology" }, + { source: "arts", target: "literature" }, + { source: "ai", target: "ethics" }, + { source: "psychology", target: "biology" } + ]; + + // Als Fallback verwenden, falls die API fehlschlägt + this.defaultNodes = defaultNodes; + this.defaultLinks = defaultLinks; + } + + init() { + // SVG erstellen, wenn noch nicht vorhanden + if (!this.svg) { + // Container zuerst leeren und Loading-State ausblenden + const loadingOverlay = this.container.select('.mindmap-loading'); + if (loadingOverlay) { + loadingOverlay.style('opacity', 0.8); + } + + this.svg = this.container + .append('svg') + .attr('width', '100%') + .attr('height', this.height) + .attr('viewBox', `0 0 ${this.width} ${this.height}`) + .attr('class', 'mindmap-svg') + .call( + d3.zoom() + .scaleExtent([0.1, 5]) + .on('zoom', (event) => { + this.handleZoom(event.transform); + }) + ); + + // Hauptgruppe für alles, was zoom-transformierbar ist + this.g = this.svg.append('g'); + + // SVG-Definitionen für Filter und Effekte + const defs = this.g.append('defs'); + + // Verbesserte Glasmorphismus- und Glow-Effekte + + // Basis Glow-Effekt + D3Extensions.createGlowFilter(defs, 'glow-effect', '#b38fff', 8); + + // Spezifische Effekte für verschiedene Zustände + D3Extensions.createGlowFilter(defs, 'hover-glow', '#58a9ff', 6); + D3Extensions.createGlowFilter(defs, 'selected-glow', '#b38fff', 10); + + // Schatten für alle Knoten + D3Extensions.createShadowFilter(defs, 'shadow-effect'); + + // Glasmorphismus-Effekt für Knoten + D3Extensions.createGlassMorphismFilter(defs, 'glass-effect'); + + // Erweiterte Effekte + this.createAdvancedNodeEffects(defs); + + // Tooltip initialisieren mit verbessertem Glasmorphism-Stil + 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(24, 28, 45, 0.85)') + .style('color', '#ffffff') + .style('border', '1px solid rgba(179, 143, 255, 0.3)') + .style('border-radius', '16px') + .style('padding', '12px 16px') + .style('font-size', '14px') + .style('font-weight', '500') + .style('line-height', '1.5') + .style('max-width', '280px') + .style('box-shadow', '0 12px 30px rgba(0, 0, 0, 0.5), 0 0 15px rgba(179, 143, 255, 0.25)') + .style('backdrop-filter', 'blur(20px)') + .style('-webkit-backdrop-filter', 'blur(20px)') + .style('z-index', '1000'); + } else { + this.tooltipDiv = d3.select('body').select('.node-tooltip'); + } + + // Initialisierung abgeschlossen - Loading-Overlay ausblenden + setTimeout(() => { + if (loadingOverlay) { + loadingOverlay.transition() + .duration(500) + .style('opacity', 0) + .on('end', function() { + loadingOverlay.style('display', 'none'); + }); + } + }, 1000); + } + + // Force-Simulation initialisieren + this.simulation = d3.forceSimulation() + .force('link', d3.forceLink().id(d => d.id).distance(this.linkDistance)) + .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.5)); + + // Globale Mindmap-Instanz für externe Zugriffe setzen + window.mindmapInstance = this; + } + + // Erstellt erweiterte Effekte für die Knoten + createAdvancedNodeEffects(defs) { + // Verbesserte innere Leuchteffekte für Knoten + const innerGlow = defs.append('filter') + .attr('id', 'inner-glow') + .attr('x', '-50%') + .attr('y', '-50%') + .attr('width', '200%') + .attr('height', '200%'); + + // Farbiger innerer Glühen + innerGlow.append('feGaussianBlur') + .attr('in', 'SourceAlpha') + .attr('stdDeviation', 2) + .attr('result', 'blur'); + + innerGlow.append('feOffset') + .attr('in', 'blur') + .attr('dx', 0) + .attr('dy', 0) + .attr('result', 'offsetBlur'); + + innerGlow.append('feFlood') + .attr('flood-color', 'rgba(179, 143, 255, 0.8)') + .attr('result', 'glowColor'); + + innerGlow.append('feComposite') + .attr('in', 'glowColor') + .attr('in2', 'offsetBlur') + .attr('operator', 'in') + .attr('result', 'innerGlow'); + + // Verbinden der Filter + const innerGlowMerge = innerGlow.append('feMerge'); + innerGlowMerge.append('feMergeNode') + .attr('in', 'innerGlow'); + innerGlowMerge.append('feMergeNode') + .attr('in', 'SourceGraphic'); + + // 3D-Glaseffekt mit verbesserter Tiefe + D3Extensions.create3DGlassEffect(defs, '3d-glass'); + + // Pulseffekt für Hervorhebung + const pulseFilter = defs.append('filter') + .attr('id', 'pulse-effect') + .attr('x', '-50%') + .attr('y', '-50%') + .attr('width', '200%') + .attr('height', '200%'); + + // Animation definieren + const pulseAnimation = pulseFilter.append('feComponentTransfer') + .append('feFuncA') + .attr('type', 'linear') + .attr('slope', '1.5'); + } + + // Behandelt die Zoom-Transformation für die SVG + handleZoom(transform) { + this.g.attr('transform', transform); + this.zoomFactor = transform.k; + + // Knotengröße an Zoom anpassen + if (this.nodeElements) { + this.nodeElements.selectAll('circle') + .attr('r', d => { + return d === this.selectedNode + ? this.selectedNodeRadius / Math.sqrt(transform.k) + : this.nodeRadius / Math.sqrt(transform.k); + }); + + this.textElements + .style('font-size', `${16 / Math.sqrt(transform.k)}px`); + } + } + + // Lädt die Mindmap-Daten + async loadData() { + try { + // Zeige Lade-Animation + this.showLoading(); + + // Demo-Logik: Verwende direkt die Standardknoten + this.nodes = this.defaultNodes; + this.links = this.defaultLinks; + + // Simuliere einen API-Aufruf (in einer echten Anwendung würde hier ein Fetch stehen) + await new Promise(resolve => setTimeout(resolve, 1000)); + + // Visualisierung aktualisieren + this.updateVisualization(); + + // Lade-Animation ausblenden + this.hideLoading(); + + // Zufällige Knoten pulsieren lassen + this.pulseRandomNodes(); + } catch (error) { + console.error('Fehler beim Laden der Mindmap-Daten:', error); + + // Bei einem Fehler die Fallback-Daten verwenden + this.nodes = this.defaultNodes; + this.links = this.defaultLinks; + + // Visualisierung auch im Fehlerfall aktualisieren + this.updateVisualization(); + this.hideLoading(); + } + } + + // Startet ein zufälliges Pulsen von Knoten für visuelle Aufmerksamkeit + pulseRandomNodes() { + // Zufälligen Knoten auswählen + const randomNode = () => { + const randomIndex = Math.floor(Math.random() * this.nodes.length); + return this.nodes[randomIndex]; + }; + + // Initiales Pulsen starten + const initialPulse = () => { + const node = randomNode(); + this.pulseNode(node); + + // Nächstes Pulsen in 3-7 Sekunden + setTimeout(() => { + const nextNode = randomNode(); + this.pulseNode(nextNode); + + // Regelmäßig wiederholen + setInterval(() => { + const pulseNode = randomNode(); + this.pulseNode(pulseNode); + }, 5000 + Math.random() * 5000); + }, 3000 + Math.random() * 4000); + }; + + // Verzögertes Starten nach vollständigem Laden + setTimeout(initialPulse, 1000); + } + + // Lässt einen Knoten pulsieren für visuelle Hervorhebung + pulseNode(node) { + if (!this.nodeElements) return; + + const nodeElement = this.nodeElements.filter(d => d.id === node.id); + + if (nodeElement.size() > 0) { + const circle = nodeElement.select('circle'); + + // Speichern des ursprünglichen Radius + const originalRadius = circle.attr('r'); + + // Animiertes Pulsieren + circle.transition() + .duration(600) + .attr('r', originalRadius * 1.3) + .attr('filter', 'url(#pulse-effect)') + .transition() + .duration(600) + .attr('r', originalRadius) + .attr('filter', 'url(#glass-effect)'); + } + } + + // Zeigt den Ladebildschirm an + showLoading() { + const loadingOverlay = this.container.select('.mindmap-loading'); + + if (loadingOverlay && !loadingOverlay.empty()) { + loadingOverlay + .style('display', 'flex') + .style('opacity', 1); + + // Ladebalken-Animation + const progressBar = loadingOverlay.select('.loading-progress'); + if (!progressBar.empty()) { + let progress = 0; + const progressInterval = setInterval(() => { + progress += Math.random() * 15; + if (progress > 90) { + progress = 90 + Math.random() * 5; + clearInterval(progressInterval); + } + updateProgress(progress); + }, 200); + + function updateProgress(progress) { + progressBar.style('width', `${Math.min(progress, 95)}%`); + } + } + } + } + + // Blendet den Ladebildschirm aus + hideLoading() { + const loadingOverlay = this.container.select('.mindmap-loading'); + + if (loadingOverlay && !loadingOverlay.empty()) { + // Lade-Fortschritt auf 100% setzen + const progressBar = loadingOverlay.select('.loading-progress'); + if (!progressBar.empty()) { + progressBar.transition() + .duration(300) + .style('width', '100%'); + } + + // Overlay ausblenden + setTimeout(() => { + loadingOverlay.transition() + .duration(500) + .style('opacity', 0) + .on('end', function() { + loadingOverlay.style('display', 'none'); + }); + }, 400); + } + } + + // Verarbeitet hierarchische Daten in flache Knoten und Links + processHierarchicalData(hierarchicalNodes, parentId = null) { + let nodes = []; + let links = []; + + for (const node of hierarchicalNodes) { + nodes.push({ + id: node.id, + name: node.name, + description: node.description || '', + thought_count: node.thought_count || 0 + }); + + if (parentId) { + links.push({ + source: parentId, + target: node.id + }); + } + + if (node.children && node.children.length > 0) { + const { nodes: childNodes, links: childLinks } = this.processHierarchicalData(node.children, node.id); + nodes = [...nodes, ...childNodes]; + links = [...links, ...childLinks]; + } + } + + return { nodes, links }; + } + + // Generiert eine konsistente Farbe basierend auf dem Knotennamen + generateColorFromString(str) { + const colors = [ + '#b38fff', '#58a9ff', '#14b8a6', '#f472b6', '#84cc16', + '#f97316', '#4c1d95', '#2dd4bf', '#ec4899', '#eab308' + ]; + + let hash = 0; + for (let i = 0; i < str.length; i++) { + hash = str.charCodeAt(i) + ((hash << 5) - hash); + } + + return colors[Math.abs(hash) % colors.length]; + } + + /** + * Aktualisiert die Visualisierung basierend auf den aktuellen Daten + */ + updateVisualization() { + if (!this.g || !this.nodes.length) return; + + // Daten für Simulation vorbereiten + // Kopieren der Knoten und Links, um Referenzen zu erhalten + const nodes = this.nodes.map(d => Object.assign({}, d)); + const links = this.links.map(d => Object.assign({}, d)); + + // Links erstellen oder aktualisieren + this.linkElements = this.g.selectAll('.link') + .data(links, d => `${d.source.id || d.source}-${d.target.id || d.target}`); + + // Links entfernen, die nicht mehr existieren + this.linkElements.exit().remove(); + + // Neue Links erstellen + const linkEnter = this.linkElements + .enter().append('path') + .attr('class', 'link') + .attr('stroke-width', 2) + .attr('stroke', 'rgba(255, 255, 255, 0.3)') + .attr('fill', 'none') + .attr('marker-end', 'url(#arrowhead)'); + + // Alle Links aktualisieren + this.linkElements = linkEnter.merge(this.linkElements); + + // Pfeilspitzen für die Links definieren + if (!this.g.select('#arrowhead').size()) { + this.g.append('defs').append('marker') + .attr('id', 'arrowhead') + .attr('viewBox', '0 -5 10 10') + .attr('refX', 28) // Abstand vom Ende des Links zum Knoten + .attr('refY', 0) + .attr('orient', 'auto') + .attr('markerWidth', 6) + .attr('markerHeight', 6) + .attr('xoverflow', 'visible') + .append('path') + .attr('d', 'M 0,-3 L 8,0 L 0,3') + .attr('fill', 'rgba(255, 255, 255, 0.6)'); + } + + // Knoten erstellen oder aktualisieren + this.nodeElements = this.g.selectAll('.node') + .data(nodes, d => d.id); + + // Knoten entfernen, die nicht mehr existieren + this.nodeElements.exit().remove(); + + // Container für neue Knoten erstellen + const nodeEnter = this.nodeElements + .enter().append('g') + .attr('class', 'node') + .call(d3.drag() + .on('start', (event, d) => this.dragStarted(event, d)) + .on('drag', (event, d) => this.dragged(event, d)) + .on('end', (event, d) => this.dragEnded(event, d)) + ) + .on('mouseover', (event, d) => this.nodeMouseover(event, d)) + .on('mouseout', (event, d) => this.nodeMouseout(event, d)) + .on('click', (event, d) => this.nodeClicked(event, d)); + + // Kreisformen für die Knoten hinzufügen + nodeEnter.append('circle') + .attr('r', d => this.nodeRadius) + .attr('fill', d => this.getNodeColor(d)) + .attr('stroke', 'rgba(255, 255, 255, 0.12)') + .attr('stroke-width', 2) + .style('filter', 'url(#glass-effect)'); + + // Label für die Knoten hinzufügen + nodeEnter.append('text') + .attr('class', 'node-label') + .attr('dy', 4) + .attr('text-anchor', 'middle') + .text(d => this.truncateNodeLabel(d.name)) + .style('font-size', d => D3Extensions.getAdaptiveFontSize(d.name, 16, 10) + 'px'); + + // Alle Knoten aktualisieren + this.nodeElements = nodeEnter.merge(this.nodeElements); + + // Simulation mit den neuen Daten aktualisieren + this.simulation + .nodes(nodes) + .force('link', d3.forceLink(links).id(d => d.id).distance(this.linkDistance)); + + // Simulation-Tick-Funktion setzen + this.simulation.on('tick', () => this.ticked()); + + // Simulation neustarten + this.simulation.alpha(1).restart(); + + // Nach kurzer Verzögerung die Knoten mit zusätzlichen Effekten versehen + setTimeout(() => { + this.nodeElements.selectAll('circle') + .transition() + .duration(500) + .attr('r', d => { + // Größe abhängig von der Anzahl der Gedanken + const baseRadius = this.nodeRadius; + const bonus = d.thought_count ? Math.min(d.thought_count / 3, 6) : 0; + return baseRadius + bonus; + }); + }, 300); + } + + // Lange Knotenbeschriftungen abkürzen + truncateNodeLabel(label) { + if (!label) return ''; + + const maxLength = 18; // Maximale Zeichenlänge + + if (label.length <= maxLength) { + return label; + } else { + return label.substring(0, maxLength - 3) + '...'; + } + } + + // Farbe basierend auf Knotentyp erhalten + getNodeColor(node) { + // Verwende die ID als Typ, falls vorhanden + const nodeType = node.id.toLowerCase(); + return this.colorPalette[nodeType] || this.colorPalette.default; + } + + // Aktualisiert die Positionen in jedem Simulationsschritt + ticked() { + if (!this.linkElements || !this.nodeElements) return; + + // Aktualisierung der Linkpositionen mit gebogenem Pfad + this.linkElements + .attr('d', d => { + const dx = d.target.x - d.source.x; + const dy = d.target.y - d.source.y; + const dr = Math.sqrt(dx * dx + dy * dy) * 1.5; // Kurvenstärke + return `M${d.source.x},${d.source.y}A${dr},${dr} 0 0,1 ${d.target.x},${d.target.y}`; + }); + + // Aktualisierung der Knotenpositionen + this.nodeElements + .attr('transform', d => `translate(${d.x},${d.y})`); + } + + // D3.js Drag-Funktionen + dragStarted(event, d) { + if (!event.active) this.simulation.alphaTarget(0.3).restart(); + d.fx = d.x; + d.fy = d.y; + } + + dragged(event, d) { + d.fx = event.x; + d.fy = event.y; + } + + dragEnded(event, d) { + if (!event.active) this.simulation.alphaTarget(0); + d.fx = null; + d.fy = null; + } + + // Hover-Effekte für Knoten + nodeMouseover(event, d) { + if (this.tooltipEnabled) { + // Tooltip-Inhalt erstellen + const tooltipContent = ` +
${d.name}
+
${d.description || 'Keine Beschreibung verfügbar'}
+ ${d.thought_count > 0 ? `
${d.thought_count} Gedanken verknüpft
` : ''} + `; + + this.tooltipDiv.html(tooltipContent) + .style('opacity', 0.95); + + // Tooltip positionieren (oberhalb des Nodes) + const nodeRect = event.target.getBoundingClientRect(); + const tooltipWidth = 250; + const tooltipHeight = 100; // Ungefähre Höhe des Tooltips + + const leftPos = nodeRect.left + (nodeRect.width / 2) - (tooltipWidth / 2); + const topPos = nodeRect.top - tooltipHeight - 10; // 10px Abstand + + this.tooltipDiv + .style('left', `${leftPos}px`) + .style('top', `${topPos}px`) + .style('width', `${tooltipWidth}px`); + } + + // Speichern des aktuellen Hover-Nodes + this.mouseoverNode = d; + + // Highlights für verbundene Nodes und Links hinzufügen + if (this.g) { + // Verbundene Nodes identifizieren + const connectedNodes = this.getConnectedNodes(d); + const connectedNodeIds = connectedNodes.map(node => node.id); + + // Alle Nodes etwas transparenter machen + this.g.selectAll('.node') + .transition() + .duration(200) + .style('opacity', node => { + if (node.id === d.id || connectedNodeIds.includes(node.id)) { + return 1.0; + } else { + return 0.5; + } + }); + + // Den Hover-Node hervorheben mit größerem Radius + this.g.selectAll('.node') + .filter(node => node.id === d.id) + .select('circle') + .transition() + .duration(200) + .attr('r', this.nodeRadius * 1.2) + .style('filter', 'url(#hover-glow)') + .style('stroke', 'rgba(255, 255, 255, 0.25)'); + + // Verbundene Links hervorheben + this.g.selectAll('.link') + .transition() + .duration(200) + .style('opacity', link => { + const sourceId = link.source.id || link.source; + const targetId = link.target.id || link.target; + + if (sourceId === d.id || targetId === d.id) { + return 0.9; + } else { + return 0.3; + } + }) + .style('stroke-width', link => { + const sourceId = link.source.id || link.source; + const targetId = link.target.id || link.target; + + if (sourceId === d.id || targetId === d.id) { + return 3; + } else { + return 2; + } + }) + .style('stroke', link => { + const sourceId = link.source.id || link.source; + const targetId = link.target.id || link.target; + + if (sourceId === d.id || targetId === d.id) { + return 'rgba(179, 143, 255, 0.7)'; + } else { + return 'rgba(255, 255, 255, 0.3)'; + } + }); + } + } + + nodeMouseout(event, d) { + if (this.tooltipEnabled) { + this.tooltipDiv.transition() + .duration(200) + .style('opacity', 0); + } + + this.mouseoverNode = null; + + // Highlights zurücksetzen, falls kein Node ausgewählt ist + if (!this.selectedNode && this.g) { + // Alle Nodes wieder auf volle Deckkraft setzen + this.g.selectAll('.node') + .transition() + .duration(200) + .style('opacity', 1.0); + + // Hover-Node-Radius zurücksetzen + this.g.selectAll('.node') + .filter(node => node.id === d.id) + .select('circle') + .transition() + .duration(200) + .attr('r', this.nodeRadius) + .style('filter', 'none') + .style('stroke', 'rgba(255, 255, 255, 0.12)'); + + // Links zurücksetzen + this.g.selectAll('.link') + .transition() + .duration(200) + .style('opacity', 0.7) + .style('stroke-width', 2) + .style('stroke', 'rgba(255, 255, 255, 0.3)'); + } + // Falls ein Node ausgewählt ist, den Highlight-Status für diesen beibehalten + else if (this.selectedNode && this.g) { + const connectedNodes = this.getConnectedNodes(this.selectedNode); + const connectedNodeIds = connectedNodes.map(node => node.id); + + // Alle Nodes auf den richtigen Highlight-Status setzen + this.g.selectAll('.node') + .transition() + .duration(200) + .style('opacity', node => { + if (node.id === this.selectedNode.id || connectedNodeIds.includes(node.id)) { + return 1.0; + } else { + return 0.5; + } + }); + + // Hover-Node zurücksetzen, wenn er nicht der ausgewählte ist + if (d.id !== this.selectedNode.id) { + this.g.selectAll('.node') + .filter(node => node.id === d.id) + .select('circle') + .transition() + .duration(200) + .attr('r', this.nodeRadius) + .style('filter', 'none') + .style('stroke', 'rgba(255, 255, 255, 0.12)'); + } + + // Links auf den richtigen Highlight-Status setzen + this.g.selectAll('.link') + .transition() + .duration(200) + .style('opacity', link => { + const sourceId = link.source.id || link.source; + const targetId = link.target.id || link.target; + + if (sourceId === this.selectedNode.id || targetId === this.selectedNode.id) { + return 0.9; + } else { + return 0.3; + } + }) + .style('stroke-width', link => { + const sourceId = link.source.id || link.source; + const targetId = link.target.id || link.target; + + if (sourceId === this.selectedNode.id || targetId === this.selectedNode.id) { + return 3; + } else { + return 2; + } + }) + .style('stroke', link => { + const sourceId = link.source.id || link.source; + const targetId = link.target.id || link.target; + + if (sourceId === this.selectedNode.id || targetId === this.selectedNode.id) { + return 'rgba(179, 143, 255, 0.7)'; + } else { + return 'rgba(255, 255, 255, 0.3)'; + } + }); + } + } + + // Findet alle verbundenen Knoten zu einem gegebenen Knoten + getConnectedNodes(node) { + if (!this.links || !this.nodes) return []; + + return this.nodes.filter(n => + this.links.some(link => + (link.source.id === node.id && link.target.id === n.id) || + (link.target.id === node.id && link.source.id === n.id) + ) + ); + } + + // Prüft, ob zwei Knoten verbunden sind + isConnected(a, b) { + return this.links.some(link => + (link.source.id === a.id && link.target.id === b.id) || + (link.target.id === a.id && link.source.id === b.id) + ); + } + + // Klick-Handler für Knoten + nodeClicked(event, d) { + event.preventDefault(); + event.stopPropagation(); + + // Selection-Handling: Knoten auswählen/abwählen + if (this.selectedNode === d) { + // Wenn der gleiche Knoten geklickt wird, Selektion aufheben + this.selectedNode = null; + this.nodeElements.classed('selected', false); + + this.nodeElements + .select('circle:not(.node-background):not(.thought-indicator)') + .transition() + .duration(300) + .attr('r', this.nodeRadius) + .style('filter', 'url(#glass-with-reflection)') + .attr('stroke-width', 2); + + // Gedankenbereich ausblenden, wenn vorhanden + const thoughtContainer = document.getElementById('thought-container'); + if (thoughtContainer) { + // Sanfte Ausblendanimation + thoughtContainer.style.transition = 'all 0.3s ease-out'; + thoughtContainer.style.opacity = '0'; + thoughtContainer.style.transform = 'translateY(10px)'; + + setTimeout(() => { + // Gedankenbereich komplett ausblenden + thoughtContainer.style.display = 'none'; + + // "Empty state" anzeigen oder andere UI-Anpassungen vornehmen + const emptyStateEl = document.getElementById('mindmap-empty-state'); + if (emptyStateEl) { + emptyStateEl.style.display = 'flex'; + emptyStateEl.style.opacity = '0'; + setTimeout(() => { + emptyStateEl.style.transition = 'all 0.5s ease'; + emptyStateEl.style.opacity = '1'; + }, 50); + } + }, 300); + } + + // Alle Kanten zurücksetzen + this.linkElements + .classed('highlighted', false) + .transition() + .duration(300) + .style('stroke', 'rgba(255, 255, 255, 0.3)') + .style('stroke-width', 2) + .style('opacity', 0.7); + + // Interface-Callback für Knoten-Abwahl + if (typeof window.onNodeDeselected === 'function') { + window.onNodeDeselected(); + } + + return; + } + + // Bisher ausgewählten Knoten zurücksetzen + if (this.selectedNode) { + this.nodeElements + .filter(n => n === this.selectedNode) + .classed('selected', false) + .select('circle:not(.node-background):not(.thought-indicator)') + .transition() + .duration(300) + .attr('r', this.nodeRadius) + .style('filter', 'url(#glass-with-reflection)') + .attr('stroke-width', 2); + } + + // Neuen Knoten auswählen + this.selectedNode = d; + + // Selected-Klasse für den Knoten setzen + this.nodeElements + .classed('selected', n => n === d); + + // Visuelles Feedback für Auswahl + this.nodeElements + .filter(n => n === d) + .select('circle:not(.node-background):not(.thought-indicator)') + .transition() + .duration(300) + .attr('r', this.selectedNodeRadius) + .style('filter', 'url(#selected-glow)') + .attr('stroke-width', 3) + .attr('stroke', 'rgba(179, 143, 255, 0.6)'); + + // Verbundene Kanten hervorheben + const connectedLinks = this.links.filter(link => + link.source === d || link.source.id === d.id || + link.target === d || link.target.id === d.id + ); + + // Alle Kanten zurücksetzen und dann verbundene hervorheben + this.linkElements + .classed('highlighted', false) + .transition() + .duration(300) + .style('stroke', 'rgba(255, 255, 255, 0.3)') + .style('stroke-width', 2) + .style('opacity', 0.7); + + this.linkElements + .filter(link => + connectedLinks.some(l => + (l.source === link.source || l.source.id === link.source.id) && + (l.target === link.target || l.target.id === link.target.id) + ) + ) + .classed('highlighted', true) + .transition() + .duration(300) + .style('stroke', 'rgba(179, 143, 255, 0.7)') + .style('stroke-width', 3) + .style('opacity', 0.9); + + // Knoten zentrieren + this.centerNodeInView(d); + + // Gedanken laden + this.loadThoughtsForNode(d); + + // Callback für UI-Integration + if (typeof this.onNodeClick === 'function') { + this.onNodeClick(d); + } + + // Interface-Callback für externe Verwendung + if (typeof window.onNodeSelected === 'function') { + window.onNodeSelected(d); + } + } + + // Lädt die Gedanken für einen Knoten und zeigt sie an + loadThoughtsForNode(node) { + // UI-Element für Gedanken finden + const thoughtContainer = document.getElementById('thought-container'); + const loadingIndicator = document.getElementById('thoughts-loading'); + const thoughtsList = document.getElementById('thoughts-list'); + const thoughtsTitle = document.getElementById('thoughts-title'); + const emptyStateEl = document.getElementById('mindmap-empty-state'); + + if (!thoughtContainer || !thoughtsList) { + console.error('Gedanken-Container nicht gefunden'); + return; + } + + // "Empty state" ausblenden + if (emptyStateEl) { + emptyStateEl.style.transition = 'all 0.3s ease'; + emptyStateEl.style.opacity = '0'; + setTimeout(() => { + emptyStateEl.style.display = 'none'; + }, 300); + } + + // Container anzeigen mit Animation + thoughtContainer.style.display = 'block'; + setTimeout(() => { + thoughtContainer.style.transition = 'all 0.4s ease'; + thoughtContainer.style.opacity = '1'; + thoughtContainer.style.transform = 'translateY(0)'; + }, 50); + + // Titel setzen + if (thoughtsTitle) { + thoughtsTitle.textContent = `Gedanken zu "${node.name}"`; + } + + // Ladeanimation anzeigen + if (loadingIndicator) { + loadingIndicator.style.display = 'flex'; + } + + // Bisherige Gedanken leeren + if (thoughtsList) { + thoughtsList.innerHTML = ''; + } + + // Verzögerung für Animation + setTimeout(() => { + // API-Aufruf simulieren (später durch echten Aufruf ersetzen) + this.fetchThoughtsForNode(node.id) + .then(thoughts => { + // Ladeanimation ausblenden + if (loadingIndicator) { + loadingIndicator.style.display = 'none'; + } + + // Gedanken anzeigen oder "leer"-Zustand + if (thoughts && thoughts.length > 0) { + this.renderThoughts(thoughts, thoughtsList); + } else { + this.renderEmptyThoughts(thoughtsList, node); + } + }) + .catch(error => { + console.error('Fehler beim Laden der Gedanken:', error); + if (loadingIndicator) { + loadingIndicator.style.display = 'none'; + } + this.renderErrorState(thoughtsList); + }); + }, 600); // Verzögerung für bessere UX + } + + // Zentriert einen Knoten in der Ansicht + centerNodeInView(node) { + // Sanfter Übergang zur Knotenzentrierüng + const transform = d3.zoomTransform(this.svg.node()); + const scale = transform.k; + + const x = -node.x * scale + this.width / 2; + const y = -node.y * scale + this.height / 2; + + this.svg.transition() + .duration(750) + .call( + d3.zoom().transform, + d3.zoomIdentity.translate(x, y).scale(scale) + ); + } + + // Fehlermeldung anzeigen + showError(message) { + const errorBanner = d3.select('body').selectAll('.error-banner').data([0]); + + const errorEnter = errorBanner.enter() + .append('div') + .attr('class', 'error-banner') + .style('position', 'fixed') + .style('bottom', '-100px') + .style('left', '50%') + .style('transform', 'translateX(-50%)') + .style('background', 'rgba(220, 38, 38, 0.9)') + .style('color', 'white') + .style('padding', '12px 20px') + .style('border-radius', '8px') + .style('z-index', '1000') + .style('box-shadow', '0 10px 25px rgba(0, 0, 0, 0.3)') + .style('font-weight', '500') + .style('max-width', '90%') + .style('text-align', 'center'); + + const banner = errorEnter.merge(errorBanner); + banner.html(` ${message}`) + .transition() + .duration(500) + .style('bottom', '20px') + .transition() + .delay(5000) + .duration(500) + .style('bottom', '-100px'); + } -const root = d3.hierarchy(data); -const treeLayout = d3.tree().size([width - margin, height - margin]); -treeLayout(root); + // Fokussieren auf einen bestimmten Knoten per ID + focusNode(nodeId) { + const targetNode = this.nodes.find(n => n.id === nodeId); + if (!targetNode) return; + + // Ausgewählten Zustand zurücksetzen + this.selectedNode = null; + + // Node-Klick simulieren + this.nodeClicked(null, targetNode); + + // Fokussieren mit einer Animation + if (targetNode.x && targetNode.y) { + const transform = d3.zoomIdentity + .translate(this.width / 2 - targetNode.x * 1.2, this.height / 2 - targetNode.y * 1.2) + .scale(1.2); + + this.svg.transition() + .duration(750) + .call( + d3.zoom().transform, + transform + ); + } + } + + // Filtert Knoten nach Suchbegriff + filterBySearchTerm(searchTerm) { + if (!searchTerm || searchTerm.trim() === '') { + // Alle Knoten anzeigen, wenn kein Suchbegriff + this.nodeElements + .style('display', 'block') + .selectAll('circle') + .style('opacity', 1); + + this.textElements + .style('opacity', 1); + + this.linkElements + .style('display', 'block') + .style('stroke-opacity', 0.5); + + return; + } + + searchTerm = searchTerm.toLowerCase().trim(); + + // Knoten finden, die dem Suchbegriff entsprechen + const matchingNodes = this.nodes.filter(node => + node.name.toLowerCase().includes(searchTerm) || + (node.description && node.description.toLowerCase().includes(searchTerm)) + ); + + const matchingNodeIds = matchingNodes.map(n => n.id); + + // Nur passende Knoten und ihre Verbindungen anzeigen + this.nodeElements + .style('display', d => matchingNodeIds.includes(d.id) ? 'block' : 'none') + .selectAll('circle') + .style('opacity', 1); + + this.textElements + .style('opacity', d => matchingNodeIds.includes(d.id) ? 1 : 0.2); + + this.linkElements + .style('display', link => + matchingNodeIds.includes(link.source.id) && matchingNodeIds.includes(link.target.id) ? 'block' : 'none') + .style('stroke-opacity', 0.7); + + // Wenn nur ein Knoten gefunden wurde, darauf fokussieren + if (matchingNodes.length === 1) { + this.focusNode(matchingNodes[0].id); + } + + // Wenn mehr als ein Knoten gefunden wurde, Simulation mit reduzierter Stärke neu starten + if (matchingNodes.length > 1) { + this.simulation.alpha(0.3).restart(); + } + } +} -const links = svg.selectAll(".link") - .data(root.links()) - .enter() - .append("line") - .attr("class", "link") - .attr("x1", d => d.source.x + margin) - .attr("y1", d => d.source.y + margin) - .attr("x2", d => d.target.x + margin) - .attr("y2", d => d.target.y + margin) - .attr("stroke", "#2c3e50"); +// D3-Erweiterungen für spezielle Effekte +class D3Extensions { + static createGlowFilter(defs, id, color = '#b38fff', strength = 5) { + const filter = defs.append('filter') + .attr('id', id) + .attr('height', '300%') + .attr('width', '300%') + .attr('x', '-100%') + .attr('y', '-100%'); + + // Farbe und Sättigung + const colorMatrix = filter.append('feColorMatrix') + .attr('type', 'matrix') + .attr('values', ` + 1 0 0 0 ${color === '#b38fff' ? 0.7 : 0.35} + 0 1 0 0 ${color === '#58a9ff' ? 0.7 : 0.35} + 0 0 1 0 ${color === '#58a9ff' ? 0.7 : 0.55} + 0 0 0 1 0 + `) + .attr('result', 'colored'); + + // Weichzeichner für Glühen + const blur = filter.append('feGaussianBlur') + .attr('in', 'colored') + .attr('stdDeviation', strength) + .attr('result', 'blur'); + + // Kombination von Original und Glühen + const merge = filter.append('feMerge'); + + merge.append('feMergeNode') + .attr('in', 'blur'); + + merge.append('feMergeNode') + .attr('in', 'SourceGraphic'); + + return filter; + } + + static createShadowFilter(defs, id) { + const filter = defs.append('filter') + .attr('id', id) + .attr('height', '200%') + .attr('width', '200%') + .attr('x', '-50%') + .attr('y', '-50%'); + + // Offset der Lichtquelle + const offset = filter.append('feOffset') + .attr('in', 'SourceAlpha') + .attr('dx', 3) + .attr('dy', 4) + .attr('result', 'offset'); + + // Weichzeichnung für Schatten + const blur = filter.append('feGaussianBlur') + .attr('in', 'offset') + .attr('stdDeviation', 5) + .attr('result', 'blur'); + + // Schatten-Opazität + const opacity = filter.append('feComponentTransfer'); + + opacity.append('feFuncA') + .attr('type', 'linear') + .attr('slope', 0.3); + + // Zusammenführen + const merge = filter.append('feMerge'); + + merge.append('feMergeNode'); + merge.append('feMergeNode') + .attr('in', 'SourceGraphic'); + + return filter; + } + + static createGlassMorphismFilter(defs, id) { + const filter = defs.append('filter') + .attr('id', id) + .attr('width', '300%') + .attr('height', '300%') + .attr('x', '-100%') + .attr('y', '-100%'); + + // Basis-Hintergrundfarbe + const bgColor = filter.append('feFlood') + .attr('flood-color', 'rgba(24, 28, 45, 0.75)') + .attr('result', 'bgColor'); + + // Weichzeichnung des Originalelements + const blur = filter.append('feGaussianBlur') + .attr('in', 'SourceGraphic') + .attr('stdDeviation', '3') + .attr('result', 'blur'); + + // Komposition des Glaseffekts mit Original + const composite1 = filter.append('feComposite') + .attr('in', 'bgColor') + .attr('in2', 'blur') + .attr('operator', 'in') + .attr('result', 'glass'); + + // Leichter Farbakzent + const colorMatrix = filter.append('feColorMatrix') + .attr('in', 'glass') + .attr('type', 'matrix') + .attr('values', '1 0 0 0 0.1 0 1 0 0 0.1 0 0 1 0 0.3 0 0 0 1 0') + .attr('result', 'coloredGlass'); + + // Leichte Transparenz an den Rändern + const specLight = filter.append('feSpecularLighting') + .attr('in', 'blur') + .attr('surfaceScale', '3') + .attr('specularConstant', '0.75') + .attr('specularExponent', '20') + .attr('lighting-color', '#ffffff') + .attr('result', 'specLight'); + + specLight.append('fePointLight') + .attr('x', '-20') + .attr('y', '-30') + .attr('z', '120'); + + // Lichtkombination + const composite2 = filter.append('feComposite') + .attr('in', 'specLight') + .attr('in2', 'coloredGlass') + .attr('operator', 'in') + .attr('result', 'lightedGlass'); + + // Alle Effekte kombinieren + const merge = filter.append('feMerge') + .attr('result', 'glassMerge'); + + merge.append('feMergeNode') + .attr('in', 'coloredGlass'); + + merge.append('feMergeNode') + .attr('in', 'lightedGlass'); + + merge.append('feMergeNode') + .attr('in', 'SourceGraphic'); + + return filter; + } + + // Erstellt einen erweiterten 3D-Glaseffekt mit Lichtreflexion + static create3DGlassEffect(defs, id) { + const filter = defs.append('filter') + .attr('id', id) + .attr('width', '300%') + .attr('height', '300%') + .attr('x', '-100%') + .attr('y', '-100%'); + + // Hintergrund-Färbung mit Transparenz + const bgColor = filter.append('feFlood') + .attr('flood-color', 'rgba(24, 28, 45, 0.7)') + .attr('result', 'bgColor'); + + // Alpha-Kanal modifizieren + const composite1 = filter.append('feComposite') + .attr('in', 'bgColor') + .attr('in2', 'SourceAlpha') + .attr('operator', 'in') + .attr('result', 'shape'); + + // Leichte Unschärfe hinzufügen + const blur = filter.append('feGaussianBlur') + .attr('in', 'shape') + .attr('stdDeviation', '2') + .attr('result', 'blurredShape'); + + // Lichtquelle für 3D-Effekt + const specLight = filter.append('feSpecularLighting') + .attr('in', 'blurredShape') + .attr('surfaceScale', '5') + .attr('specularConstant', '1') + .attr('specularExponent', '20') + .attr('lighting-color', '#ffffff') + .attr('result', 'specLight'); + + specLight.append('fePointLight') + .attr('x', '50') + .attr('y', '-50') + .attr('z', '200'); + + // Farbmatrix für Lichttönung + const colorMatrix = filter.append('feColorMatrix') + .attr('in', 'specLight') + .attr('type', 'matrix') + .attr('values', '1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0') + .attr('result', 'coloredLight'); + + // Alle Effekte kombinieren + const merge = filter.append('feMerge'); + + merge.append('feMergeNode') + .attr('in', 'blurredShape'); + + merge.append('feMergeNode') + .attr('in', 'coloredLight'); + + merge.append('feMergeNode') + .attr('in', 'SourceGraphic'); + + return filter; + } +} -const nodes = svg.selectAll(".node") - .data(root.descendants()) - .enter() - .append("g") - .attr("class", "node") - .attr("transform", d => `translate(${d.x + margin},${d.y + margin})`); - -nodes.append("circle") - .attr("r", 20) - .attr("fill", "#3498db"); - -nodes.append("text") - .attr("dx", 25) - .attr("dy", 5) - .text(d => d.data.name); \ No newline at end of file +// Globales Objekt für Zugriff außerhalb des Moduls +window.MindMapVisualization = MindMapVisualization; \ No newline at end of file diff --git a/website/static/style.css b/website/static/style.css deleted file mode 100644 index 6dc98ff..0000000 --- a/website/static/style.css +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/website/templates/admin.html b/website/templates/admin.html index 3726b05..d22aaaf 100644 --- a/website/templates/admin.html +++ b/website/templates/admin.html @@ -1,45 +1,83 @@ {% extends "base.html" %} -{% block title %}Admin | Wissenschaftliche Mindmap{% endblock %} +{% block title %}Admin-Bereich{% endblock %} {% block content %}
-
-

Admin Bereich

-

Verwalte Benutzer, Gedanken und die Mindmap-Struktur.

-
+

Admin-Bereich

-
- -
-
-

Benutzer

- {{ users|length }} + +
+
+ + + + +
+ + +
+
+

Benutzerverwaltung

+
-
- - - - - - - +
+
IDBenutzernameEmailRolle
+ + + + + + + + {% for user in users %} - - - - - + + + + + + {% endfor %} @@ -47,63 +85,224 @@ - -
-
-

Mindmap Struktur

- {{ nodes|length }} -
- -
-
- {% for node in nodes %} -
-
- {{ node.name }} - ID: {{ node.id }} -
- {% if node.parent %} -

Eltern: {{ node.parent.name }}

- {% else %} -

Hauptknoten

- {% endif %} -
- {% endfor %} -
-
- -
-
+ +
+
IDBenutzernameE-MailAdminGedankenAktionen
{{ user.id }}{{ user.username }}{{ user.email }} +
{{ user.id }}{{ user.username }}{{ user.email }} {% if user.is_admin %} - Admin + Admin {% else %} - Benutzer + User {% endif %} {{ user.thoughts|length }} + + +
+ + + + + + + + + + + {% for node in nodes %} + + + + + + + + {% endfor %} + +
IDNameElternknotenGedankenAktionen
{{ node.id }}{{ node.name }} + {% if node.parent %} + {{ node.parent.name }} + {% else %} + Wurzelknoten + {% endif %} + {{ node.thoughts|length }} + + +
+
- -
-
-

Gedanken

- {{ thoughts|length }} -
- -
-
- {% for thought in thoughts %} -
-
- {{ thought.branch }} - {{ thought.timestamp.strftime('%d.%m.%Y') }} -
-

{{ thought.content }}

-
- Von: {{ thought.author.username }} - {{ thought.comments|length }} Kommentar(e) + +
+
+

Gedanken-Verwaltung

+
+
+ +
+
- {% endfor %} + +
+
+ +
+ + + + + + + + + + + + + {% for thought in thoughts %} + + + + + + + + + {% endfor %} + +
IDTitelAutorDatumBewertungAktionen
{{ thought.id }}{{ thought.title }}{{ thought.author.username }}{{ thought.timestamp.strftime('%d.%m.%Y') }} +
+ {{ "%.1f"|format(thought.average_rating) }} +
+ {% for i in range(5) %} + {% if i < thought.average_rating|int %} + + {% elif i < (thought.average_rating|int + 0.5) %} + + {% else %} + + {% endif %} + {% endfor %} +
+
+
+ + + +
+
+
+ + +
+

Systemstatistiken

+ +
+
+
+
+ +
+
+

Benutzer

+

{{ users|length }}

+
+
+
+ +
+
+
+ +
+
+

Knoten

+

{{ nodes|length }}

+
+
+
+ +
+
+
+ +
+
+

Gedanken

+

{{ thoughts|length }}

+
+
+
+ +
+
+
+ +
+
+

Kommentare

+

+ {% set comment_count = 0 %} + {% for thought in thoughts %} + {% set comment_count = comment_count + thought.comments|length %} + {% endfor %} + {{ comment_count }} +

+
+
+
+
+ +
+
+

Aktive Benutzer

+
+

Hier würde ein Aktivitätsdiagramm angezeigt werden

+
+
+ +
+

Gedanken pro Kategorie

+
+

Hier würde eine Verteilungsstatistik angezeigt werden

+
+ + +
+

System-Log

+
+
[INFO] [{{ now.strftime('%Y-%m-%d %H:%M:%S') }}] System gestartet
+
[INFO] [{{ now.strftime('%Y-%m-%d %H:%M:%S') }}] Admin-Bereich aufgerufen von {{ current_user.username }}
+
[WARN] [{{ now.strftime('%Y-%m-%d %H:%M:%S') }}] Hohe Serverauslastung erkannt
+
[INFO] [{{ now.strftime('%Y-%m-%d %H:%M:%S') }}] Backup erfolgreich erstellt
+
[ERROR] [{{ now.strftime('%Y-%m-%d %H:%M:%S') }}] API-Zugriffsfehler (Timeout) bei externer Anfrage
+
+
+{% endblock %} + +{% block extra_js %} + {% endblock %} \ No newline at end of file diff --git a/website/templates/base.html b/website/templates/base.html index 89e707d..9fb4f1e 100644 --- a/website/templates/base.html +++ b/website/templates/base.html @@ -22,14 +22,22 @@ + + + + + + - - - + + + + {% block extra_css %}{% endblock %} - + +
- toggleDarkMode() { - this.darkMode = !this.darkMode; - document.querySelector('html').classList.toggle('dark', this.darkMode); - - // Speichere den Dark Mode-Status auf dem Server - fetch('/set_dark_mode', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ darkMode: this.darkMode }) - }) - .then(response => response.json()) - .then(data => { - if (data.success) { - // Zusätzlich im localStorage speichern für sofortige Reaktion bei Seitenwechsel - localStorage.setItem('darkMode', this.darkMode ? 'dark' : 'light'); - // Event auslösen für andere Komponenten - document.dispatchEvent(new CustomEvent('darkModeToggled', { - detail: { isDark: this.darkMode } - })); - } else { - console.error('Fehler beim Speichern der Dark Mode-Einstellung:', data.error); - } - }) - .catch(error => { - console.error('Fehler beim Speichern der Dark Mode-Einstellung:', error); - }); - } -}" -:class="{ 'dark': darkMode }" -class="font-sans antialiased relative min-h-screen bg-gray-50 text-gray-900" -x-init="fetch('/get_dark_mode') - .then(response => response.json()) - .then(data => { - if (data.success) { - darkMode = data.darkMode === 'true'; - document.querySelector('html').classList.toggle('dark', darkMode); - } else { - // Fallback zu localStorage wenn Server-Abfrage fehlschlägt - const savedTheme = localStorage.getItem('darkMode'); - if (savedTheme) { - darkMode = savedTheme === 'dark'; - document.querySelector('html').classList.toggle('dark', darkMode); - } - } - }) - .catch(error => { - console.error('Fehler beim Laden der Dark Mode-Einstellung:', error); - // Fallback zu localStorage wenn Fetch fehlschlägt - const savedTheme = localStorage.getItem('darkMode'); - if (savedTheme) { - darkMode = savedTheme === 'dark'; - document.querySelector('html').classList.toggle('dark', darkMode); - } - })"> - - - -
-
-
-
-
-
- - -
-
- -
- {% with messages = get_flashed_messages(with_categories=true) %} - {% if messages %} - {% for category, message in messages %} -
-
-
- {% if category == 'error' %} - - {% elif category == 'success' %} - - {% else %} - - {% endif %} -
-
-

{{ message }}

-
-
- -
-
-
- {% endfor %} - {% endif %} - {% endwith %} -
- - -
- {% block content %}{% endblock %} -
- - - - - - - {% block extra_js %}{% endblock %} + +
+ {% block content %}{% endblock %} +
+ + +
+
+
+
+ MindMap +

Eine interaktive Plattform zum Visualisieren, Erforschen und Teilen von Wissen.

+
+ + + + + {# +
+ + + +
+ #} +
+ +
+ © {{ current_year }} MindMap. Alle Rechte vorbehalten. +
+
+
+
+ + + {% block scripts %}{% endblock %} \ No newline at end of file diff --git a/website/templates/errors/403.html b/website/templates/errors/403.html new file mode 100644 index 0000000..faaf0a6 --- /dev/null +++ b/website/templates/errors/403.html @@ -0,0 +1,23 @@ +{% extends "base.html" %} + +{% block title %}403 - Zugriff verweigert{% endblock %} + +{% block content %} +
+
+
+

403

+

Zugriff verweigert

+

Sie haben nicht die erforderlichen Berechtigungen, um auf diese Seite zuzugreifen. Bitte melden Sie sich an oder nutzen Sie ein Konto mit entsprechenden Rechten.

+ +
+
+
+{% endblock %} \ No newline at end of file diff --git a/website/templates/errors/404.html b/website/templates/errors/404.html new file mode 100644 index 0000000..36f85f2 --- /dev/null +++ b/website/templates/errors/404.html @@ -0,0 +1,23 @@ +{% extends "base.html" %} + +{% block title %}404 - Seite nicht gefunden{% endblock %} + +{% block content %} +
+
+
+

404

+

Seite nicht gefunden

+

Die gesuchte Seite existiert nicht oder wurde verschoben. Bitte prüfen Sie die URL oder nutzen Sie die Navigation.

+ +
+
+
+{% endblock %} \ No newline at end of file diff --git a/website/templates/errors/429.html b/website/templates/errors/429.html new file mode 100644 index 0000000..1caab92 --- /dev/null +++ b/website/templates/errors/429.html @@ -0,0 +1,23 @@ +{% extends "base.html" %} + +{% block title %}429 - Zu viele Anfragen{% endblock %} + +{% block content %} +
+
+
+

429

+

Zu viele Anfragen

+

Sie haben zu viele Anfragen in kurzer Zeit gestellt. Bitte warten Sie einen Moment und versuchen Sie es dann erneut.

+ +
+
+
+{% endblock %} \ No newline at end of file diff --git a/website/templates/errors/500.html b/website/templates/errors/500.html new file mode 100644 index 0000000..e3c6cbe --- /dev/null +++ b/website/templates/errors/500.html @@ -0,0 +1,23 @@ +{% extends "base.html" %} + +{% block title %}500 - Serverfehler{% endblock %} + +{% block content %} +
+
+
+

500

+

Interner Serverfehler

+

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.

+ +
+
+
+{% endblock %} \ No newline at end of file diff --git a/website/templates/index.html b/website/templates/index.html index 4c8bac4..ffeebdd 100644 --- a/website/templates/index.html +++ b/website/templates/index.html @@ -4,9 +4,19 @@ {% block extra_css %} {% endblock %} {% block content %} + +
-
- -
- - -
- {% for i in range(15) %} -
- {% endfor %} -
- +
-
-
+
+

Wissen neu
vernetzen
@@ -130,14 +96,14 @@ in einem interaktiven Wissensnetzwerk.

@@ -222,72 +188,72 @@
-
-
+
+
-

Visualisiere Wissen

-

- Sieh Wissen als vernetztes System, entdecke Zusammenhänge und erkenne überraschende +

Visualisiere Wissen

+

+ Sieh Wissen als vernetztes System, entdecke Zusammenhänge und erkenne überraschende Verbindungen zwischen verschiedenen Themengebieten.

-
-
+
+
-

Teile Gedanken

-

- Füge deine eigenen Ideen und Perspektiven hinzu. Erstelle Verbindungen zu +

Teile Gedanken

+

+ Füge deine eigenen Ideen und Perspektiven hinzu. Erstelle Verbindungen zu vorhandenen Gedanken und bereichere die wachsende Wissensbasis.

-
-
+
+
-

Community

-

+

Community

+

Sei Teil einer Gemeinschaft, die gemeinsam ein verteiltes Wissensarchiv aufbaut und sich in thematisch fokussierten Bereichen austauscht.

-
-
+
+
-

KI-Assistenz

-

+

KI-Assistenz

+

Lass dir von künstlicher Intelligenz helfen, neue Zusammenhänge zu entdecken, Inhalte zusammenzufassen und Fragen zu beantworten.

-
-
+
+
-

Intelligente Suche

-

- Finde genau die Informationen, die du suchst, mit fortschrittlichen Such- und +

Intelligente Suche

+

+ Finde genau die Informationen, die du suchst, mit fortschrittlichen Such- und Filterfunktionen für eine präzise Navigation durch das Wissen.

-
-
+
+
-

Geführte Pfade

-

+

Geführte Pfade

+

Folge kuratierten Lernpfaden durch komplexe Themen oder erschaffe selbst Routen für andere, die deinen Gedankengängen folgen möchten.

@@ -298,10 +264,8 @@
-
-
-
+

Bereit, Wissen neu zu entdecken?

@@ -310,7 +274,7 @@

- + Zur Mindmap @@ -327,7 +291,7 @@
- diff --git a/website/templates/layout.html b/website/templates/layout.html new file mode 100644 index 0000000..33d7242 --- /dev/null +++ b/website/templates/layout.html @@ -0,0 +1,11 @@ + +
+ +
+ + +
+ +
\ No newline at end of file diff --git a/website/templates/mindmap.html b/website/templates/mindmap.html index cd4e340..ebe91de 100644 --- a/website/templates/mindmap.html +++ b/website/templates/mindmap.html @@ -5,87 +5,212 @@ {% block extra_css %} {% endblock %} {% block content %} -
- -
- ┌─┐ ┌─┐ ┌─┐ ┌─┐ - │ │ │ │ │ │ │ │ - └─┘ └─┘ └─┘ └─┘ -
-
- ╔══╗ ╔═══╗ ╔══╗ - ║ ║ ║ ║ ║ ║ - ╚══╝ ╚═══╝ ╚══╝ -
- - -
-

- Mindmap -

-

- Erkunden Sie interaktiv verknüpfte Wissensgebiete und ihre Verbindungen. Fügen Sie eigene Gedanken hinzu und erstellen Sie ein kollaboratives Wissensnetz. -

-
- - Interaktiv - - - Wissensvernetzung - - - Kollaborativ - +
+ +
+ +
+
+ +
+

Visualisiere Wissen

+

Sieh Wissen als vernetztes System, entdecke Zusammenhänge und erkenne überraschende Verbindungen zwischen verschiedenen Themengebieten.

+
+ + +
+
+ +
+

Teile Gedanken

+

Füge deine eigenen Ideen und Perspektiven hinzu. Erstelle Verbindungen zu vorhandenen Gedanken und bereichere die wachsende Wissensbasis.

+
+ + +
+
+ +
+

Community

+

Sei Teil einer Gemeinschaft, die gemeinsam ein verteiltes Wissensarchiv aufbaut und sich in thematisch fokussierten Bereichen austauscht.

- - -
- -
-
- -
-
-
- -
- -
- -
- - - - + + +
+

Wissenslandschaft erkunden

+

Interagiere mit der Mindmap, um Verbindungen zu entdecken und neue Ideen hinzuzufügen

+
+ + +
+
+ +
+
+
+

Wissenslandschaft wird geladen...

+
+
- - -
- -
- - -
-
Legende
-
-
-
- Hauptkategorien -
-
-
- Unterkategorien -
-
-
- Konzepte -
-
-
-
- - -
- -
-
-
-
- -
-

Mindmap erkunden

-

Wählen Sie einen Knoten in der Mindmap aus, um zugehörige Gedanken anzuzeigen.

-
- - Klicken Sie auf einen Knoten -
-
+ +
+
+ + +
+
+ + +
+
+
+ + +
+ +
+
+ +
+

KI-Assistenz

+

Lass dir von künstlicher Intelligenz helfen, neue Zusammenhänge zu entdecken, Inhalte zusammenzufassen und Fragen zu beantworten.

+
+ + +
+
+ +
+

Intelligente Suche

+

Finde genau die Informationen, die du suchst, mit fortschrittlichen Such- und Filterfunktionen für eine präzise Navigation durch das Wissen.

+
+ + +
+
+ +
+

Geführte Pfade

+

Folge kuratierten Lernpfaden durch komplexe Themen oder erschaffe selbst Routen für andere, die deinen Gedankengängen folgen möchten.

@@ -330,55 +515,148 @@ {% block extra_js %} - - + + + - - - + + + + + {% endblock %} \ No newline at end of file diff --git a/website/templates/profile.html b/website/templates/profile.html index da208e1..e919c91 100644 --- a/website/templates/profile.html +++ b/website/templates/profile.html @@ -1,131 +1,953 @@ {% extends "base.html" %} -{% block title %}Profil | Wissenschaftliche Mindmap{% endblock %} +{% block title %}Profil{% endblock %} + +{% block extra_css %} + +{% endblock %} {% block content %} -
-
-
-
-

Hallo, {{ current_user.username }}

-

{{ current_user.email }}

-
- - +
+ +
+ +
+ +
+ {% if user.profile_image %} + {{ user.name }} + {% else %} +
+ +
+ {% endif %} +
+ + +
+

{{ user.name|default('Max Mustermann') }}

+
+ @{{ user.username|default('maxmustermann') }} + {% if user.verified %} + + Verifiziert + + {% endif %}
+ +

+ {{ user.bio|default('Willkommen auf meinem Profil! Ich bin daran interessiert, Wissen zu vernetzen und neue Verbindungen zwischen verschiedenen Themengebieten zu entdecken. Mein Ziel ist es, ein tieferes Verständnis für komplexe Zusammenhänge zu entwickeln.') }} +

+ + +
+
+ + {{ user.location|default('Berlin, Deutschland') }} +
+
+ + Mitglied seit {{ user.joined_date|default('Januar 2023') }} +
+
+ + {{ user.website|default('www.beispiel.de') }} +
+
+ + +
+ + + +
+
-
-

Deine Gedanken

- - {% if thoughts %} -
- {% for thought in thoughts %} -
-
- {{ thought.branch }} - {{ thought.timestamp.strftime('%d.%m.%Y, %H:%M') }} -
-

{{ thought.content }}

- -
-
- {{ thought.comments|length }} Kommentar(e) -
- - Details anzeigen -
-
- {% endfor %} -
- {% else %} -
-

Du hast noch keine Gedanken geteilt.

- Zur Mindmap gehen und mitmachen -
- {% endif %} -
-
- - -