Compare commits
69 Commits
49e5e19b7c
...
tills-bran
| Author | SHA1 | Date | |
|---|---|---|---|
| a7bb9563b3 | |||
| 4e0c470663 | |||
| b5300f74bd | |||
| 6a53e621ca | |||
| a59ce652af | |||
| 27cfc95081 | |||
| c513666391 | |||
| ae30dbce57 | |||
| 817ddd98e9 | |||
| bfce2fc7b7 | |||
| efbcd567ee | |||
| a873765d08 | |||
| efbcadb95a | |||
| da3ccaffe9 | |||
| f4e04573bd | |||
| aa253f3871 | |||
| cfd6a25b21 | |||
| d307763007 | |||
| d7e6912e08 | |||
| ffe96074f4 | |||
| 49ccf3908a | |||
| 9514645904 | |||
| 63f45abb3e | |||
| 7d74b5a7bf | |||
| 55f2553780 | |||
| 0852ea070b | |||
| 7a0533ac09 | |||
| 65c44ab371 | |||
| 5399169b11 | |||
| 05f6f149ad | |||
| 12ed413c04 | |||
| ccf5a1d678 | |||
| eb23d638e6 | |||
| 750dba2d6f | |||
| 6aa3780a96 | |||
| 8890a62026 | |||
| 6cf9b2a627 | |||
| 4f6aea8e20 | |||
| e5f485d9d7 | |||
| cf3fc09a63 | |||
| 10747a8336 | |||
| 7eb958f3c8 | |||
| 4a3092a4d2 | |||
| 2d8cdc052f | |||
| 968515ce2b | |||
| 88f8e98df0 | |||
| e5409eef68 | |||
| 013bf76446 | |||
| 808a3c7bbe | |||
| d117978005 | |||
| 48d8463481 | |||
| 08314ec703 | |||
| 0bb7d8d0dc | |||
| 4a28c2c453 | |||
| 66d987857a | |||
| d58aba26c2 | |||
| 8f0a6d4372 | |||
| 5372fe220e | |||
| 11ab15127c | |||
| 0705ecce59 | |||
| 1c59b0b616 | |||
| d42c43db50 | |||
| e46264b201 | |||
| 74307ba345 | |||
| 14474c4eab | |||
| 4797cc3b72 | |||
| ab280b55af | |||
| 84b492d8d2 | |||
| b0db3398f2 |
17
.env
17
.env
@@ -1,15 +1,2 @@
|
||||
# MindMap Umgebungsvariablen
|
||||
# Kopiere diese Datei zu .env und passe die Werte an
|
||||
|
||||
# Flask
|
||||
FLASK_APP=app.py
|
||||
FLASK_DEBUG=1
|
||||
SECRET_KEY=your-secret-key-replace-in-production
|
||||
|
||||
# OpenAI API
|
||||
OPENAI_API_KEY=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
|
||||
|
||||
# Datenbank
|
||||
# Bei Bedarf kann hier eine andere Datenbank-URL angegeben werden
|
||||
# Der Pfad wird relativ zum Projektverzeichnis angegeben
|
||||
# SQLALCHEMY_DATABASE_URI=sqlite:////absoluter/pfad/zu/database/systades.db
|
||||
SECRET_KEY=eed9298856dc9363cd32778265780d6904ba24e6a6b815a2cc382bcdd767ea7b
|
||||
OPENAI_API_KEY=sk-dein-openai-api-schluessel-hier
|
||||
|
||||
33
Dockerfile
33
Dockerfile
@@ -1,33 +0,0 @@
|
||||
# Dockerfile
|
||||
FROM python:3.11-slim
|
||||
|
||||
# Arbeitsverzeichnis in Container
|
||||
WORKDIR /app
|
||||
|
||||
# Systemabhängigkeiten installieren und Verzeichnisse anlegen
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends gcc && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
mkdir -p /app/database
|
||||
|
||||
# pip auf den neuesten Stand bringen und Requirements installieren
|
||||
COPY requirements.txt ./
|
||||
RUN pip install --upgrade pip && \
|
||||
pip install --no-cache-dir -U -r requirements.txt
|
||||
|
||||
# Anwendungscode kopieren
|
||||
COPY . .
|
||||
|
||||
# Berechtigungen für database-Ordner
|
||||
RUN chmod -R 777 /app/database
|
||||
|
||||
# Exponiere Port 5000 für Flask
|
||||
EXPOSE 5000
|
||||
|
||||
# Setze Umgebungsvariablen
|
||||
ENV FLASK_APP=app.py
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
|
||||
# Startkommando mit spezifischen Flags für Produktion
|
||||
CMD ["python", "app.py"]
|
||||
Binary file not shown.
Binary file not shown.
801
app.py
801
app.py
@@ -2,7 +2,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from datetime import datetime, timedelta
|
||||
from flask import Flask, render_template, request, redirect, url_for, flash, jsonify, session, g
|
||||
from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
@@ -19,7 +19,6 @@ from openai import OpenAI
|
||||
from dotenv import load_dotenv
|
||||
from flask_socketio import SocketIO, emit
|
||||
from flask_migrate import Migrate
|
||||
import sqlalchemy
|
||||
|
||||
# Modelle importieren
|
||||
from models import (
|
||||
@@ -46,10 +45,10 @@ app.config['UPLOAD_FOLDER'] = os.getenv('UPLOAD_FOLDER', os.path.join(os.getcwd(
|
||||
app.config['WTF_CSRF_ENABLED'] = False
|
||||
|
||||
# OpenAI API-Konfiguration
|
||||
api_key = "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"
|
||||
# api_key = os.environ.get("OPENAI_API_KEY")
|
||||
# if not api_key:
|
||||
# print("WARNUNG: Kein OPENAI_API_KEY in Umgebungsvariablen gefunden. KI-Funktionalität wird nicht verfügbar sein.")
|
||||
api_key = os.environ.get("OPENAI_API_KEY")
|
||||
if not api_key:
|
||||
print("WARNUNG: Kein OPENAI_API_KEY in Umgebungsvariablen gefunden. KI-Funktionalität wird nicht verfügbar sein.")
|
||||
api_key = "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"
|
||||
|
||||
client = OpenAI(api_key=api_key)
|
||||
|
||||
@@ -199,31 +198,97 @@ def initialize_database():
|
||||
# Erstelle alle Tabellen
|
||||
db.create_all()
|
||||
|
||||
# Prüfen, ob bereits Kategorien existieren
|
||||
categories_count = Category.query.count()
|
||||
users_count = User.query.count()
|
||||
|
||||
# Erstelle Standarddaten, wenn es keine Kategorien gibt
|
||||
if categories_count == 0:
|
||||
create_default_categories()
|
||||
|
||||
# Admin-Benutzer erstellen, wenn keine Benutzer vorhanden sind
|
||||
if users_count == 0:
|
||||
admin_user = User(
|
||||
# Prüfe, ob bereits Benutzer existieren
|
||||
if User.query.count() == 0:
|
||||
print("Erstelle Admin-Benutzer...")
|
||||
admin = User(
|
||||
username="admin",
|
||||
email="admin@example.com",
|
||||
role="admin",
|
||||
is_active=True
|
||||
is_admin=True
|
||||
)
|
||||
admin_user.set_password("admin123") # Sicheres Passwort in der Produktion verwenden!
|
||||
db.session.add(admin_user)
|
||||
db.session.commit()
|
||||
print("Admin-Benutzer wurde erstellt!")
|
||||
admin.set_password("admin123") # In echter Umgebung ein sicheres Passwort verwenden!
|
||||
db.session.add(admin)
|
||||
|
||||
return True
|
||||
# Prüfe, ob bereits Kategorien existieren
|
||||
if Category.query.count() == 0:
|
||||
print("Erstelle Standard-Kategorien...")
|
||||
create_default_categories()
|
||||
|
||||
# Stelle sicher, dass die Standard-Knoten für die öffentliche Mindmap existieren
|
||||
if MindMapNode.query.count() == 0:
|
||||
print("Erstelle Standard-Knoten für die Mindmap...")
|
||||
|
||||
# Hauptknoten: Wissen
|
||||
root_node = MindMapNode(
|
||||
name="Wissen",
|
||||
description="Zentrale Wissensbasis",
|
||||
color_code="#4299E1",
|
||||
is_public=True
|
||||
)
|
||||
db.session.add(root_node)
|
||||
db.session.flush() # Um die ID zu generieren
|
||||
|
||||
# Verwandte Kategorien finden
|
||||
philosophy = Category.query.filter_by(name="Philosophie").first()
|
||||
science = Category.query.filter_by(name="Wissenschaft").first()
|
||||
technology = Category.query.filter_by(name="Technologie").first()
|
||||
arts = Category.query.filter_by(name="Künste").first()
|
||||
|
||||
# Erstelle Hauptthemenknoten
|
||||
nodes = [
|
||||
MindMapNode(
|
||||
name="Philosophie",
|
||||
description="Philosophisches Denken",
|
||||
color_code="#9F7AEA",
|
||||
category=philosophy,
|
||||
is_public=True
|
||||
),
|
||||
MindMapNode(
|
||||
name="Wissenschaft",
|
||||
description="Wissenschaftliche Erkenntnisse",
|
||||
color_code="#48BB78",
|
||||
category=science,
|
||||
is_public=True
|
||||
),
|
||||
MindMapNode(
|
||||
name="Technologie",
|
||||
description="Technologische Entwicklungen",
|
||||
color_code="#ED8936",
|
||||
category=technology,
|
||||
is_public=True
|
||||
),
|
||||
MindMapNode(
|
||||
name="Künste",
|
||||
description="Künstlerische Ausdrucksformen",
|
||||
color_code="#ED64A6",
|
||||
category=arts,
|
||||
is_public=True
|
||||
)
|
||||
]
|
||||
|
||||
# Füge Knoten zur Datenbank hinzu
|
||||
for node in nodes:
|
||||
db.session.add(node)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
# Nachdem wir die IDs haben, füge die Verbindungen hinzu
|
||||
all_nodes = MindMapNode.query.all()
|
||||
root = MindMapNode.query.filter_by(name="Wissen").first()
|
||||
|
||||
if root:
|
||||
for node in all_nodes:
|
||||
if node.id != root.id:
|
||||
root.children.append(node)
|
||||
|
||||
# Speichere die Änderungen
|
||||
db.session.commit()
|
||||
|
||||
print("Datenbankinitialisierung abgeschlossen.")
|
||||
except Exception as e:
|
||||
print(f"Fehler bei Datenbank-Initialisierung: {e}")
|
||||
return False
|
||||
print(f"Fehler bei der Datenbankinitialisierung: {str(e)}")
|
||||
db.session.rollback()
|
||||
raise
|
||||
|
||||
# Instead of before_first_request, which is deprecated in newer Flask versions
|
||||
# Use a function to initialize the database that will be called during app creation
|
||||
@@ -250,8 +315,7 @@ def admin_required(f):
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(id):
|
||||
# Verwende session.get() anstelle von query.get() für SQLAlchemy 2.0 Kompatibilität
|
||||
return db.session.get(User, int(id))
|
||||
return User.query.get(int(id))
|
||||
|
||||
# Routes for authentication
|
||||
@app.route('/login', methods=['GET', 'POST'])
|
||||
@@ -261,16 +325,14 @@ def login():
|
||||
password = request.form.get('password')
|
||||
|
||||
user = User.query.filter_by(username=username).first()
|
||||
|
||||
if user and user.check_password(password):
|
||||
login_user(user)
|
||||
# Aktualisiere letzten Login-Zeitpunkt
|
||||
user.last_login = datetime.now(timezone.utc)
|
||||
user.last_login = datetime.utcnow()
|
||||
db.session.commit()
|
||||
|
||||
next_page = request.args.get('next')
|
||||
return redirect(next_page or url_for('index'))
|
||||
|
||||
flash('Ungültiger Benutzername oder Passwort')
|
||||
return render_template('login.html')
|
||||
|
||||
@@ -292,21 +354,15 @@ def register():
|
||||
user = User(username=username, email=email)
|
||||
user.set_password(password)
|
||||
db.session.add(user)
|
||||
db.session.commit() # Commit, um eine ID für den Benutzer zu erhalten
|
||||
|
||||
# Erstelle eine Standard-Mindmap für den neuen Benutzer
|
||||
try:
|
||||
default_mindmap = UserMindmap(
|
||||
name='Meine Mindmap',
|
||||
description='Meine persönliche Wissenslandschaft',
|
||||
user_id=user.id
|
||||
)
|
||||
db.session.add(default_mindmap)
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
print(f"Fehler beim Erstellen der Standard-Mindmap: {e}")
|
||||
# Stelle sicher, dass wir trotzdem weitermachen können
|
||||
db.session.rollback()
|
||||
default_mindmap = UserMindmap(
|
||||
name='Meine Mindmap',
|
||||
description='Meine persönliche Wissenslandschaft',
|
||||
user=user
|
||||
)
|
||||
db.session.add(default_mindmap)
|
||||
db.session.commit()
|
||||
|
||||
login_user(user)
|
||||
flash('Dein Konto wurde erfolgreich erstellt!', 'success')
|
||||
@@ -327,152 +383,71 @@ def index():
|
||||
# Route for the mindmap page
|
||||
@app.route('/mindmap')
|
||||
def mindmap():
|
||||
"""Zeigt die Mindmap-Seite an."""
|
||||
"""Zeigt die öffentliche Mindmap an."""
|
||||
try:
|
||||
# Sicherstellen, dass wir Kategorien haben
|
||||
if Category.query.count() == 0:
|
||||
create_default_categories()
|
||||
|
||||
# Benutzer-Mindmaps, falls angemeldet
|
||||
user_mindmaps = []
|
||||
if current_user.is_authenticated:
|
||||
user_mindmaps = UserMindmap.query.filter_by(user_id=current_user.id).all()
|
||||
# Hole alle Kategorien der obersten Ebene
|
||||
categories = Category.query.filter_by(parent_id=None).all()
|
||||
|
||||
# Stelle sicher, dass der "Wissen"-Knoten existiert
|
||||
wissen_node = MindMapNode.query.filter_by(name="Wissen").first()
|
||||
if not wissen_node:
|
||||
wissen_node = MindMapNode(
|
||||
name="Wissen",
|
||||
description="Zentrale Wissensbasis",
|
||||
color_code="#4299E1",
|
||||
is_public=True
|
||||
)
|
||||
db.session.add(wissen_node)
|
||||
db.session.commit()
|
||||
print("'Wissen'-Knoten wurde erstellt")
|
||||
# Transformiere Kategorien in ein anzeigbares Format für die Vorlage
|
||||
category_tree = [build_category_tree(cat) for cat in categories]
|
||||
|
||||
# Überprüfe, ob es Kategorien gibt, sonst erstelle sie
|
||||
if Category.query.count() == 0:
|
||||
create_default_categories()
|
||||
print("Kategorien wurden erstellt")
|
||||
|
||||
# Stelle sicher, dass die Route für statische Dateien korrekt ist
|
||||
mindmap_js_path = url_for('static', filename='js/mindmap-init.js')
|
||||
|
||||
return render_template('mindmap.html', user_mindmaps=user_mindmaps, mindmap_js_path=mindmap_js_path)
|
||||
return render_template('mindmap.html', categories=category_tree)
|
||||
except Exception as e:
|
||||
# Bei Fehler leere Kategorienliste übergeben und Fehler protokollieren
|
||||
print(f"Fehler beim Laden der Mindmap-Kategorien: {str(e)}")
|
||||
return render_template('mindmap.html', categories=[], error=str(e))
|
||||
|
||||
# Route for user profile
|
||||
@app.route('/profile')
|
||||
@login_required
|
||||
def profile():
|
||||
try:
|
||||
# Versuche auf die neue Benutzermodellstruktur zuzugreifen
|
||||
_ = current_user.bio # Dies wird fehlschlagen, wenn die Spalte nicht existiert
|
||||
# Lade Benutzer-Mindmaps
|
||||
user_mindmaps = UserMindmap.query.filter_by(user_id=current_user.id).all()
|
||||
|
||||
# Wenn keine Ausnahme, fahre mit normalem Profil fort
|
||||
# Lade Benutzer-Mindmaps
|
||||
user_mindmaps = UserMindmap.query.filter_by(user_id=current_user.id).all()
|
||||
# Lade Statistiken
|
||||
thought_count = Thought.query.filter_by(user_id=current_user.id).count()
|
||||
bookmark_count = db.session.query(user_thought_bookmark).filter(
|
||||
user_thought_bookmark.c.user_id == current_user.id).count()
|
||||
|
||||
# Prüfe, ob der Benutzer eine Standard-Mindmap hat, sonst erstelle eine
|
||||
if not user_mindmaps:
|
||||
try:
|
||||
default_mindmap = UserMindmap(
|
||||
name='Meine Mindmap',
|
||||
description='Meine persönliche Wissenslandschaft',
|
||||
user_id=current_user.id
|
||||
)
|
||||
db.session.add(default_mindmap)
|
||||
db.session.commit()
|
||||
# Berechne tatsächliche Werte für Benutzerstatistiken
|
||||
contributions_count = Comment.query.filter_by(user_id=current_user.id).count()
|
||||
|
||||
# Aktualisiere die Liste nach dem Erstellen
|
||||
user_mindmaps = [default_mindmap]
|
||||
except Exception as e:
|
||||
print(f"Fehler beim Erstellen der Standard-Mindmap in Profil: {e}")
|
||||
# Flash-Nachricht für den Benutzer
|
||||
flash('Es gab ein Problem beim Laden deiner Mindmaps. Bitte versuche es später erneut.', 'warning')
|
||||
# Berechne Verbindungen (Anzahl der Gedankenverknüpfungen)
|
||||
connections_count = ThoughtRelation.query.filter(
|
||||
(ThoughtRelation.source_id.in_(
|
||||
db.session.query(Thought.id).filter_by(user_id=current_user.id)
|
||||
)) |
|
||||
(ThoughtRelation.target_id.in_(
|
||||
db.session.query(Thought.id).filter_by(user_id=current_user.id)
|
||||
))
|
||||
).count()
|
||||
|
||||
# Lade Statistiken
|
||||
thought_count = Thought.query.filter_by(user_id=current_user.id).count()
|
||||
bookmark_count = db.session.query(user_thought_bookmark).filter(
|
||||
user_thought_bookmark.c.user_id == current_user.id).count()
|
||||
# Berechne durchschnittliche Bewertung der Gedanken des Benutzers
|
||||
avg_rating = db.session.query(func.avg(ThoughtRating.relevance_score)).join(
|
||||
Thought, Thought.id == ThoughtRating.thought_id
|
||||
).filter(Thought.user_id == current_user.id).scalar() or 0
|
||||
|
||||
# Berechne tatsächliche Werte für Benutzerstatistiken
|
||||
contributions_count = Comment.query.filter_by(user_id=current_user.id).count()
|
||||
# Hole die Anzahl der Follower (falls implementiert)
|
||||
# In diesem Beispiel nehmen wir an, dass es keine Follower-Funktionalität gibt
|
||||
followers_count = 0
|
||||
|
||||
# Berechne Verbindungen (Anzahl der Gedankenverknüpfungen)
|
||||
connections_count = ThoughtRelation.query.filter(
|
||||
(ThoughtRelation.source_id.in_(
|
||||
db.session.query(Thought.id).filter_by(user_id=current_user.id)
|
||||
)) |
|
||||
(ThoughtRelation.target_id.in_(
|
||||
db.session.query(Thought.id).filter_by(user_id=current_user.id)
|
||||
))
|
||||
).count()
|
||||
# Hole den Standort des Benutzers aus der Datenbank, falls vorhanden
|
||||
location = "Deutschland" # Standardwert
|
||||
|
||||
# Berechne durchschnittliche Bewertung der Gedanken des Benutzers
|
||||
avg_rating = db.session.query(func.avg(ThoughtRating.relevance_score)).join(
|
||||
Thought, Thought.id == ThoughtRating.thought_id
|
||||
).filter(Thought.user_id == current_user.id).scalar() or 0
|
||||
|
||||
# Sammle alle Statistiken in einem Wörterbuch
|
||||
stats = {
|
||||
'thought_count': thought_count,
|
||||
'bookmark_count': bookmark_count,
|
||||
'connections_count': connections_count,
|
||||
'contributions_count': contributions_count,
|
||||
'followers_count': 0, # Platzhalter für zukünftige Funktionalität
|
||||
'rating': round(avg_rating, 1)
|
||||
}
|
||||
|
||||
# Hole die letzten Gedanken des Benutzers
|
||||
thoughts = Thought.query.filter_by(user_id=current_user.id).order_by(Thought.created_at.desc()).limit(5).all()
|
||||
|
||||
# Hole den Standort des Benutzers aus der Datenbank, falls vorhanden
|
||||
location = "Deutschland" # Standardwert
|
||||
|
||||
return render_template('profile.html',
|
||||
user=current_user,
|
||||
user_mindmaps=user_mindmaps,
|
||||
stats=stats,
|
||||
thoughts=thoughts,
|
||||
location=location)
|
||||
|
||||
except (AttributeError, sqlalchemy.exc.OperationalError) as e:
|
||||
# Die Spalte existiert nicht, verwende stattdessen das einfache Profil
|
||||
print(f"Verwende einfaches Profil wegen Datenbankfehler: {e}")
|
||||
flash('Dein Profil wird im einfachen Modus angezeigt, bis die Datenbank aktualisiert wird.', 'warning')
|
||||
|
||||
# Lade nur die grundlegenden Informationen
|
||||
user_mindmaps = UserMindmap.query.filter_by(user_id=current_user.id).all()
|
||||
thoughts = Thought.query.filter_by(user_id=current_user.id).order_by(Thought.created_at.desc()).limit(5).all()
|
||||
|
||||
return render_template('simple_profile.html',
|
||||
user=current_user,
|
||||
user_mindmaps=user_mindmaps,
|
||||
thoughts=thoughts)
|
||||
except Exception as e:
|
||||
# Eine andere Ausnahme ist aufgetreten
|
||||
print(f"Fehler beim Laden des Profils: {e}")
|
||||
flash('Beim Laden Ihres Benutzerprofils ist ein Fehler aufgetreten. Wir zeigen Ihnen die Standard-Ansicht.', 'error')
|
||||
|
||||
# Erstelle grundlegende stats
|
||||
stats = {
|
||||
'thought_count': 0,
|
||||
'bookmark_count': 0,
|
||||
'connections_count': 0,
|
||||
'contributions_count': 0,
|
||||
'followers_count': 0,
|
||||
'rating': 0.0
|
||||
}
|
||||
|
||||
# Leere Listen für Mindmaps und Gedanken
|
||||
user_mindmaps = []
|
||||
thoughts = []
|
||||
location = "Deutschland"
|
||||
|
||||
# Zeige das normale Profil mit minimalen Daten an
|
||||
return render_template('profile.html',
|
||||
user=current_user,
|
||||
user_mindmaps=user_mindmaps,
|
||||
stats=stats,
|
||||
thoughts=thoughts,
|
||||
location=location)
|
||||
return render_template('profile.html',
|
||||
user=current_user,
|
||||
user_mindmaps=user_mindmaps,
|
||||
thought_count=thought_count,
|
||||
bookmark_count=bookmark_count,
|
||||
connections_count=connections_count,
|
||||
contributions_count=contributions_count,
|
||||
followers_count=followers_count,
|
||||
rating=round(avg_rating, 1),
|
||||
location=location)
|
||||
|
||||
# Route für Benutzereinstellungen
|
||||
@app.route('/settings', methods=['GET', 'POST'])
|
||||
@@ -481,94 +456,32 @@ def settings():
|
||||
if request.method == 'POST':
|
||||
action = request.form.get('action')
|
||||
|
||||
# Bestimme, ob es eine AJAX-Anfrage ist
|
||||
is_ajax = request.headers.get('X-Requested-With') == 'XMLHttpRequest' or request.content_type and 'multipart/form-data' in request.content_type
|
||||
|
||||
if action == 'update_profile':
|
||||
try:
|
||||
current_user.bio = request.form.get('bio', '')
|
||||
current_user.location = request.form.get('location', '')
|
||||
current_user.website = request.form.get('website', '')
|
||||
current_user.bio = request.form.get('bio')
|
||||
|
||||
# Update avatar if provided
|
||||
avatar_url = request.form.get('avatar_url')
|
||||
if avatar_url:
|
||||
current_user.avatar = avatar_url
|
||||
# Update avatar if provided
|
||||
avatar_url = request.form.get('avatar_url')
|
||||
if avatar_url:
|
||||
current_user.avatar = avatar_url
|
||||
|
||||
db.session.commit()
|
||||
|
||||
if is_ajax:
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': 'Profil erfolgreich aktualisiert!'
|
||||
})
|
||||
else:
|
||||
flash('Profil erfolgreich aktualisiert!', 'success')
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
app.logger.error(f"Fehler beim Aktualisieren des Profils: {str(e)}")
|
||||
|
||||
if is_ajax:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': 'Fehler beim Aktualisieren des Profils'
|
||||
}), 500
|
||||
else:
|
||||
flash('Fehler beim Aktualisieren des Profils', 'error')
|
||||
db.session.commit()
|
||||
flash('Profil erfolgreich aktualisiert!', 'success')
|
||||
|
||||
elif action == 'update_password':
|
||||
try:
|
||||
current_password = request.form.get('current_password')
|
||||
new_password = request.form.get('new_password')
|
||||
confirm_password = request.form.get('confirm_password')
|
||||
current_password = request.form.get('current_password')
|
||||
new_password = request.form.get('new_password')
|
||||
confirm_password = request.form.get('confirm_password')
|
||||
|
||||
if not current_user.check_password(current_password):
|
||||
if is_ajax:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': 'Aktuelles Passwort ist nicht korrekt'
|
||||
}), 400
|
||||
else:
|
||||
flash('Aktuelles Passwort ist nicht korrekt', 'error')
|
||||
elif new_password != confirm_password:
|
||||
if is_ajax:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': 'Neue Passwörter stimmen nicht überein'
|
||||
}), 400
|
||||
else:
|
||||
flash('Neue Passwörter stimmen nicht überein', 'error')
|
||||
else:
|
||||
current_user.set_password(new_password)
|
||||
db.session.commit()
|
||||
if not current_user.check_password(current_password):
|
||||
flash('Aktuelles Passwort ist nicht korrekt', 'error')
|
||||
elif new_password != confirm_password:
|
||||
flash('Neue Passwörter stimmen nicht überein', 'error')
|
||||
else:
|
||||
current_user.set_password(new_password)
|
||||
db.session.commit()
|
||||
flash('Passwort erfolgreich aktualisiert!', 'success')
|
||||
|
||||
if is_ajax:
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': 'Passwort erfolgreich aktualisiert!'
|
||||
})
|
||||
else:
|
||||
flash('Passwort erfolgreich aktualisiert!', 'success')
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
app.logger.error(f"Fehler beim Aktualisieren des Passworts: {str(e)}")
|
||||
|
||||
if is_ajax:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': 'Fehler beim Aktualisieren des Passworts'
|
||||
}), 500
|
||||
else:
|
||||
flash('Fehler beim Aktualisieren des Passworts', 'error')
|
||||
|
||||
if not is_ajax:
|
||||
return redirect(url_for('settings'))
|
||||
else:
|
||||
# Standardantwort für AJAX, falls keine spezifische Antwort zurückgegeben wurde
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': 'Einstellungen aktualisiert'
|
||||
})
|
||||
return redirect(url_for('settings'))
|
||||
|
||||
return render_template('settings.html')
|
||||
|
||||
@@ -685,156 +598,16 @@ def delete_mindmap(mindmap_id):
|
||||
# API-Endpunkte für Mindmap-Daten
|
||||
@app.route('/api/mindmap/public')
|
||||
def get_public_mindmap():
|
||||
"""Liefert die Standard-Mindmap-Struktur basierend auf Kategorien."""
|
||||
try:
|
||||
# Hole alle Hauptkategorien
|
||||
categories = Category.query.filter_by(parent_id=None).all()
|
||||
"""Liefert die öffentliche Mindmap-Struktur."""
|
||||
# Hole alle Kategorien der obersten Ebene
|
||||
root_categories = Category.query.filter_by(parent_id=None).all()
|
||||
|
||||
# Transformiere zu einer Baumstruktur
|
||||
category_tree = [build_category_tree(cat) for cat in categories]
|
||||
# Baue Baumstruktur auf
|
||||
result = []
|
||||
for category in root_categories:
|
||||
result.append(build_category_tree(category))
|
||||
|
||||
return jsonify(category_tree)
|
||||
except Exception as e:
|
||||
print(f"Fehler beim Abrufen der Mindmap: {str(e)}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Mindmap konnte nicht geladen werden'
|
||||
}), 500
|
||||
|
||||
@app.route('/api/mindmap/public/add_node', methods=['POST'])
|
||||
@login_required
|
||||
def add_node_to_public_mindmap():
|
||||
"""Fügt einen neuen Knoten zur öffentlichen Mindmap hinzu."""
|
||||
try:
|
||||
data = request.json
|
||||
|
||||
name = data.get('name')
|
||||
description = data.get('description', '')
|
||||
color_code = data.get('color_code', '#8b5cf6')
|
||||
x_position = data.get('x_position', 0)
|
||||
y_position = data.get('y_position', 0)
|
||||
|
||||
if not name:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Knotenname ist erforderlich'
|
||||
}), 400
|
||||
|
||||
# Neuen Knoten erstellen
|
||||
new_node = MindMapNode(
|
||||
name=name,
|
||||
description=description,
|
||||
color_code=color_code
|
||||
)
|
||||
|
||||
db.session.add(new_node)
|
||||
db.session.flush() # ID generieren
|
||||
|
||||
# Als Beitrag des aktuellen Benutzers markieren
|
||||
new_node.contributed_by = current_user.id
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'node_id': new_node.id,
|
||||
'message': 'Knoten erfolgreich hinzugefügt'
|
||||
})
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
print(f"Fehler beim Hinzufügen des Knotens: {str(e)}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': f'Fehler beim Hinzufügen des Knotens: {str(e)}'
|
||||
}), 500
|
||||
|
||||
@app.route('/api/mindmap/public/update_node/<int:node_id>', methods=['PUT'])
|
||||
@login_required
|
||||
def update_public_node(node_id):
|
||||
"""Aktualisiert einen Knoten in der öffentlichen Mindmap."""
|
||||
try:
|
||||
node = MindMapNode.query.get_or_404(node_id)
|
||||
data = request.json
|
||||
|
||||
# Aktualisiere Knotendaten
|
||||
if 'name' in data:
|
||||
node.name = data['name']
|
||||
if 'description' in data:
|
||||
node.description = data['description']
|
||||
if 'color_code' in data:
|
||||
node.color_code = data['color_code']
|
||||
|
||||
# Als bearbeitet markieren
|
||||
node.last_modified = datetime.now(timezone.utc)
|
||||
node.last_modified_by = current_user.id
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': 'Knoten erfolgreich aktualisiert'
|
||||
})
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
print(f"Fehler beim Aktualisieren des Knotens: {str(e)}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': f'Fehler beim Aktualisieren des Knotens: {str(e)}'
|
||||
}), 500
|
||||
|
||||
@app.route('/api/mindmap/public/remove_node/<int:node_id>', methods=['DELETE'])
|
||||
@login_required
|
||||
def remove_node_from_public_mindmap(node_id):
|
||||
"""Entfernt einen Knoten aus der öffentlichen Mindmap."""
|
||||
try:
|
||||
node = MindMapNode.query.get_or_404(node_id)
|
||||
|
||||
# Lösche den Knoten
|
||||
db.session.delete(node)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': 'Knoten erfolgreich entfernt'
|
||||
})
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
print(f"Fehler beim Entfernen des Knotens: {str(e)}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': f'Fehler beim Entfernen des Knotens: {str(e)}'
|
||||
}), 500
|
||||
|
||||
@app.route('/api/mindmap/public/update_layout', methods=['POST'])
|
||||
@login_required
|
||||
def update_public_layout():
|
||||
"""Aktualisiert die Positionen der Knoten in der öffentlichen Mindmap."""
|
||||
try:
|
||||
data = request.json
|
||||
positions = data.get('positions', [])
|
||||
|
||||
for pos in positions:
|
||||
node_id = pos.get('node_id')
|
||||
node = MindMapNode.query.get(node_id)
|
||||
|
||||
if node:
|
||||
# Position aktualisieren
|
||||
node.x_position = pos.get('x_position', 0)
|
||||
node.y_position = pos.get('y_position', 0)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': 'Layout erfolgreich aktualisiert'
|
||||
})
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
print(f"Fehler beim Aktualisieren des Layouts: {str(e)}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': f'Fehler beim Aktualisieren des Layouts: {str(e)}'
|
||||
}), 500
|
||||
return jsonify(result)
|
||||
|
||||
def build_category_tree(category):
|
||||
"""
|
||||
@@ -1114,7 +887,7 @@ def update_note(note_id):
|
||||
if color_code:
|
||||
note.color_code = color_code
|
||||
|
||||
note.last_modified = datetime.now(timezone.utc)
|
||||
note.last_modified = datetime.utcnow()
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
@@ -1142,64 +915,32 @@ def delete_note(note_id):
|
||||
# API routes for mindmap and thoughts
|
||||
@app.route('/api/mindmap')
|
||||
def get_mindmap():
|
||||
"""Gibt die Mindmap-Struktur zurück"""
|
||||
try:
|
||||
# Hauptknoten abrufen (Root-Knoten)
|
||||
wissen_node = MindMapNode.query.filter_by(name="Wissen").first()
|
||||
if not wissen_node:
|
||||
# Wissen-Knoten erstellen, falls nicht vorhanden
|
||||
wissen_node = MindMapNode(
|
||||
name="Wissen",
|
||||
description="Zentrale Wissensbasis",
|
||||
color_code="#4299E1",
|
||||
is_public=True
|
||||
)
|
||||
db.session.add(wissen_node)
|
||||
db.session.commit()
|
||||
"""API-Endpunkt zur Bereitstellung der Mindmap-Daten in hierarchischer Form."""
|
||||
# Root-Knoten: Knoten ohne Eltern
|
||||
root_nodes = MindMapNode.query.\
|
||||
outerjoin(node_relationship, MindMapNode.id == node_relationship.c.child_id).\
|
||||
filter(node_relationship.c.parent_id == None).all()
|
||||
|
||||
# Alle anderen aktiven Knoten holen
|
||||
nodes = MindMapNode.query.filter(MindMapNode.is_public == True).all()
|
||||
result = []
|
||||
for node in root_nodes:
|
||||
node_data = build_node_tree(node)
|
||||
result.append(node_data)
|
||||
return jsonify({"nodes": result})
|
||||
|
||||
# Ergebnisdaten vorbereiten
|
||||
nodes_data = []
|
||||
edges_data = []
|
||||
|
||||
# Knoten hinzufügen
|
||||
for node in nodes:
|
||||
nodes_data.append({
|
||||
'id': node.id,
|
||||
'name': node.name,
|
||||
'description': node.description or '',
|
||||
'color_code': node.color_code or '#9F7AEA'
|
||||
})
|
||||
|
||||
# Wenn es nicht der Wissen-Knoten ist, Verbindung zum Wissen-Knoten hinzufügen
|
||||
if node.id != wissen_node.id:
|
||||
edges_data.append({
|
||||
'source': wissen_node.id,
|
||||
'target': node.id
|
||||
})
|
||||
|
||||
# Beziehungen zwischen Knoten abfragen und hinzufügen
|
||||
relationships = db.session.query(node_relationship).all()
|
||||
for rel in relationships:
|
||||
if rel.parent_id != wissen_node.id: # Doppelte Kanten vermeiden
|
||||
edges_data.append({
|
||||
'source': rel.parent_id,
|
||||
'target': rel.child_id
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'nodes': nodes_data,
|
||||
'edges': edges_data
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
print(f"Fehler beim Abrufen der Mindmap: {str(e)}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Mindmap konnte nicht geladen werden'
|
||||
}), 500
|
||||
def build_node_tree(node):
|
||||
"""Erzeugt eine hierarchische Darstellung eines Knotens inkl. seiner Kindknoten."""
|
||||
thought_count = len(node.thoughts)
|
||||
node_data = {
|
||||
"id": node.id,
|
||||
"name": node.name,
|
||||
"description": node.description or "",
|
||||
"thought_count": thought_count,
|
||||
"children": []
|
||||
}
|
||||
for child in node.children:
|
||||
child_data = build_node_tree(child)
|
||||
node_data["children"].append(child_data)
|
||||
return node_data
|
||||
|
||||
@app.route('/api/nodes/<int:node_id>/thoughts')
|
||||
def get_node_thoughts(node_id):
|
||||
@@ -1365,7 +1106,7 @@ def update_thought(thought_id):
|
||||
if 'color_code' in data:
|
||||
thought.color_code = data['color_code']
|
||||
|
||||
thought.last_modified = datetime.now(timezone.utc)
|
||||
thought.last_modified = datetime.utcnow()
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
@@ -1570,11 +1311,8 @@ def chat_with_assistant():
|
||||
})
|
||||
|
||||
try:
|
||||
# OpenAI-Client mit dem API-Key initialisieren
|
||||
client = OpenAI(api_key=api_key)
|
||||
|
||||
# Überprüfen ob OpenAI API-Key konfiguriert ist
|
||||
if not api_key or api_key.startswith("sk-dummy"):
|
||||
if not client.api_key or client.api_key.startswith("sk-dummy"):
|
||||
print("Warnung: OpenAI API-Key ist nicht oder nur als Dummy konfiguriert!")
|
||||
return jsonify({
|
||||
'error': 'Der OpenAI API-Key ist nicht korrekt konfiguriert. Bitte konfigurieren Sie die Umgebungsvariable OPENAI_API_KEY.'
|
||||
@@ -1584,13 +1322,12 @@ def chat_with_assistant():
|
||||
import time
|
||||
start_time = time.time()
|
||||
|
||||
# Erhöhtes Timeout für die API-Anfrage
|
||||
response = client.chat.completions.create(
|
||||
model="gpt-4o-mini",
|
||||
messages=api_messages,
|
||||
max_tokens=1000,
|
||||
max_tokens=1000, # Erhöht für ausführlichere Antworten und detaillierte Führungen
|
||||
temperature=0.7,
|
||||
timeout=60 # Erhöht auf 60 Sekunden für bessere Zuverlässigkeit
|
||||
timeout=20 # 20 Sekunden Timeout
|
||||
)
|
||||
|
||||
print(f"OpenAI API-Antwortzeit: {time.time() - start_time:.2f} Sekunden")
|
||||
@@ -1604,30 +1341,12 @@ def chat_with_assistant():
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
error_message = str(e)
|
||||
stack_trace = traceback.format_exc()
|
||||
print(f"Fehler bei der OpenAI-Anfrage: {str(e)}")
|
||||
print(traceback.format_exc())
|
||||
|
||||
print(f"Fehler bei der OpenAI-Anfrage: {error_message}")
|
||||
print(f"Stack Trace: {stack_trace}")
|
||||
|
||||
# Überprüfen auf spezifische Fehlertypen
|
||||
if "timeout" in error_message.lower():
|
||||
return jsonify({
|
||||
'error': 'Die Anfrage hat zu lange gedauert. Bitte versuchen Sie es später erneut.'
|
||||
}), 504
|
||||
elif "rate limit" in error_message.lower():
|
||||
return jsonify({
|
||||
'error': 'API-Ratelimit erreicht. Bitte warten Sie einen Moment und versuchen Sie es erneut.'
|
||||
}), 429
|
||||
elif "internal server error" in error_message.lower() or "500" in error_message:
|
||||
return jsonify({
|
||||
'error': 'Es ist ein Serverfehler aufgetreten. Unser Team wurde benachrichtigt.'
|
||||
}), 500
|
||||
else:
|
||||
# Allgemeine Fehlermeldung
|
||||
return jsonify({
|
||||
'error': 'Bei der Verarbeitung Ihrer Anfrage ist ein Fehler aufgetreten. Bitte versuchen Sie es später erneut.'
|
||||
}), 500
|
||||
return jsonify({
|
||||
'error': f'Fehler bei der OpenAI-Anfrage: {str(e)}'
|
||||
}), 500
|
||||
|
||||
def check_database_query(user_message):
|
||||
"""
|
||||
@@ -1760,42 +1479,14 @@ def refresh_mindmap():
|
||||
if Category.query.count() == 0:
|
||||
create_default_categories()
|
||||
|
||||
# Überprüfe, ob wir bereits einen "Wissen"-Knoten haben
|
||||
wissen_node = MindMapNode.query.filter_by(name="Wissen").first()
|
||||
|
||||
# Wenn kein "Wissen"-Knoten existiert, erstelle ihn
|
||||
if not wissen_node:
|
||||
wissen_node = MindMapNode(
|
||||
name="Wissen",
|
||||
description="Zentrale Wissensbasis",
|
||||
color_code="#4299E1",
|
||||
is_public=True
|
||||
)
|
||||
db.session.add(wissen_node)
|
||||
db.session.commit()
|
||||
|
||||
# Hole alle Kategorien und Knoten
|
||||
categories = Category.query.filter_by(parent_id=None).all()
|
||||
category_tree = [build_category_tree(cat) for cat in categories]
|
||||
|
||||
# Hole alle Mindmap-Knoten außer dem "Wissen"-Knoten
|
||||
nodes = MindMapNode.query.filter(MindMapNode.id != wissen_node.id).all()
|
||||
|
||||
# Vorbereiten der Node- und Edge-Arrays für die Antwort
|
||||
# Hole alle Mindmap-Knoten
|
||||
nodes = MindMapNode.query.all()
|
||||
node_data = []
|
||||
edge_data = []
|
||||
|
||||
# Zuerst den "Wissen"-Knoten hinzufügen
|
||||
node_data.append({
|
||||
'id': wissen_node.id,
|
||||
'name': wissen_node.name,
|
||||
'description': wissen_node.description or '',
|
||||
'color_code': wissen_node.color_code or '#4299E1',
|
||||
'thought_count': len(wissen_node.thoughts),
|
||||
'category_id': wissen_node.category_id
|
||||
})
|
||||
|
||||
# Dann die anderen Knoten
|
||||
for node in nodes:
|
||||
node_obj = {
|
||||
'id': node.id,
|
||||
@@ -1806,28 +1497,15 @@ def refresh_mindmap():
|
||||
'category_id': node.category_id
|
||||
}
|
||||
|
||||
# Verbinde alle Top-Level-Knoten mit dem Wissen-Knoten
|
||||
if not node.parents.all():
|
||||
edge_data.append({
|
||||
'source': wissen_node.id,
|
||||
'target': node.id
|
||||
})
|
||||
|
||||
# Verbindungen zwischen vorhandenen Knoten hinzufügen
|
||||
node_children = node.children.all()
|
||||
for child in node_children:
|
||||
edge_data.append({
|
||||
'source': node.id,
|
||||
'target': child.id
|
||||
})
|
||||
# Verbindungen hinzufügen
|
||||
node_obj['connections'] = [{'target': child.id} for child in node.children]
|
||||
|
||||
node_data.append(node_obj)
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'categories': category_tree,
|
||||
'nodes': node_data,
|
||||
'edges': edge_data
|
||||
'nodes': node_data
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
@@ -1837,46 +1515,21 @@ def refresh_mindmap():
|
||||
'error': 'Datenbankverbindung konnte nicht hergestellt werden'
|
||||
}), 500
|
||||
|
||||
|
||||
# Route zur Mindmap HTML-Seite
|
||||
@app.route('/mindmap')
|
||||
def mindmap_page():
|
||||
return render_template('mindmap.html')
|
||||
|
||||
# Weiterleitung für Community/Forum-Routen
|
||||
@app.route('/community')
|
||||
@app.route('/Community')
|
||||
@app.route('/forum')
|
||||
@app.route('/Forum')
|
||||
@app.route('/community_forum')
|
||||
def redirect_community():
|
||||
"""Leitet alle Community/Forum-URLs zur Startseite um"""
|
||||
return redirect(url_for('index'))
|
||||
# Fehlerbehandlung
|
||||
@app.errorhandler(404)
|
||||
def not_found(e):
|
||||
return jsonify({'error': 'Nicht gefunden'}), 404
|
||||
|
||||
@app.route('/static/js/mindmap-init.js')
|
||||
def serve_mindmap_init_js():
|
||||
"""Bedient die Mindmap-Initialisierungsdatei."""
|
||||
return app.send_static_file('js/mindmap-init.js'), 200, {'Content-Type': 'application/javascript'}
|
||||
@app.errorhandler(400)
|
||||
def bad_request(e):
|
||||
return jsonify({'error': 'Fehlerhafte Anfrage'}), 400
|
||||
|
||||
# Datenbank-Update-Route (admin-geschützt)
|
||||
@app.route('/admin/update-database', methods=['GET', 'POST'])
|
||||
@admin_required
|
||||
def admin_update_database():
|
||||
"""Admin-Route zum Aktualisieren der Datenbank"""
|
||||
message = None
|
||||
success = None
|
||||
|
||||
if request.method == 'POST':
|
||||
try:
|
||||
import update_db
|
||||
update_success = update_db.update_user_table()
|
||||
if update_success:
|
||||
message = "Die Datenbank wurde erfolgreich aktualisiert."
|
||||
success = True
|
||||
else:
|
||||
message = "Es gab ein Problem bei der Aktualisierung der Datenbank."
|
||||
success = False
|
||||
except Exception as e:
|
||||
message = f"Fehler: {str(e)}"
|
||||
success = False
|
||||
|
||||
return render_template('admin/update_database.html', message=message, success=success)
|
||||
@app.errorhandler(500)
|
||||
def server_error(e):
|
||||
return jsonify({'error': 'Serverfehler'}), 500
|
||||
Binary file not shown.
Binary file not shown.
103
db_operations.py
103
db_operations.py
@@ -1,103 +0,0 @@
|
||||
import sqlite3
|
||||
import os
|
||||
import random
|
||||
|
||||
# Verbindung zur Datenbank herstellen
|
||||
db_path = os.path.join(os.getcwd(), 'database', 'systades.db')
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Schema der mind_map_node Tabelle anzeigen
|
||||
cursor.execute("PRAGMA table_info(mind_map_node)")
|
||||
columns = cursor.fetchall()
|
||||
print("Tabellenschema mind_map_node:")
|
||||
for column in columns:
|
||||
print(f"{column[1]} ({column[2]})")
|
||||
|
||||
# Existierende Knoten anzeigen
|
||||
cursor.execute("SELECT id, name, description, color_code FROM mind_map_node LIMIT 5")
|
||||
existing_nodes = cursor.fetchall()
|
||||
print("\nBestehende Knoten:")
|
||||
for node in existing_nodes:
|
||||
print(f"ID: {node[0]}, Name: {node[1]}, Beschreibung: {node[2]}")
|
||||
|
||||
# Mögliche Kategorien abrufen (für die Verknüpfung)
|
||||
cursor.execute("SELECT id, name FROM category")
|
||||
categories = cursor.fetchall()
|
||||
print("\nVerfügbare Kategorien:")
|
||||
for category in categories:
|
||||
print(f"ID: {category[0]}, Name: {category[1]}")
|
||||
|
||||
# Wissenschaftliche Themengebiete für neue Knoten
|
||||
scientific_nodes = [
|
||||
{
|
||||
"name": "Quantenphysik",
|
||||
"description": "Die Quantenphysik befasst sich mit dem Verhalten von Materie und Energie auf atomarer und subatomarer Ebene.",
|
||||
"color_code": "#4B0082", # Indigo
|
||||
"icon": "fa-atom"
|
||||
},
|
||||
{
|
||||
"name": "Neurowissenschaften",
|
||||
"description": "Interdisziplinäre Wissenschaft, die sich mit der Struktur und Funktion des Nervensystems und des Gehirns beschäftigt.",
|
||||
"color_code": "#FF4500", # Orange-Rot
|
||||
"icon": "fa-brain"
|
||||
},
|
||||
{
|
||||
"name": "Künstliche Intelligenz",
|
||||
"description": "Forschungsgebiet der Informatik, das sich mit der Automatisierung intelligenten Verhaltens befasst.",
|
||||
"color_code": "#008080", # Teal
|
||||
"icon": "fa-robot"
|
||||
},
|
||||
{
|
||||
"name": "Klimaforschung",
|
||||
"description": "Wissenschaftliche Untersuchung des Klimas, seiner Variationen und Veränderungen auf allen zeitlichen und räumlichen Skalen.",
|
||||
"color_code": "#2E8B57", # Seegrün
|
||||
"icon": "fa-cloud-sun"
|
||||
},
|
||||
{
|
||||
"name": "Genetik",
|
||||
"description": "Teilgebiet der Biologie, das sich mit Vererbung sowie der Funktion und Wirkung von Genen beschäftigt.",
|
||||
"color_code": "#800080", # Lila
|
||||
"icon": "fa-dna"
|
||||
},
|
||||
{
|
||||
"name": "Astrophysik",
|
||||
"description": "Zweig der Astronomie, der sich mit den physikalischen Eigenschaften des Universums befasst.",
|
||||
"color_code": "#191970", # Mitternachtsblau
|
||||
"icon": "fa-star"
|
||||
}
|
||||
]
|
||||
|
||||
# Neue Knoten hinzufügen
|
||||
print("\nFüge neue wissenschaftliche Knoten hinzu...")
|
||||
for node in scientific_nodes:
|
||||
# Prüfen, ob der Knoten bereits existiert
|
||||
cursor.execute("SELECT id FROM mind_map_node WHERE name = ?", (node["name"],))
|
||||
existing = cursor.fetchone()
|
||||
if existing:
|
||||
print(f"Knoten '{node['name']}' existiert bereits mit ID {existing[0]}")
|
||||
continue
|
||||
|
||||
# Zufällige Kategorie wählen, wenn vorhanden
|
||||
category_id = None
|
||||
if categories:
|
||||
category_id = random.choice(categories)[0]
|
||||
|
||||
# Neuen Knoten einfügen
|
||||
cursor.execute("""
|
||||
INSERT INTO mind_map_node (name, description, color_code, icon, is_public, category_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""", (
|
||||
node["name"],
|
||||
node["description"],
|
||||
node["color_code"],
|
||||
node["icon"],
|
||||
True,
|
||||
category_id
|
||||
))
|
||||
print(f"Knoten '{node['name']}' hinzugefügt")
|
||||
|
||||
# Änderungen übernehmen und Verbindung schließen
|
||||
conn.commit()
|
||||
print("\nDatenbank erfolgreich aktualisiert!")
|
||||
conn.close()
|
||||
@@ -1,17 +0,0 @@
|
||||
version: '3.9'
|
||||
|
||||
services:
|
||||
web:
|
||||
build: .
|
||||
image: systades_app:latest
|
||||
container_name: systades_app
|
||||
restart: always
|
||||
env_file:
|
||||
- .env
|
||||
ports:
|
||||
- "5000:5000"
|
||||
volumes:
|
||||
- ./database:/app/database
|
||||
|
||||
volumes:
|
||||
db_data:
|
||||
@@ -2,12 +2,10 @@
|
||||
# Kopiere diese Datei zu .env und passe die Werte an
|
||||
|
||||
# Flask
|
||||
FLASK_APP=app.py
|
||||
FLASK_ENV=development
|
||||
SECRET_KEY=your-secret-key-replace-in-production
|
||||
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
|
||||
|
||||
@@ -12,7 +12,7 @@ from datetime import datetime
|
||||
|
||||
# Erstelle eine temporäre Flask-App, um die Datenbank zu initialisieren
|
||||
app = Flask(__name__)
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///database/systades.db'
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///systades.db'
|
||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||
db.init_app(app)
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,40 +0,0 @@
|
||||
"""Add missing user fields
|
||||
|
||||
Revision ID: 5a23f8c6db37
|
||||
Revises: d4406f5b12f7
|
||||
Create Date: 2025-05-02 10:45:00.000000
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '5a23f8c6db37'
|
||||
down_revision = 'd4406f5b12f7'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('user', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('bio', sa.Text(), nullable=True))
|
||||
batch_op.add_column(sa.Column('location', sa.String(length=100), nullable=True))
|
||||
batch_op.add_column(sa.Column('website', sa.String(length=200), nullable=True))
|
||||
batch_op.add_column(sa.Column('avatar', sa.String(length=200), nullable=True))
|
||||
batch_op.add_column(sa.Column('last_login', sa.DateTime(), nullable=True))
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('user', schema=None) as batch_op:
|
||||
batch_op.drop_column('last_login')
|
||||
batch_op.drop_column('avatar')
|
||||
batch_op.drop_column('website')
|
||||
batch_op.drop_column('location')
|
||||
batch_op.drop_column('bio')
|
||||
|
||||
# ### end Alembic commands ###
|
||||
54
models.py
54
models.py
@@ -53,20 +53,11 @@ class User(db.Model, UserMixin):
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
is_active = db.Column(db.Boolean, default=True)
|
||||
role = db.Column(db.String(20), default="user") # 'user', 'admin', 'moderator'
|
||||
bio = db.Column(db.Text, nullable=True) # Profil-Bio
|
||||
location = db.Column(db.String(100), nullable=True) # Standort
|
||||
website = db.Column(db.String(200), nullable=True) # Website
|
||||
avatar = db.Column(db.String(200), nullable=True) # Profilbild-URL
|
||||
last_login = db.Column(db.DateTime, nullable=True) # Letzter Login
|
||||
|
||||
# Relationships
|
||||
threads = db.relationship('Thread', backref='creator', lazy=True)
|
||||
messages = db.relationship('Message', backref='author', lazy=True)
|
||||
projects = db.relationship('Project', backref='owner', lazy=True)
|
||||
mindmaps = db.relationship('UserMindmap', backref='user', lazy=True)
|
||||
thoughts = db.relationship('Thought', backref='author', lazy=True)
|
||||
bookmarked_thoughts = db.relationship('Thought', secondary=user_thought_bookmark,
|
||||
lazy='dynamic', backref=db.backref('bookmarked_by', lazy='dynamic'))
|
||||
|
||||
def __repr__(self):
|
||||
return f'<User {self.username}>'
|
||||
@@ -77,14 +68,6 @@ class User(db.Model, UserMixin):
|
||||
def check_password(self, password):
|
||||
return check_password_hash(self.password, password)
|
||||
|
||||
@property
|
||||
def is_admin(self):
|
||||
return self.role == 'admin'
|
||||
|
||||
@is_admin.setter
|
||||
def is_admin(self, value):
|
||||
self.role = 'admin' if value else 'user'
|
||||
|
||||
class Category(db.Model):
|
||||
"""Wissenschaftliche Kategorien für die Gliederung der öffentlichen Mindmap"""
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
@@ -323,40 +306,3 @@ class Document(db.Model):
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Document {self.title}>'
|
||||
|
||||
# Forum-Kategorie-Modell - entspricht den Hauptknotenpunkten der Mindmap
|
||||
class ForumCategory(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
node_id = db.Column(db.Integer, db.ForeignKey('mind_map_node.id'), nullable=False)
|
||||
title = db.Column(db.String(200), nullable=False)
|
||||
description = db.Column(db.Text)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
is_active = db.Column(db.Boolean, default=True)
|
||||
|
||||
# Beziehungen
|
||||
node = db.relationship('MindMapNode', backref='forum_category')
|
||||
posts = db.relationship('ForumPost', backref='category', lazy=True, cascade="all, delete-orphan")
|
||||
|
||||
def __repr__(self):
|
||||
return f'<ForumCategory {self.title}>'
|
||||
|
||||
# Forum-Beitrag-Modell
|
||||
class ForumPost(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
title = db.Column(db.String(200), nullable=False)
|
||||
content = db.Column(db.Text, nullable=False)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||
category_id = db.Column(db.Integer, db.ForeignKey('forum_category.id'), nullable=False)
|
||||
parent_id = db.Column(db.Integer, db.ForeignKey('forum_post.id'), nullable=True)
|
||||
is_pinned = db.Column(db.Boolean, default=False)
|
||||
is_locked = db.Column(db.Boolean, default=False)
|
||||
view_count = db.Column(db.Integer, default=0)
|
||||
|
||||
# Beziehungen
|
||||
author = db.relationship('User', backref='forum_posts')
|
||||
replies = db.relationship('ForumPost', backref=db.backref('parent', remote_side=[id]), lazy=True)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<ForumPost {self.title}>'
|
||||
53
start.sh
53
start.sh
@@ -1,53 +0,0 @@
|
||||
#!/usr/bin/env powershell
|
||||
# Windows PowerShell-Version des Start-Skripts
|
||||
# Datum: 01.05.2025
|
||||
|
||||
# Docker-Status prüfen
|
||||
Write-Host "Prüfe Docker-Status..." -ForegroundColor Cyan
|
||||
try {
|
||||
$status = docker ps -q
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "Docker ist nicht gestartet. Bitte starten Sie Docker Desktop." -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
} catch {
|
||||
Write-Host "Docker ist nicht verfügbar. Bitte installieren Sie Docker Desktop und starten Sie es." -ForegroundColor Red
|
||||
Write-Host $_.Exception.Message
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Alte Container stoppen und entfernen
|
||||
$containerExists = docker ps -a --filter "name=systades_app" -q
|
||||
if ($containerExists) {
|
||||
Write-Host "Stoppe und entferne alten Container..." -ForegroundColor Yellow
|
||||
docker rm -f systades_app
|
||||
}
|
||||
|
||||
# Alte Images löschen
|
||||
Write-Host "Entferne altes Image..." -ForegroundColor Yellow
|
||||
docker rmi -f systades_app:latest
|
||||
|
||||
# Stelle sicher, dass das Datenbankverzeichnis existiert
|
||||
if (-not (Test-Path "database")) {
|
||||
New-Item -Path "database" -ItemType Directory -Force
|
||||
}
|
||||
|
||||
# Docker-Compose Setup neu bauen
|
||||
Write-Host "Baue Container neu..." -ForegroundColor Green
|
||||
docker-compose build --no-cache
|
||||
|
||||
# Docker-Compose neu starten
|
||||
Write-Host "Starte Container..." -ForegroundColor Green
|
||||
docker-compose up -d --force-recreate
|
||||
|
||||
# Warte kurz und prüfe, ob der Container läuft
|
||||
Write-Host "Prüfe Container-Status..." -ForegroundColor Cyan
|
||||
Start-Sleep -Seconds 3
|
||||
docker ps | Select-String "systades_app"
|
||||
|
||||
# Ausgabe
|
||||
Write-Host "`nSystemstatus:" -ForegroundColor Cyan
|
||||
Write-Host "----------------------------------------"
|
||||
Write-Host "Systades-Anwendung ist jetzt unter http://localhost:5000 erreichbar." -ForegroundColor Green
|
||||
Write-Host "Container-Logs können mit 'docker logs -f systades_app' angezeigt werden." -ForegroundColor Green
|
||||
Write-Host "----------------------------------------"
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
/* ChatGPT Assistent Styles - Verbesserte Version */
|
||||
#chatgpt-assistant {
|
||||
font-family: 'Inter', sans-serif;
|
||||
bottom: 5.5rem;
|
||||
z-index: 100;
|
||||
max-height: 85vh;
|
||||
}
|
||||
|
||||
#assistant-chat {
|
||||
@@ -13,15 +10,6 @@
|
||||
border-radius: 0.75rem;
|
||||
overflow: hidden;
|
||||
max-width: calc(100vw - 2rem);
|
||||
max-height: 80vh !important;
|
||||
}
|
||||
|
||||
#assistant-history {
|
||||
max-height: calc(80vh - 150px);
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(156, 163, 175, 0.5) transparent;
|
||||
padding-bottom: 2rem; /* Zusätzlicher Abstand unten */
|
||||
}
|
||||
|
||||
#assistant-toggle {
|
||||
@@ -34,6 +22,11 @@
|
||||
transform: scale(1.1) rotate(10deg);
|
||||
}
|
||||
|
||||
#assistant-history {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(156, 163, 175, 0.5) transparent;
|
||||
}
|
||||
|
||||
#assistant-history::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
@@ -149,21 +142,14 @@
|
||||
.typing-indicator span {
|
||||
height: 8px;
|
||||
width: 8px;
|
||||
background-color: #888;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin: 0 2px;
|
||||
opacity: 0.6;
|
||||
opacity: 0.4;
|
||||
animation: bounce 1.4s infinite ease-in-out;
|
||||
}
|
||||
|
||||
body.dark .typing-indicator span {
|
||||
background-color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
body:not(.dark) .typing-indicator span {
|
||||
background-color: rgba(107, 114, 128, 0.8);
|
||||
}
|
||||
|
||||
.typing-indicator span:nth-child(1) { animation-delay: 0s; }
|
||||
.typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
|
||||
.typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
|
||||
@@ -187,12 +173,11 @@ body:not(.dark) .typing-indicator span {
|
||||
@media (max-width: 640px) {
|
||||
#assistant-chat {
|
||||
width: calc(100vw - 2rem) !important;
|
||||
max-height: 65vh !important;
|
||||
}
|
||||
|
||||
#chatgpt-assistant {
|
||||
right: 1rem;
|
||||
bottom: 6rem;
|
||||
bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,37 +201,3 @@ main {
|
||||
footer {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Verbesserte Farbkontraste für Nachrichtenblasen */
|
||||
.user-message {
|
||||
background-color: rgba(124, 58, 237, 0.1) !important;
|
||||
color: #4B5563 !important;
|
||||
}
|
||||
|
||||
body.dark .user-message {
|
||||
background-color: rgba(124, 58, 237, 0.2) !important;
|
||||
color: #F9FAFB !important;
|
||||
}
|
||||
|
||||
.assistant-message {
|
||||
background-color: #F3F4F6 !important;
|
||||
color: #1F2937 !important;
|
||||
border-left: 3px solid #8B5CF6;
|
||||
}
|
||||
|
||||
body.dark .assistant-message {
|
||||
background-color: rgba(31, 41, 55, 0.5) !important;
|
||||
color: #F9FAFB !important;
|
||||
border-left: 3px solid #8B5CF6;
|
||||
}
|
||||
|
||||
/* Chat-Assistent-Position im Footer-Bereich anpassen */
|
||||
.chat-assistant {
|
||||
max-height: 75vh;
|
||||
bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.chat-assistant .chat-messages {
|
||||
max-height: calc(75vh - 180px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
@@ -40,8 +40,8 @@
|
||||
--light-bg: #f9fafb;
|
||||
--light-text: #1e293b;
|
||||
--light-heading: #0f172a;
|
||||
--light-primary: #7c3aed;
|
||||
--light-primary-hover: #6d28d9;
|
||||
--light-primary: #3b82f6;
|
||||
--light-primary-hover: #4f46e5;
|
||||
--light-secondary: #6b7280;
|
||||
--light-border: #e5e7eb;
|
||||
--light-card-bg: rgba(255, 255, 255, 0.92);
|
||||
@@ -68,37 +68,18 @@ body {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Strikte Trennung: Dark Mode */
|
||||
html.dark body,
|
||||
body.dark {
|
||||
/* Dark Mode */
|
||||
html.dark body {
|
||||
background-color: var(--bg-primary-dark);
|
||||
color: var(--text-primary-dark);
|
||||
}
|
||||
|
||||
/* Strikte Trennung: Light Mode */
|
||||
html:not(.dark) body,
|
||||
/* Light Mode */
|
||||
body:not(.dark) {
|
||||
background-color: var(--light-bg);
|
||||
color: var(--light-text);
|
||||
}
|
||||
|
||||
/* Verbesserte Trennung: Container und Karten */
|
||||
body.dark .card,
|
||||
body.dark .glass-card,
|
||||
body.dark .panel {
|
||||
background-color: var(--bg-secondary-dark);
|
||||
border-color: var(--border-dark);
|
||||
color: var(--text-primary-dark);
|
||||
}
|
||||
|
||||
body:not(.dark) .card,
|
||||
body:not(.dark) .glass-card,
|
||||
body:not(.dark) .panel {
|
||||
background-color: var(--light-card-bg);
|
||||
border-color: var(--light-border);
|
||||
color: var(--light-text);
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-weight: 600;
|
||||
@@ -407,7 +388,7 @@ html.dark ::-webkit-scrollbar-thumb:hover {
|
||||
}
|
||||
|
||||
.section-heading {
|
||||
font-size: 1.75rem;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -474,60 +455,22 @@ body:not(.dark) a:hover {
|
||||
}
|
||||
|
||||
/* Light Mode Buttons */
|
||||
body:not(.dark) button:not(.toggle):not(.plain-btn) {
|
||||
color: white !important;
|
||||
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
body:not(.dark) .btn,
|
||||
body:not(.dark) .btn-primary,
|
||||
body:not(.dark) .btn-secondary,
|
||||
body:not(.dark) .btn-success,
|
||||
body:not(.dark) .btn-danger,
|
||||
body:not(.dark) .btn-warning,
|
||||
body:not(.dark) .btn-info {
|
||||
color: white !important;
|
||||
body:not(.dark) button:not(.toggle) {
|
||||
background-color: var(--light-primary);
|
||||
color: white;
|
||||
border: none;
|
||||
box-shadow: var(--light-shadow);
|
||||
border-radius: 0.375rem;
|
||||
padding: 0.5rem 1rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
body:not(.dark) .btn:hover,
|
||||
body:not(.dark) button:not(.toggle):hover {
|
||||
background: linear-gradient(135deg, #7c3aed, #6d28d9);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(109, 40, 217, 0.3);
|
||||
}
|
||||
|
||||
/* Dark/Light Mode Switch Button */
|
||||
.theme-toggle {
|
||||
position: relative;
|
||||
width: 48px;
|
||||
height: 24px;
|
||||
background: linear-gradient(to right, #7c3aed, #3b82f6);
|
||||
border-radius: 24px;
|
||||
padding: 2px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.theme-toggle::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.theme-toggle.dark::after {
|
||||
transform: translateX(24px);
|
||||
}
|
||||
|
||||
.theme-toggle:hover::after {
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
|
||||
background-color: var(--light-primary-hover);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Light Mode Cards und Panels */
|
||||
@@ -581,488 +524,3 @@ body:not(.dark) .navbar {
|
||||
box-shadow: var(--light-shadow);
|
||||
border-bottom: 1px solid var(--light-border);
|
||||
}
|
||||
|
||||
/* Erweiterte Light-Mode-spezifische Stile */
|
||||
body:not(.dark) .glass-effect {
|
||||
background-color: rgba(255, 255, 255, 0.7);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(209, 213, 219, 0.3);
|
||||
}
|
||||
|
||||
body:not(.dark) .card {
|
||||
background-color: rgba(255, 255, 255, 0.85);
|
||||
border: 1px solid var(--light-border);
|
||||
box-shadow: var(--light-shadow);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
body:not(.dark) .card:hover {
|
||||
box-shadow: 0 8px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Light Mode Buttons mit verbesserter Lesbarkeit */
|
||||
body:not(.dark) .btn-primary {
|
||||
background: linear-gradient(135deg, #6d28d9, #5b21b6);
|
||||
color: white;
|
||||
border: none;
|
||||
transition: all 0.2s ease;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
box-shadow: 0 2px 4px rgba(91, 33, 182, 0.3);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
body:not(.dark) .btn-primary:hover {
|
||||
background: linear-gradient(135deg, #7c3aed, #6d28d9);
|
||||
box-shadow: 0 4px 12px rgba(109, 40, 217, 0.4), 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
body:not(.dark) .btn-secondary {
|
||||
background: linear-gradient(135deg, #ffffff, #f9fafb);
|
||||
color: #1f2937;
|
||||
border: 2px solid #e5e7eb;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
body:not(.dark) .btn-secondary:hover {
|
||||
background: linear-gradient(135deg, #f9fafb, #f3f4f6);
|
||||
border-color: #d1d5db;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.12);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
body:not(.dark) .btn-outline {
|
||||
background-color: transparent;
|
||||
color: var(--light-primary);
|
||||
border: 1px solid var(--light-primary);
|
||||
}
|
||||
|
||||
body:not(.dark) .btn-outline:hover {
|
||||
background-color: rgba(124, 58, 237, 0.05);
|
||||
}
|
||||
|
||||
/* Light Mode Formulare */
|
||||
body:not(.dark) input,
|
||||
body:not(.dark) select,
|
||||
body:not(.dark) textarea {
|
||||
background-color: white;
|
||||
border: 1px solid #d1d5db;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
body:not(.dark) input:focus,
|
||||
body:not(.dark) select:focus,
|
||||
body:not(.dark) textarea:focus {
|
||||
border-color: var(--light-primary);
|
||||
box-shadow: 0 0 0 2px rgba(124, 58, 237, 0.2);
|
||||
}
|
||||
|
||||
/* Light Mode Navigation */
|
||||
body:not(.dark) .sidebar {
|
||||
background-color: white;
|
||||
border-right: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
body:not(.dark) .sidebar-link {
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
body:not(.dark) .sidebar-link:hover {
|
||||
background-color: #f3f4f6;
|
||||
color: var(--light-primary);
|
||||
}
|
||||
|
||||
body:not(.dark) .sidebar-link.active {
|
||||
background-color: rgba(124, 58, 237, 0.08);
|
||||
color: var(--light-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Light Mode Tabellen */
|
||||
body:not(.dark) table {
|
||||
border-color: #e5e7eb;
|
||||
}
|
||||
|
||||
body:not(.dark) th {
|
||||
background-color: #f9fafb;
|
||||
color: #111827;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
body:not(.dark) tr:nth-child(even) {
|
||||
background-color: #f9fafb;
|
||||
}
|
||||
|
||||
body:not(.dark) tr:hover {
|
||||
background-color: #f3f4f6;
|
||||
}
|
||||
|
||||
/* Light Mode Icons */
|
||||
body:not(.dark) .icon {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
body:not(.dark) .icon-primary {
|
||||
color: var(--light-primary);
|
||||
}
|
||||
|
||||
/* Light Mode Alerts/Benachrichtigungen */
|
||||
body:not(.dark) .alert-info {
|
||||
background-color: #eff6ff;
|
||||
border-left: 4px solid #3b82f6;
|
||||
color: #1e40af;
|
||||
}
|
||||
|
||||
body:not(.dark) .alert-success {
|
||||
background-color: #ecfdf5;
|
||||
border-left: 4px solid #10b981;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
body:not(.dark) .alert-warning {
|
||||
background-color: #fffbeb;
|
||||
border-left: 4px solid #f59e0b;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
body:not(.dark) .alert-error {
|
||||
background-color: #fef2f2;
|
||||
border-left: 4px solid #ef4444;
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
/* Light Mode Badge */
|
||||
body:not(.dark) .badge {
|
||||
background-color: #e5e7eb;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
body:not(.dark) .badge-primary {
|
||||
background-color: rgba(124, 58, 237, 0.1);
|
||||
color: var(--light-primary);
|
||||
}
|
||||
|
||||
/* Light Mode Mindmap spezifisch */
|
||||
body:not(.dark) #cy {
|
||||
background-color: rgba(255, 255, 255, 0.7);
|
||||
border: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
body:not(.dark) .node {
|
||||
border: 2px solid white;
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
body:not(.dark) .node:hover,
|
||||
body:not(.dark) .node.selected {
|
||||
box-shadow: 0 0 0 2px rgba(124, 58, 237, 0.5), 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
body:not(.dark) .edge {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
body:not(.dark) .edge:hover,
|
||||
body:not(.dark) .edge.selected {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Footer im Light Mode */
|
||||
body:not(.dark) footer {
|
||||
background-color: rgba(249, 250, 251, 0.7);
|
||||
border-top: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
/* Alpine.js Transitions im Light Mode */
|
||||
body:not(.dark) [x-cloak] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Suchfeldstyling im Light Mode */
|
||||
body:not(.dark) .search-container input {
|
||||
background-color: white;
|
||||
border: 1px solid #d1d5db;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
body:not(.dark) .search-container input:focus {
|
||||
border-color: var(--light-primary);
|
||||
box-shadow: 0 0 0 2px rgba(124, 58, 237, 0.2);
|
||||
}
|
||||
|
||||
body:not(.dark) .search-results {
|
||||
background-color: white;
|
||||
border: 1px solid #e5e7eb;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
body:not(.dark) .search-result-item:hover {
|
||||
background-color: #f3f4f6;
|
||||
}
|
||||
|
||||
/* Profile und Benutzermenü im Light Mode */
|
||||
body:not(.dark) .avatar {
|
||||
border: 2px solid white;
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
body:not(.dark) .user-dropdown {
|
||||
background-color: white;
|
||||
border: 1px solid #e5e7eb;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
body:not(.dark) .user-dropdown-item:hover {
|
||||
background-color: #f3f4f6;
|
||||
}
|
||||
|
||||
/* Medienabfragen für Responsivität */
|
||||
@media (max-width: 640px) {
|
||||
/* Optimierungen für Smartphones */
|
||||
body {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
.hero-heading {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.section-heading {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.card, .panel, .glass-card {
|
||||
padding: 1rem;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
/* Optimierte Touch-Ziele für mobile Geräte */
|
||||
button, .btn, .nav-link, .menu-item {
|
||||
min-height: 44px;
|
||||
min-width: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Verbesserte Lesbarkeit auf kleinen Bildschirmen */
|
||||
p, li, input, textarea, button, .text-sm {
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Anpassungen für Tabellen auf kleinen Bildschirmen */
|
||||
table {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Optimierte Formulare */
|
||||
input, select, textarea {
|
||||
font-size: 16px; /* Verhindert iOS-Zoom bei Fokus */
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Verbesserter Abstand für Touch-Targets */
|
||||
nav a, nav button, .menu-item {
|
||||
margin: 0.25rem 0;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 641px) and (max-width: 1024px) {
|
||||
/* Optimierungen für Tablets */
|
||||
.container {
|
||||
padding-left: 1.5rem;
|
||||
padding-right: 1.5rem;
|
||||
}
|
||||
|
||||
/* Zweispaltige Layouts für mittlere Bildschirme */
|
||||
.grid-cols-1 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
/* Optimierte Navigationsleiste */
|
||||
.navbar {
|
||||
padding: 0.75rem 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1025px) {
|
||||
/* Optimierungen für Desktop */
|
||||
.container {
|
||||
padding-left: 2rem;
|
||||
padding-right: 2rem;
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Mehrspaltige Layouts für große Bildschirme */
|
||||
.grid-cols-1 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
/* Hover-Effekte nur auf Desktop-Geräten */
|
||||
.card:hover, .panel:hover, .glass-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.15), 0 10px 10px -5px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
/* Desktop-spezifische Animationen */
|
||||
.animate-hover {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.animate-hover:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive design improvements */
|
||||
.responsive-grid {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
}
|
||||
|
||||
.responsive-flex {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.responsive-flex > * {
|
||||
flex: 1 1 280px;
|
||||
}
|
||||
|
||||
/* Accessibility improvements */
|
||||
.focus-visible:focus-visible {
|
||||
outline: 2px solid var(--accent-primary-light);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
body.dark .focus-visible:focus-visible {
|
||||
outline: 2px solid var(--accent-primary-dark);
|
||||
}
|
||||
|
||||
/* Print styles */
|
||||
@media print {
|
||||
body {
|
||||
background: white !important;
|
||||
color: black !important;
|
||||
}
|
||||
|
||||
nav, footer, button, .no-print {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
main, article, .card, .panel, .container {
|
||||
width: 100% !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
color: black !important;
|
||||
background: white !important;
|
||||
}
|
||||
|
||||
a {
|
||||
color: black !important;
|
||||
text-decoration: underline !important;
|
||||
}
|
||||
|
||||
a::after {
|
||||
content: " (" attr(href) ")";
|
||||
font-size: 0.8em;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
page-break-after: avoid;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
img {
|
||||
page-break-inside: avoid;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
table {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
@page {
|
||||
margin: 2cm;
|
||||
}
|
||||
}
|
||||
|
||||
/* Light Mode KI-Chatfenster */
|
||||
body:not(.dark) .chat-container {
|
||||
background-color: rgba(255, 255, 255, 0.95);
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
body:not(.dark) .chat-message-ai {
|
||||
background-color: rgba(124, 58, 237, 0.1);
|
||||
border: 1px solid rgba(124, 58, 237, 0.2);
|
||||
}
|
||||
|
||||
body:not(.dark) .chat-message-user {
|
||||
background-color: rgba(59, 130, 246, 0.1);
|
||||
border: 1px solid rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
|
||||
/* Anpassung der Chatfenster-Größe */
|
||||
.chat-assistant {
|
||||
max-height: 85vh; /* Vergrößert von 80vh */
|
||||
bottom: 1rem; /* Etwas höher positionieren */
|
||||
}
|
||||
|
||||
.chat-assistant .chat-messages {
|
||||
max-height: calc(85vh - 180px); /* Angepasst für größeres Fenster */
|
||||
overflow-y: auto;
|
||||
padding-bottom: 2rem; /* Zusätzlicher Abstand um Abschneiden zu vermeiden */
|
||||
}
|
||||
|
||||
/* Verbesserungen für das Mobilmenü */
|
||||
@media (max-width: 768px) {
|
||||
.mobile-menu-container {
|
||||
max-height: 85vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#chatgpt-assistant {
|
||||
bottom: 4.5rem !important;
|
||||
}
|
||||
|
||||
.chat-assistant {
|
||||
max-height: 70vh !important;
|
||||
}
|
||||
|
||||
.chat-assistant .chat-messages {
|
||||
max-height: calc(70vh - 160px) !important;
|
||||
}
|
||||
}
|
||||
@@ -1,253 +0,0 @@
|
||||
/* Mindmap Container Styles */
|
||||
.mindmap-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 600px;
|
||||
background: var(--bg-primary);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Toolbar Styles */
|
||||
.mindmap-toolbar {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
z-index: 1000;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.dark .mindmap-toolbar {
|
||||
background: rgba(30, 41, 59, 0.8);
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* Toolbar Buttons */
|
||||
.mindmap-toolbar button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.mindmap-toolbar button:hover {
|
||||
background: var(--accent-primary);
|
||||
color: white;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.mindmap-toolbar button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.mindmap-toolbar button i {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* Export Group Styles */
|
||||
.export-group {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.export-options {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
margin-top: 8px;
|
||||
padding: 8px;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 160px;
|
||||
}
|
||||
|
||||
.dark .export-options {
|
||||
background: rgba(30, 41, 59, 0.9);
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.export-group:hover .export-options {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.export-options button {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
padding: 8px 12px;
|
||||
justify-content: flex-start;
|
||||
font-size: 14px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/* Context Menu Styles */
|
||||
.mindmap-context-menu {
|
||||
position: fixed;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
padding: 8px;
|
||||
z-index: 1000;
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.dark .mindmap-context-menu {
|
||||
background: rgba(30, 41, 59, 0.9);
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.mindmap-context-menu button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.mindmap-context-menu button:hover {
|
||||
background: var(--accent-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.mindmap-context-menu button i {
|
||||
margin-right: 8px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
/* Node Styles */
|
||||
.mindmap-node {
|
||||
background-color: var(--bg-secondary);
|
||||
border: 2px solid var(--accent-primary);
|
||||
border-radius: 8px;
|
||||
padding: 8px 12px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.mindmap-node:hover {
|
||||
box-shadow: 0 0 0 2px var(--accent-primary);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.mindmap-node.selected {
|
||||
border-color: var(--accent-secondary);
|
||||
box-shadow: 0 0 0 3px var(--accent-secondary);
|
||||
}
|
||||
|
||||
/* Edge Styles */
|
||||
.mindmap-edge {
|
||||
width: 2px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.dark .mindmap-edge {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.mindmap-edge:hover {
|
||||
width: 3px;
|
||||
background-color: var(--accent-primary);
|
||||
}
|
||||
|
||||
/* Animation Styles */
|
||||
@keyframes nodeAppear {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.mindmap-node-new {
|
||||
animation: nodeAppear 0.3s ease forwards;
|
||||
}
|
||||
|
||||
/* Responsive Styles */
|
||||
@media (max-width: 768px) {
|
||||
.mindmap-toolbar {
|
||||
flex-wrap: wrap;
|
||||
width: calc(100% - 32px);
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.export-options {
|
||||
left: 0;
|
||||
right: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* Loading State */
|
||||
.mindmap-loading {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.mindmap-loading-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid var(--bg-secondary);
|
||||
border-top-color: var(--accent-primary);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Tooltip Styles */
|
||||
.mindmap-tooltip {
|
||||
position: absolute;
|
||||
background: var(--bg-secondary);
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
pointer-events: none;
|
||||
z-index: 1000;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
max-width: 200px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.dark .mindmap-tooltip {
|
||||
background: rgba(30, 41, 59, 0.9);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
<svg width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="100" cy="100" r="98" fill="url(#gradient)" stroke="#7C3AED" stroke-width="4"/>
|
||||
<circle cx="100" cy="80" r="36" fill="white"/>
|
||||
<path d="M100 140C77.9086 140 60 157.909 60 180H140C140 157.909 122.091 140 100 140Z" fill="white"/>
|
||||
<defs>
|
||||
<linearGradient id="gradient" x1="0" y1="0" x2="200" y2="200" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#8B5CF6"/>
|
||||
<stop offset="1" stop-color="#3B82F6"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 583 B |
@@ -1,54 +1,25 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate favicon.ico from SVG using cairosvg and PIL
|
||||
"""
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
import io
|
||||
from cairosvg import svg2png
|
||||
from PIL import Image
|
||||
import cairosvg
|
||||
|
||||
# Verzeichnis dieses Skripts
|
||||
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
# Pfad zum SVG-Favicon
|
||||
svg_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'favicon.svg')
|
||||
# Ausgabepfad für das PNG
|
||||
png_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'favicon.png')
|
||||
# Ausgabepfad für das ICO
|
||||
ico_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'favicon.ico')
|
||||
|
||||
def svg_to_ico(svg_path, ico_path, sizes=[16, 32, 48, 64, 128, 256]):
|
||||
"""Convert SVG to multi-size ICO file"""
|
||||
img_io = io.BytesIO()
|
||||
# SVG zu PNG konvertieren
|
||||
cairosvg.svg2png(url=svg_path, write_to=png_path, output_width=512, output_height=512)
|
||||
|
||||
# Höchste Auflösung für Zwischenspeicherung
|
||||
max_size = max(sizes)
|
||||
# PNG zu ICO konvertieren
|
||||
img = Image.open(png_path)
|
||||
img.save(ico_path, sizes=[(16, 16), (32, 32), (48, 48), (64, 64), (128, 128)])
|
||||
|
||||
# SVG in PNG konvertieren
|
||||
with open(svg_path, 'rb') as svg_file:
|
||||
svg_data = svg_file.read()
|
||||
svg2png(bytestring=svg_data, write_to=img_io, output_width=max_size, output_height=max_size)
|
||||
print(f"Favicon erfolgreich erstellt: {ico_path}")
|
||||
|
||||
# PNG in verschiedene Größen konvertieren
|
||||
img = Image.open(img_io)
|
||||
|
||||
# Alle Größen für das ICO-Format vorbereiten
|
||||
img_list = []
|
||||
for size in sizes:
|
||||
resized_img = img.resize((size, size), Image.LANCZOS)
|
||||
img_list.append(resized_img)
|
||||
|
||||
# ICO-Datei speichern
|
||||
img_list[0].save(
|
||||
ico_path,
|
||||
format='ICO',
|
||||
sizes=[(img.width, img.height) for img in img_list],
|
||||
append_images=img_list[1:]
|
||||
)
|
||||
print(f"Favicon {ico_path} wurde erstellt!")
|
||||
|
||||
# Ursprüngliches Favicon konvertieren
|
||||
svg_to_ico(
|
||||
os.path.join(CURRENT_DIR, 'favicon.svg'),
|
||||
os.path.join(CURRENT_DIR, 'favicon.ico')
|
||||
)
|
||||
|
||||
# Neues Neuron-Favicon konvertieren
|
||||
svg_to_ico(
|
||||
os.path.join(CURRENT_DIR, 'neuron-favicon.svg'),
|
||||
os.path.join(CURRENT_DIR, 'neuron-favicon.ico')
|
||||
)
|
||||
# Optional: PNG-Datei löschen, wenn nur ICO benötigt wird
|
||||
# os.remove(png_path)
|
||||
@@ -1,29 +0,0 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Hintergrund -->
|
||||
<rect width="32" height="32" rx="8" fill="#6d28d9" />
|
||||
|
||||
<!-- Mindmap-Punkte -->
|
||||
<!-- Zentraler Punkt -->
|
||||
<circle cx="16" cy="16" r="3.5" fill="#a78bfa" />
|
||||
|
||||
<!-- Umgebende Punkte -->
|
||||
<circle cx="8" cy="10" r="2.5" fill="#8b5cf6" />
|
||||
<circle cx="24" cy="10" r="2.5" fill="#8b5cf6" />
|
||||
<circle cx="16" cy="26" r="2.5" fill="#8b5cf6" />
|
||||
|
||||
<!-- Verbindende Linien -->
|
||||
<path d="M16 16 L8 10" stroke="white" stroke-width="1" stroke-linecap="round" />
|
||||
<path d="M16 16 L24 10" stroke="white" stroke-width="1" stroke-linecap="round" />
|
||||
<path d="M16 16 L16 26" stroke="white" stroke-width="1" stroke-linecap="round" />
|
||||
|
||||
<!-- Weitere Verbindungslinien für mehr Komplexität -->
|
||||
<path d="M8 10 L16 26" stroke="#c4b5fd" stroke-width="0.8" stroke-linecap="round" stroke-dasharray="2 1" />
|
||||
<path d="M24 10 L16 26" stroke="#c4b5fd" stroke-width="0.8" stroke-linecap="round" stroke-dasharray="2 1" />
|
||||
<path d="M8 10 L24 10" stroke="#c4b5fd" stroke-width="0.8" stroke-linecap="round" stroke-dasharray="2 1" />
|
||||
|
||||
<!-- Kleine Dekoration-Punkte für Hintergrund-Ähnlichkeit -->
|
||||
<circle cx="5" cy="20" r="0.8" fill="#ddd6fe" opacity="0.7" />
|
||||
<circle cx="27" cy="20" r="0.8" fill="#ddd6fe" opacity="0.7" />
|
||||
<circle cx="20" cy="5" r="0.8" fill="#ddd6fe" opacity="0.7" />
|
||||
<circle cx="12" cy="5" r="0.8" fill="#ddd6fe" opacity="0.7" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB |
@@ -1,59 +0,0 @@
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Hintergrund mit Farbverlauf -->
|
||||
<rect width="64" height="64" rx="16" fill="url(#paint0_linear)" />
|
||||
|
||||
<!-- Mindmap-Punkte -->
|
||||
<!-- Zentraler Punkt -->
|
||||
<circle cx="32" cy="32" r="8" fill="url(#glow_gradient)" filter="url(#glow)" />
|
||||
|
||||
<!-- Umgebende Punkte -->
|
||||
<circle cx="16" cy="20" r="6" fill="#8b5cf6" />
|
||||
<circle cx="48" cy="20" r="6" fill="#8b5cf6" />
|
||||
<circle cx="32" cy="52" r="6" fill="#8b5cf6" />
|
||||
<circle cx="16" cy="48" r="4" fill="#a78bfa" />
|
||||
<circle cx="48" cy="48" r="4" fill="#a78bfa" />
|
||||
|
||||
<!-- Verbindende Linien (Hauptpfade) -->
|
||||
<path d="M32 32 L16 20" stroke="white" stroke-width="2" stroke-linecap="round" />
|
||||
<path d="M32 32 L48 20" stroke="white" stroke-width="2" stroke-linecap="round" />
|
||||
<path d="M32 32 L32 52" stroke="white" stroke-width="2" stroke-linecap="round" />
|
||||
<path d="M32 32 L16 48" stroke="white" stroke-width="2" stroke-linecap="round" />
|
||||
<path d="M32 32 L48 48" stroke="white" stroke-width="2" stroke-linecap="round" />
|
||||
|
||||
<!-- Zusätzliche Verbindungslinien -->
|
||||
<path d="M16 20 L16 48" stroke="#c4b5fd" stroke-width="1.5" stroke-linecap="round" stroke-dasharray="3 2" />
|
||||
<path d="M48 20 L48 48" stroke="#c4b5fd" stroke-width="1.5" stroke-linecap="round" stroke-dasharray="3 2" />
|
||||
<path d="M16 20 L48 20" stroke="#c4b5fd" stroke-width="1.5" stroke-linecap="round" stroke-dasharray="3 2" />
|
||||
<path d="M16 48 L32 52" stroke="#c4b5fd" stroke-width="1.5" stroke-linecap="round" stroke-dasharray="3 2" />
|
||||
<path d="M48 48 L32 52" stroke="#c4b5fd" stroke-width="1.5" stroke-linecap="round" stroke-dasharray="3 2" />
|
||||
|
||||
<!-- Kleine Dekoration-Punkte für Hintergrund-Ähnlichkeit -->
|
||||
<circle cx="10" cy="36" r="1.5" fill="#ddd6fe" opacity="0.7" />
|
||||
<circle cx="54" cy="36" r="1.5" fill="#ddd6fe" opacity="0.7" />
|
||||
<circle cx="40" cy="10" r="1.5" fill="#ddd6fe" opacity="0.7" />
|
||||
<circle cx="24" cy="10" r="1.5" fill="#ddd6fe" opacity="0.7" />
|
||||
<circle cx="20" cy="36" r="1.2" fill="#ddd6fe" opacity="0.5" />
|
||||
<circle cx="44" cy="36" r="1.2" fill="#ddd6fe" opacity="0.5" />
|
||||
<circle cx="32" cy="16" r="1.2" fill="#ddd6fe" opacity="0.5" />
|
||||
|
||||
<!-- Definitionen für Farbverläufe und Effekte -->
|
||||
<defs>
|
||||
<!-- Haupthintergrund-Farbverlauf -->
|
||||
<linearGradient id="paint0_linear" x1="0" y1="0" x2="64" y2="64" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#6d28d9" />
|
||||
<stop offset="1" stop-color="#4c1d95" />
|
||||
</linearGradient>
|
||||
|
||||
<!-- Glüheffekt für den zentralen Punkt -->
|
||||
<filter id="glow" x="20" y="20" width="24" height="24" filterUnits="userSpaceOnUse">
|
||||
<feGaussianBlur stdDeviation="2" result="blur" />
|
||||
<feComposite in="SourceGraphic" in2="blur" operator="over" />
|
||||
</filter>
|
||||
|
||||
<!-- Farbverlauf für den zentralen Punkt -->
|
||||
<linearGradient id="glow_gradient" x1="24" y1="24" x2="40" y2="40" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#a78bfa" />
|
||||
<stop offset="1" stop-color="#8b5cf6" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.0 KiB |
@@ -1,648 +0,0 @@
|
||||
/**
|
||||
* Mindmap-Initialisierer
|
||||
* Lädt und initialisiert die Mindmap-Visualisierung
|
||||
*/
|
||||
|
||||
// Warte bis DOM geladen ist
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Prüfe, ob wir auf der Mindmap-Seite sind
|
||||
const cyContainer = document.getElementById('cy');
|
||||
|
||||
if (!cyContainer) {
|
||||
console.log('Kein Mindmap-Container gefunden, überspringe Initialisierung.');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Initialisiere Mindmap-Visualisierung...');
|
||||
|
||||
// Prüfe, ob Cytoscape.js verfügbar ist
|
||||
if (typeof cytoscape === 'undefined') {
|
||||
loadScript('/static/js/cytoscape.min.js', initMindmap);
|
||||
} else {
|
||||
initMindmap();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Lädt ein Script dynamisch
|
||||
* @param {string} src - Quelldatei
|
||||
* @param {Function} callback - Callback nach dem Laden
|
||||
*/
|
||||
function loadScript(src, callback) {
|
||||
const script = document.createElement('script');
|
||||
script.src = src;
|
||||
script.onload = callback;
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialisiert die Mindmap-Visualisierung
|
||||
*/
|
||||
function initMindmap() {
|
||||
const cyContainer = document.getElementById('cy');
|
||||
|
||||
// Erstelle Cytoscape-Instanz
|
||||
const cy = cytoscape({
|
||||
container: cyContainer,
|
||||
style: getNeuralNetworkStyles(),
|
||||
layout: {
|
||||
name: 'cose',
|
||||
animate: true,
|
||||
animationDuration: 1500,
|
||||
nodeDimensionsIncludeLabels: true,
|
||||
padding: 100,
|
||||
spacingFactor: 1.5,
|
||||
randomize: true,
|
||||
componentSpacing: 100,
|
||||
nodeRepulsion: 8000,
|
||||
edgeElasticity: 100,
|
||||
nestingFactor: 1.2,
|
||||
gravity: 80,
|
||||
idealEdgeLength: 150
|
||||
},
|
||||
wheelSensitivity: 0.1, // Sanfterer Zoom
|
||||
minZoom: 0.3,
|
||||
maxZoom: 2.5,
|
||||
});
|
||||
|
||||
// Daten vom Server laden
|
||||
loadMindmapData(cy);
|
||||
|
||||
// Event-Handler zuweisen
|
||||
setupEventListeners(cy);
|
||||
|
||||
// Globale Referenz für Externe Zugriffe
|
||||
window.cy = cy;
|
||||
window.mindmapInstance = {
|
||||
cy: cy,
|
||||
selectedNode: null,
|
||||
centerNodeInView: function(node) {
|
||||
cy.animate({
|
||||
center: { eles: node },
|
||||
zoom: 1.2,
|
||||
duration: 800,
|
||||
easing: 'ease-in-out-cubic'
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt die Mindmap-Daten vom Server
|
||||
* @param {Object} cy - Cytoscape-Instanz
|
||||
*/
|
||||
function loadMindmapData(cy) {
|
||||
fetch('/api/mindmap')
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP Fehler: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (!data.nodes || data.nodes.length === 0) {
|
||||
console.log('Keine Daten gefunden, versuche Refresh-API...');
|
||||
return fetch('/api/refresh-mindmap')
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP Fehler beim Refresh: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
});
|
||||
}
|
||||
return data;
|
||||
})
|
||||
.then(data => {
|
||||
console.log('Mindmap-Daten geladen:', data);
|
||||
|
||||
// Cytoscape-Elemente vorbereiten
|
||||
const elements = [];
|
||||
|
||||
// Prüfen, ob "Wissen"-Knoten existiert
|
||||
let rootNode = data.nodes.find(node => node.name === "Wissen");
|
||||
|
||||
// Wenn nicht, Root-Knoten hinzufügen
|
||||
if (!rootNode) {
|
||||
rootNode = {
|
||||
id: 'root',
|
||||
name: 'Wissen',
|
||||
description: 'Zentrale Wissensbasis',
|
||||
color_code: '#4299E1'
|
||||
};
|
||||
data.nodes.unshift(rootNode);
|
||||
}
|
||||
|
||||
// Knoten hinzufügen mit zufälligen Werten für neuronales Netzwerk
|
||||
data.nodes.forEach(node => {
|
||||
// Neuronenzell-Größe und Aktivität
|
||||
const neuronSize = Math.floor(Math.random() * 8) + 3;
|
||||
const neuronActivity = Math.random() * 0.7 + 0.3; // Aktivitätslevel zwischen 0.3 und 1.0
|
||||
|
||||
elements.push({
|
||||
group: 'nodes',
|
||||
data: {
|
||||
id: node.id.toString(),
|
||||
name: node.name,
|
||||
description: node.description || '',
|
||||
color: node.color_code || '#8B5CF6',
|
||||
isRoot: node.name === 'Wissen',
|
||||
neuronSize: neuronSize,
|
||||
neuronActivity: neuronActivity
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Kanten hinzufügen, wenn vorhanden
|
||||
if (data.edges && data.edges.length > 0) {
|
||||
data.edges.forEach(edge => {
|
||||
elements.push({
|
||||
group: 'edges',
|
||||
data: {
|
||||
id: `${edge.source}-${edge.target}`,
|
||||
source: edge.source.toString(),
|
||||
target: edge.target.toString(),
|
||||
strength: Math.random() * 0.6 + 0.2 // Zufällige Verbindungsstärke zwischen 0.2 und 0.8
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Wenn keine Kanten definiert sind, verbinde alle Knoten mit dem Root-Knoten
|
||||
const rootId = rootNode.id.toString();
|
||||
|
||||
data.nodes.forEach(node => {
|
||||
if (node.id.toString() !== rootId) {
|
||||
elements.push({
|
||||
group: 'edges',
|
||||
data: {
|
||||
id: `${rootId}-${node.id}`,
|
||||
source: rootId,
|
||||
target: node.id.toString(),
|
||||
strength: Math.random() * 0.6 + 0.2
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Elemente zu Cytoscape hinzufügen
|
||||
cy.elements().remove();
|
||||
cy.add(elements);
|
||||
|
||||
// Layout anwenden
|
||||
cy.layout({
|
||||
name: 'cose',
|
||||
animate: true,
|
||||
animationDuration: 1800,
|
||||
nodeDimensionsIncludeLabels: true,
|
||||
padding: 100,
|
||||
spacingFactor: 1.8,
|
||||
randomize: false,
|
||||
fit: true,
|
||||
componentSpacing: 100,
|
||||
nodeRepulsion: 8000,
|
||||
edgeElasticity: 100
|
||||
}).run();
|
||||
|
||||
// Neuronale Netzwerk-Effekte hinzufügen
|
||||
setTimeout(() => {
|
||||
addNeuralNetworkEffects(cy);
|
||||
}, 2000);
|
||||
|
||||
// Nach dem Laden Event auslösen
|
||||
document.dispatchEvent(new CustomEvent('mindmap-loaded'));
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Fehler beim Laden der Mindmap-Daten:', error);
|
||||
|
||||
// Fallback mit Standard-Daten
|
||||
const fallbackData = {
|
||||
nodes: [
|
||||
{ id: 1, name: 'Wissen', description: 'Zentrale Wissensbasis', color_code: '#4299E1' },
|
||||
{ id: 2, name: 'Philosophie', description: 'Philosophisches Denken', color_code: '#9F7AEA' },
|
||||
{ id: 3, name: 'Wissenschaft', description: 'Wissenschaftliche Erkenntnisse', color_code: '#48BB78' },
|
||||
{ id: 4, name: 'Technologie', description: 'Technologische Entwicklungen', color_code: '#ED8936' },
|
||||
{ id: 5, name: 'Künste', description: 'Künstlerische Ausdrucksformen', color_code: '#ED64A6' }
|
||||
],
|
||||
edges: [
|
||||
{ source: 1, target: 2 },
|
||||
{ source: 1, target: 3 },
|
||||
{ source: 1, target: 4 },
|
||||
{ source: 1, target: 5 }
|
||||
]
|
||||
};
|
||||
|
||||
const fallbackElements = [];
|
||||
|
||||
// Knoten hinzufügen mit Neuronen-Eigenschaften
|
||||
fallbackData.nodes.forEach(node => {
|
||||
const neuronSize = Math.floor(Math.random() * 8) + 3;
|
||||
const neuronActivity = Math.random() * 0.7 + 0.3;
|
||||
|
||||
fallbackElements.push({
|
||||
group: 'nodes',
|
||||
data: {
|
||||
id: node.id.toString(),
|
||||
name: node.name,
|
||||
description: node.description || '',
|
||||
color: node.color_code || '#8B5CF6',
|
||||
isRoot: node.name === 'Wissen',
|
||||
neuronSize: neuronSize,
|
||||
neuronActivity: neuronActivity
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Kanten hinzufügen
|
||||
fallbackData.edges.forEach(edge => {
|
||||
fallbackElements.push({
|
||||
group: 'edges',
|
||||
data: {
|
||||
id: `${edge.source}-${edge.target}`,
|
||||
source: edge.source.toString(),
|
||||
target: edge.target.toString(),
|
||||
strength: Math.random() * 0.6 + 0.2
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Elemente zu Cytoscape hinzufügen
|
||||
cy.elements().remove();
|
||||
cy.add(fallbackElements);
|
||||
|
||||
// Layout anwenden
|
||||
cy.layout({
|
||||
name: 'cose',
|
||||
animate: true,
|
||||
animationDuration: 1800,
|
||||
nodeDimensionsIncludeLabels: true,
|
||||
fit: true
|
||||
}).run();
|
||||
|
||||
// Neuronale Netzwerk-Effekte hinzufügen
|
||||
setTimeout(() => {
|
||||
addNeuralNetworkEffects(cy);
|
||||
}, 2000);
|
||||
|
||||
// Nach dem Laden Event auslösen
|
||||
document.dispatchEvent(new CustomEvent('mindmap-loaded'));
|
||||
});
|
||||
}
|
||||
|
||||
// Neuronales Netzwerk-Effekte hinzufügen
|
||||
function addNeuralNetworkEffects(cy) {
|
||||
cy.nodes().forEach(node => {
|
||||
const originalPos = node.position();
|
||||
const activity = node.data('neuronActivity') || 0.5;
|
||||
|
||||
// Subtile Pulsierende Bewegung für Neuronen
|
||||
setInterval(() => {
|
||||
const randomFactor = Math.random() * 0.5 + 0.5; // Zufälliger Faktor zwischen 0.5 und 1
|
||||
const offset = (Math.random() - 0.5) * activity;
|
||||
const pulseIntensity = 0.7 + (Math.sin(Date.now() / 2000) * 0.3 * activity * randomFactor);
|
||||
|
||||
// Leichtes Pulsieren und Bewegung basierend auf "Neuronenaktivität"
|
||||
node.animate({
|
||||
position: {
|
||||
x: originalPos.x + offset,
|
||||
y: originalPos.y + offset
|
||||
},
|
||||
style: {
|
||||
'background-opacity': Math.max(0.7, pulseIntensity),
|
||||
'shadow-opacity': Math.max(0.5, pulseIntensity * 0.8)
|
||||
},
|
||||
duration: 3000 + (Math.random() * 2000),
|
||||
easing: 'ease-in-out-cubic',
|
||||
complete: function() {
|
||||
node.animate({
|
||||
position: { x: originalPos.x, y: originalPos.y },
|
||||
style: {
|
||||
'background-opacity': 0.9,
|
||||
'shadow-opacity': 0.6
|
||||
},
|
||||
duration: 3000 + (Math.random() * 2000),
|
||||
easing: 'ease-in-out-cubic'
|
||||
});
|
||||
}
|
||||
});
|
||||
}, 6000 + Math.random() * 4000); // Leicht zufällige Intervalle für organischeres Aussehen
|
||||
|
||||
// Zusätzliche "synaptische" Effekte für Kanten
|
||||
node.connectedEdges().forEach(edge => {
|
||||
const strength = edge.data('strength') || 0.5;
|
||||
|
||||
// Pulsierende Aktivität entlang der Kanten
|
||||
setInterval(() => {
|
||||
const pulseIntensity = 0.5 + (Math.sin(Date.now() / 1500) * 0.5 * strength);
|
||||
|
||||
edge.animate({
|
||||
style: {
|
||||
'line-opacity': Math.max(0.3, pulseIntensity),
|
||||
'width': 1 + (pulseIntensity * 0.6)
|
||||
},
|
||||
duration: 1200,
|
||||
easing: 'ease-in-out'
|
||||
});
|
||||
}, 3000 + (Math.random() * 2000));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Richtet Event-Listener für die Mindmap ein
|
||||
* @param {Object} cy - Cytoscape-Instanz
|
||||
*/
|
||||
function setupEventListeners(cy) {
|
||||
// Klick auf Knoten
|
||||
cy.on('tap', 'node', function(evt) {
|
||||
const node = evt.target;
|
||||
|
||||
// Alle vorherigen Hervorhebungen zurücksetzen
|
||||
cy.nodes().forEach(n => {
|
||||
n.removeStyle();
|
||||
n.connectedEdges().removeStyle();
|
||||
});
|
||||
|
||||
// Speichere ausgewählten Knoten
|
||||
window.mindmapInstance.selectedNode = node;
|
||||
|
||||
// Aktiviere leuchtenden Effekt statt Umkreisung
|
||||
node.style({
|
||||
'background-opacity': 1,
|
||||
'background-color': node.data('color'),
|
||||
'shadow-color': node.data('color'),
|
||||
'shadow-opacity': 1,
|
||||
'shadow-blur': 15,
|
||||
'shadow-offset-x': 0,
|
||||
'shadow-offset-y': 0
|
||||
});
|
||||
|
||||
// Verbundene Kanten und Knoten hervorheben
|
||||
const connectedEdges = node.connectedEdges();
|
||||
const connectedNodes = node.neighborhood('node');
|
||||
|
||||
connectedEdges.style({
|
||||
'line-color': '#a78bfa',
|
||||
'target-arrow-color': '#a78bfa',
|
||||
'source-arrow-color': '#a78bfa',
|
||||
'line-opacity': 0.8,
|
||||
'width': 2
|
||||
});
|
||||
|
||||
connectedNodes.style({
|
||||
'shadow-opacity': 0.7,
|
||||
'shadow-blur': 10,
|
||||
'shadow-color': '#a78bfa'
|
||||
});
|
||||
|
||||
// Info-Panel aktualisieren
|
||||
updateInfoPanel(node);
|
||||
|
||||
// Seitenleiste aktualisieren
|
||||
updateSidebar(node);
|
||||
});
|
||||
|
||||
// Klick auf Hintergrund - Auswahl zurücksetzen
|
||||
cy.on('tap', function(evt) {
|
||||
if (evt.target === cy) {
|
||||
resetSelection(cy);
|
||||
}
|
||||
});
|
||||
|
||||
// Smooth Zoom mit Mouse Wheel
|
||||
cy.on('mousewheel', function(evt) {
|
||||
const delta = evt.originalEvent.deltaY;
|
||||
const factor = delta > 0 ? 0.95 : 1.05;
|
||||
|
||||
cy.animate({
|
||||
zoom: cy.zoom() * factor,
|
||||
duration: 100
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Aktualisiert das Info-Panel mit Daten des ausgewählten Knotens
|
||||
function updateInfoPanel(node) {
|
||||
const nodeInfoPanel = document.getElementById('node-info-panel');
|
||||
const nodeDescription = document.getElementById('node-description');
|
||||
const connectedNodes = document.getElementById('connected-nodes');
|
||||
const panelTitle = nodeInfoPanel.querySelector('.info-panel-title');
|
||||
|
||||
if (!nodeInfoPanel || !nodeDescription || !connectedNodes) return;
|
||||
|
||||
// Titel und Beschreibung aktualisieren
|
||||
panelTitle.textContent = node.data('name');
|
||||
nodeDescription.textContent = node.data('description') || 'Keine Beschreibung verfügbar.';
|
||||
|
||||
// Verbundene Knoten anzeigen
|
||||
connectedNodes.innerHTML = '';
|
||||
|
||||
// Verbundene Knoten sammeln (direktes Neighborhood)
|
||||
const connectedNodesList = [];
|
||||
node.neighborhood('node').forEach(n => {
|
||||
if (!connectedNodesList.includes(n) && n.id() !== node.id()) {
|
||||
connectedNodesList.push(n);
|
||||
}
|
||||
});
|
||||
|
||||
// Verbundene Knoten im Panel anzeigen
|
||||
if (connectedNodesList.length > 0) {
|
||||
connectedNodesList.forEach(connectedNode => {
|
||||
const nodeLink = document.createElement('span');
|
||||
nodeLink.className = 'inline-block px-2 py-1 text-xs rounded-md m-1 cursor-pointer';
|
||||
nodeLink.style.backgroundColor = connectedNode.data('color');
|
||||
nodeLink.textContent = connectedNode.data('name');
|
||||
|
||||
// Beim Klick auf den verbundenen Knoten zu diesem wechseln
|
||||
nodeLink.addEventListener('click', function() {
|
||||
resetSelection(cy);
|
||||
|
||||
// Verzögerung vor der neuen Auswahl für besseren visuellen Übergang
|
||||
setTimeout(() => {
|
||||
connectedNode.trigger('tap');
|
||||
}, 50);
|
||||
});
|
||||
|
||||
connectedNodes.appendChild(nodeLink);
|
||||
});
|
||||
} else {
|
||||
connectedNodes.innerHTML = '<span class="text-sm italic">Keine verbundenen Knoten</span>';
|
||||
}
|
||||
|
||||
// Panel anzeigen mit Animation
|
||||
nodeInfoPanel.style.transition = 'all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1)';
|
||||
nodeInfoPanel.classList.add('visible');
|
||||
}
|
||||
|
||||
// Aktualisiert die Seitenleiste
|
||||
function updateSidebar(node) {
|
||||
// Alle standard Panels ausblenden
|
||||
document.querySelectorAll('[data-sidebar]').forEach(panel => {
|
||||
if (panel.getAttribute('data-sidebar') === 'node-description') {
|
||||
// Beschreibungs-Panel anzeigen
|
||||
panel.classList.remove('hidden');
|
||||
|
||||
// Titel und Beschreibung aktualisieren
|
||||
const titleElement = panel.querySelector('[data-node-title]');
|
||||
const descriptionElement = panel.querySelector('[data-node-description]');
|
||||
|
||||
if (titleElement) {
|
||||
titleElement.textContent = node.data('name');
|
||||
}
|
||||
|
||||
if (descriptionElement) {
|
||||
descriptionElement.innerHTML = formatDescription(node.data('description') || 'Keine Beschreibung verfügbar.');
|
||||
}
|
||||
} else {
|
||||
// Andere Panels ausblenden
|
||||
panel.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Formatiert die Beschreibung mit etwas HTML-Markup
|
||||
function formatDescription(text) {
|
||||
return text
|
||||
.replace(/\n/g, '<br>')
|
||||
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/\*(.*?)\*/g, '<em>$1</em>')
|
||||
.replace(/`(.*?)`/g, '<code>$1</code>');
|
||||
}
|
||||
|
||||
// Setzt die Auswahl zurück
|
||||
function resetSelection(cy) {
|
||||
// Alle Stile zurücksetzen
|
||||
cy.nodes().forEach(n => {
|
||||
n.removeStyle();
|
||||
});
|
||||
|
||||
cy.edges().forEach(e => {
|
||||
e.removeStyle();
|
||||
});
|
||||
|
||||
// Kein Knoten ausgewählt
|
||||
window.mindmapInstance.selectedNode = null;
|
||||
|
||||
// Info-Panel ausblenden
|
||||
const nodeInfoPanel = document.getElementById('node-info-panel');
|
||||
if (nodeInfoPanel) {
|
||||
nodeInfoPanel.classList.remove('visible');
|
||||
}
|
||||
|
||||
// Standard-Seitenleisten-Panels anzeigen
|
||||
document.querySelectorAll('[data-sidebar]').forEach(panel => {
|
||||
if (panel.getAttribute('data-sidebar') === 'node-description') {
|
||||
panel.classList.add('hidden');
|
||||
} else {
|
||||
panel.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die Stile für das neuronale Netzwerk-Design zurück
|
||||
* @returns {Array} Stildefinitionen für Cytoscape
|
||||
*/
|
||||
function getNeuralNetworkStyles() {
|
||||
return [
|
||||
// Neuronen (Knoten)
|
||||
{
|
||||
selector: 'node',
|
||||
style: {
|
||||
'label': 'data(name)',
|
||||
'text-valign': 'bottom',
|
||||
'text-halign': 'center',
|
||||
'color': '#ffffff',
|
||||
'text-outline-width': 2,
|
||||
'text-outline-color': '#0a0e19',
|
||||
'text-outline-opacity': 0.9,
|
||||
'font-size': 10,
|
||||
'text-margin-y': 6,
|
||||
'width': 'mapData(neuronSize, 3, 10, 15, 40)',
|
||||
'height': 'mapData(neuronSize, 3, 10, 15, 40)',
|
||||
'background-color': 'data(color)',
|
||||
'background-opacity': 0.9,
|
||||
'border-width': 0,
|
||||
'shape': 'ellipse',
|
||||
'shadow-blur': 'mapData(neuronActivity, 0.3, 1, 5, 15)',
|
||||
'shadow-color': 'data(color)',
|
||||
'shadow-opacity': 0.6,
|
||||
'shadow-offset-x': 0,
|
||||
'shadow-offset-y': 0,
|
||||
'text-wrap': 'wrap',
|
||||
'text-max-width': 100,
|
||||
'transition-property': 'background-color, shadow-color, shadow-opacity, shadow-blur',
|
||||
'transition-duration': '0.3s'
|
||||
}
|
||||
},
|
||||
// Synapsen (Kanten)
|
||||
{
|
||||
selector: 'edge',
|
||||
style: {
|
||||
'curve-style': 'bezier',
|
||||
'line-color': '#8a8aaa',
|
||||
'width': 'mapData(strength, 0.2, 0.8, 0.8, 2)',
|
||||
'line-opacity': 'mapData(strength, 0.2, 0.8, 0.4, 0.7)',
|
||||
'target-arrow-shape': 'none', // Keine Pfeilspitzen bei Neuronen
|
||||
'target-arrow-color': '#8a8aaa',
|
||||
'arrow-scale': 0.6,
|
||||
'transition-property': 'line-color, line-opacity, width',
|
||||
'transition-duration': '0.3s'
|
||||
}
|
||||
},
|
||||
// Schwache Verbindungen
|
||||
{
|
||||
selector: 'edge[strength <= 0.4]',
|
||||
style: {
|
||||
'line-style': 'dotted'
|
||||
}
|
||||
},
|
||||
// Mittlere Verbindungen
|
||||
{
|
||||
selector: 'edge[strength > 0.4][strength <= 0.6]',
|
||||
style: {
|
||||
'line-style': 'dashed'
|
||||
}
|
||||
},
|
||||
// Starke Verbindungen
|
||||
{
|
||||
selector: 'edge[strength > 0.6]',
|
||||
style: {
|
||||
'line-style': 'solid'
|
||||
}
|
||||
},
|
||||
// Wurzelknoten (speziell gestaltet)
|
||||
{
|
||||
selector: 'node[isRoot]',
|
||||
style: {
|
||||
'font-size': 12,
|
||||
'font-weight': 'bold',
|
||||
'width': 50,
|
||||
'height': 50,
|
||||
'background-color': '#6366f1',
|
||||
'shadow-blur': 20,
|
||||
'shadow-color': '#6366f1',
|
||||
'shadow-opacity': 0.8,
|
||||
'text-margin-y': 8
|
||||
}
|
||||
},
|
||||
// Hover-Effekt für Knoten
|
||||
{
|
||||
selector: 'node:hover',
|
||||
style: {
|
||||
'shadow-blur': 20,
|
||||
'shadow-opacity': 0.9,
|
||||
'transition-property': 'shadow-opacity, shadow-blur',
|
||||
'transition-duration': '0.2s'
|
||||
}
|
||||
},
|
||||
// Hover-Effekt für Kanten
|
||||
{
|
||||
selector: 'edge:hover',
|
||||
style: {
|
||||
'line-color': '#a78bfa',
|
||||
'line-opacity': 0.8,
|
||||
'width': 2
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -1,241 +0,0 @@
|
||||
/**
|
||||
* Mindmap Interaction Enhancement
|
||||
* Verbessert die Interaktion mit der Mindmap und steuert die Seitenleisten-Anzeige
|
||||
*/
|
||||
|
||||
// Globale Variablen
|
||||
let cy;
|
||||
let selectedNode = null;
|
||||
let isLegendVisible = true;
|
||||
|
||||
// Initialisierung der Mindmap
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Cytoscape-Container initialisieren
|
||||
cy = cytoscape({
|
||||
container: document.getElementById('cy'),
|
||||
style: [
|
||||
{
|
||||
selector: 'node',
|
||||
style: {
|
||||
'background-color': '#60a5fa',
|
||||
'label': 'data(label)',
|
||||
'text-valign': 'center',
|
||||
'text-halign': 'center',
|
||||
'text-wrap': 'wrap',
|
||||
'text-max-width': '100px',
|
||||
'font-size': '12px',
|
||||
'color': '#fff',
|
||||
'text-outline-color': '#000',
|
||||
'text-outline-width': '2px',
|
||||
'width': '40px',
|
||||
'height': '40px',
|
||||
'border-width': '2px',
|
||||
'border-color': '#fff',
|
||||
'border-opacity': '0.5',
|
||||
'padding': '10px',
|
||||
'text-events': 'yes'
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: 'edge',
|
||||
style: {
|
||||
'width': '2px',
|
||||
'line-color': 'rgba(255, 255, 255, 0.3)',
|
||||
'target-arrow-color': 'rgba(255, 255, 255, 0.3)',
|
||||
'target-arrow-shape': 'triangle',
|
||||
'curve-style': 'bezier',
|
||||
'label': 'data(label)',
|
||||
'font-size': '10px',
|
||||
'color': '#fff',
|
||||
'text-outline-color': '#000',
|
||||
'text-outline-width': '2px',
|
||||
'text-rotation': 'autorotate'
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: ':selected',
|
||||
style: {
|
||||
'background-color': '#8b5cf6',
|
||||
'line-color': '#8b5cf6',
|
||||
'target-arrow-color': '#8b5cf6',
|
||||
'source-arrow-color': '#8b5cf6',
|
||||
'text-outline-color': '#000',
|
||||
'text-outline-width': '2px',
|
||||
'border-width': '3px',
|
||||
'border-color': '#fff',
|
||||
'border-opacity': '1'
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: '.highlighted',
|
||||
style: {
|
||||
'background-color': '#10b981',
|
||||
'line-color': '#10b981',
|
||||
'target-arrow-color': '#10b981',
|
||||
'source-arrow-color': '#10b981',
|
||||
'transition-property': 'background-color, line-color, target-arrow-color',
|
||||
'transition-duration': '0.3s'
|
||||
}
|
||||
}
|
||||
],
|
||||
layout: {
|
||||
name: 'cose',
|
||||
idealEdgeLength: 100,
|
||||
nodeOverlap: 20,
|
||||
refresh: 20,
|
||||
fit: true,
|
||||
padding: 30,
|
||||
randomize: false,
|
||||
componentSpacing: 100,
|
||||
nodeRepulsion: 400000,
|
||||
edgeElasticity: 100,
|
||||
nestingFactor: 5,
|
||||
gravity: 80,
|
||||
numIter: 1000,
|
||||
initialTemp: 200,
|
||||
coolingFactor: 0.95,
|
||||
minTemp: 1.0
|
||||
}
|
||||
});
|
||||
|
||||
// Event-Listener für Knoten
|
||||
cy.on('tap', 'node', function(evt) {
|
||||
const node = evt.target;
|
||||
updateNodeInfo(node);
|
||||
highlightConnectedNodes(node);
|
||||
});
|
||||
|
||||
// Event-Listener für Hintergrund-Klick
|
||||
cy.on('tap', function(evt) {
|
||||
if (evt.target === cy) {
|
||||
resetHighlighting();
|
||||
hideNodeInfo();
|
||||
}
|
||||
});
|
||||
|
||||
// Zoom-Kontrollen
|
||||
document.getElementById('zoom-in').addEventListener('click', function() {
|
||||
cy.zoom({
|
||||
level: cy.zoom() * 1.2,
|
||||
renderedPosition: { x: cy.width() / 2, y: cy.height() / 2 }
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('zoom-out').addEventListener('click', function() {
|
||||
cy.zoom({
|
||||
level: cy.zoom() / 1.2,
|
||||
renderedPosition: { x: cy.width() / 2, y: cy.height() / 2 }
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('reset-view').addEventListener('click', function() {
|
||||
cy.fit();
|
||||
});
|
||||
|
||||
// Legende ein-/ausblenden
|
||||
document.getElementById('toggle-legend').addEventListener('click', function() {
|
||||
const legend = document.querySelector('.category-legend');
|
||||
isLegendVisible = !isLegendVisible;
|
||||
legend.style.display = isLegendVisible ? 'flex' : 'none';
|
||||
});
|
||||
|
||||
// Tastatursteuerung
|
||||
document.addEventListener('keydown', function(evt) {
|
||||
switch(evt.key) {
|
||||
case '+':
|
||||
cy.zoom({
|
||||
level: cy.zoom() * 1.2,
|
||||
renderedPosition: { x: cy.width() / 2, y: cy.height() / 2 }
|
||||
});
|
||||
break;
|
||||
case '-':
|
||||
cy.zoom({
|
||||
level: cy.zoom() / 1.2,
|
||||
renderedPosition: { x: cy.width() / 2, y: cy.height() / 2 }
|
||||
});
|
||||
break;
|
||||
case 'Escape':
|
||||
resetHighlighting();
|
||||
hideNodeInfo();
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Knoteninformationen aktualisieren
|
||||
function updateNodeInfo(node) {
|
||||
const infoPanel = document.getElementById('node-info');
|
||||
const infoContent = infoPanel.querySelector('.info-content');
|
||||
|
||||
// Knotendaten abrufen
|
||||
const nodeData = node.data();
|
||||
|
||||
// Info-Panel aktualisieren
|
||||
infoContent.innerHTML = `
|
||||
<h4 class="text-lg font-semibold mb-2">${nodeData.label}</h4>
|
||||
<p class="mb-3">${nodeData.description || 'Keine Beschreibung verfügbar.'}</p>
|
||||
<div class="mt-4">
|
||||
<h5 class="text-sm font-semibold mb-2">Verknüpfte Konzepte:</h5>
|
||||
<ul class="space-y-1">
|
||||
${getConnectedNodesList(node)}
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Panel anzeigen
|
||||
infoPanel.classList.add('visible');
|
||||
}
|
||||
|
||||
// Verbundene Knoten hervorheben
|
||||
function highlightConnectedNodes(node) {
|
||||
// Vorherige Hervorhebungen zurücksetzen
|
||||
resetHighlighting();
|
||||
|
||||
// Ausgewählten Knoten hervorheben
|
||||
node.addClass('highlighted');
|
||||
|
||||
// Verbundene Knoten und Kanten hervorheben
|
||||
const connectedElements = node.neighborhood();
|
||||
connectedElements.addClass('highlighted');
|
||||
}
|
||||
|
||||
// Hervorhebungen zurücksetzen
|
||||
function resetHighlighting() {
|
||||
cy.elements().removeClass('highlighted');
|
||||
}
|
||||
|
||||
// Info-Panel ausblenden
|
||||
function hideNodeInfo() {
|
||||
const infoPanel = document.getElementById('node-info');
|
||||
infoPanel.classList.remove('visible');
|
||||
}
|
||||
|
||||
// Liste der verbundenen Knoten generieren
|
||||
function getConnectedNodesList(node) {
|
||||
const connectedNodes = node.neighborhood('node');
|
||||
if (connectedNodes.length === 0) {
|
||||
return '<li class="text-gray-400">Keine direkten Verbindungen</li>';
|
||||
}
|
||||
|
||||
return connectedNodes.map(connectedNode => {
|
||||
const data = connectedNode.data();
|
||||
return `
|
||||
<li class="flex items-center space-x-2">
|
||||
<span class="w-2 h-2 rounded-full" style="background-color: ${getNodeColor(data.category)}"></span>
|
||||
<span>${data.label}</span>
|
||||
</li>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// Farbe basierend auf Kategorie
|
||||
function getNodeColor(category) {
|
||||
const colors = {
|
||||
'Philosophie': '#60a5fa',
|
||||
'Wissenschaft': '#8b5cf6',
|
||||
'Technologie': '#10b981',
|
||||
'Künste': '#f59e0b',
|
||||
'Psychologie': '#ef4444'
|
||||
};
|
||||
return colors[category] || '#60a5fa';
|
||||
}
|
||||
@@ -247,63 +247,130 @@ class ChatGPTAssistant {
|
||||
|
||||
const bubble = document.createElement('div');
|
||||
bubble.className = sender === 'user'
|
||||
? 'user-message rounded-lg py-2 px-3 max-w-[85%]'
|
||||
: 'assistant-message rounded-lg py-2 px-3 max-w-[85%]';
|
||||
? '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%]';
|
||||
|
||||
// Nachrichtentext einfügen, falls Markdown-Parser verfügbar, nutzen
|
||||
if (this.markdownParser) {
|
||||
bubble.innerHTML = this.markdownParser.parse(text);
|
||||
// Formatierung des Texts (mit Markdown für Assistent-Nachrichten)
|
||||
let formattedText = '';
|
||||
|
||||
if (sender === 'assistant' && this.markdownParser) {
|
||||
// Für Assistentnachrichten Markdown verwenden
|
||||
try {
|
||||
formattedText = this.markdownParser.parse(text);
|
||||
|
||||
// CSS für Markdown-Formatierung hinzufügen
|
||||
const markdownStyles = `
|
||||
.markdown-bubble h1, .markdown-bubble h2, .markdown-bubble h3,
|
||||
.markdown-bubble h4, .markdown-bubble h5, .markdown-bubble h6 {
|
||||
font-weight: bold;
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.markdown-bubble h1 { font-size: 1.4rem; }
|
||||
.markdown-bubble h2 { font-size: 1.3rem; }
|
||||
.markdown-bubble h3 { font-size: 1.2rem; }
|
||||
.markdown-bubble h4 { font-size: 1.1rem; }
|
||||
.markdown-bubble ul, .markdown-bubble ol {
|
||||
padding-left: 1.5rem;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
.markdown-bubble ul { list-style-type: disc; }
|
||||
.markdown-bubble ol { list-style-type: decimal; }
|
||||
.markdown-bubble p { margin: 0.5rem 0; }
|
||||
.markdown-bubble code {
|
||||
font-family: monospace;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
padding: 1px 4px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.markdown-bubble pre {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
.markdown-bubble pre code {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
.markdown-bubble blockquote {
|
||||
border-left: 3px solid rgba(0, 0, 0, 0.2);
|
||||
padding-left: 0.8rem;
|
||||
margin: 0.5rem 0;
|
||||
font-style: italic;
|
||||
}
|
||||
.dark .markdown-bubble code {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
.dark .markdown-bubble pre {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
.dark .markdown-bubble blockquote {
|
||||
border-left-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
`;
|
||||
|
||||
// Füge die Styles hinzu, wenn sie noch nicht vorhanden sind
|
||||
if (!document.querySelector('#markdown-chat-styles')) {
|
||||
const style = document.createElement('style');
|
||||
style.id = 'markdown-chat-styles';
|
||||
style.textContent = markdownStyles;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
// Klasse für Markdown-Formatierung hinzufügen
|
||||
bubble.classList.add('markdown-bubble');
|
||||
} catch (error) {
|
||||
console.error('Fehler bei der Markdown-Formatierung:', error);
|
||||
// Fallback zur einfachen Formatierung
|
||||
formattedText = text.split('\n').map(line => {
|
||||
if (line.trim() === '') return '<br>';
|
||||
return `<p>${line}</p>`;
|
||||
}).join('');
|
||||
}
|
||||
} else {
|
||||
bubble.textContent = text;
|
||||
// Für Benutzernachrichten einfache Formatierung
|
||||
formattedText = text.split('\n').map(line => {
|
||||
if (line.trim() === '') return '<br>';
|
||||
return `<p>${line}</p>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// Links in der Nachricht klickbar machen
|
||||
const links = bubble.querySelectorAll('a');
|
||||
links.forEach(link => {
|
||||
link.target = '_blank';
|
||||
link.rel = 'noopener noreferrer';
|
||||
link.className = 'text-primary-600 dark:text-primary-400 underline';
|
||||
});
|
||||
|
||||
// Code-Blöcke stylen
|
||||
const codeBlocks = bubble.querySelectorAll('pre');
|
||||
codeBlocks.forEach(block => {
|
||||
block.className = 'bg-gray-100 dark:bg-dark-900 p-2 rounded my-2 overflow-x-auto';
|
||||
});
|
||||
|
||||
const inlineCode = bubble.querySelectorAll('code:not(pre code)');
|
||||
inlineCode.forEach(code => {
|
||||
code.className = 'bg-gray-100 dark:bg-dark-900 px-1 rounded font-mono text-sm';
|
||||
});
|
||||
bubble.innerHTML = formattedText;
|
||||
|
||||
messageEl.appendChild(bubble);
|
||||
this.chatHistory.appendChild(messageEl);
|
||||
|
||||
// Scrolle zum Ende des Chat-Verlaufs
|
||||
this.chatHistory.scrollTop = this.chatHistory.scrollHeight;
|
||||
if (this.chatHistory) {
|
||||
this.chatHistory.appendChild(messageEl);
|
||||
|
||||
// Scroll zum Ende des Verlaufs
|
||||
this.chatHistory.scrollTop = this.chatHistory.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeigt Vorschläge für mögliche Fragen an
|
||||
* @param {Array} suggestions - Array von Vorschlägen
|
||||
* Zeigt Vorschläge als klickbare Pills an
|
||||
* @param {string[]} suggestions - Liste von Vorschlägen
|
||||
*/
|
||||
showSuggestions(suggestions) {
|
||||
if (!this.suggestionArea || !suggestions || !suggestions.length) return;
|
||||
if (!this.suggestionArea) return;
|
||||
|
||||
// Vorherige Vorschläge entfernen
|
||||
this.suggestionArea.innerHTML = '';
|
||||
|
||||
// Neue Vorschläge hinzufügen
|
||||
suggestions.forEach((text, index) => {
|
||||
const pill = document.createElement('button');
|
||||
pill.className = 'suggestion-pill text-sm px-3 py-1.5 rounded-full bg-primary-100 dark:bg-primary-900 text-primary-800 dark:text-primary-200 hover:bg-primary-200 dark:hover:bg-primary-800 transition-all duration-200';
|
||||
pill.style.animationDelay = `${index * 0.1}s`;
|
||||
pill.textContent = text;
|
||||
this.suggestionArea.appendChild(pill);
|
||||
});
|
||||
if (suggestions && suggestions.length > 0) {
|
||||
suggestions.forEach(suggestion => {
|
||||
const pill = document.createElement('button');
|
||||
pill.className = 'suggestion-pill text-sm bg-gray-200 dark:bg-dark-600 hover:bg-gray-300 dark:hover:bg-dark-500 text-gray-800 dark:text-gray-200 rounded-full px-3 py-1 mb-2 transition-colors';
|
||||
pill.textContent = suggestion;
|
||||
this.suggestionArea.appendChild(pill);
|
||||
});
|
||||
|
||||
// Vorschlagsbereich anzeigen
|
||||
this.suggestionArea.classList.remove('hidden');
|
||||
this.suggestionArea.classList.remove('hidden');
|
||||
} else {
|
||||
this.suggestionArea.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -342,27 +409,14 @@ class ChatGPTAssistant {
|
||||
messages: this.messages
|
||||
}),
|
||||
cache: 'no-cache', // Kein Cache verwenden
|
||||
credentials: 'same-origin', // Cookies senden
|
||||
timeout: 60000 // 60 Sekunden Timeout
|
||||
credentials: 'same-origin' // Cookies senden
|
||||
});
|
||||
|
||||
// Ladeindikator entfernen
|
||||
this.removeLoadingIndicator();
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
let errorMessage;
|
||||
|
||||
try {
|
||||
// Versuche, die Fehlermeldung zu parsen
|
||||
const errorData = JSON.parse(errorText);
|
||||
errorMessage = errorData.error || `Serverfehler: ${response.status} ${response.statusText}`;
|
||||
} catch {
|
||||
// Bei Parsing-Fehler verwende Standardfehlermeldung
|
||||
errorMessage = `Serverfehler: ${response.status} ${response.statusText}`;
|
||||
}
|
||||
|
||||
throw new Error(errorMessage);
|
||||
throw new Error(`Serverfehler: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
@@ -388,45 +442,24 @@ class ChatGPTAssistant {
|
||||
// Ladeindikator entfernen, falls noch vorhanden
|
||||
this.removeLoadingIndicator();
|
||||
|
||||
// Spezielle Fehlermeldungen für bestimmte Fehlertypen
|
||||
const errorMessage = error.message || '';
|
||||
let userFriendlyMessage = 'Es gab ein Problem mit der Anfrage.';
|
||||
|
||||
if (errorMessage.includes('timeout') || errorMessage.includes('Zeitüberschreitung')) {
|
||||
userFriendlyMessage = 'Die Antwort hat zu lange gedauert. Der Server ist möglicherweise überlastet.';
|
||||
} else if (errorMessage.includes('500') || errorMessage.includes('Internal Server Error')) {
|
||||
userFriendlyMessage = 'Ein Serverfehler ist aufgetreten. Wir arbeiten an einer Lösung.';
|
||||
} else if (errorMessage.includes('429') || errorMessage.includes('rate limit')) {
|
||||
userFriendlyMessage = 'Die API-Anfragelimits wurden erreicht. Bitte warte einen Moment.';
|
||||
}
|
||||
|
||||
// Fehlermeldung anzeigen oder Wiederholungsversuch starten
|
||||
if (this.retryCount < this.maxRetries) {
|
||||
this.retryCount++;
|
||||
this.addMessage('assistant', `${userFriendlyMessage} Ich versuche es erneut... (Versuch ${this.retryCount}/${this.maxRetries})`);
|
||||
this.addMessage('assistant', 'Es gab ein Problem mit der Anfrage. Ich versuche es erneut...');
|
||||
|
||||
// Letzte Benutzernachricht speichern für den Wiederholungsversuch
|
||||
const lastUserMessageIndex = this.messages.findLastIndex(msg => msg.role === 'user');
|
||||
if (lastUserMessageIndex >= 0) {
|
||||
const lastUserMessage = this.messages[lastUserMessageIndex].content;
|
||||
// Kurze Verzögerung vor dem erneuten Versuch
|
||||
setTimeout(() => {
|
||||
// Letzte Benutzernachricht aus dem Messages-Array entfernen
|
||||
const lastUserMessage = this.messages[this.messages.length - 2].content;
|
||||
this.messages = this.messages.slice(0, -2); // Entferne Benutzernachricht und Fehlermeldung
|
||||
|
||||
// Kurze Verzögerung vor dem erneuten Versuch mit exponentieller Backoff-Strategie
|
||||
const retryDelay = 1500 * Math.pow(2, this.retryCount - 1); // 1.5s, 3s, 6s, ...
|
||||
|
||||
setTimeout(() => {
|
||||
// Entferne Fehlermeldung aus dem Messages-Array, behalte aber die Benutzernachricht
|
||||
this.messages = this.messages.filter(msg =>
|
||||
!(msg.role === 'assistant' && msg.content.includes('versuche es erneut'))
|
||||
);
|
||||
|
||||
// Erneuter Versand mit gleicher Nachricht
|
||||
this.inputField.value = lastUserMessage;
|
||||
this.sendMessage();
|
||||
}, retryDelay);
|
||||
}
|
||||
// Erneuter Versand mit gleicher Nachricht
|
||||
this.inputField.value = lastUserMessage;
|
||||
this.sendMessage();
|
||||
}, 1500);
|
||||
} else {
|
||||
// Maximale Anzahl an Wiederholungsversuchen erreicht
|
||||
this.addMessage('assistant', 'Es tut mir leid, aber es gab ein Problem bei der Verarbeitung deiner Anfrage. Bitte versuche es später noch einmal oder kontaktiere den Support, falls das Problem weiterhin besteht.');
|
||||
this.addMessage('assistant', 'Es tut mir leid, aber es gab ein Problem bei der Verarbeitung deiner Anfrage. Bitte versuche es später noch einmal.');
|
||||
this.retryCount = 0; // Zurücksetzen für die nächste Anfrage
|
||||
}
|
||||
} finally {
|
||||
@@ -479,33 +512,26 @@ class ChatGPTAssistant {
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeigt eine Ladeanimation an
|
||||
* Zeigt einen Ladeindikator im Chat an
|
||||
*/
|
||||
showLoadingIndicator() {
|
||||
if (!this.chatHistory) return;
|
||||
|
||||
// Prüfen, ob bereits ein Ladeindikator angezeigt wird
|
||||
if (document.getElementById('assistant-loading-indicator')) return;
|
||||
// Entferne vorhandenen Ladeindikator (falls vorhanden)
|
||||
this.removeLoadingIndicator();
|
||||
|
||||
const loadingEl = document.createElement('div');
|
||||
loadingEl.id = 'assistant-loading';
|
||||
loadingEl.className = 'flex justify-start';
|
||||
loadingEl.id = 'assistant-loading-indicator';
|
||||
|
||||
const bubble = document.createElement('div');
|
||||
bubble.className = 'assistant-message rounded-lg py-3 px-4 max-w-[85%] flex items-center';
|
||||
bubble.className = 'bg-gray-100 dark:bg-dark-700 text-gray-800 dark:text-white rounded-lg py-2 px-3';
|
||||
bubble.innerHTML = '<div class="typing-indicator"><span></span><span></span><span></span></div>';
|
||||
|
||||
const typingIndicator = document.createElement('div');
|
||||
typingIndicator.className = 'typing-indicator';
|
||||
typingIndicator.innerHTML = `
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
`;
|
||||
|
||||
bubble.appendChild(typingIndicator);
|
||||
loadingEl.appendChild(bubble);
|
||||
|
||||
this.chatHistory.appendChild(loadingEl);
|
||||
|
||||
// Scroll zum Ende des Verlaufs
|
||||
this.chatHistory.scrollTop = this.chatHistory.scrollHeight;
|
||||
}
|
||||
|
||||
@@ -513,7 +539,7 @@ class ChatGPTAssistant {
|
||||
* Entfernt den Ladeindikator aus dem Chat
|
||||
*/
|
||||
removeLoadingIndicator() {
|
||||
const loadingIndicator = document.getElementById('assistant-loading-indicator');
|
||||
const loadingIndicator = document.getElementById('assistant-loading');
|
||||
if (loadingIndicator) {
|
||||
loadingIndicator.remove();
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -1,553 +0,0 @@
|
||||
/**
|
||||
* Update Mindmap
|
||||
* Dieses Skript fügt Knoten zur Mindmap hinzu und stellt sicher,
|
||||
* dass sie im neuronalen Netzwerk-Design angezeigt werden.
|
||||
*/
|
||||
|
||||
// Warte bis DOM geladen ist
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Prüfe, ob wir auf der Mindmap-Seite sind
|
||||
const cyContainer = document.getElementById('cy');
|
||||
|
||||
if (!cyContainer) {
|
||||
console.log('Kein Mindmap-Container gefunden, überspringe Initialisierung.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Auf das Laden der Mindmap warten
|
||||
document.addEventListener('mindmap-loaded', function() {
|
||||
console.log('Mindmap geladen, wende neuronales Netzwerk-Design an...');
|
||||
updateMindmap();
|
||||
});
|
||||
});
|
||||
|
||||
// Mindmap-Daten
|
||||
const mindmapData = {
|
||||
nodes: [
|
||||
// Philosophie
|
||||
{
|
||||
id: 'philosophy',
|
||||
label: 'Philosophie',
|
||||
category: 'Philosophie',
|
||||
description: 'Die Lehre vom Denken und der Erkenntnis'
|
||||
},
|
||||
{
|
||||
id: 'epistemology',
|
||||
label: 'Erkenntnistheorie',
|
||||
category: 'Philosophie',
|
||||
description: 'Untersuchung der Natur und Grenzen menschlicher Erkenntnis'
|
||||
},
|
||||
{
|
||||
id: 'ethics',
|
||||
label: 'Ethik',
|
||||
category: 'Philosophie',
|
||||
description: 'Lehre vom moralisch richtigen Handeln'
|
||||
},
|
||||
|
||||
// Wissenschaft
|
||||
{
|
||||
id: 'science',
|
||||
label: 'Wissenschaft',
|
||||
category: 'Wissenschaft',
|
||||
description: 'Systematische Erforschung der Natur und Gesellschaft'
|
||||
},
|
||||
{
|
||||
id: 'physics',
|
||||
label: 'Physik',
|
||||
category: 'Wissenschaft',
|
||||
description: 'Lehre von der Materie und ihren Wechselwirkungen'
|
||||
},
|
||||
{
|
||||
id: 'biology',
|
||||
label: 'Biologie',
|
||||
category: 'Wissenschaft',
|
||||
description: 'Lehre von den Lebewesen und ihren Lebensprozessen'
|
||||
},
|
||||
|
||||
// Technologie
|
||||
{
|
||||
id: 'technology',
|
||||
label: 'Technologie',
|
||||
category: 'Technologie',
|
||||
description: 'Anwendung wissenschaftlicher Erkenntnisse'
|
||||
},
|
||||
{
|
||||
id: 'ai',
|
||||
label: 'Künstliche Intelligenz',
|
||||
category: 'Technologie',
|
||||
description: 'Maschinelles Lernen und intelligente Systeme'
|
||||
},
|
||||
{
|
||||
id: 'robotics',
|
||||
label: 'Robotik',
|
||||
category: 'Technologie',
|
||||
description: 'Entwicklung und Steuerung von Robotern'
|
||||
},
|
||||
|
||||
// Künste
|
||||
{
|
||||
id: 'arts',
|
||||
label: 'Künste',
|
||||
category: 'Künste',
|
||||
description: 'Kreativer Ausdruck und künstlerische Gestaltung'
|
||||
},
|
||||
{
|
||||
id: 'music',
|
||||
label: 'Musik',
|
||||
category: 'Künste',
|
||||
description: 'Kunst der Töne und Klänge'
|
||||
},
|
||||
{
|
||||
id: 'literature',
|
||||
label: 'Literatur',
|
||||
category: 'Künste',
|
||||
description: 'Schriftliche Werke und Dichtkunst'
|
||||
},
|
||||
|
||||
// Psychologie
|
||||
{
|
||||
id: 'psychology',
|
||||
label: 'Psychologie',
|
||||
category: 'Psychologie',
|
||||
description: 'Wissenschaft vom Erleben und Verhalten'
|
||||
},
|
||||
{
|
||||
id: 'cognitive',
|
||||
label: 'Kognitive Psychologie',
|
||||
category: 'Psychologie',
|
||||
description: 'Studium mentaler Prozesse'
|
||||
},
|
||||
{
|
||||
id: 'behavioral',
|
||||
label: 'Verhaltenspsychologie',
|
||||
category: 'Psychologie',
|
||||
description: 'Analyse von Verhaltensmustern'
|
||||
}
|
||||
],
|
||||
|
||||
edges: [
|
||||
// Philosophie-Verbindungen
|
||||
{ source: 'philosophy', target: 'epistemology', label: 'umfasst' },
|
||||
{ source: 'philosophy', target: 'ethics', label: 'umfasst' },
|
||||
{ source: 'philosophy', target: 'science', label: 'beeinflusst' },
|
||||
|
||||
// Wissenschaft-Verbindungen
|
||||
{ source: 'science', target: 'physics', label: 'umfasst' },
|
||||
{ source: 'science', target: 'biology', label: 'umfasst' },
|
||||
{ source: 'science', target: 'technology', label: 'fördert' },
|
||||
|
||||
// Technologie-Verbindungen
|
||||
{ source: 'technology', target: 'ai', label: 'umfasst' },
|
||||
{ source: 'technology', target: 'robotics', label: 'umfasst' },
|
||||
{ source: 'ai', target: 'cognitive', label: 'beeinflusst' },
|
||||
|
||||
// Künste-Verbindungen
|
||||
{ source: 'arts', target: 'music', label: 'umfasst' },
|
||||
{ source: 'arts', target: 'literature', label: 'umfasst' },
|
||||
{ source: 'arts', target: 'psychology', label: 'beeinflusst' },
|
||||
|
||||
// Psychologie-Verbindungen
|
||||
{ source: 'psychology', target: 'cognitive', label: 'umfasst' },
|
||||
{ source: 'psychology', target: 'behavioral', label: 'umfasst' },
|
||||
{ source: 'psychology', target: 'ethics', label: 'beeinflusst' },
|
||||
|
||||
// Interdisziplinäre Verbindungen
|
||||
{ source: 'cognitive', target: 'ai', label: 'inspiriert' },
|
||||
{ source: 'physics', target: 'technology', label: 'ermöglicht' },
|
||||
{ source: 'literature', target: 'philosophy', label: 'reflektiert' }
|
||||
]
|
||||
};
|
||||
|
||||
// Mindmap aktualisieren
|
||||
function updateMindmap() {
|
||||
if (!cy) return;
|
||||
|
||||
// Bestehende Elemente entfernen
|
||||
cy.elements().remove();
|
||||
|
||||
// Neue Knoten hinzufügen
|
||||
mindmapData.nodes.forEach(node => {
|
||||
cy.add({
|
||||
group: 'nodes',
|
||||
data: {
|
||||
id: node.id,
|
||||
label: node.label,
|
||||
category: node.category,
|
||||
description: node.description
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Neue Kanten hinzufügen
|
||||
mindmapData.edges.forEach(edge => {
|
||||
cy.add({
|
||||
group: 'edges',
|
||||
data: {
|
||||
source: edge.source,
|
||||
target: edge.target,
|
||||
label: edge.label
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Layout anwenden
|
||||
cy.layout({
|
||||
name: 'cose',
|
||||
idealEdgeLength: 100,
|
||||
nodeOverlap: 20,
|
||||
refresh: 20,
|
||||
fit: true,
|
||||
padding: 30,
|
||||
randomize: false,
|
||||
componentSpacing: 100,
|
||||
nodeRepulsion: 400000,
|
||||
edgeElasticity: 100,
|
||||
nestingFactor: 5,
|
||||
gravity: 80,
|
||||
numIter: 1000,
|
||||
initialTemp: 200,
|
||||
coolingFactor: 0.95,
|
||||
minTemp: 1.0
|
||||
}).run();
|
||||
}
|
||||
|
||||
/**
|
||||
* Erweitert die Mindmap mit dem neuronalen Netzwerk-Design
|
||||
*/
|
||||
function enhanceMindmap() {
|
||||
// Auf die bestehende Cytoscape-Instanz zugreifen
|
||||
const cy = window.cy;
|
||||
|
||||
if (!cy) {
|
||||
console.error('Keine Cytoscape-Instanz gefunden.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Aktualisiere das Layout für eine bessere Verteilung
|
||||
cy.layout({
|
||||
name: 'cose',
|
||||
animate: true,
|
||||
animationDuration: 1800,
|
||||
nodeDimensionsIncludeLabels: true,
|
||||
padding: 100,
|
||||
spacingFactor: 1.8,
|
||||
randomize: false,
|
||||
fit: true,
|
||||
componentSpacing: 100,
|
||||
nodeRepulsion: 8000,
|
||||
edgeElasticity: 100,
|
||||
nestingFactor: 1.2,
|
||||
gravity: 80
|
||||
}).run();
|
||||
|
||||
// Neuronen-Namen mit besserer Lesbarkeit umgestalten
|
||||
cy.style()
|
||||
.selector('node')
|
||||
.style({
|
||||
'text-background-color': 'rgba(10, 14, 25, 0.7)',
|
||||
'text-background-opacity': 0.7,
|
||||
'text-background-padding': '2px',
|
||||
'text-border-opacity': 0.2,
|
||||
'text-border-width': 1,
|
||||
'text-border-color': '#8b5cf6'
|
||||
})
|
||||
.update();
|
||||
|
||||
// Sicherstellen, dass alle Knoten Neuronen-Eigenschaften haben
|
||||
cy.nodes().forEach(node => {
|
||||
if (!node.data('neuronSize')) {
|
||||
const neuronSize = Math.floor(Math.random() * 8) + 3;
|
||||
node.data('neuronSize', neuronSize);
|
||||
}
|
||||
|
||||
if (!node.data('neuronActivity')) {
|
||||
const neuronActivity = Math.random() * 0.7 + 0.3;
|
||||
node.data('neuronActivity', neuronActivity);
|
||||
}
|
||||
|
||||
// Zusätzliche Neuronale Eigenschaften
|
||||
node.data('pulseFrequency', Math.random() * 4 + 2); // Pulsfrequenz (2-6 Hz)
|
||||
node.data('refractionPeriod', Math.random() * 300 + 700); // Refraktionszeit (700-1000ms)
|
||||
node.data('threshold', Math.random() * 0.3 + 0.6); // Aktivierungsschwelle (0.6-0.9)
|
||||
});
|
||||
|
||||
// Sicherstellen, dass alle Kanten Synapse-Eigenschaften haben
|
||||
cy.edges().forEach(edge => {
|
||||
if (!edge.data('strength')) {
|
||||
const strength = Math.random() * 0.6 + 0.2;
|
||||
edge.data('strength', strength);
|
||||
}
|
||||
|
||||
// Zusätzliche synaptische Eigenschaften
|
||||
edge.data('conductionVelocity', Math.random() * 0.5 + 0.3); // Leitungsgeschwindigkeit (0.3-0.8)
|
||||
edge.data('latency', Math.random() * 100 + 50); // Signalverzögerung (50-150ms)
|
||||
});
|
||||
|
||||
// Neuronales Netzwerk-Stil anwenden
|
||||
applyNeuralNetworkStyle(cy);
|
||||
|
||||
console.log('Mindmap wurde erfolgreich im neuronalen Netzwerk-Stil aktualisiert');
|
||||
|
||||
// Spezielle Effekte für das neuronale Netzwerk hinzufügen
|
||||
startNeuralActivitySimulation(cy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wendet detaillierte neuronale Netzwerkstile auf die Mindmap an
|
||||
* @param {Object} cy - Cytoscape-Instanz
|
||||
*/
|
||||
function applyNeuralNetworkStyle(cy) {
|
||||
// Wende erweiterte Stile für Neuronen und Synapsen an
|
||||
cy.style()
|
||||
.selector('node')
|
||||
.style({
|
||||
'label': 'data(name)',
|
||||
'text-valign': 'bottom',
|
||||
'text-halign': 'center',
|
||||
'color': '#ffffff',
|
||||
'text-outline-width': 1.5,
|
||||
'text-outline-color': '#0a0e19',
|
||||
'text-outline-opacity': 0.9,
|
||||
'font-size': 10,
|
||||
'text-margin-y': 7,
|
||||
'width': 'mapData(neuronSize, 3, 10, 15, 40)',
|
||||
'height': 'mapData(neuronSize, 3, 10, 15, 40)',
|
||||
'background-color': 'data(color)',
|
||||
'background-opacity': 0.85,
|
||||
'border-width': 0,
|
||||
'shape': 'ellipse',
|
||||
'shadow-blur': 'mapData(neuronActivity, 0.3, 1, 5, 15)',
|
||||
'shadow-color': 'data(color)',
|
||||
'shadow-opacity': 0.6,
|
||||
'shadow-offset-x': 0,
|
||||
'shadow-offset-y': 0
|
||||
})
|
||||
.selector('edge')
|
||||
.style({
|
||||
'width': 'mapData(strength, 0.2, 0.8, 0.7, 2)',
|
||||
'curve-style': 'bezier',
|
||||
'line-color': '#8a8aaa',
|
||||
'line-opacity': 'mapData(strength, 0.2, 0.8, 0.4, 0.7)',
|
||||
'line-style': function(ele) {
|
||||
const strength = ele.data('strength');
|
||||
if (strength <= 0.4) return 'dotted';
|
||||
if (strength <= 0.6) return 'dashed';
|
||||
return 'solid';
|
||||
},
|
||||
'target-arrow-shape': 'none',
|
||||
'source-endpoint': '0% 50%',
|
||||
'target-endpoint': '100% 50%'
|
||||
})
|
||||
.selector('node[isRoot]')
|
||||
.style({
|
||||
'font-size': 12,
|
||||
'font-weight': 'bold',
|
||||
'width': 50,
|
||||
'height': 50,
|
||||
'background-color': '#6366f1',
|
||||
'shadow-blur': 20,
|
||||
'shadow-color': '#6366f1',
|
||||
'shadow-opacity': 0.8,
|
||||
'text-margin-y': 8
|
||||
})
|
||||
.update();
|
||||
}
|
||||
|
||||
/**
|
||||
* Simuliert neuronale Aktivität in der Mindmap
|
||||
* @param {Object} cy - Cytoscape-Instanz
|
||||
*/
|
||||
function startNeuralActivitySimulation(cy) {
|
||||
// Neuronen-Zustand für die Simulation
|
||||
const neuronStates = new Map();
|
||||
|
||||
// Initialisieren aller Neuronen-Zustände
|
||||
cy.nodes().forEach(node => {
|
||||
neuronStates.set(node.id(), {
|
||||
potential: Math.random() * 0.3, // Startpotential
|
||||
lastFired: 0, // Zeitpunkt der letzten Aktivierung
|
||||
isRefractory: false, // Refraktärphase
|
||||
refractoryUntil: 0 // Ende der Refraktärphase
|
||||
});
|
||||
});
|
||||
|
||||
// Neuronale Aktivität simulieren
|
||||
function simulateNeuralActivity() {
|
||||
const currentTime = Date.now();
|
||||
const nodes = cy.nodes().toArray();
|
||||
|
||||
// Zufällige Stimulation eines Neurons
|
||||
if (Math.random() > 0.7) {
|
||||
const randomNodeIndex = Math.floor(Math.random() * nodes.length);
|
||||
const randomNode = nodes[randomNodeIndex];
|
||||
|
||||
const state = neuronStates.get(randomNode.id());
|
||||
if (state && !state.isRefractory) {
|
||||
state.potential += 0.5; // Erhöhe das Potential durch externe Stimulation
|
||||
}
|
||||
}
|
||||
|
||||
// Neuronen aktualisieren
|
||||
nodes.forEach(node => {
|
||||
const nodeId = node.id();
|
||||
const state = neuronStates.get(nodeId);
|
||||
const threshold = node.data('threshold') || 0.7;
|
||||
const refractoryPeriod = node.data('refractionPeriod') || 1000;
|
||||
|
||||
// Überprüfen, ob die Refraktärphase beendet ist
|
||||
if (state.isRefractory && currentTime >= state.refractoryUntil) {
|
||||
state.isRefractory = false;
|
||||
state.potential = 0.1; // Ruhepotential nach Refraktärphase
|
||||
}
|
||||
|
||||
// Wenn nicht in Refraktärphase und Potential über Schwelle
|
||||
if (!state.isRefractory && state.potential >= threshold) {
|
||||
// Neuron feuert
|
||||
fireNeuron(node, state, currentTime);
|
||||
} else if (!state.isRefractory) {
|
||||
// Potential langsam verlieren, wenn nicht gefeuert wird
|
||||
state.potential *= 0.95;
|
||||
}
|
||||
});
|
||||
|
||||
// Simulation fortsetzen
|
||||
requestAnimationFrame(simulateNeuralActivity);
|
||||
}
|
||||
|
||||
// Neuron "feuern" lassen
|
||||
function fireNeuron(node, state, currentTime) {
|
||||
// Neuron aktivieren
|
||||
node.animate({
|
||||
style: {
|
||||
'background-opacity': 1,
|
||||
'shadow-opacity': 1,
|
||||
'shadow-blur': 25
|
||||
},
|
||||
duration: 300,
|
||||
easing: 'ease-in-cubic',
|
||||
complete: function() {
|
||||
// Zurück zum normalen Zustand
|
||||
node.animate({
|
||||
style: {
|
||||
'background-opacity': 0.85,
|
||||
'shadow-opacity': 0.6,
|
||||
'shadow-blur': 'mapData(neuronActivity, 0.3, 1, 5, 15)'
|
||||
},
|
||||
duration: 600,
|
||||
easing: 'ease-out-cubic'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Refraktärphase setzen
|
||||
state.isRefractory = true;
|
||||
state.lastFired = currentTime;
|
||||
state.refractoryPeriod = node.data('refractionPeriod') || 1000;
|
||||
state.refractoryUntil = currentTime + state.refractoryPeriod;
|
||||
state.potential = 0; // Potential zurücksetzen
|
||||
|
||||
// Signal über verbundene Synapsen weiterleiten
|
||||
propagateSignal(node, currentTime);
|
||||
}
|
||||
|
||||
// Signal über Synapsen propagieren
|
||||
function propagateSignal(sourceNode, currentTime) {
|
||||
// Verbundene Kanten auswählen
|
||||
const edges = sourceNode.connectedEdges().filter(edge =>
|
||||
edge.source().id() === sourceNode.id() // Nur ausgehende Kanten
|
||||
);
|
||||
|
||||
// Durch alle Kanten iterieren
|
||||
edges.forEach(edge => {
|
||||
// Signalverzögerung basierend auf synaptischen Eigenschaften
|
||||
const latency = edge.data('latency') || 100;
|
||||
const strength = edge.data('strength') || 0.5;
|
||||
|
||||
// Signal entlang der Kante senden
|
||||
setTimeout(() => {
|
||||
edge.animate({
|
||||
style: {
|
||||
'line-color': '#a78bfa',
|
||||
'line-opacity': 0.9,
|
||||
'width': 2.5
|
||||
},
|
||||
duration: 200,
|
||||
easing: 'ease-in',
|
||||
complete: function() {
|
||||
// Kante zurücksetzen
|
||||
edge.animate({
|
||||
style: {
|
||||
'line-color': '#8a8aaa',
|
||||
'line-opacity': 'mapData(strength, 0.2, 0.8, 0.4, 0.7)',
|
||||
'width': 'mapData(strength, 0.2, 0.8, 0.7, 2)'
|
||||
},
|
||||
duration: 400,
|
||||
easing: 'ease-out'
|
||||
});
|
||||
|
||||
// Zielknoten potenzial erhöhen
|
||||
const targetNode = edge.target();
|
||||
const targetState = neuronStates.get(targetNode.id());
|
||||
|
||||
if (targetState && !targetState.isRefractory) {
|
||||
// Potentialzunahme basierend auf synaptischer Stärke
|
||||
targetState.potential += strength * 0.4;
|
||||
|
||||
// Subtile Anzeige der Potenzialänderung
|
||||
targetNode.animate({
|
||||
style: {
|
||||
'background-opacity': Math.min(1, 0.85 + (strength * 0.2)),
|
||||
'shadow-opacity': Math.min(1, 0.6 + (strength * 0.3)),
|
||||
'shadow-blur': Math.min(25, 10 + (strength * 15))
|
||||
},
|
||||
duration: 300,
|
||||
easing: 'ease-in-out'
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}, latency);
|
||||
});
|
||||
}
|
||||
|
||||
// Starte die Simulation
|
||||
simulateNeuralActivity();
|
||||
}
|
||||
|
||||
// Hilfe-Funktion zum Hinzufügen eines Flash-Hinweises
|
||||
function showFlash(message, type = 'info') {
|
||||
const flashContainer = document.getElementById('flash-messages') || createFlashContainer();
|
||||
|
||||
const flashMsg = document.createElement('div');
|
||||
flashMsg.className = `flash-message flash-${type} mb-2 p-3 rounded`;
|
||||
flashMsg.innerHTML = `
|
||||
<div class="flex items-center justify-between">
|
||||
<div>${message}</div>
|
||||
<button class="close-flash ml-2">×</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
flashContainer.appendChild(flashMsg);
|
||||
|
||||
// Nach 5 Sekunden automatisch ausblenden
|
||||
setTimeout(() => {
|
||||
flashMsg.style.opacity = '0';
|
||||
setTimeout(() => flashMsg.remove(), 300);
|
||||
}, 5000);
|
||||
|
||||
// Close-Button
|
||||
const closeBtn = flashMsg.querySelector('.close-flash');
|
||||
closeBtn.addEventListener('click', () => {
|
||||
flashMsg.style.opacity = '0';
|
||||
setTimeout(() => flashMsg.remove(), 300);
|
||||
});
|
||||
}
|
||||
|
||||
// Hilfsfunktion zum Erstellen eines Flash-Containers, falls keiner existiert
|
||||
function createFlashContainer() {
|
||||
const container = document.createElement('div');
|
||||
container.id = 'flash-messages';
|
||||
container.className = 'fixed top-4 right-4 z-50 w-64';
|
||||
document.body.appendChild(container);
|
||||
return container;
|
||||
}
|
||||
@@ -8,7 +8,7 @@ class MindMapVisualization {
|
||||
this.containerSelector = containerSelector;
|
||||
this.container = d3.select(containerSelector);
|
||||
this.width = options.width || this.container.node().clientWidth || 800;
|
||||
this.height = options.height || 800; // Erhöhung der Standardhöhe für bessere Sichtbarkeit
|
||||
this.height = options.height || 600;
|
||||
this.nodeRadius = options.nodeRadius || 22;
|
||||
this.selectedNodeRadius = options.selectedNodeRadius || 28;
|
||||
this.linkDistance = options.linkDistance || 150;
|
||||
@@ -35,25 +35,25 @@ class MindMapVisualization {
|
||||
// Flash-Nachrichten-Container
|
||||
this.flashContainer = null;
|
||||
|
||||
// Authentischere Farbpalette mit dezenten, professionellen Farben
|
||||
// Erweiterte Farbpalette für Knotentypen
|
||||
this.colorPalette = {
|
||||
'default': '#6b7280', // Grau
|
||||
'root': '#4f46e5', // Indigo
|
||||
'philosophy': '#3b82f6', // Blau
|
||||
'science': '#10b981', // Smaragdgrün
|
||||
'technology': '#6366f1', // Indigo
|
||||
'arts': '#8b5cf6', // Violett
|
||||
'ai': '#7c3aed', // Violett
|
||||
'ethics': '#475569', // Slate
|
||||
'math': '#0369a1', // Dunkelblau
|
||||
'psychology': '#0284c7', // Hellblau
|
||||
'biology': '#059669', // Grün
|
||||
'literature': '#4338ca', // Indigo
|
||||
'history': '#0f766e', // Teal
|
||||
'economics': '#374151', // Dunkelgrau
|
||||
'sociology': '#4f46e5', // Indigo
|
||||
'design': '#0891b2', // Cyan
|
||||
'languages': '#2563eb' // Blau
|
||||
'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
|
||||
@@ -438,38 +438,22 @@ class MindMapVisualization {
|
||||
.attr('slope', '1.5');
|
||||
}
|
||||
|
||||
// Behandelt die Zoom-Transformation für die SVG mit verbesserter Anpassung
|
||||
// Behandelt die Zoom-Transformation für die SVG
|
||||
handleZoom(transform) {
|
||||
this.g.attr('transform', transform);
|
||||
this.zoomFactor = transform.k;
|
||||
|
||||
// Knotengröße dynamisch an Zoom anpassen für bessere Lesbarkeit
|
||||
// Knotengröße an Zoom anpassen
|
||||
if (this.nodeElements) {
|
||||
// Berechne relativen Radius basierend auf Zoom
|
||||
const nodeScaleFactor = 1 / Math.sqrt(transform.k);
|
||||
const minRadiusFactor = 0.6; // Minimale Größe beim Herauszoomen
|
||||
const maxRadiusFactor = 1.2; // Maximale Größe beim Hineinzoomen
|
||||
|
||||
// Beschränke den Skalierungsfaktor in sinnvollen Grenzen
|
||||
const cappedScaleFactor = Math.max(minRadiusFactor, Math.min(nodeScaleFactor, maxRadiusFactor));
|
||||
|
||||
this.nodeElements.selectAll('circle')
|
||||
.attr('r', d => {
|
||||
return d === this.selectedNode
|
||||
? this.selectedNodeRadius * cappedScaleFactor
|
||||
: this.nodeRadius * cappedScaleFactor;
|
||||
})
|
||||
.attr('stroke-width', 2 * cappedScaleFactor); // Strichstärke anpassen
|
||||
? this.selectedNodeRadius / Math.sqrt(transform.k)
|
||||
: this.nodeRadius / Math.sqrt(transform.k);
|
||||
});
|
||||
|
||||
// Schriftgröße dynamisch anpassen
|
||||
this.nodeElements.selectAll('text')
|
||||
.style('font-size', `${14 * cappedScaleFactor}px`)
|
||||
.attr('dy', 4 * cappedScaleFactor);
|
||||
|
||||
// Verbindungslinien anpassen
|
||||
this.linkElements
|
||||
.attr('stroke-width', 1.5 * cappedScaleFactor)
|
||||
.attr('marker-end', transform.k < 0.6 ? 'none' : 'url(#arrowhead)'); // Pfeile bei starkem Zoom ausblenden
|
||||
this.textElements
|
||||
.style('font-size', `${16 / Math.sqrt(transform.k)}px`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -737,10 +721,9 @@ class MindMapVisualization {
|
||||
|
||||
// Generiert eine konsistente Farbe basierend auf dem Knotennamen
|
||||
generateColorFromString(str) {
|
||||
// Authentischere, dezente Farben für wissenschaftliche Darstellung
|
||||
const colors = [
|
||||
'#4f46e5', '#0369a1', '#0f766e', '#374151', '#4338ca',
|
||||
'#0284c7', '#059669', '#475569', '#6366f1', '#0891b2'
|
||||
'#b38fff', '#58a9ff', '#14b8a6', '#f472b6', '#84cc16',
|
||||
'#f97316', '#4c1d95', '#2dd4bf', '#ec4899', '#eab308'
|
||||
];
|
||||
|
||||
let hash = 0;
|
||||
@@ -881,11 +864,9 @@ class MindMapVisualization {
|
||||
return node.color_code;
|
||||
}
|
||||
|
||||
// Kategorietyp-basierte Färbung mit dezenten, wissenschaftlichen Farben
|
||||
if (node.type === 'category') {
|
||||
// Kategorietyp-basierte Färbung
|
||||
if (node.type === 'category' || node.type === 'subcategory') {
|
||||
return this.colorPalette.root;
|
||||
} else if (node.type === 'subcategory') {
|
||||
return this.colorPalette.science;
|
||||
}
|
||||
|
||||
// Fallback für verschiedene Knotentypen
|
||||
@@ -1208,7 +1189,7 @@ class MindMapVisualization {
|
||||
});
|
||||
}
|
||||
|
||||
// Klick-Handler für Knoten mit verbesserter Seitenleisten-Interaktion
|
||||
// Klick-Handler für Knoten
|
||||
nodeClicked(event, d) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -1227,12 +1208,6 @@ class MindMapVisualization {
|
||||
.style('filter', 'url(#glass-with-reflection)')
|
||||
.attr('stroke-width', 2);
|
||||
|
||||
// Event auslösen: Knoten abgewählt
|
||||
document.dispatchEvent(new CustomEvent('mindmap-node-deselected'));
|
||||
|
||||
// Standardinformationspanels wieder anzeigen
|
||||
this.showDefaultSidebar();
|
||||
|
||||
// Gedankenbereich ausblenden, wenn vorhanden
|
||||
const thoughtContainer = document.getElementById('thought-container');
|
||||
if (thoughtContainer) {
|
||||
@@ -1263,7 +1238,7 @@ class MindMapVisualization {
|
||||
.classed('highlighted', false)
|
||||
.transition()
|
||||
.duration(300)
|
||||
.style('stroke', 'rgba(75, 85, 99, 0.4)')
|
||||
.style('stroke', 'rgba(255, 255, 255, 0.3)')
|
||||
.style('stroke-width', 2)
|
||||
.style('opacity', 0.7);
|
||||
|
||||
@@ -1298,11 +1273,6 @@ class MindMapVisualization {
|
||||
this.nodeElements
|
||||
.classed('selected', n => n === d);
|
||||
|
||||
// Event auslösen: Knoten ausgewählt
|
||||
document.dispatchEvent(new CustomEvent('mindmap-node-selected', {
|
||||
detail: d
|
||||
}));
|
||||
|
||||
// Visuelles Feedback für Auswahl
|
||||
this.nodeElements
|
||||
.filter(n => n === d)
|
||||
@@ -1312,7 +1282,7 @@ class MindMapVisualization {
|
||||
.attr('r', this.selectedNodeRadius)
|
||||
.style('filter', 'url(#selected-glow)')
|
||||
.attr('stroke-width', 3)
|
||||
.attr('stroke', 'rgba(79, 70, 229, 0.6)');
|
||||
.attr('stroke', 'rgba(179, 143, 255, 0.6)');
|
||||
|
||||
// Verbundene Kanten hervorheben
|
||||
const connectedLinks = this.links.filter(link =>
|
||||
@@ -1325,7 +1295,7 @@ class MindMapVisualization {
|
||||
.classed('highlighted', false)
|
||||
.transition()
|
||||
.duration(300)
|
||||
.style('stroke', 'rgba(75, 85, 99, 0.4)')
|
||||
.style('stroke', 'rgba(255, 255, 255, 0.3)')
|
||||
.style('stroke-width', 2)
|
||||
.style('opacity', 0.7);
|
||||
|
||||
@@ -1339,7 +1309,7 @@ class MindMapVisualization {
|
||||
.classed('highlighted', true)
|
||||
.transition()
|
||||
.duration(300)
|
||||
.style('stroke', 'rgba(79, 70, 229, 0.7)')
|
||||
.style('stroke', 'rgba(179, 143, 255, 0.7)')
|
||||
.style('stroke-width', 3)
|
||||
.style('opacity', 0.9);
|
||||
|
||||
@@ -1360,133 +1330,6 @@ class MindMapVisualization {
|
||||
}
|
||||
}
|
||||
|
||||
// Neue Methode: Zeigt die Standardseitenleiste an (Über die Mindmap und Kategorien)
|
||||
showDefaultSidebar() {
|
||||
// Finde die Seitenleistenelemente
|
||||
const aboutMindmapPanel = document.querySelector('[data-sidebar="about-mindmap"]');
|
||||
const categoriesPanel = document.querySelector('[data-sidebar="categories"]');
|
||||
const nodeDescriptionPanel = document.querySelector('[data-sidebar="node-description"]');
|
||||
|
||||
if (aboutMindmapPanel && categoriesPanel && nodeDescriptionPanel) {
|
||||
// Beschreibungspanel ausblenden
|
||||
nodeDescriptionPanel.style.display = 'none';
|
||||
|
||||
// Standardpanels einblenden mit Animation
|
||||
aboutMindmapPanel.style.display = 'block';
|
||||
categoriesPanel.style.display = 'block';
|
||||
|
||||
setTimeout(() => {
|
||||
aboutMindmapPanel.style.opacity = '1';
|
||||
aboutMindmapPanel.style.transform = 'translateY(0)';
|
||||
|
||||
categoriesPanel.style.opacity = '1';
|
||||
categoriesPanel.style.transform = 'translateY(0)';
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
|
||||
// Neue Methode: Zeigt die Knotenbeschreibung in der Seitenleiste an
|
||||
showNodeDescriptionSidebar(node) {
|
||||
// Finde die Seitenleistenelemente
|
||||
const aboutMindmapPanel = document.querySelector('[data-sidebar="about-mindmap"]');
|
||||
const categoriesPanel = document.querySelector('[data-sidebar="categories"]');
|
||||
const nodeDescriptionPanel = document.querySelector('[data-sidebar="node-description"]');
|
||||
|
||||
if (aboutMindmapPanel && categoriesPanel && nodeDescriptionPanel) {
|
||||
// Standardpanels ausblenden
|
||||
aboutMindmapPanel.style.transition = 'all 0.3s ease';
|
||||
categoriesPanel.style.transition = 'all 0.3s ease';
|
||||
|
||||
aboutMindmapPanel.style.opacity = '0';
|
||||
aboutMindmapPanel.style.transform = 'translateY(10px)';
|
||||
|
||||
categoriesPanel.style.opacity = '0';
|
||||
categoriesPanel.style.transform = 'translateY(10px)';
|
||||
|
||||
setTimeout(() => {
|
||||
aboutMindmapPanel.style.display = 'none';
|
||||
categoriesPanel.style.display = 'none';
|
||||
|
||||
// Beschreibungspanel vorbereiten
|
||||
const titleElement = nodeDescriptionPanel.querySelector('[data-node-title]');
|
||||
const descriptionElement = nodeDescriptionPanel.querySelector('[data-node-description]');
|
||||
|
||||
if (titleElement && descriptionElement) {
|
||||
titleElement.textContent = node.name || 'Unbenannter Knoten';
|
||||
|
||||
// Beschreibung setzen oder Standardbeschreibung generieren
|
||||
let description = node.description;
|
||||
if (!description || description.trim() === '') {
|
||||
description = this.generateNodeDescription(node);
|
||||
}
|
||||
|
||||
descriptionElement.textContent = description;
|
||||
}
|
||||
|
||||
// Beschreibungspanel einblenden mit Animation
|
||||
nodeDescriptionPanel.style.display = 'block';
|
||||
nodeDescriptionPanel.style.opacity = '0';
|
||||
nodeDescriptionPanel.style.transform = 'translateY(10px)';
|
||||
|
||||
setTimeout(() => {
|
||||
nodeDescriptionPanel.style.transition = 'all 0.4s ease';
|
||||
nodeDescriptionPanel.style.opacity = '1';
|
||||
nodeDescriptionPanel.style.transform = 'translateY(0)';
|
||||
}, 50);
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
// Neue Methode: Generiert automatisch eine Beschreibung für einen Knoten ohne Beschreibung
|
||||
generateNodeDescription(node) {
|
||||
const descriptions = {
|
||||
"Wissen": "Der zentrale Knotenpunkt der Mindmap, der alle wissenschaftlichen Disziplinen und Wissensgebiete verbindet. Hier finden sich grundlegende Erkenntnisse und Verbindungen zu spezifischeren Fachgebieten.",
|
||||
|
||||
"Quantenphysik": "Ein Zweig der Physik, der sich mit dem Verhalten und den Interaktionen von Materie und Energie auf der kleinsten Skala beschäftigt. Quantenmechanische Phänomene wie Superposition und Verschränkung bilden die Grundlage für moderne Technologien wie Quantencomputer und -kommunikation.",
|
||||
|
||||
"Neurowissenschaften": "Interdisziplinäres Forschungsgebiet, das die Struktur, Funktion und Entwicklung des Nervensystems und des Gehirns untersucht. Die Erkenntnisse beeinflussen unser Verständnis von Bewusstsein, Kognition, Verhalten und neurologischen Erkrankungen.",
|
||||
|
||||
"Künstliche Intelligenz": "Forschungsgebiet der Informatik, das sich mit der Entwicklung von Systemen befasst, die menschliche Intelligenzformen simulieren können. KI umfasst maschinelles Lernen, neuronale Netze und verschiedene Ansätze zur Problemlösung und Mustererkennung.",
|
||||
|
||||
"Klimaforschung": "Wissenschaftliche Disziplin, die sich mit der Untersuchung des Erdklimas, seinen Veränderungen und den zugrundeliegenden physikalischen Prozessen beschäftigt. Sie liefert wichtige Erkenntnisse zu Klimawandel, Wettermuster und globalen Umweltveränderungen.",
|
||||
|
||||
"Genetik": "Wissenschaft der Gene, Vererbung und der Variation von Organismen. Moderne genetische Forschung umfasst Genomik, Gentechnologie und das Verständnis der molekularen Grundlagen des Lebens sowie ihrer Anwendungen in Medizin und Biotechnologie.",
|
||||
|
||||
"Astrophysik": "Zweig der Astronomie, der die physikalischen Eigenschaften und Prozesse von Himmelskörpern und des Universums untersucht. Sie erforscht Phänomene wie Schwarze Löcher, Galaxien, kosmische Strahlung und die Entstehung und Entwicklung des Universums.",
|
||||
|
||||
"Philosophie": "Disziplin, die sich mit fundamentalen Fragen des Wissens, der Realität und der Existenz auseinandersetzt. Sie umfasst Bereiche wie Metaphysik, Erkenntnistheorie, Ethik und Logik und bildet die Grundlage für kritisches Denken und wissenschaftliche Methodik.",
|
||||
|
||||
"Wissenschaft": "Systematische Erforschung der Natur und der materiellen Welt durch Beobachtung, Experimente und die Formulierung überprüfbarer Theorien. Sie umfasst Naturwissenschaften, Sozialwissenschaften und angewandte Wissenschaften.",
|
||||
|
||||
"Technologie": "Anwendung wissenschaftlicher Erkenntnisse für praktische Zwecke. Sie umfasst die Entwicklung von Werkzeugen, Maschinen, Materialien und Prozessen zur Lösung von Problemen und zur Verbesserung der menschlichen Lebensbedingungen.",
|
||||
|
||||
"Künste": "Ausdruck menschlicher Kreativität und Imagination in verschiedenen Formen wie Malerei, Musik, Literatur, Theater und Film. Die Künste erforschen ästhetische, emotionale und intellektuelle Dimensionen der menschlichen Erfahrung.",
|
||||
|
||||
"Biologie": "Wissenschaft des Lebens und der lebenden Organismen. Sie umfasst Bereiche wie Molekularbiologie, Evolutionsbiologie, Ökologie und beschäftigt sich mit der Struktur, Funktion, Entwicklung und Evolution lebender Systeme.",
|
||||
|
||||
"Mathematik": "Wissenschaft der Muster, Strukturen und Beziehungen. Sie ist die Sprache der Naturwissenschaften und bildet die Grundlage für quantitative Analysen, logisches Denken und Problemlösung in allen wissenschaftlichen Disziplinen.",
|
||||
|
||||
"Psychologie": "Wissenschaftliche Untersuchung des menschlichen Verhaltens und der mentalen Prozesse. Sie erforscht Bereiche wie Kognition, Emotion, Persönlichkeit, soziale Interaktionen und die Behandlung psychischer Störungen.",
|
||||
|
||||
"Ethik": "Teilgebiet der Philosophie, das sich mit moralischen Prinzipien, Werten und der Frage nach richtigem und falschem Handeln beschäftigt. Sie bildet die Grundlage für moralische Entscheidungsfindung in allen Lebensbereichen."
|
||||
};
|
||||
|
||||
// Verwende vordefinierte Beschreibung, wenn verfügbar
|
||||
if (node.name && descriptions[node.name]) {
|
||||
return descriptions[node.name];
|
||||
}
|
||||
|
||||
// Generische Beschreibung basierend auf dem Knotentyp
|
||||
switch (node.type) {
|
||||
case 'category':
|
||||
return `Dieser Knoten repräsentiert die Kategorie "${node.name}", die verschiedene verwandte Konzepte und Ideen zusammenfasst. Wählen Sie einen der verbundenen Unterthemen, um mehr Details zu erfahren.`;
|
||||
case 'subcategory':
|
||||
return `"${node.name}" ist eine Unterkategorie, die spezifische Aspekte eines größeren Themenbereichs beleuchtet. Die verbundenen Knoten zeigen wichtige Konzepte und Ideen innerhalb dieses Bereichs.`;
|
||||
default:
|
||||
return `Dieser Knoten repräsentiert das Konzept "${node.name}". Erforschen Sie die verbundenen Knoten, um Zusammenhänge und verwandte Ideen zu entdecken.`;
|
||||
}
|
||||
}
|
||||
|
||||
// Lädt die Gedanken für einen Knoten und zeigt sie an
|
||||
loadThoughtsForNode(node) {
|
||||
// UI-Element für Gedanken finden
|
||||
|
||||
@@ -18,11 +18,11 @@ class NeuralNetworkBackground {
|
||||
|
||||
// Standardkonfiguration mit subtileren Werten
|
||||
this.config = {
|
||||
nodeCount: 10, // Weniger Knoten
|
||||
nodeCount: 50, // Weniger Knoten
|
||||
nodeSize: 1.2, // Kleinere Knoten
|
||||
connectionDistance: 150, // Reduzierte Verbindungsdistanz
|
||||
connectionOpacity: 0.3, // Sanftere Verbindungslinien
|
||||
clusterCount: 7, // Weniger Cluster
|
||||
clusterCount: 2, // Weniger Cluster
|
||||
clusterRadius: 380, // Größerer Cluster-Radius für mehr Verteilung
|
||||
animationSpeed: 0.25, // Langsamere Animation
|
||||
flowDensity: 0.05, // Deutlich weniger Flussanimationen
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,46 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Datenbank aktualisieren{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4 py-10">
|
||||
<div class="bg-gray-800 bg-opacity-70 rounded-lg p-6 mb-6">
|
||||
<h1 class="text-2xl font-bold text-purple-400 mb-4">Datenbank aktualisieren</h1>
|
||||
|
||||
{% if message %}
|
||||
<div class="mb-6 p-4 rounded-lg {{ 'bg-green-800 bg-opacity-50' if success else 'bg-red-800 bg-opacity-50' }}">
|
||||
<p class="text-white">{{ message }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="mb-6">
|
||||
<p class="text-gray-300 mb-4">
|
||||
Diese Funktion aktualisiert die Datenbankstruktur, um mit dem aktuellen Datenmodell kompatibel zu sein.
|
||||
Dabei werden folgende Änderungen vorgenommen:
|
||||
</p>
|
||||
|
||||
<ul class="list-disc pl-6 text-gray-300 mb-6">
|
||||
<li>Hinzufügen von <code>bio</code>, <code>location</code>, <code>website</code>, <code>avatar</code> und <code>last_login</code> zur Benutzer-Tabelle</li>
|
||||
</ul>
|
||||
|
||||
<div class="bg-yellow-800 bg-opacity-30 p-4 rounded-lg mb-6">
|
||||
<p class="text-yellow-200">
|
||||
<i class="fas fa-exclamation-triangle mr-2"></i>
|
||||
<strong>Warnung:</strong> Bitte stelle sicher, dass du ein Backup der Datenbank erstellt hast, bevor du fortfährst.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ url_for('admin_update_database') }}">
|
||||
<div class="flex justify-between">
|
||||
<a href="{{ url_for('index') }}" class="px-4 py-2 bg-gray-700 text-white rounded-lg hover:bg-gray-600">
|
||||
Zurück zur Startseite
|
||||
</a>
|
||||
<button type="submit" class="px-4 py-2 bg-purple-700 text-white rounded-lg hover:bg-purple-600">
|
||||
Datenbank aktualisieren
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -6,7 +6,8 @@
|
||||
<title>Systades - {% block title %}{% endblock %}</title>
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" href="{{ url_for('static', filename='img/neuron-favicon.svg') }}" type="image/svg+xml">
|
||||
<link rel="icon" href="{{ url_for('static', filename='img/favicon.svg') }}" type="image/svg+xml">
|
||||
<link rel="icon" href="{{ url_for('static', filename='img/favicon.ico') }}" sizes="any">
|
||||
|
||||
<!-- Meta Tags -->
|
||||
<meta name="description" content="Eine interaktive Plattform zum Visualisieren, Erforschen und Teilen von Wissen">
|
||||
@@ -58,20 +59,6 @@
|
||||
800: '#0e1220',
|
||||
900: '#0a0e19'
|
||||
}
|
||||
},
|
||||
keyframes: {
|
||||
float: {
|
||||
'0%, 100%': { transform: 'translateY(0)' },
|
||||
'50%': { transform: 'translateY(-5px)' }
|
||||
},
|
||||
'bounce-slow': {
|
||||
'0%, 100%': { transform: 'translateY(0)' },
|
||||
'50%': { transform: 'translateY(-8px)' }
|
||||
}
|
||||
},
|
||||
animation: {
|
||||
float: 'float 3s ease-in-out infinite',
|
||||
'bounce-slow': 'bounce-slow 2s ease-in-out infinite'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -82,8 +69,8 @@
|
||||
<link href="{{ url_for('static', filename='fonts/inter.css') }}" rel="stylesheet">
|
||||
<link href="{{ url_for('static', filename='fonts/jetbrains-mono.css') }}" rel="stylesheet">
|
||||
|
||||
<!-- Font Awesome vom CDN -->
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" rel="stylesheet">
|
||||
<!-- Icons - Self-hosted Font Awesome -->
|
||||
<link href="{{ url_for('static', filename='css/all.min.css') }}" rel="stylesheet">
|
||||
|
||||
<!-- Assistent CSS -->
|
||||
<link href="{{ url_for('static', filename='css/assistant.css') }}" rel="stylesheet">
|
||||
@@ -100,9 +87,6 @@
|
||||
<!-- Neural Network Background CSS -->
|
||||
<link href="{{ url_for('static', filename='css/neural-network-background.css') }}" rel="stylesheet">
|
||||
|
||||
<!-- Mindmap CSS -->
|
||||
<link href="{{ url_for('static', filename='css/mindmap.css') }}" rel="stylesheet">
|
||||
|
||||
<!-- D3.js für Visualisierungen -->
|
||||
<script src="https://d3js.org/d3.v7.min.js"></script>
|
||||
|
||||
@@ -127,20 +111,18 @@
|
||||
<!-- Seitenspezifische Styles -->
|
||||
{% block extra_css %}{% endblock %}
|
||||
|
||||
<!-- Custom dark/light mode styles -->
|
||||
<!-- Custom dark mode styles -->
|
||||
<!-- ► ► Farb‑Token strikt getrennt ◄ ◄ -->
|
||||
<style>
|
||||
/* Light‑Mode */
|
||||
:root {
|
||||
--bg-primary:#f8fafc;
|
||||
--bg-secondary:#f1f5f9;
|
||||
--bg-primary:#f4f6fa;
|
||||
--bg-secondary:#e9ecf3;
|
||||
--text-primary:#232837;
|
||||
--text-secondary:#475569;
|
||||
--accent-primary:#7c3aed;
|
||||
--accent-secondary:#8b5cf6;
|
||||
--glow-effect:0 0 8px rgba(139,92,246,.08);
|
||||
background-image: linear-gradient(to bottom right, rgba(248, 250, 252, 0.8), rgba(241, 245, 249, 0.8));
|
||||
background-attachment: fixed;
|
||||
}
|
||||
/* Dark‑Mode */
|
||||
.dark {
|
||||
@@ -154,8 +136,7 @@
|
||||
}
|
||||
|
||||
body {
|
||||
@apply min-h-screen bg-[color:var(--bg-primary)] text-[color:var(--text-primary)];
|
||||
transition: background-color 0.5s ease-in-out, color 0.3s ease-in-out, background-image 0.5s ease-in-out;
|
||||
@apply min-h-screen bg-[color:var(--bg-primary)] text-[color:var(--text-primary)] transition-colors duration-300;
|
||||
}
|
||||
|
||||
/* Utilities */
|
||||
@@ -168,151 +149,6 @@
|
||||
.glass-navbar { @apply glass-morphism border backdrop-blur-xl; }
|
||||
.light .glass-navbar { background-color:rgba(255,255,255,.8); border-color:rgba(0,0,0,.05); }
|
||||
.dark .glass-navbar { background-color:rgba(10,14,25,.8); border-color:rgba(255,255,255,.05); }
|
||||
|
||||
/* Light-Mode spezifische Stile */
|
||||
body:not(.dark) {
|
||||
background-color: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.nav-link-light {
|
||||
color: var(--text-secondary);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.nav-link-light:hover {
|
||||
color: var(--text-primary);
|
||||
background-color: rgba(126, 34, 206, 0.1);
|
||||
}
|
||||
|
||||
.nav-link-light-active {
|
||||
color: var(--accent-primary);
|
||||
background-color: rgba(126, 34, 206, 0.15);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Kartendesign im Light-Mode */
|
||||
body:not(.dark) .card {
|
||||
background-color: white;
|
||||
border: 1px solid #e5e7eb;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
body:not(.dark) .card:hover {
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--light-primary-hover);
|
||||
}
|
||||
|
||||
/* Light Mode Buttons */
|
||||
body:not(.dark) .btn,
|
||||
body:not(.dark) button:not(.toggle) {
|
||||
background: linear-gradient(135deg, #7c3aed, #6d28d9);
|
||||
color: white !important;
|
||||
border: none;
|
||||
box-shadow: 0 2px 4px rgba(124, 58, 237, 0.25);
|
||||
border-radius: 8px;
|
||||
padding: 0.625rem 1.25rem;
|
||||
transition: all 0.2s ease;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
body:not(.dark) .btn:hover,
|
||||
body:not(.dark) button:not(.toggle):hover {
|
||||
background: linear-gradient(135deg, #8b5cf6, #7c3aed);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(124, 58, 237, 0.3);
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
/* KI-Chat Button im Light-Mode */
|
||||
body:not(.dark) [onclick*="MindMap.assistant.toggleAssistant"] {
|
||||
background: linear-gradient(135deg, #7c3aed, #4f46e5);
|
||||
color: white !important;
|
||||
font-weight: 500;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
body:not(.dark) [onclick*="MindMap.assistant.toggleAssistant"]:hover {
|
||||
background: linear-gradient(135deg, #8b5cf6, #6366f1);
|
||||
box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3);
|
||||
}
|
||||
|
||||
/* Style improvements for the theme toggle button */
|
||||
.theme-toggle {
|
||||
position: relative;
|
||||
width: 48px;
|
||||
height: 24px;
|
||||
border-radius: 24px;
|
||||
padding: 2px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body.dark .theme-toggle {
|
||||
background: linear-gradient(to right, #7c3aed, #3b82f6);
|
||||
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.3), 0 0 10px rgba(124, 58, 237, 0.3);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
body:not(.dark) .theme-toggle {
|
||||
background: linear-gradient(to right, #8b5cf6, #60a5fa);
|
||||
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1), 0 0 10px rgba(124, 58, 237, 0.15);
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.theme-toggle::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
top: 2px;
|
||||
transition: all 0.3s ease;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
body.dark .theme-toggle::after {
|
||||
background: #f1f5f9 url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%237c3aed' width='14' height='14'%3E%3Cpath d='M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z'%3E%3C/path%3E%3C/svg%3E") no-repeat center center;
|
||||
transform: translateX(24px);
|
||||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
body:not(.dark) .theme-toggle::after {
|
||||
background: #fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23f59e0b' width='14' height='14'%3E%3Cpath d='M12 2.25a.75.75 0 01.75.75v2.25a.75.75 0 01-1.5 0V3a.75.75 0 01.75-.75zM7.5 12a4.5 4.5 0 119 0 4.5 4.5 0 01-9 0zM18.894 6.166a.75.75 0 00-1.06-1.06l-1.591 1.59a.75.75 0 101.06 1.061l1.591-1.59zM21.75 12a.75.75 0 01-.75.75h-2.25a.75.75 0 010-1.5H21a.75.75 0 01.75.75zM17.834 18.894a.75.75 0 001.06-1.06l-1.59-1.591a.75.75 0 10-1.061 1.06l1.59 1.591zM12 18a.75.75 0 01.75.75V21a.75.75 0 01-1.5 0v-2.25A.75.75 0 0112 18zM7.758 17.303a.75.75 0 00-1.061-1.06l-1.591 1.59a.75.75 0 001.06 1.061l1.591-1.59zM6 12a.75.75 0 01-.75.75H3a.75.75 0 010-1.5h2.25A.75.75 0 016 12zM6.697 7.757a.75.75 0 001.06-1.06l-1.59-1.591a.75.75 0 00-1.061 1.06l1.59 1.591z'%3E%3C/path%3E%3C/svg%3E") no-repeat center center;
|
||||
transform: translateX(2px);
|
||||
box-shadow: 0 0 8px rgba(124, 58, 237, 0.2);
|
||||
}
|
||||
|
||||
.theme-toggle:hover::after {
|
||||
box-shadow: 0 0 12px rgba(124, 58, 237, 0.4);
|
||||
}
|
||||
|
||||
/* Fixes for light mode button text colors */
|
||||
body:not(.dark) .btn-primary {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
/* Fix for KI-Chat container */
|
||||
#chatgpt-assistant {
|
||||
position: fixed;
|
||||
bottom: 1.5rem;
|
||||
right: 1.5rem;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.chat-assistant {
|
||||
max-height: 80vh !important;
|
||||
}
|
||||
|
||||
.chat-assistant .chat-messages {
|
||||
max-height: calc(80vh - 160px) !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body data-page="{{ request.endpoint }}" class="relative overflow-x-hidden dark bg-gray-900 text-white" x-data="{
|
||||
@@ -322,17 +158,6 @@
|
||||
showSettingsModal: false,
|
||||
|
||||
init() {
|
||||
this.initDarkMode();
|
||||
},
|
||||
|
||||
initDarkMode() {
|
||||
// Lade zuerst den Wert aus dem localStorage (client-seitig)
|
||||
const storedMode = localStorage.getItem('colorMode');
|
||||
if (storedMode) {
|
||||
this.darkMode = storedMode === 'dark';
|
||||
}
|
||||
|
||||
// Dann hole die Server-Einstellung, die Vorrang hat
|
||||
this.fetchDarkModeFromSession();
|
||||
},
|
||||
|
||||
@@ -342,7 +167,7 @@
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
this.darkMode = data.darkMode === 'true';
|
||||
this.applyDarkMode();
|
||||
document.querySelector('html').classList.toggle('dark', this.darkMode);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
@@ -350,17 +175,10 @@
|
||||
});
|
||||
},
|
||||
|
||||
applyDarkMode() {
|
||||
document.querySelector('html').classList.toggle('dark', this.darkMode);
|
||||
document.querySelector('body').classList.toggle('dark', this.darkMode);
|
||||
localStorage.setItem('colorMode', this.darkMode ? 'dark' : 'light');
|
||||
},
|
||||
|
||||
toggleDarkMode() {
|
||||
this.darkMode = !this.darkMode;
|
||||
this.applyDarkMode();
|
||||
document.querySelector('html').classList.toggle('dark', this.darkMode);
|
||||
|
||||
// Server über Änderung informieren
|
||||
fetch('/api/set_dark_mode', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -371,10 +189,12 @@
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
// Event auslösen für andere Komponenten
|
||||
localStorage.setItem('darkMode', this.darkMode ? 'dark' : 'light');
|
||||
document.dispatchEvent(new CustomEvent('darkModeToggled', {
|
||||
detail: { isDark: this.darkMode }
|
||||
}));
|
||||
} else {
|
||||
console.error('Fehler beim Speichern der Dark Mode-Einstellung:', data.error);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
@@ -390,7 +210,6 @@
|
||||
<div class="container mx-auto flex justify-between items-center">
|
||||
<!-- Logo -->
|
||||
<a href="{{ url_for('index') }}" class="flex items-center group">
|
||||
<img src="{{ url_for('static', filename='img/neuron-logo.svg') }}" alt="Systades Logo" class="w-8 h-8 mr-2 transform transition-transform group-hover:scale-110">
|
||||
<span class="text-2xl font-bold gradient-text transform transition-transform group-hover:scale-105">Systades</span>
|
||||
</a>
|
||||
|
||||
@@ -422,7 +241,7 @@
|
||||
class="nav-link flex items-center"
|
||||
x-bind:class="darkMode
|
||||
? 'bg-gradient-to-r from-purple-900/90 to-indigo-800/90 text-white font-medium px-4 py-2 rounded-xl hover:shadow-lg hover:shadow-purple-800/30 transition-all duration-300'
|
||||
: 'bg-gradient-to-r from-purple-600 to-indigo-500 text-white font-medium px-4 py-2 rounded-xl hover:shadow-md transition-all duration-300'">
|
||||
: 'bg-gradient-to-r from-purple-600/30 to-indigo-500/30 text-gray-800 font-medium px-4 py-2 rounded-xl hover:shadow-md transition-all duration-300'">
|
||||
<i class="fa-solid fa-robot mr-2"></i>KI-Chat
|
||||
</button>
|
||||
{% if current_user.is_authenticated %}
|
||||
@@ -438,14 +257,25 @@
|
||||
|
||||
<!-- Rechte Seite -->
|
||||
<div class="flex items-center space-x-4">
|
||||
<!-- Dark/Light Mode Schalter -->
|
||||
<button
|
||||
@click="toggleDarkMode()"
|
||||
class="theme-toggle relative w-12 h-6 rounded-full transition-all duration-300 flex items-center overflow-hidden"
|
||||
aria-label="Dark Mode umschalten"
|
||||
>
|
||||
<span class="sr-only" x-text="darkMode ? 'Zum Light Mode wechseln' : 'Zum Dark Mode wechseln'"></span>
|
||||
</button>
|
||||
<!-- Dark Mode Toggle Switch -->
|
||||
<div class="flex items-center cursor-pointer" @click="toggleDarkMode">
|
||||
<div class="relative w-12 h-6">
|
||||
<input type="checkbox" id="darkModeToggle" class="sr-only" x-model="darkMode">
|
||||
<div class="block w-12 h-6 rounded-full transition-colors duration-300"
|
||||
x-bind:class="darkMode ? 'bg-purple-800/50' : 'bg-gray-400/50'"></div>
|
||||
<div class="dot absolute left-1 top-1 w-4 h-4 rounded-full transition-transform duration-300 shadow-md"
|
||||
x-bind:class="darkMode ? 'bg-purple-600 transform translate-x-6' : 'bg-white'"></div>
|
||||
</div>
|
||||
<div class="ml-3 hidden sm:block"
|
||||
x-bind:class="darkMode ? 'text-white/90' : 'text-gray-700'">
|
||||
<span x-text="darkMode ? 'Dunkel' : 'Hell'"></span>
|
||||
</div>
|
||||
<div class="ml-2 sm:hidden"
|
||||
x-bind:class="darkMode ? 'text-white/90' : 'text-gray-700'">
|
||||
<i class="fa-solid" :class="darkMode ? 'fa-sun' : 'fa-moon'"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Profil-Link oder Login -->
|
||||
{% if current_user.is_authenticated %}
|
||||
<div class="relative" x-data="{ open: false }">
|
||||
@@ -459,21 +289,12 @@
|
||||
{% if current_user.avatar %}
|
||||
<img src="{{ current_user.avatar }}" alt="{{ current_user.username }}" class="w-full h-full object-cover">
|
||||
{% else %}
|
||||
<svg width="100%" height="100%" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="100" cy="100" r="98" fill="url(#user-gradient)" stroke="#7C3AED" stroke-width="4"/>
|
||||
<circle cx="100" cy="80" r="36" fill="white"/>
|
||||
<path d="M100 140C77.9086 140 60 157.909 60 180H140C140 157.909 122.091 140 100 140Z" fill="white"/>
|
||||
<defs>
|
||||
<linearGradient id="user-gradient" x1="0" y1="0" x2="200" y2="200" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#8B5CF6"/>
|
||||
<stop offset="1" stop-color="#3B82F6"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
{{ current_user.username[0].upper() }}
|
||||
{% endif %}
|
||||
</div>
|
||||
<span class="hidden md:block">{{ current_user.username }}</span>
|
||||
<i class="fas fa-chevron-down text-xs opacity-60 ml-1.5"></i>
|
||||
<span class="text-sm hidden lg:block">{{ current_user.username }}</span>
|
||||
<i class="fa-solid fa-chevron-down text-xs hidden lg:block transition-transform duration-200"
|
||||
x-bind:class="open ? 'transform rotate-180' : ''"></i>
|
||||
</button>
|
||||
|
||||
<!-- Dropdown-Menü -->
|
||||
@@ -591,7 +412,7 @@
|
||||
class="block w-full text-left py-3.5 px-4 rounded-xl transition-all duration-200 flex items-center"
|
||||
x-bind:class="darkMode
|
||||
? 'bg-gradient-to-r from-purple-600/30 to-blue-500/30 text-white hover:from-purple-600/40 hover:to-blue-500/40'
|
||||
: 'bg-gradient-to-r from-purple-600 to-blue-500 text-white hover:from-purple-600/90 hover:to-blue-500/90'">
|
||||
: 'bg-gradient-to-r from-purple-500/10 to-blue-400/10 text-gray-900 hover:from-purple-500/20 hover:to-blue-400/20'">
|
||||
<i class="fa-solid fa-robot w-5 mr-3"></i>KI-Chat
|
||||
</button>
|
||||
{% if current_user.is_authenticated %}
|
||||
@@ -657,10 +478,6 @@
|
||||
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
||||
Mindmap
|
||||
</a>
|
||||
<a href="{{ url_for('search_thoughts_page') }}" class="text-sm transition-all duration-200"
|
||||
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
||||
Suche
|
||||
</a>
|
||||
{% if current_user.is_authenticated %}
|
||||
<a href="{{ url_for('profile') }}" class="text-sm transition-all duration-200"
|
||||
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
||||
@@ -743,186 +560,6 @@
|
||||
|
||||
<!-- KI-Chat Initialisierung -->
|
||||
<script>
|
||||
// ChatGPT-Assistent Klasse
|
||||
class ChatGPTAssistant {
|
||||
constructor() {
|
||||
this.chatContainer = null;
|
||||
this.messages = [];
|
||||
this.isOpen = false;
|
||||
}
|
||||
|
||||
init() {
|
||||
// Chat-Container erstellen, falls noch nicht vorhanden
|
||||
if (!document.getElementById('chat-assistant-container')) {
|
||||
this.createChatInterface();
|
||||
}
|
||||
|
||||
// Event-Listener für Chat-Button
|
||||
const chatButton = document.getElementById('chat-assistant-button');
|
||||
if (chatButton) {
|
||||
chatButton.addEventListener('click', () => this.toggleChat());
|
||||
}
|
||||
|
||||
// Event-Listener für Senden-Button
|
||||
const sendButton = document.getElementById('chat-send-button');
|
||||
if (sendButton) {
|
||||
sendButton.addEventListener('click', () => this.sendMessage());
|
||||
}
|
||||
|
||||
// Event-Listener für Eingabefeld (Enter-Taste)
|
||||
const inputField = document.getElementById('chat-input');
|
||||
if (inputField) {
|
||||
inputField.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
this.sendMessage();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log('KI-Assistent erfolgreich initialisiert');
|
||||
}
|
||||
|
||||
createChatInterface() {
|
||||
// Chat-Button erstellen
|
||||
const chatButton = document.createElement('button');
|
||||
chatButton.id = 'chat-assistant-button';
|
||||
chatButton.className = 'fixed bottom-6 right-6 bg-primary-600 text-white rounded-full p-4 shadow-lg z-50 hover:bg-primary-700 transition-all';
|
||||
chatButton.innerHTML = '<i class="fas fa-robot text-xl"></i>';
|
||||
document.body.appendChild(chatButton);
|
||||
|
||||
// Chat-Container erstellen
|
||||
const chatContainer = document.createElement('div');
|
||||
chatContainer.id = 'chat-assistant-container';
|
||||
chatContainer.className = 'fixed bottom-24 right-6 w-80 md:w-96 bg-white dark:bg-gray-800 rounded-xl shadow-xl z-50 flex flex-col transition-all duration-300 transform scale-0 origin-bottom-right';
|
||||
chatContainer.style.height = '500px';
|
||||
chatContainer.style.maxHeight = '70vh';
|
||||
|
||||
// Chat-Header
|
||||
chatContainer.innerHTML = `
|
||||
<div class="p-4 border-b dark:border-gray-700 flex justify-between items-center">
|
||||
<h3 class="font-bold text-gray-800 dark:text-white">Systades Assistent</h3>
|
||||
<button id="chat-close-button" class="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div id="chat-messages" class="flex-1 overflow-y-auto p-4 space-y-4"></div>
|
||||
<div class="p-4 border-t dark:border-gray-700">
|
||||
<div class="flex space-x-2">
|
||||
<input id="chat-input" type="text" placeholder="Frage stellen..." class="flex-1 px-4 py-2 rounded-lg border dark:border-gray-700 dark:bg-gray-700 dark:text-white focus:outline-none focus:ring-2 focus:ring-primary-500">
|
||||
<button id="chat-send-button" class="bg-primary-600 text-white px-4 py-2 rounded-lg hover:bg-primary-700 transition-all">
|
||||
<i class="fas fa-paper-plane"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(chatContainer);
|
||||
this.chatContainer = chatContainer;
|
||||
|
||||
// Event-Listener für Schließen-Button
|
||||
const closeButton = document.getElementById('chat-close-button');
|
||||
if (closeButton) {
|
||||
closeButton.addEventListener('click', () => this.toggleChat());
|
||||
}
|
||||
}
|
||||
|
||||
toggleChat() {
|
||||
this.isOpen = !this.isOpen;
|
||||
if (this.isOpen) {
|
||||
this.chatContainer.classList.remove('scale-0');
|
||||
this.chatContainer.classList.add('scale-100');
|
||||
} else {
|
||||
this.chatContainer.classList.remove('scale-100');
|
||||
this.chatContainer.classList.add('scale-0');
|
||||
}
|
||||
}
|
||||
|
||||
async sendMessage() {
|
||||
const inputField = document.getElementById('chat-input');
|
||||
const messageText = inputField.value.trim();
|
||||
|
||||
if (!messageText) return;
|
||||
|
||||
// Benutzer-Nachricht anzeigen
|
||||
this.addMessage('user', messageText);
|
||||
inputField.value = '';
|
||||
|
||||
// Lade-Indikator anzeigen
|
||||
this.addMessage('assistant', '...', 'loading-message');
|
||||
|
||||
try {
|
||||
// API-Anfrage senden
|
||||
const response = await fetch('/api/assistant', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
messages: this.messages.map(msg => ({
|
||||
role: msg.role,
|
||||
content: msg.content
|
||||
}))
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Lade-Nachricht entfernen
|
||||
const loadingMessage = document.getElementById('loading-message');
|
||||
if (loadingMessage) {
|
||||
loadingMessage.remove();
|
||||
}
|
||||
|
||||
if (data.error) {
|
||||
this.addMessage('assistant', 'Entschuldigung, es ist ein Fehler aufgetreten: ' + data.error);
|
||||
} else {
|
||||
this.addMessage('assistant', data.response);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler bei der API-Anfrage:', error);
|
||||
|
||||
// Lade-Nachricht entfernen
|
||||
const loadingMessage = document.getElementById('loading-message');
|
||||
if (loadingMessage) {
|
||||
loadingMessage.remove();
|
||||
}
|
||||
|
||||
this.addMessage('assistant', 'Entschuldigung, es ist ein Fehler aufgetreten. Bitte versuche es später erneut.');
|
||||
}
|
||||
}
|
||||
|
||||
addMessage(role, content, id = null) {
|
||||
const messagesContainer = document.getElementById('chat-messages');
|
||||
|
||||
// Nachricht zum Array hinzufügen (außer Lade-Nachrichten)
|
||||
if (id !== 'loading-message') {
|
||||
this.messages.push({ role, content });
|
||||
}
|
||||
|
||||
// Nachricht zum DOM hinzufügen
|
||||
const messageElement = document.createElement('div');
|
||||
messageElement.className = `p-3 rounded-lg ${role === 'user' ? 'bg-primary-100 dark:bg-primary-900/30 ml-6' : 'bg-gray-100 dark:bg-gray-700 mr-6'}`;
|
||||
if (id) {
|
||||
messageElement.id = id;
|
||||
}
|
||||
|
||||
messageElement.innerHTML = `
|
||||
<div class="flex items-start">
|
||||
<div class="w-8 h-8 rounded-full flex items-center justify-center ${role === 'user' ? 'bg-primary-600' : 'bg-gray-600'} text-white mr-2">
|
||||
<i class="fas ${role === 'user' ? 'fa-user' : 'fa-robot'} text-xs"></i>
|
||||
</div>
|
||||
<div class="flex-1 text-sm ${role === 'user' ? 'text-gray-800 dark:text-gray-200' : 'text-gray-700 dark:text-gray-300'}">
|
||||
${content}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
messagesContainer.appendChild(messageElement);
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialisiere den ChatGPT-Assistenten direkt, um sicherzustellen,
|
||||
// dass er auf jeder Seite verfügbar ist, selbst wenn MindMap nicht geladen ist
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
@@ -941,83 +578,37 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Dark/Light-Mode vereinheitlicht -->
|
||||
<!-- Dark/Light-Mode persistent und robust -->
|
||||
<script>
|
||||
// Globaler Zugriff für externe Skripte
|
||||
window.MindMap = window.MindMap || {};
|
||||
|
||||
// Funktion zum Anwenden des Dark Mode, strikt getrennt
|
||||
function applyDarkModeClasses(isDarkMode) {
|
||||
if (isDarkMode) {
|
||||
document.documentElement.classList.add('dark');
|
||||
document.body.classList.add('dark');
|
||||
localStorage.setItem('colorMode', 'dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
document.body.classList.remove('dark');
|
||||
localStorage.setItem('colorMode', 'light');
|
||||
}
|
||||
|
||||
// Alpine.js darkMode-Variable aktualisieren, falls zutreffend
|
||||
const appEl = document.querySelector('body');
|
||||
if (appEl && appEl.__x) {
|
||||
appEl.__x.$data.darkMode = isDarkMode;
|
||||
}
|
||||
|
||||
// Event für andere Komponenten auslösen
|
||||
document.dispatchEvent(new CustomEvent('darkModeToggled', {
|
||||
detail: { isDark: isDarkMode }
|
||||
}));
|
||||
}
|
||||
|
||||
window.MindMap.toggleDarkMode = function() {
|
||||
const isDark = document.documentElement.classList.contains('dark');
|
||||
const newIsDark = !isDark;
|
||||
|
||||
// DOM aktualisieren
|
||||
applyDarkModeClasses(newIsDark);
|
||||
|
||||
// Server aktualisieren
|
||||
fetch('/api/set_dark_mode', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ darkMode: newIsDark })
|
||||
})
|
||||
.catch(console.error);
|
||||
};
|
||||
|
||||
// Initialisierung beim Laden
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Reihenfolge der Prüfungen: Serverseitige Einstellung > Lokale Einstellung > Browser-Präferenz
|
||||
|
||||
// 1. Zuerst lokale Einstellung prüfen
|
||||
const storedMode = localStorage.getItem('colorMode');
|
||||
if (storedMode) {
|
||||
applyDarkModeClasses(storedMode === 'dark');
|
||||
} else {
|
||||
// 2. Fallback auf Browser-Präferenz
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
applyDarkModeClasses(prefersDark);
|
||||
}
|
||||
|
||||
// 3. Serverseitige Einstellung abrufen und anwenden
|
||||
fetch('/api/get_dark_mode')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
const serverDarkMode = data.darkMode === true || data.darkMode === 'true';
|
||||
applyDarkModeClasses(serverDarkMode);
|
||||
}
|
||||
})
|
||||
.catch(error => console.error('Fehler beim Abrufen des Dark Mode Status:', error));
|
||||
|
||||
// Listener für Änderungen der Browser-Präferenz
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
|
||||
if (localStorage.getItem('colorMode') === null) {
|
||||
applyDarkModeClasses(e.matches);
|
||||
(function() {
|
||||
function applyMode(mode) {
|
||||
if (mode === 'dark') {
|
||||
document.documentElement.classList.add('dark');
|
||||
localStorage.setItem('colorMode', 'dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
localStorage.setItem('colorMode', 'light');
|
||||
}
|
||||
}
|
||||
// Beim Laden: Präferenz aus localStorage oder System übernehmen
|
||||
const stored = localStorage.getItem('colorMode');
|
||||
if (stored === 'dark' || stored === 'light') {
|
||||
applyMode(stored);
|
||||
} else {
|
||||
// Systempräferenz als Fallback
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
applyMode(prefersDark ? 'dark' : 'light');
|
||||
}
|
||||
// Umschalter für alle Mode-Toggles
|
||||
window.toggleColorMode = function() {
|
||||
const isDark = document.documentElement.classList.contains('dark');
|
||||
applyMode(isDark ? 'light' : 'dark');
|
||||
};
|
||||
// Optional: globales Event für andere Skripte
|
||||
window.addEventListener('storage', function(e) {
|
||||
if (e.key === 'colorMode') applyMode(e.newValue);
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,192 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}{{ category.title }} - Forum{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.thread-item {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.thread-item:hover {
|
||||
transform: translateX(2px);
|
||||
}
|
||||
.thread-pinned {
|
||||
border-left-width: 4px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<!-- Breadcrumb Navigation -->
|
||||
<div class="mb-6 flex items-center text-sm">
|
||||
<a href="{{ url_for('community') }}" class="opacity-75 hover:opacity-100 transition-opacity">
|
||||
<i class="fas fa-home mr-1"></i> Forum
|
||||
</a>
|
||||
<span class="mx-2 opacity-50">/</span>
|
||||
<span class="font-medium">{{ category.title }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Kategorie-Header -->
|
||||
<div class="mb-8 flex flex-wrap items-center justify-between gap-4">
|
||||
<div class="flex items-center">
|
||||
<!-- Kategorie-Icon -->
|
||||
<div class="w-12 h-12 rounded-xl mr-4 flex items-center justify-center text-white"
|
||||
style="background-color: {{ node.color_code or '#6d28d9' }}">
|
||||
<i class="fas {{ node.icon or 'fa-folder' }} text-2xl"></i>
|
||||
</div>
|
||||
|
||||
<!-- Kategorie-Info -->
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold">{{ category.title }}</h1>
|
||||
<p class="opacity-75">{{ category.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Neues Thema erstellen -->
|
||||
<a href="{{ url_for('new_post', category_id=category.id) }}"
|
||||
class="px-5 py-2.5 rounded-lg transition-all duration-300 flex items-center"
|
||||
x-bind:class="darkMode
|
||||
? 'bg-indigo-700 hover:bg-indigo-600 text-white'
|
||||
: 'bg-indigo-500 hover:bg-indigo-600 text-white'">
|
||||
<i class="fas fa-plus-circle mr-2"></i>
|
||||
Neues Thema
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Threads anzeigen -->
|
||||
<div class="mb-8 rounded-xl overflow-hidden"
|
||||
x-bind:class="darkMode ? 'bg-gray-800/60 border border-white/10' : 'bg-white border border-gray-200'">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="p-4 border-b" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
|
||||
<div class="grid grid-cols-12 gap-4">
|
||||
<div class="col-span-7 font-medium">Thema</div>
|
||||
<div class="col-span-1 text-center font-medium hidden md:block">Antworten</div>
|
||||
<div class="col-span-2 text-center font-medium hidden md:block">Autor</div>
|
||||
<div class="col-span-2 text-center font-medium hidden md:block">Letzte Antwort</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Thread-Liste -->
|
||||
{% if threads_data %}
|
||||
{% for thread_data in threads_data %}
|
||||
{% set thread = thread_data.thread %}
|
||||
<div class="thread-item p-4 border-b last:border-b-0 {{ 'thread-pinned' if thread.is_pinned }}"
|
||||
x-bind:class="darkMode
|
||||
? 'border-white/10 hover:bg-gray-700/50 {{ 'border-l-yellow-500' if thread.is_pinned }}'
|
||||
: 'border-gray-200 hover:bg-gray-50 {{ 'border-l-yellow-500' if thread.is_pinned }}'">
|
||||
<a href="{{ url_for('forum_post', post_id=thread.id) }}" class="block">
|
||||
<div class="grid grid-cols-12 gap-4">
|
||||
<!-- Thema -->
|
||||
<div class="col-span-12 md:col-span-7">
|
||||
<div class="flex items-start">
|
||||
<!-- Status-Icons -->
|
||||
<div class="flex flex-col items-center mr-3 pt-1">
|
||||
{% if thread.is_pinned %}
|
||||
<i class="fas fa-thumbtack text-yellow-500" title="Angepinnt"></i>
|
||||
{% endif %}
|
||||
{% if thread.is_locked %}
|
||||
<i class="fas fa-lock text-red-500 mt-1" title="Gesperrt"></i>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Themen-Info -->
|
||||
<div>
|
||||
<h3 class="font-medium leading-snug mb-1 {% if thread.is_locked %}opacity-70{% endif %}">
|
||||
{{ thread.title }}
|
||||
</h3>
|
||||
<div class="flex items-center text-xs opacity-70 mt-1">
|
||||
<span><i class="fas fa-eye mr-1"></i> {{ thread.view_count }}</span>
|
||||
<span class="mx-2 block md:hidden">•</span>
|
||||
<span class="block md:hidden"><i class="fas fa-reply mr-1"></i> {{ thread_data.reply_count }}</span>
|
||||
<span class="mx-2">•</span>
|
||||
<span><i class="fas fa-clock mr-1"></i> {{ thread.created_at.strftime('%d.%m.%Y, %H:%M') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Antworten -->
|
||||
<div class="col-span-1 text-center hidden md:flex items-center justify-center">
|
||||
<span class="px-2.5 py-1 rounded-full text-sm font-medium"
|
||||
x-bind:class="darkMode
|
||||
? 'bg-indigo-900/40 text-indigo-300'
|
||||
: 'bg-indigo-100 text-indigo-800'">
|
||||
{{ thread_data.reply_count }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Autor -->
|
||||
<div class="col-span-2 text-center hidden md:flex items-center justify-center">
|
||||
<div class="flex items-center">
|
||||
<div class="w-7 h-7 rounded-full flex items-center justify-center text-white text-xs font-medium overflow-hidden mr-2"
|
||||
style="background: linear-gradient(135deg, #8b5cf6, #6366f1);">
|
||||
{% if thread.author.avatar %}
|
||||
<img src="{{ thread.author.avatar }}" alt="{{ thread.author.username }}" class="w-full h-full object-cover">
|
||||
{% else %}
|
||||
{{ thread.author.username[0].upper() }}
|
||||
{% endif %}
|
||||
</div>
|
||||
<span class="text-sm truncate max-w-[80px]">{{ thread.author.username }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Letzte Antwort -->
|
||||
<div class="col-span-2 text-center hidden md:block text-sm">
|
||||
{% if thread_data.latest_reply %}
|
||||
<div>{{ thread_data.latest_reply.created_at.strftime('%d.%m.%Y') }}</div>
|
||||
<div class="opacity-75 text-xs">{{ thread_data.latest_reply.created_at.strftime('%H:%M') }} Uhr</div>
|
||||
{% else %}
|
||||
<span class="opacity-60">Keine Antworten</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="p-8 text-center">
|
||||
<div class="text-3xl mb-3 opacity-30"><i class="fas fa-comments"></i></div>
|
||||
<h3 class="text-xl font-semibold mb-2">Keine Themen vorhanden</h3>
|
||||
<p class="opacity-75 mb-4">In dieser Kategorie wurden noch keine Themen erstellt.</p>
|
||||
<a href="{{ url_for('new_post', category_id=category.id) }}"
|
||||
class="inline-block px-5 py-2.5 rounded-lg transition-all duration-300"
|
||||
x-bind:class="darkMode
|
||||
? 'bg-indigo-700 hover:bg-indigo-600 text-white'
|
||||
: 'bg-indigo-500 hover:bg-indigo-600 text-white'">
|
||||
<i class="fas fa-plus-circle mr-2"></i>
|
||||
Erstes Thema erstellen
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Link zur Mindmap -->
|
||||
<div class="rounded-xl p-5 mb-4 flex items-center"
|
||||
x-bind:class="darkMode ? 'bg-purple-900/20 border border-purple-800/30' : 'bg-purple-50 border border-purple-100'">
|
||||
<div class="text-3xl mr-4 opacity-80">
|
||||
<i class="fas fa-diagram-project" style="color: {{ node.color_code }}"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-medium mb-1">Mindmap-Knotenpunkt: {{ node.name }}</h3>
|
||||
<p class="text-sm opacity-75">In der Mindmap findest du weitere Informationen zu diesem Themenbereich.</p>
|
||||
</div>
|
||||
<div class="ml-auto">
|
||||
<a href="{{ url_for('mindmap') }}"
|
||||
class="px-4 py-2 rounded-lg inline-block text-sm transition-all"
|
||||
x-bind:class="darkMode
|
||||
? 'bg-purple-800/60 hover:bg-purple-700/60 text-white'
|
||||
: 'bg-white hover:bg-purple-100 text-purple-800 border border-purple-200'">
|
||||
Zur Mindmap
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// Hier können bei Bedarf kategoriespezifische Scripts eingefügt werden
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,344 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Beitrag bearbeiten{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.markdown-preview {
|
||||
min-height: 200px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.markdown-preview p {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.markdown-preview h1, .markdown-preview h2, .markdown-preview h3,
|
||||
.markdown-preview h4, .markdown-preview h5, .markdown-preview h6 {
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.markdown-preview h1 { font-size: 1.8rem; }
|
||||
.markdown-preview h2 { font-size: 1.5rem; }
|
||||
.markdown-preview h3 { font-size: 1.3rem; }
|
||||
.markdown-preview h4 { font-size: 1.1rem; }
|
||||
.markdown-preview ul, .markdown-preview ol {
|
||||
margin-left: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.markdown-preview ul { list-style-type: disc; }
|
||||
.markdown-preview ol { list-style-type: decimal; }
|
||||
.markdown-preview pre {
|
||||
background-color: rgba(0,0,0,0.05);
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
overflow-x: auto;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
.markdown-preview code {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.9em;
|
||||
padding: 0.1em 0.3em;
|
||||
border-radius: 0.3em;
|
||||
background-color: rgba(0,0,0,0.05);
|
||||
}
|
||||
.markdown-preview pre code {
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
}
|
||||
.markdown-preview blockquote {
|
||||
border-left: 4px solid;
|
||||
padding-left: 1rem;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
margin-bottom: 1rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.dark .markdown-preview code {
|
||||
background-color: rgba(255,255,255,0.1);
|
||||
}
|
||||
.dark .markdown-preview blockquote {
|
||||
border-color: rgba(255,255,255,0.2);
|
||||
}
|
||||
.node-mention {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background-color: rgba(109, 40, 217, 0.1);
|
||||
color: #6d28d9;
|
||||
border-radius: 4px;
|
||||
padding: 1px 6px;
|
||||
font-size: 0.9em;
|
||||
margin: 0 2px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.dark .node-mention {
|
||||
background-color: rgba(167, 139, 250, 0.2);
|
||||
color: #a78bfa;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<!-- Breadcrumb Navigation -->
|
||||
<div class="mb-6 flex items-center text-sm">
|
||||
<a href="{{ url_for('community') }}" class="opacity-75 hover:opacity-100 transition-opacity">
|
||||
<i class="fas fa-home mr-1"></i> Forum
|
||||
</a>
|
||||
<span class="mx-2 opacity-50">/</span>
|
||||
<a href="{{ url_for('forum_category', category_id=post.category_id) }}" class="opacity-75 hover:opacity-100 transition-opacity">
|
||||
{{ post.category.title }}
|
||||
</a>
|
||||
<span class="mx-2 opacity-50">/</span>
|
||||
{% if post.parent_id %}
|
||||
<a href="{{ url_for('forum_post', post_id=post.parent_id) }}" class="opacity-75 hover:opacity-100 transition-opacity truncate max-w-[200px]">
|
||||
{{ post.parent.title }}
|
||||
</a>
|
||||
<span class="mx-2 opacity-50">/</span>
|
||||
{% endif %}
|
||||
<span class="font-medium">Beitrag bearbeiten</span>
|
||||
</div>
|
||||
|
||||
<!-- Formular-Header -->
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-bold mb-2">Beitrag bearbeiten</h1>
|
||||
<p class="opacity-75">
|
||||
{% if post.parent_id %}
|
||||
Antwort auf <span class="font-medium">{{ post.parent.title }}</span>
|
||||
{% else %}
|
||||
in der Kategorie <span class="font-medium">{{ post.category.title }}</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Formular -->
|
||||
<div class="mb-8 rounded-xl overflow-hidden"
|
||||
x-bind:class="darkMode ? 'bg-gray-800/60 border border-white/10' : 'bg-white border border-gray-200'">
|
||||
<div class="p-4 border-b font-medium" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
|
||||
<i class="fas fa-edit mr-2"></i>
|
||||
Beitrag bearbeiten
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<form action="{{ url_for('edit_post', post_id=post.id) }}" method="POST" x-data="{
|
||||
title: '{{ post.title|safe }}',
|
||||
content: '{{ post.content|replace('\n', '\\n')|replace('\'', '\\\'')|safe }}',
|
||||
showPreview: false,
|
||||
previewHtml: '',
|
||||
|
||||
updatePreview() {
|
||||
// Verarbeite den Inhalt
|
||||
if (this.content.trim() === '') {
|
||||
this.previewHtml = '<div class=\'opacity-50 italic\'>Die Vorschau wird hier angezeigt...</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Verarbeite Markdown
|
||||
let html = marked.parse(this.content);
|
||||
|
||||
// Ersetze @Knotenname mit entsprechenden Links
|
||||
html = html.replace(/@([a-zA-Z0-9äöüÄÖÜß_-]+)/g, '<span class=\'node-mention\'><i class=\'fas fa-diagram-project fa-xs mr-1\'></i>$1</span>');
|
||||
|
||||
this.previewHtml = html;
|
||||
}
|
||||
}">
|
||||
<div class="mb-6">
|
||||
<label for="title" class="block mb-2 font-medium">Titel</label>
|
||||
<div class="rounded-lg overflow-hidden"
|
||||
x-bind:class="darkMode ? 'border border-white/20' : 'border border-gray-300'">
|
||||
<input type="text" id="title" name="title"
|
||||
class="w-full px-4 py-3"
|
||||
x-bind:class="darkMode
|
||||
? 'bg-gray-700/50 text-white placeholder-gray-400 focus:border-indigo-500'
|
||||
: 'bg-white text-gray-700 placeholder-gray-400 focus:border-indigo-500'"
|
||||
x-model="title"
|
||||
required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<label for="content" class="font-medium">Inhalt</label>
|
||||
<div class="flex space-x-2">
|
||||
<button type="button"
|
||||
class="px-3 py-1 rounded text-sm flex items-center"
|
||||
x-bind:class="darkMode
|
||||
? 'bg-gray-700 hover:bg-gray-600 text-white'
|
||||
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'"
|
||||
@click="showPreview = false"
|
||||
x-bind:disabled="!showPreview"
|
||||
x-bind:class="{'opacity-50': !showPreview}">
|
||||
<i class="fas fa-edit mr-1"></i> Bearbeiten
|
||||
</button>
|
||||
<button type="button"
|
||||
class="px-3 py-1 rounded text-sm flex items-center"
|
||||
x-bind:class="darkMode
|
||||
? 'bg-gray-700 hover:bg-gray-600 text-white'
|
||||
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'"
|
||||
@click="updatePreview(); showPreview = true"
|
||||
x-bind:disabled="showPreview"
|
||||
x-bind:class="{'opacity-50': showPreview}">
|
||||
<i class="fas fa-eye mr-1"></i> Vorschau
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Editor -->
|
||||
<div class="rounded-lg overflow-hidden mb-2"
|
||||
x-bind:class="darkMode ? 'border border-white/20' : 'border border-gray-300'"
|
||||
x-show="!showPreview">
|
||||
<textarea id="content" name="content" rows="12"
|
||||
class="w-full p-3 resize-y"
|
||||
x-bind:class="darkMode
|
||||
? 'bg-gray-700/50 text-white placeholder-gray-400 focus:border-indigo-500'
|
||||
: 'bg-white text-gray-700 placeholder-gray-400 focus:border-indigo-500'"
|
||||
x-model="content"
|
||||
required></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Preview -->
|
||||
<div class="rounded-lg overflow-hidden mb-2 p-4 markdown-preview"
|
||||
x-bind:class="darkMode
|
||||
? 'border border-white/20 bg-gray-700/30'
|
||||
: 'border border-gray-300 bg-gray-50'"
|
||||
x-show="showPreview"
|
||||
x-html="previewHtml">
|
||||
</div>
|
||||
|
||||
<!-- Markdown-Hilfsmittel -->
|
||||
<div class="mb-4" x-show="!showPreview">
|
||||
<div class="text-xs opacity-70">
|
||||
<p>Du kannst Knotenpunkte der Mindmap durch <code>@Knotenname</code> verlinken.</p>
|
||||
<p>Dieser Editor unterstützt Markdown-Formatierung:</p>
|
||||
<div class="flex flex-wrap gap-2 mt-1">
|
||||
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="**" data-before="" data-after="" title="Fett">
|
||||
<i class="fas fa-bold"></i>
|
||||
</button>
|
||||
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="*" data-before="" data-after="" title="Kursiv">
|
||||
<i class="fas fa-italic"></i>
|
||||
</button>
|
||||
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="`" data-before="" data-after="" title="Code">
|
||||
<i class="fas fa-code"></i>
|
||||
</button>
|
||||
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="[Link-Text](URL)" data-before="" data-after="" title="Link">
|
||||
<i class="fas fa-link"></i>
|
||||
</button>
|
||||
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="\n```\nCode-Block\n```" data-before="" data-after="" title="Code-Block">
|
||||
<i class="fas fa-file-code"></i>
|
||||
</button>
|
||||
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format=">" data-before="" data-after="" title="Zitat">
|
||||
<i class="fas fa-quote-right"></i>
|
||||
</button>
|
||||
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="- " data-before="" data-after="" title="Liste">
|
||||
<i class="fas fa-list-ul"></i>
|
||||
</button>
|
||||
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="1. " data-before="" data-after="" title="Nummerierte Liste">
|
||||
<i class="fas fa-list-ol"></i>
|
||||
</button>
|
||||
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="# " data-before="" data-after="" title="Überschrift">
|
||||
<i class="fas fa-heading"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<a href="{{ url_for('forum_post', post_id=post.parent_id or post.id) }}"
|
||||
class="px-5 py-2.5 rounded-lg transition-all duration-300 flex items-center"
|
||||
x-bind:class="darkMode
|
||||
? 'bg-gray-700 hover:bg-gray-600 text-white'
|
||||
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'">
|
||||
Abbrechen
|
||||
</a>
|
||||
<button type="submit"
|
||||
class="px-5 py-2.5 rounded-lg transition-all duration-300 flex items-center"
|
||||
x-bind:class="darkMode
|
||||
? 'bg-indigo-700 hover:bg-indigo-600 text-white'
|
||||
: 'bg-indigo-500 hover:bg-indigo-600 text-white'">
|
||||
<i class="fas fa-save mr-2"></i>
|
||||
Änderungen speichern
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Markdown-Buttons für den Beitragseditor
|
||||
document.querySelectorAll('.markdown-button').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const textarea = document.getElementById('content');
|
||||
const format = this.dataset.format;
|
||||
const before = this.dataset.before || '';
|
||||
const after = this.dataset.after || '';
|
||||
|
||||
// Hole die aktuelle Auswahl
|
||||
const start = textarea.selectionStart;
|
||||
const end = textarea.selectionEnd;
|
||||
const selection = textarea.value.substring(start, end);
|
||||
|
||||
// Wende die Formatierung an
|
||||
let formattedText;
|
||||
if (format.includes('\n')) {
|
||||
// Für Formate mit Zeilenumbrüchen (z.B. Code-Blöcke)
|
||||
formattedText = format.replace('Code-Block', selection || 'Code-Block');
|
||||
} else if (format.includes('[Link-Text](URL)')) {
|
||||
formattedText = format.replace('Link-Text', selection || 'Link-Text');
|
||||
} else if (format === '- ' || format === '1. ' || format === '# ' || format === '> ') {
|
||||
// Für Listen und Überschriften: am Anfang der Zeile einfügen
|
||||
const beforeSelection = textarea.value.substring(0, start);
|
||||
const afterSelection = textarea.value.substring(end);
|
||||
|
||||
// Finde den Anfang der aktuellen Zeile
|
||||
const lastNewline = beforeSelection.lastIndexOf('\n');
|
||||
const lineStart = lastNewline === -1 ? 0 : lastNewline + 1;
|
||||
|
||||
// Füge das Format am Zeilenanfang ein
|
||||
formattedText = beforeSelection.substring(0, lineStart) +
|
||||
format +
|
||||
beforeSelection.substring(lineStart) +
|
||||
selection +
|
||||
afterSelection;
|
||||
|
||||
// Setze die neue Cursor-Position
|
||||
const newCursorPos = end + format.length;
|
||||
textarea.value = formattedText;
|
||||
textarea.setSelectionRange(newCursorPos, newCursorPos);
|
||||
|
||||
// Alpine.js Model aktualisieren
|
||||
textarea.dispatchEvent(new Event('input'));
|
||||
return; // Früher zurückkehren, da wir die Formatierung bereits angewendet haben
|
||||
} else {
|
||||
// Für einfache Formatierungen wie fett, kursiv, Code
|
||||
formattedText = before + format + selection + format + after;
|
||||
}
|
||||
|
||||
// Ersetze den Text
|
||||
textarea.value = textarea.value.substring(0, start) + formattedText + textarea.value.substring(end);
|
||||
|
||||
// Setze den Fokus zurück auf das Textarea
|
||||
textarea.focus();
|
||||
|
||||
// Alpine.js Model aktualisieren
|
||||
textarea.dispatchEvent(new Event('input'));
|
||||
|
||||
// Setze die Auswahl neu, wenn es eine Auswahl gab
|
||||
if (selection) {
|
||||
const newStart = start + before.length + format.length;
|
||||
const newEnd = newStart + selection.length;
|
||||
textarea.setSelectionRange(newStart, newEnd);
|
||||
} else {
|
||||
// Setze den Cursor in die Mitte von **|** oder `|`
|
||||
const newCursorPos = start + before.length + format.length;
|
||||
textarea.setSelectionRange(newCursorPos, newCursorPos);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,125 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Community Forum{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.forum-category {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.forum-category:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.category-icon {
|
||||
font-size: 1.5rem;
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<!-- Seitenüberschrift -->
|
||||
<div class="mb-8 text-center">
|
||||
<h1 class="text-3xl font-bold mb-2 gradient-text">Community Forum</h1>
|
||||
<p class="text-lg opacity-75">Diskutiere mit anderen Nutzern über die Hauptthemenbereiche der Mindmap</p>
|
||||
</div>
|
||||
|
||||
<!-- Forumskategorien -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
||||
{% if categories_data %}
|
||||
{% for cat_data in categories_data %}
|
||||
<a href="{{ url_for('forum_category', category_id=cat_data.category.id) }}" class="forum-category block">
|
||||
<div class="rounded-xl p-5 h-full"
|
||||
x-bind:class="darkMode ? 'bg-gray-800/60 hover:bg-gray-800/80 border border-white/10' : 'bg-white hover:bg-gray-50 border border-gray-200 shadow-md'">
|
||||
<div class="flex items-start">
|
||||
<!-- Kategorie-Icon -->
|
||||
<div class="category-icon mr-4 text-white"
|
||||
style="background-color: {{ cat_data.category.node.color_code or '#6d28d9' }}">
|
||||
<i class="fas {{ cat_data.category.node.icon or 'fa-folder' }}"></i>
|
||||
</div>
|
||||
|
||||
<!-- Kategorie-Info -->
|
||||
<div class="flex-grow">
|
||||
<h3 class="text-xl font-semibold mb-2">{{ cat_data.category.title }}</h3>
|
||||
<p class="opacity-75 text-sm mb-3">{{ cat_data.category.description }}</p>
|
||||
|
||||
<!-- Statistik -->
|
||||
<div class="flex flex-wrap gap-4 text-sm opacity-80">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-comment-alt mr-2"></i>
|
||||
<span>{{ cat_data.total_posts }} Themen</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-reply mr-2"></i>
|
||||
<span>{{ cat_data.total_replies }} Antworten</span>
|
||||
</div>
|
||||
{% if cat_data.latest_post %}
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-clock mr-2"></i>
|
||||
<span>Neuster Beitrag: {{ cat_data.latest_post.created_at.strftime('%d.%m.%Y, %H:%M') }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pfeil-Icon -->
|
||||
<div class="ml-2">
|
||||
<i class="fas fa-chevron-right opacity-50"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="col-span-2 text-center py-8">
|
||||
<div class="text-3xl mb-3 opacity-30"><i class="fas fa-exclamation-circle"></i></div>
|
||||
<h3 class="text-xl font-semibold mb-2">Keine Forum-Kategorien gefunden</h3>
|
||||
<p class="opacity-75">Es sind derzeit keine Kategorien für Diskussionen verfügbar.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Hinweis zur Nutzung -->
|
||||
<div class="rounded-xl p-6 text-center mb-8"
|
||||
x-bind:class="darkMode ? 'bg-indigo-900/30 border border-indigo-700/30' : 'bg-indigo-50 border border-indigo-100'">
|
||||
<h3 class="text-xl font-semibold mb-3">
|
||||
<i class="fas fa-lightbulb mr-2 text-yellow-500"></i>
|
||||
So funktioniert das Forum
|
||||
</h3>
|
||||
<p class="mb-4">Das Community-Forum ist nach den Hauptknotenpunkten der Systades-Mindmap strukturiert.
|
||||
In deinen Beiträgen kannst du Knotenpunkte mit <code>@Knotenname</code> verlinken.</p>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mt-6">
|
||||
<div class="p-4 rounded-lg"
|
||||
x-bind:class="darkMode ? 'bg-indigo-800/40' : 'bg-white border border-indigo-100'">
|
||||
<div class="text-2xl mb-2"><i class="fas fa-users text-indigo-400"></i></div>
|
||||
<h4 class="font-medium mb-1">Fachliche Diskussionen</h4>
|
||||
<p class="text-sm opacity-75">Tausche dich mit anderen zu spezifischen Themen aus</p>
|
||||
</div>
|
||||
<div class="p-4 rounded-lg"
|
||||
x-bind:class="darkMode ? 'bg-indigo-800/40' : 'bg-white border border-indigo-100'">
|
||||
<div class="text-2xl mb-2"><i class="fas fa-link text-indigo-400"></i></div>
|
||||
<h4 class="font-medium mb-1">Wissensvernetzung</h4>
|
||||
<p class="text-sm opacity-75">Verknüpfe Inhalte durch Knotenreferenzen</p>
|
||||
</div>
|
||||
<div class="p-4 rounded-lg"
|
||||
x-bind:class="darkMode ? 'bg-indigo-800/40' : 'bg-white border border-indigo-100'">
|
||||
<div class="text-2xl mb-2"><i class="fas fa-markdown text-indigo-400"></i></div>
|
||||
<h4 class="font-medium mb-1">Markdown Support</h4>
|
||||
<p class="text-sm opacity-75">Formatiere deine Beiträge mit Markdown</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// Hier können bei Bedarf forumspezifische Scripts eingefügt werden
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,355 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Neues Thema - {{ category.title }}{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.markdown-preview {
|
||||
min-height: 200px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.markdown-preview p {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.markdown-preview h1, .markdown-preview h2, .markdown-preview h3,
|
||||
.markdown-preview h4, .markdown-preview h5, .markdown-preview h6 {
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.markdown-preview h1 { font-size: 1.8rem; }
|
||||
.markdown-preview h2 { font-size: 1.5rem; }
|
||||
.markdown-preview h3 { font-size: 1.3rem; }
|
||||
.markdown-preview h4 { font-size: 1.1rem; }
|
||||
.markdown-preview ul, .markdown-preview ol {
|
||||
margin-left: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.markdown-preview ul { list-style-type: disc; }
|
||||
.markdown-preview ol { list-style-type: decimal; }
|
||||
.markdown-preview pre {
|
||||
background-color: rgba(0,0,0,0.05);
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
overflow-x: auto;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
.markdown-preview code {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.9em;
|
||||
padding: 0.1em 0.3em;
|
||||
border-radius: 0.3em;
|
||||
background-color: rgba(0,0,0,0.05);
|
||||
}
|
||||
.markdown-preview pre code {
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
}
|
||||
.markdown-preview blockquote {
|
||||
border-left: 4px solid;
|
||||
padding-left: 1rem;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
margin-bottom: 1rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.dark .markdown-preview code {
|
||||
background-color: rgba(255,255,255,0.1);
|
||||
}
|
||||
.dark .markdown-preview blockquote {
|
||||
border-color: rgba(255,255,255,0.2);
|
||||
}
|
||||
.node-mention {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background-color: rgba(109, 40, 217, 0.1);
|
||||
color: #6d28d9;
|
||||
border-radius: 4px;
|
||||
padding: 1px 6px;
|
||||
font-size: 0.9em;
|
||||
margin: 0 2px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.dark .node-mention {
|
||||
background-color: rgba(167, 139, 250, 0.2);
|
||||
color: #a78bfa;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<!-- Breadcrumb Navigation -->
|
||||
<div class="mb-6 flex items-center text-sm">
|
||||
<a href="{{ url_for('community') }}" class="opacity-75 hover:opacity-100 transition-opacity">
|
||||
<i class="fas fa-home mr-1"></i> Forum
|
||||
</a>
|
||||
<span class="mx-2 opacity-50">/</span>
|
||||
<a href="{{ url_for('forum_category', category_id=category.id) }}" class="opacity-75 hover:opacity-100 transition-opacity">
|
||||
{{ category.title }}
|
||||
</a>
|
||||
<span class="mx-2 opacity-50">/</span>
|
||||
<span class="font-medium">Neues Thema</span>
|
||||
</div>
|
||||
|
||||
<!-- Formular-Header -->
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-bold mb-2">Neues Thema erstellen</h1>
|
||||
<p class="opacity-75">in der Kategorie <span class="font-medium">{{ category.title }}</span></p>
|
||||
</div>
|
||||
|
||||
<!-- Formular -->
|
||||
<div class="mb-8 rounded-xl overflow-hidden"
|
||||
x-bind:class="darkMode ? 'bg-gray-800/60 border border-white/10' : 'bg-white border border-gray-200'">
|
||||
<div class="p-4 border-b font-medium" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
|
||||
<i class="fas fa-plus-circle mr-2"></i>
|
||||
Neues Thema
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<form action="{{ url_for('new_post', category_id=category.id) }}" method="POST" x-data="{
|
||||
title: '',
|
||||
content: '',
|
||||
showPreview: false,
|
||||
previewHtml: '',
|
||||
|
||||
updatePreview() {
|
||||
// Verarbeite den Inhalt
|
||||
if (this.content.trim() === '') {
|
||||
this.previewHtml = '<div class=\'opacity-50 italic\'>Die Vorschau wird hier angezeigt...</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Verarbeite Markdown
|
||||
let html = marked.parse(this.content);
|
||||
|
||||
// Ersetze @Knotenname mit entsprechenden Links
|
||||
html = html.replace(/@([a-zA-Z0-9äöüÄÖÜß_-]+)/g, '<span class=\'node-mention\'><i class=\'fas fa-diagram-project fa-xs mr-1\'></i>$1</span>');
|
||||
|
||||
this.previewHtml = html;
|
||||
}
|
||||
}">
|
||||
<div class="mb-6">
|
||||
<label for="title" class="block mb-2 font-medium">Titel des Themas</label>
|
||||
<div class="rounded-lg overflow-hidden"
|
||||
x-bind:class="darkMode ? 'border border-white/20' : 'border border-gray-300'">
|
||||
<input type="text" id="title" name="title"
|
||||
class="w-full px-4 py-3"
|
||||
x-bind:class="darkMode
|
||||
? 'bg-gray-700/50 text-white placeholder-gray-400 focus:border-indigo-500'
|
||||
: 'bg-white text-gray-700 placeholder-gray-400 focus:border-indigo-500'"
|
||||
placeholder="Ein prägnanter Titel für dein Thema"
|
||||
x-model="title"
|
||||
required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<label for="content" class="font-medium">Inhalt</label>
|
||||
<div class="flex space-x-2">
|
||||
<button type="button"
|
||||
class="px-3 py-1 rounded text-sm flex items-center"
|
||||
x-bind:class="darkMode
|
||||
? 'bg-gray-700 hover:bg-gray-600 text-white'
|
||||
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'"
|
||||
@click="showPreview = false"
|
||||
x-bind:disabled="!showPreview"
|
||||
x-bind:class="{'opacity-50': !showPreview}">
|
||||
<i class="fas fa-edit mr-1"></i> Bearbeiten
|
||||
</button>
|
||||
<button type="button"
|
||||
class="px-3 py-1 rounded text-sm flex items-center"
|
||||
x-bind:class="darkMode
|
||||
? 'bg-gray-700 hover:bg-gray-600 text-white'
|
||||
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'"
|
||||
@click="updatePreview(); showPreview = true"
|
||||
x-bind:disabled="showPreview"
|
||||
x-bind:class="{'opacity-50': showPreview}">
|
||||
<i class="fas fa-eye mr-1"></i> Vorschau
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Editor -->
|
||||
<div class="rounded-lg overflow-hidden mb-2"
|
||||
x-bind:class="darkMode ? 'border border-white/20' : 'border border-gray-300'"
|
||||
x-show="!showPreview">
|
||||
<textarea id="content" name="content" rows="12"
|
||||
class="w-full p-3 resize-y"
|
||||
x-bind:class="darkMode
|
||||
? 'bg-gray-700/50 text-white placeholder-gray-400 focus:border-indigo-500'
|
||||
: 'bg-white text-gray-700 placeholder-gray-400 focus:border-indigo-500'"
|
||||
placeholder="Schreibe deinen Beitrag hier (unterstützt Markdown und @Knotenname-Erwähnungen)..."
|
||||
x-model="content"
|
||||
required></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Preview -->
|
||||
<div class="rounded-lg overflow-hidden mb-2 p-4 markdown-preview"
|
||||
x-bind:class="darkMode
|
||||
? 'border border-white/20 bg-gray-700/30'
|
||||
: 'border border-gray-300 bg-gray-50'"
|
||||
x-show="showPreview"
|
||||
x-html="previewHtml">
|
||||
</div>
|
||||
|
||||
<!-- Markdown-Hilfsmittel -->
|
||||
<div class="mb-4" x-show="!showPreview">
|
||||
<div class="text-xs opacity-70">
|
||||
<p>Du kannst Knotenpunkte der Mindmap durch <code>@Knotenname</code> verlinken.</p>
|
||||
<p>Dieser Editor unterstützt Markdown-Formatierung:</p>
|
||||
<div class="flex flex-wrap gap-2 mt-1">
|
||||
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="**" data-before="" data-after="" title="Fett">
|
||||
<i class="fas fa-bold"></i>
|
||||
</button>
|
||||
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="*" data-before="" data-after="" title="Kursiv">
|
||||
<i class="fas fa-italic"></i>
|
||||
</button>
|
||||
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="`" data-before="" data-after="" title="Code">
|
||||
<i class="fas fa-code"></i>
|
||||
</button>
|
||||
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="[Link-Text](URL)" data-before="" data-after="" title="Link">
|
||||
<i class="fas fa-link"></i>
|
||||
</button>
|
||||
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="\n```\nCode-Block\n```" data-before="" data-after="" title="Code-Block">
|
||||
<i class="fas fa-file-code"></i>
|
||||
</button>
|
||||
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format=">" data-before="" data-after="" title="Zitat">
|
||||
<i class="fas fa-quote-right"></i>
|
||||
</button>
|
||||
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="- " data-before="" data-after="" title="Liste">
|
||||
<i class="fas fa-list-ul"></i>
|
||||
</button>
|
||||
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="1. " data-before="" data-after="" title="Nummerierte Liste">
|
||||
<i class="fas fa-list-ol"></i>
|
||||
</button>
|
||||
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="# " data-before="" data-after="" title="Überschrift">
|
||||
<i class="fas fa-heading"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<a href="{{ url_for('forum_category', category_id=category.id) }}"
|
||||
class="px-5 py-2.5 rounded-lg transition-all duration-300 flex items-center"
|
||||
x-bind:class="darkMode
|
||||
? 'bg-gray-700 hover:bg-gray-600 text-white'
|
||||
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'">
|
||||
Abbrechen
|
||||
</a>
|
||||
<button type="submit"
|
||||
class="px-5 py-2.5 rounded-lg transition-all duration-300 flex items-center"
|
||||
x-bind:class="darkMode
|
||||
? 'bg-indigo-700 hover:bg-indigo-600 text-white'
|
||||
: 'bg-indigo-500 hover:bg-indigo-600 text-white'">
|
||||
<i class="fas fa-paper-plane mr-2"></i>
|
||||
Thema erstellen
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Link zur Mindmap -->
|
||||
<div class="rounded-xl p-5 mb-4 flex items-center"
|
||||
x-bind:class="darkMode ? 'bg-purple-900/20 border border-purple-800/30' : 'bg-purple-50 border border-purple-100'">
|
||||
<div class="text-3xl mr-4 opacity-80">
|
||||
<i class="fas fa-diagram-project" style="color: {{ category.node.color_code }}"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-medium mb-1">Mindmap-Knotenpunkt: {{ category.node.name }}</h3>
|
||||
<p class="text-sm opacity-75">Dieser Diskussionsbereich ist mit dem Mindmap-Knotenpunkt "{{ category.node.name }}" verknüpft.</p>
|
||||
</div>
|
||||
<div class="ml-auto">
|
||||
<a href="{{ url_for('mindmap') }}"
|
||||
class="px-4 py-2 rounded-lg inline-block text-sm transition-all"
|
||||
x-bind:class="darkMode
|
||||
? 'bg-purple-800/60 hover:bg-purple-700/60 text-white'
|
||||
: 'bg-white hover:bg-purple-100 text-purple-800 border border-purple-200'">
|
||||
Zur Mindmap
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Markdown-Buttons für den Beitragseditor
|
||||
document.querySelectorAll('.markdown-button').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const textarea = document.getElementById('content');
|
||||
const format = this.dataset.format;
|
||||
const before = this.dataset.before || '';
|
||||
const after = this.dataset.after || '';
|
||||
|
||||
// Hole die aktuelle Auswahl
|
||||
const start = textarea.selectionStart;
|
||||
const end = textarea.selectionEnd;
|
||||
const selection = textarea.value.substring(start, end);
|
||||
|
||||
// Wende die Formatierung an
|
||||
let formattedText;
|
||||
if (format.includes('\n')) {
|
||||
// Für Formate mit Zeilenumbrüchen (z.B. Code-Blöcke)
|
||||
formattedText = format.replace('Code-Block', selection || 'Code-Block');
|
||||
} else if (format.includes('[Link-Text](URL)')) {
|
||||
formattedText = format.replace('Link-Text', selection || 'Link-Text');
|
||||
} else if (format === '- ' || format === '1. ' || format === '# ' || format === '> ') {
|
||||
// Für Listen und Überschriften: am Anfang der Zeile einfügen
|
||||
const beforeSelection = textarea.value.substring(0, start);
|
||||
const afterSelection = textarea.value.substring(end);
|
||||
|
||||
// Finde den Anfang der aktuellen Zeile
|
||||
const lastNewline = beforeSelection.lastIndexOf('\n');
|
||||
const lineStart = lastNewline === -1 ? 0 : lastNewline + 1;
|
||||
|
||||
// Füge das Format am Zeilenanfang ein
|
||||
formattedText = beforeSelection.substring(0, lineStart) +
|
||||
format +
|
||||
beforeSelection.substring(lineStart) +
|
||||
selection +
|
||||
afterSelection;
|
||||
|
||||
// Setze die neue Cursor-Position
|
||||
const newCursorPos = end + format.length;
|
||||
textarea.value = formattedText;
|
||||
textarea.setSelectionRange(newCursorPos, newCursorPos);
|
||||
|
||||
// Alpine.js Model aktualisieren
|
||||
textarea.dispatchEvent(new Event('input'));
|
||||
return; // Früher zurückkehren, da wir die Formatierung bereits angewendet haben
|
||||
} else {
|
||||
// Für einfache Formatierungen wie fett, kursiv, Code
|
||||
formattedText = before + format + selection + format + after;
|
||||
}
|
||||
|
||||
// Ersetze den Text
|
||||
textarea.value = textarea.value.substring(0, start) + formattedText + textarea.value.substring(end);
|
||||
|
||||
// Setze den Fokus zurück auf das Textarea
|
||||
textarea.focus();
|
||||
|
||||
// Alpine.js Model aktualisieren
|
||||
textarea.dispatchEvent(new Event('input'));
|
||||
|
||||
// Setze die Auswahl neu, wenn es eine Auswahl gab
|
||||
if (selection) {
|
||||
const newStart = start + before.length + format.length;
|
||||
const newEnd = newStart + selection.length;
|
||||
textarea.setSelectionRange(newStart, newEnd);
|
||||
} else {
|
||||
// Setze den Cursor in die Mitte von **|** oder `|`
|
||||
const newCursorPos = start + before.length + format.length;
|
||||
textarea.setSelectionRange(newCursorPos, newCursorPos);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,511 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}{{ post.title }} - Forum{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.post-content {
|
||||
line-height: 1.7;
|
||||
}
|
||||
.post-content p {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.post-content h1, .post-content h2, .post-content h3,
|
||||
.post-content h4, .post-content h5, .post-content h6 {
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.post-content h1 { font-size: 1.8rem; }
|
||||
.post-content h2 { font-size: 1.5rem; }
|
||||
.post-content h3 { font-size: 1.3rem; }
|
||||
.post-content h4 { font-size: 1.1rem; }
|
||||
.post-content ul, .post-content ol {
|
||||
margin-left: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.post-content ul { list-style-type: disc; }
|
||||
.post-content ol { list-style-type: decimal; }
|
||||
.post-content pre {
|
||||
background-color: rgba(0,0,0,0.05);
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
overflow-x: auto;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
.post-content code {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.9em;
|
||||
padding: 0.1em 0.3em;
|
||||
border-radius: 0.3em;
|
||||
background-color: rgba(0,0,0,0.05);
|
||||
}
|
||||
.post-content pre code {
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
}
|
||||
.post-content blockquote {
|
||||
border-left: 4px solid;
|
||||
padding-left: 1rem;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
margin-bottom: 1rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.post-content img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 0.5rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
.post-content table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
.post-content th, .post-content td {
|
||||
padding: 0.5rem;
|
||||
border: 1px solid;
|
||||
border-color: rgba(0,0,0,0.1);
|
||||
}
|
||||
.post-content th {
|
||||
background-color: rgba(0,0,0,0.05);
|
||||
}
|
||||
.post-content a {
|
||||
color: #6d28d9;
|
||||
text-decoration: none;
|
||||
}
|
||||
.post-content a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.dark .post-content code {
|
||||
background-color: rgba(255,255,255,0.1);
|
||||
}
|
||||
.dark .post-content th, .dark .post-content td {
|
||||
border-color: rgba(255,255,255,0.1);
|
||||
}
|
||||
.dark .post-content th {
|
||||
background-color: rgba(255,255,255,0.05);
|
||||
}
|
||||
.dark .post-content a {
|
||||
color: #a78bfa;
|
||||
}
|
||||
.node-mention {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background-color: rgba(109, 40, 217, 0.1);
|
||||
color: #6d28d9;
|
||||
border-radius: 4px;
|
||||
padding: 1px 6px;
|
||||
font-size: 0.9em;
|
||||
margin: 0 2px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.dark .node-mention {
|
||||
background-color: rgba(167, 139, 250, 0.2);
|
||||
color: #a78bfa;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<!-- Breadcrumb Navigation -->
|
||||
<div class="mb-6 flex items-center text-sm">
|
||||
<a href="{{ url_for('community') }}" class="opacity-75 hover:opacity-100 transition-opacity">
|
||||
<i class="fas fa-home mr-1"></i> Forum
|
||||
</a>
|
||||
<span class="mx-2 opacity-50">/</span>
|
||||
<a href="{{ url_for('forum_category', category_id=category.id) }}" class="opacity-75 hover:opacity-100 transition-opacity">
|
||||
{{ category.title }}
|
||||
</a>
|
||||
<span class="mx-2 opacity-50">/</span>
|
||||
<span class="font-medium truncate max-w-[300px]">{{ post.title }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Beitrags-Header -->
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-bold mb-2">{{ post.title }}</h1>
|
||||
<div class="flex flex-wrap items-center gap-3 text-sm opacity-75">
|
||||
<span><i class="fas fa-calendar-alt mr-1"></i> {{ post.created_at.strftime('%d.%m.%Y, %H:%M') }}</span>
|
||||
<span><i class="fas fa-eye mr-1"></i> {{ post.view_count }} Aufrufe</span>
|
||||
<span><i class="fas fa-reply mr-1"></i> {{ replies|length }} Antworten</span>
|
||||
|
||||
{% if post.is_pinned or post.is_locked %}
|
||||
<div class="flex gap-2 ml-2">
|
||||
{% if post.is_pinned %}
|
||||
<span class="px-2 py-0.5 text-xs rounded-full"
|
||||
x-bind:class="darkMode ? 'bg-yellow-700/50 text-yellow-300' : 'bg-yellow-100 text-yellow-800'">
|
||||
<i class="fas fa-thumbtack mr-1"></i> Angepinnt
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if post.is_locked %}
|
||||
<span class="px-2 py-0.5 text-xs rounded-full"
|
||||
x-bind:class="darkMode ? 'bg-red-700/50 text-red-300' : 'bg-red-100 text-red-800'">
|
||||
<i class="fas fa-lock mr-1"></i> Gesperrt
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hauptbeitrag -->
|
||||
<div class="mb-8 rounded-xl overflow-hidden"
|
||||
x-bind:class="darkMode ? 'bg-gray-800/60 border border-white/10' : 'bg-white border border-gray-200 shadow-sm'">
|
||||
<!-- Beitrags-Header -->
|
||||
<div class="p-4 border-b" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<!-- Autor-Avatar -->
|
||||
<div class="w-10 h-10 rounded-full flex items-center justify-center text-white font-medium text-sm overflow-hidden mr-3"
|
||||
style="background: linear-gradient(135deg, #8b5cf6, #6366f1);">
|
||||
{% if post.author.avatar %}
|
||||
<img src="{{ post.author.avatar }}" alt="{{ post.author.username }}" class="w-full h-full object-cover">
|
||||
{% else %}
|
||||
<svg width="100%" height="100%" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="100" cy="100" r="98" fill="url(#post-avatar-gradient)" stroke="#7C3AED" stroke-width="4"/>
|
||||
<circle cx="100" cy="80" r="36" fill="white"/>
|
||||
<path d="M100 140C77.9086 140 60 157.909 60 180H140C140 157.909 122.091 140 100 140Z" fill="white"/>
|
||||
<defs>
|
||||
<linearGradient id="post-avatar-gradient" x1="0" y1="0" x2="200" y2="200" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#8B5CF6"/>
|
||||
<stop offset="1" stop-color="#3B82F6"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Autor-Info -->
|
||||
<div>
|
||||
<div class="font-medium">{{ post.author.username }}</div>
|
||||
<div class="text-xs opacity-70">Erstellt am {{ post.created_at.strftime('%d.%m.%Y, %H:%M') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Aktionen -->
|
||||
<div class="flex items-center space-x-2">
|
||||
{% if current_user.id == post.user_id or current_user.role == 'admin' %}
|
||||
<a href="{{ url_for('edit_post', post_id=post.id) }}"
|
||||
class="p-2 rounded transition-colors"
|
||||
x-bind:class="darkMode
|
||||
? 'hover:bg-gray-700/50 text-gray-300'
|
||||
: 'hover:bg-gray-100 text-gray-600'">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<form action="{{ url_for('delete_post', post_id=post.id) }}" method="POST" class="inline" onsubmit="return confirm('Möchtest du diesen Beitrag wirklich löschen?');">
|
||||
<button type="submit"
|
||||
class="p-2 rounded transition-colors"
|
||||
x-bind:class="darkMode
|
||||
? 'hover:bg-red-800/50 text-red-300'
|
||||
: 'hover:bg-red-100 text-red-600'">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
<!-- Moderation-Optionen -->
|
||||
{% if current_user.role in ['admin', 'moderator'] %}
|
||||
<div class="ml-2 border-l" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'"></div>
|
||||
<form action="{{ url_for('toggle_pin_post', post_id=post.id) }}" method="POST" class="inline">
|
||||
<button type="submit"
|
||||
class="p-2 rounded transition-colors"
|
||||
x-bind:class="darkMode
|
||||
? 'hover:bg-yellow-800/50 text-yellow-300'
|
||||
: 'hover:bg-yellow-100 text-yellow-600'"
|
||||
title="{% if post.is_pinned %}Nicht mehr anpinnen{% else %}Anpinnen{% endif %}">
|
||||
<i class="fas fa-thumbtack"></i>
|
||||
</button>
|
||||
</form>
|
||||
<form action="{{ url_for('toggle_lock_post', post_id=post.id) }}" method="POST" class="inline">
|
||||
<button type="submit"
|
||||
class="p-2 rounded transition-colors"
|
||||
x-bind:class="darkMode
|
||||
? 'hover:bg-blue-800/50 text-blue-300'
|
||||
: 'hover:bg-blue-100 text-blue-600'"
|
||||
title="{% if post.is_locked %}Entsperren{% else %}Sperren{% endif %}">
|
||||
<i class="fas {% if post.is_locked %}fa-unlock{% else %}fa-lock{% endif %}"></i>
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Beitrags-Inhalt -->
|
||||
<div class="p-6">
|
||||
<div class="post-content markdown-content" id="main-post-content">
|
||||
{{ post.content|safe }}
|
||||
</div>
|
||||
|
||||
{% if post.updated_at and post.updated_at != post.created_at %}
|
||||
<div class="mt-6 pt-4 text-xs opacity-60 border-t" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
|
||||
<i class="fas fa-edit mr-1"></i> Zuletzt bearbeitet: {{ post.updated_at.strftime('%d.%m.%Y, %H:%M') }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Antworten-Bereich -->
|
||||
<div class="mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4">
|
||||
<i class="fas fa-reply mr-2 opacity-60"></i>
|
||||
{{ replies|length }} Antworten
|
||||
</h2>
|
||||
|
||||
<!-- Antworten-Liste -->
|
||||
{% if replies %}
|
||||
{% for reply in replies %}
|
||||
<div class="mb-5 rounded-xl overflow-hidden"
|
||||
x-bind:class="darkMode ? 'bg-gray-800/40 border border-white/10' : 'bg-white border border-gray-200'">
|
||||
<!-- Antwort-Header -->
|
||||
<div class="p-3 border-b" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<!-- Autor-Avatar -->
|
||||
<div class="w-8 h-8 rounded-full flex items-center justify-center text-white font-medium text-xs overflow-hidden mr-3"
|
||||
style="background: linear-gradient(135deg, #8b5cf6, #6366f1);">
|
||||
{% if reply.author.avatar %}
|
||||
<img src="{{ reply.author.avatar }}" alt="{{ reply.author.username }}" class="w-full h-full object-cover">
|
||||
{% else %}
|
||||
<svg width="100%" height="100%" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="100" cy="100" r="98" fill="url(#reply-avatar-gradient)" stroke="#7C3AED" stroke-width="4"/>
|
||||
<circle cx="100" cy="80" r="36" fill="white"/>
|
||||
<path d="M100 140C77.9086 140 60 157.909 60 180H140C140 157.909 122.091 140 100 140Z" fill="white"/>
|
||||
<defs>
|
||||
<linearGradient id="reply-avatar-gradient" x1="0" y1="0" x2="200" y2="200" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#8B5CF6"/>
|
||||
<stop offset="1" stop-color="#3B82F6"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Autor-Info -->
|
||||
<div>
|
||||
<div class="font-medium text-sm">{{ reply.author.username }}</div>
|
||||
<div class="text-xs opacity-70">{{ reply.created_at.strftime('%d.%m.%Y, %H:%M') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Aktionen -->
|
||||
<div class="flex items-center space-x-1">
|
||||
{% if current_user.id == reply.user_id or current_user.role == 'admin' %}
|
||||
<a href="{{ url_for('edit_post', post_id=reply.id) }}"
|
||||
class="p-1.5 rounded text-sm transition-colors"
|
||||
x-bind:class="darkMode
|
||||
? 'hover:bg-gray-700/50 text-gray-300'
|
||||
: 'hover:bg-gray-100 text-gray-600'">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<form action="{{ url_for('delete_post', post_id=reply.id) }}" method="POST" class="inline" onsubmit="return confirm('Möchtest du diese Antwort wirklich löschen?');">
|
||||
<button type="submit"
|
||||
class="p-1.5 rounded text-sm transition-colors"
|
||||
x-bind:class="darkMode
|
||||
? 'hover:bg-red-800/50 text-red-300'
|
||||
: 'hover:bg-red-100 text-red-600'">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Antwort-Inhalt -->
|
||||
<div class="p-5">
|
||||
<div class="post-content markdown-content reply-content" id="reply-content-{{ reply.id }}">
|
||||
{{ reply.content|safe }}
|
||||
</div>
|
||||
|
||||
{% if reply.updated_at and reply.updated_at != reply.created_at %}
|
||||
<div class="mt-4 pt-3 text-xs opacity-60 border-t" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
|
||||
<i class="fas fa-edit mr-1"></i> Zuletzt bearbeitet: {{ reply.updated_at.strftime('%d.%m.%Y, %H:%M') }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="rounded-xl p-6 text-center"
|
||||
x-bind:class="darkMode ? 'bg-gray-800/40 border border-white/10' : 'bg-white border border-gray-200'">
|
||||
<div class="text-3xl mb-3 opacity-30"><i class="fas fa-comments"></i></div>
|
||||
<h3 class="text-lg font-semibold mb-2">Noch keine Antworten</h3>
|
||||
<p class="opacity-75">Sei der Erste, der auf diesen Beitrag antwortet!</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Antwort-Formular -->
|
||||
{% if not post.is_locked %}
|
||||
<div class="mb-8 rounded-xl overflow-hidden"
|
||||
x-bind:class="darkMode ? 'bg-gray-800/60 border border-white/10' : 'bg-white border border-gray-200'">
|
||||
<div class="p-4 border-b font-medium" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
|
||||
<i class="fas fa-reply mr-2"></i>
|
||||
Antworten
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<form action="{{ url_for('reply_to_post', post_id=post.id) }}" method="POST">
|
||||
<div class="mb-4">
|
||||
<label for="content" class="block mb-2 font-medium">Deine Antwort</label>
|
||||
<div class="mb-2 rounded-lg overflow-hidden"
|
||||
x-bind:class="darkMode ? 'border border-white/20' : 'border border-gray-300'">
|
||||
<textarea id="content" name="content" rows="6"
|
||||
class="w-full p-3 resize-y"
|
||||
x-bind:class="darkMode
|
||||
? 'bg-gray-700/50 text-white placeholder-gray-400 focus:border-indigo-500'
|
||||
: 'bg-white text-gray-700 placeholder-gray-400 focus:border-indigo-500'"
|
||||
placeholder="Schreibe deine Antwort hier (unterstützt Markdown und @Knotenname-Erwähnungen)..."
|
||||
required></textarea>
|
||||
</div>
|
||||
<div class="text-xs opacity-70">
|
||||
<p>Du kannst Knotenpunkte der Mindmap durch <code>@Knotenname</code> verlinken.</p>
|
||||
<p>Dieser Editor unterstützt Markdown-Formatierung:</p>
|
||||
<div class="flex flex-wrap gap-2 mt-1">
|
||||
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="**" data-before="" data-after="" title="Fett">
|
||||
<i class="fas fa-bold"></i>
|
||||
</button>
|
||||
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="*" data-before="" data-after="" title="Kursiv">
|
||||
<i class="fas fa-italic"></i>
|
||||
</button>
|
||||
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="`" data-before="" data-after="" title="Code">
|
||||
<i class="fas fa-code"></i>
|
||||
</button>
|
||||
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="[Link-Text](URL)" data-before="" data-after="" title="Link">
|
||||
<i class="fas fa-link"></i>
|
||||
</button>
|
||||
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="\n```\nCode-Block\n```" data-before="" data-after="" title="Code-Block">
|
||||
<i class="fas fa-file-code"></i>
|
||||
</button>
|
||||
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format=">" data-before="" data-after="" title="Zitat">
|
||||
<i class="fas fa-quote-right"></i>
|
||||
</button>
|
||||
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="- " data-before="" data-after="" title="Liste">
|
||||
<i class="fas fa-list-ul"></i>
|
||||
</button>
|
||||
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="1. " data-before="" data-after="" title="Nummerierte Liste">
|
||||
<i class="fas fa-list-ol"></i>
|
||||
</button>
|
||||
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="# " data-before="" data-after="" title="Überschrift">
|
||||
<i class="fas fa-heading"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit"
|
||||
class="px-5 py-2.5 rounded-lg transition-all duration-300 flex items-center"
|
||||
x-bind:class="darkMode
|
||||
? 'bg-indigo-700 hover:bg-indigo-600 text-white'
|
||||
: 'bg-indigo-500 hover:bg-indigo-600 text-white'">
|
||||
<i class="fas fa-paper-plane mr-2"></i>
|
||||
Antwort senden
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="rounded-xl p-5 text-center mb-6"
|
||||
x-bind:class="darkMode ? 'bg-red-900/20 border border-red-800/30' : 'bg-red-50 border border-red-100'">
|
||||
<i class="fas fa-lock mr-2 text-red-500"></i>
|
||||
<span>Dieser Beitrag ist geschlossen. Es können keine neuen Antworten mehr verfasst werden.</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Markdown und Knotenerwähnungen verarbeiten
|
||||
const processContent = (content) => {
|
||||
// Verarbeite Markdown mit marked.js
|
||||
let html = marked.parse(content);
|
||||
|
||||
// Ersetze @Knotenname mit entsprechenden Links
|
||||
html = html.replace(/@([a-zA-Z0-9äöüÄÖÜß_-]+)/g, '<span class="node-mention"><i class="fas fa-diagram-project fa-xs mr-1"></i>$1</span>');
|
||||
|
||||
return html;
|
||||
};
|
||||
|
||||
// Markdown-Inhalt für Hauptbeitrag rendern
|
||||
const mainPostContent = document.getElementById('main-post-content');
|
||||
if (mainPostContent) {
|
||||
mainPostContent.innerHTML = processContent(mainPostContent.textContent.trim());
|
||||
}
|
||||
|
||||
// Markdown-Inhalt für Antworten rendern
|
||||
document.querySelectorAll('.reply-content').forEach(reply => {
|
||||
reply.innerHTML = processContent(reply.textContent.trim());
|
||||
});
|
||||
|
||||
// Markdown-Buttons für das Antwortformular
|
||||
document.querySelectorAll('.markdown-button').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const textarea = document.getElementById('content');
|
||||
const format = this.dataset.format;
|
||||
const before = this.dataset.before || '';
|
||||
const after = this.dataset.after || '';
|
||||
|
||||
// Hole die aktuelle Auswahl
|
||||
const start = textarea.selectionStart;
|
||||
const end = textarea.selectionEnd;
|
||||
const selection = textarea.value.substring(start, end);
|
||||
|
||||
// Wende die Formatierung an
|
||||
let formattedText;
|
||||
if (format.includes('\n')) {
|
||||
// Für Formate mit Zeilenumbrüchen (z.B. Code-Blöcke)
|
||||
formattedText = format.replace('Code-Block', selection || 'Code-Block');
|
||||
} else if (format.includes('[Link-Text](URL)')) {
|
||||
formattedText = format.replace('Link-Text', selection || 'Link-Text');
|
||||
} else if (format === '- ' || format === '1. ' || format === '# ' || format === '> ') {
|
||||
// Für Listen und Überschriften: am Anfang der Zeile einfügen
|
||||
const beforeSelection = textarea.value.substring(0, start);
|
||||
const afterSelection = textarea.value.substring(end);
|
||||
|
||||
// Finde den Anfang der aktuellen Zeile
|
||||
const lastNewline = beforeSelection.lastIndexOf('\n');
|
||||
const lineStart = lastNewline === -1 ? 0 : lastNewline + 1;
|
||||
|
||||
// Füge das Format am Zeilenanfang ein
|
||||
formattedText = beforeSelection.substring(0, lineStart) +
|
||||
format +
|
||||
beforeSelection.substring(lineStart) +
|
||||
selection +
|
||||
afterSelection;
|
||||
|
||||
// Setze die neue Cursor-Position
|
||||
const newCursorPos = end + format.length;
|
||||
textarea.value = formattedText;
|
||||
textarea.setSelectionRange(newCursorPos, newCursorPos);
|
||||
return; // Früher zurückkehren, da wir die Formatierung bereits angewendet haben
|
||||
} else {
|
||||
// Für einfache Formatierungen wie fett, kursiv, Code
|
||||
formattedText = before + format + selection + format + after;
|
||||
}
|
||||
|
||||
// Ersetze den Text
|
||||
textarea.value = textarea.value.substring(0, start) + formattedText + textarea.value.substring(end);
|
||||
|
||||
// Setze den Fokus zurück auf das Textarea
|
||||
textarea.focus();
|
||||
|
||||
// Setze die Auswahl neu, wenn es eine Auswahl gab
|
||||
if (selection) {
|
||||
const newStart = start + before.length + format.length;
|
||||
const newEnd = newStart + selection.length;
|
||||
textarea.setSelectionRange(newStart, newEnd);
|
||||
} else {
|
||||
// Setze den Cursor in die Mitte von **|** oder `|`
|
||||
const newCursorPos = start + before.length + format.length;
|
||||
textarea.setSelectionRange(newCursorPos, newCursorPos);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,137 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Community Forum Vorschau{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.forum-category {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.forum-category:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.category-icon {
|
||||
font-size: 1.5rem;
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<!-- Seitenüberschrift -->
|
||||
<div class="mb-8 text-center">
|
||||
<h1 class="text-3xl font-bold mb-2 gradient-text">Community Forum</h1>
|
||||
<p class="text-lg opacity-75">Diskutiere mit anderen Nutzern über die Hauptthemenbereiche der Mindmap</p>
|
||||
</div>
|
||||
|
||||
<!-- Login-Aufforderung -->
|
||||
<div class="rounded-xl p-6 text-center mb-8 bg-indigo-50 dark:bg-indigo-900/30 border border-indigo-100 dark:border-indigo-700/30">
|
||||
<h3 class="text-xl font-semibold mb-3">
|
||||
<i class="fas fa-lock mr-2 text-indigo-500"></i>
|
||||
Anmeldung erforderlich
|
||||
</h3>
|
||||
<p class="mb-4">Um am Community-Forum teilzunehmen und alle Funktionen nutzen zu können, musst du dich anmelden oder registrieren.</p>
|
||||
<div class="flex justify-center gap-4 mt-4">
|
||||
<a href="{{ url_for('login', next=url_for('forum')) }}" class="px-4 py-2 bg-indigo-500 hover:bg-indigo-600 text-white rounded-lg transition-colors">
|
||||
<i class="fas fa-sign-in-alt mr-2"></i>Anmelden
|
||||
</a>
|
||||
<a href="{{ url_for('register') }}" class="px-4 py-2 bg-purple-500 hover:bg-purple-600 text-white rounded-lg transition-colors">
|
||||
<i class="fas fa-user-plus mr-2"></i>Registrieren
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Forumskategorien Vorschau -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
||||
{% if categories_data %}
|
||||
{% for cat_data in categories_data %}
|
||||
<div class="forum-category block">
|
||||
<div class="rounded-xl p-5 h-full"
|
||||
x-bind:class="darkMode ? 'bg-gray-800/60 border border-white/10' : 'bg-white border border-gray-200 shadow-md'">
|
||||
<div class="flex items-start">
|
||||
<!-- Kategorie-Icon -->
|
||||
<div class="category-icon mr-4 text-white"
|
||||
style="background-color: {{ cat_data.category.node.color_code or '#6d28d9' }}">
|
||||
<i class="fas {{ cat_data.category.node.icon or 'fa-folder' }}"></i>
|
||||
</div>
|
||||
|
||||
<!-- Kategorie-Info -->
|
||||
<div class="flex-grow">
|
||||
<h3 class="text-xl font-semibold mb-2">{{ cat_data.category.title }}</h3>
|
||||
<p class="opacity-75 text-sm mb-3">{{ cat_data.category.description }}</p>
|
||||
|
||||
<!-- Statistik -->
|
||||
<div class="flex flex-wrap gap-4 text-sm opacity-80">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-comment-alt mr-2"></i>
|
||||
<span>{{ cat_data.total_posts }} Themen</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-reply mr-2"></i>
|
||||
<span>{{ cat_data.total_replies }} Antworten</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pfeil-Icon -->
|
||||
<div class="ml-2">
|
||||
<i class="fas fa-lock opacity-50"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="col-span-2 text-center py-8">
|
||||
<div class="text-3xl mb-3 opacity-30"><i class="fas fa-exclamation-circle"></i></div>
|
||||
<h3 class="text-xl font-semibold mb-2">Keine Forum-Kategorien gefunden</h3>
|
||||
<p class="opacity-75">Es sind derzeit keine Kategorien für Diskussionen verfügbar.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Hinweis zur Nutzung -->
|
||||
<div class="rounded-xl p-6 text-center mb-8"
|
||||
x-bind:class="darkMode ? 'bg-indigo-900/30 border border-indigo-700/30' : 'bg-indigo-50 border border-indigo-100'">
|
||||
<h3 class="text-xl font-semibold mb-3">
|
||||
<i class="fas fa-lightbulb mr-2 text-yellow-500"></i>
|
||||
So funktioniert das Forum
|
||||
</h3>
|
||||
<p class="mb-4">Das Community-Forum ist nach den Hauptknotenpunkten der Systades-Mindmap strukturiert.
|
||||
In deinen Beiträgen kannst du Knotenpunkte mit <code>@Knotenname</code> verlinken.</p>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mt-6">
|
||||
<div class="p-4 rounded-lg"
|
||||
x-bind:class="darkMode ? 'bg-indigo-800/40' : 'bg-white border border-indigo-100'">
|
||||
<div class="text-2xl mb-2"><i class="fas fa-users text-indigo-400"></i></div>
|
||||
<h4 class="font-medium mb-1">Fachliche Diskussionen</h4>
|
||||
<p class="text-sm opacity-75">Tausche dich mit anderen zu spezifischen Themen aus</p>
|
||||
</div>
|
||||
<div class="p-4 rounded-lg"
|
||||
x-bind:class="darkMode ? 'bg-indigo-800/40' : 'bg-white border border-indigo-100'">
|
||||
<div class="text-2xl mb-2"><i class="fas fa-link text-indigo-400"></i></div>
|
||||
<h4 class="font-medium mb-1">Wissensvernetzung</h4>
|
||||
<p class="text-sm opacity-75">Verknüpfe Inhalte durch Knotenreferenzen</p>
|
||||
</div>
|
||||
<div class="p-4 rounded-lg"
|
||||
x-bind:class="darkMode ? 'bg-indigo-800/40' : 'bg-white border border-indigo-100'">
|
||||
<div class="text-2xl mb-2"><i class="fas fa-markdown text-indigo-400"></i></div>
|
||||
<h4 class="font-medium mb-1">Markdown Support</h4>
|
||||
<p class="text-sm opacity-75">Formatiere deine Beiträge mit Markdown</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// Hier können bei Bedarf forumspezifische Scripts eingefügt werden
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,365 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Mindmap erstellen{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
/* Spezifische Stile für die Mindmap-Erstellungsseite */
|
||||
.form-container {
|
||||
background-color: var(--bg-secondary);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
body.dark .form-container {
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
body:not(.dark) .form-container {
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.form-header {
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
body:not(.dark) .form-header {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.form-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-input,
|
||||
.form-textarea {
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
body.dark .form-input,
|
||||
body.dark .form-textarea {
|
||||
background-color: rgba(15, 23, 42, 0.6);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
color: #f1f5f9;
|
||||
}
|
||||
|
||||
body:not(.dark) .form-input,
|
||||
body:not(.dark) .form-textarea {
|
||||
background-color: white;
|
||||
border: 1px solid #e2e8f0;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
body.dark .form-input:focus,
|
||||
body.dark .form-textarea:focus {
|
||||
border-color: #7c3aed;
|
||||
box-shadow: 0 0 0 2px rgba(124, 58, 237, 0.2);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
body:not(.dark) .form-input:focus,
|
||||
body:not(.dark) .form-textarea:focus {
|
||||
border-color: #7c3aed;
|
||||
box-shadow: 0 0 0 2px rgba(124, 58, 237, 0.2);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
min-height: 120px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.form-switch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.form-switch input[type="checkbox"] {
|
||||
height: 0;
|
||||
width: 0;
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.form-switch label {
|
||||
cursor: pointer;
|
||||
width: 50px;
|
||||
height: 25px;
|
||||
background: rgba(100, 116, 139, 0.3);
|
||||
display: block;
|
||||
border-radius: 25px;
|
||||
position: relative;
|
||||
margin-right: 10px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.form-switch label:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
width: 19px;
|
||||
height: 19px;
|
||||
background: #fff;
|
||||
border-radius: 19px;
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
.form-switch input:checked + label {
|
||||
background: #7c3aed;
|
||||
}
|
||||
|
||||
.form-switch input:checked + label:after {
|
||||
left: calc(100% - 3px);
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
.btn-submit {
|
||||
background-color: #7c3aed;
|
||||
color: white;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-submit:hover {
|
||||
background-color: #6d28d9;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(109, 40, 217, 0.2);
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
background-color: transparent;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
body.dark .btn-cancel {
|
||||
color: #e2e8f0;
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
body:not(.dark) .btn-cancel {
|
||||
color: #475569;
|
||||
border-color: #e2e8f0;
|
||||
}
|
||||
|
||||
.btn-cancel:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
body.dark .btn-cancel:hover {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
body:not(.dark) .btn-cancel:hover {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* Animation für den Seiteneintritt */
|
||||
@keyframes slideInUp {
|
||||
from {
|
||||
transform: translateY(20px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.form-container {
|
||||
animation: slideInUp 0.5s ease forwards;
|
||||
}
|
||||
|
||||
/* Animation für Hover-Effekte */
|
||||
.input-animation {
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.input-animation:focus {
|
||||
transform: scale(1.01);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4 py-8 animate-fadeIn">
|
||||
<div class="max-w-3xl mx-auto">
|
||||
<!-- Titel mit Animation -->
|
||||
<div class="text-center mb-8 animate-pulse">
|
||||
<h1 class="text-3xl font-bold mb-2 mystical-glow gradient-text">
|
||||
Neue Mindmap erstellen
|
||||
</h1>
|
||||
<p class="opacity-80">Erstelle deine eigene Wissenslandkarte und organisiere deine Gedanken</p>
|
||||
</div>
|
||||
|
||||
<div class="form-container">
|
||||
<div class="form-header">
|
||||
<h2 class="text-xl font-semibold">Mindmap-Details</h2>
|
||||
</div>
|
||||
|
||||
<div class="form-body">
|
||||
<form action="{{ url_for('create_mindmap') }}" method="POST">
|
||||
<div class="form-group">
|
||||
<label for="name" class="form-label">Name der Mindmap</label>
|
||||
<input type="text" id="name" name="name" class="form-input input-animation" required placeholder="z.B. Meine Philosophie-Mindmap">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="description" class="form-label">Beschreibung</label>
|
||||
<textarea id="description" name="description" class="form-textarea input-animation" placeholder="Worum geht es in dieser Mindmap?"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="form-switch">
|
||||
<input type="checkbox" id="is_private" name="is_private" checked>
|
||||
<label for="is_private"></label>
|
||||
<span>Private Mindmap (nur für dich sichtbar)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between mt-6">
|
||||
<a href="{{ url_for('profile') }}" class="btn-cancel">
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
Zurück
|
||||
</a>
|
||||
<button type="submit" class="btn-submit">
|
||||
<i class="fas fa-save"></i>
|
||||
Mindmap erstellen
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Mindmap-Vorschau -->
|
||||
<div class="mt-8">
|
||||
<h3 class="text-xl font-semibold mb-4">Vorschau</h3>
|
||||
<div class="mindmap-container">
|
||||
<div id="cy" class="w-full h-[400px] rounded-xl border"
|
||||
x-bind:class="darkMode ? 'border-gray-700' : 'border-gray-200'">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tipps-Sektion -->
|
||||
<div class="mt-8 p-5 rounded-lg border animate-fadeIn"
|
||||
x-bind:class="darkMode ? 'bg-slate-800/40 border-slate-700/50' : 'bg-white border-slate-200'">
|
||||
<h3 class="text-xl font-semibold mb-3"
|
||||
x-bind:class="darkMode ? 'text-white' : 'text-gray-800'">
|
||||
<i class="fa-solid fa-lightbulb text-yellow-400 mr-2"></i>Tipps zum Erstellen einer Mindmap
|
||||
</h3>
|
||||
<div x-bind:class="darkMode ? 'text-gray-300' : 'text-gray-600'">
|
||||
<ul class="list-disc pl-5 space-y-2">
|
||||
<li>Wähle einen prägnanten, aber aussagekräftigen Namen für deine Mindmap</li>
|
||||
<li>Beginne mit einem zentralen Konzept und arbeite dich nach außen vor</li>
|
||||
<li>Verwende verschiedene Farben für unterschiedliche Kategorien oder Themenbereiche</li>
|
||||
<li>Füge Notizen zu Knoten hinzu, um komplexere Ideen zu erklären</li>
|
||||
<li>Verknüpfe verwandte Konzepte, um Beziehungen zu visualisieren</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.26.0/cytoscape.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape-cose-bilkent/4.1.0/cytoscape-cose-bilkent.min.js"></script>
|
||||
<script nonce="{{ csp_nonce }}">
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Einfache Animationen für die Eingabefelder
|
||||
const inputs = document.querySelectorAll('.input-animation');
|
||||
|
||||
inputs.forEach(input => {
|
||||
// Subtile Skalierung bei Fokus
|
||||
input.addEventListener('focus', function() {
|
||||
this.style.transform = 'scale(1.01)';
|
||||
this.style.boxShadow = '0 4px 12px rgba(124, 58, 237, 0.15)';
|
||||
});
|
||||
|
||||
input.addEventListener('blur', function() {
|
||||
this.style.transform = 'scale(1)';
|
||||
this.style.boxShadow = 'none';
|
||||
});
|
||||
});
|
||||
|
||||
// Formular-Absenden-Animation
|
||||
const form = document.querySelector('form');
|
||||
form.addEventListener('submit', function(e) {
|
||||
const submitBtn = this.querySelector('.btn-submit');
|
||||
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Wird erstellt...';
|
||||
submitBtn.disabled = true;
|
||||
});
|
||||
|
||||
// Mindmap-Vorschau initialisieren
|
||||
const mindmap = new MindMap.Visualization('cy', {
|
||||
enableEditing: true,
|
||||
onNodeClick: function(nodeData) {
|
||||
console.log("Knoten ausgewählt:", nodeData);
|
||||
}
|
||||
});
|
||||
|
||||
// Formularfelder mit Mindmap verbinden
|
||||
const nameInput = document.getElementById('name');
|
||||
const descriptionInput = document.getElementById('description');
|
||||
|
||||
// Aktualisiere Mindmap wenn sich die Eingaben ändern
|
||||
nameInput.addEventListener('input', function() {
|
||||
if (mindmap.cy) {
|
||||
const rootNode = mindmap.cy.$('#root');
|
||||
if (rootNode.length > 0) {
|
||||
rootNode.data('name', this.value || 'Neue Mindmap');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Initialisiere die Mindmap
|
||||
mindmap.initialize().then(() => {
|
||||
console.log("Mindmap-Vorschau initialisiert");
|
||||
|
||||
// Setze initiale Werte
|
||||
if (nameInput.value) {
|
||||
const rootNode = mindmap.cy.$('#root');
|
||||
if (rootNode.length > 0) {
|
||||
rootNode.data('name', nameInput.value);
|
||||
}
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error("Fehler bei der Initialisierung der Mindmap:", error);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,423 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Mindmap bearbeiten{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
/* Spezifische Stile für die Mindmap-Bearbeitungsseite */
|
||||
.form-container {
|
||||
background-color: var(--bg-secondary);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
body.dark .form-container {
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
body:not(.dark) .form-container {
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.form-header {
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
body:not(.dark) .form-header {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.form-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-input,
|
||||
.form-textarea {
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
body.dark .form-input,
|
||||
body.dark .form-textarea {
|
||||
background-color: rgba(15, 23, 42, 0.6);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
color: #f1f5f9;
|
||||
}
|
||||
|
||||
body:not(.dark) .form-input,
|
||||
body:not(.dark) .form-textarea {
|
||||
background-color: white;
|
||||
border: 1px solid #e2e8f0;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
body.dark .form-input:focus,
|
||||
body.dark .form-textarea:focus {
|
||||
border-color: #7c3aed;
|
||||
box-shadow: 0 0 0 2px rgba(124, 58, 237, 0.2);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
body:not(.dark) .form-input:focus,
|
||||
body:not(.dark) .form-textarea:focus {
|
||||
border-color: #7c3aed;
|
||||
box-shadow: 0 0 0 2px rgba(124, 58, 237, 0.2);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
min-height: 120px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.form-switch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.form-switch input[type="checkbox"] {
|
||||
height: 0;
|
||||
width: 0;
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.form-switch label {
|
||||
cursor: pointer;
|
||||
width: 50px;
|
||||
height: 25px;
|
||||
background: rgba(100, 116, 139, 0.3);
|
||||
display: block;
|
||||
border-radius: 25px;
|
||||
position: relative;
|
||||
margin-right: 10px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.form-switch label:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
width: 19px;
|
||||
height: 19px;
|
||||
background: #fff;
|
||||
border-radius: 19px;
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
.form-switch input:checked + label {
|
||||
background: #7c3aed;
|
||||
}
|
||||
|
||||
.form-switch input:checked + label:after {
|
||||
left: calc(100% - 3px);
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
.btn-submit {
|
||||
background-color: #7c3aed;
|
||||
color: white;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-submit:hover {
|
||||
background-color: #6d28d9;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(109, 40, 217, 0.2);
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
background-color: transparent;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
body.dark .btn-cancel {
|
||||
color: #e2e8f0;
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
body:not(.dark) .btn-cancel {
|
||||
color: #475569;
|
||||
border-color: #e2e8f0;
|
||||
}
|
||||
|
||||
.btn-cancel:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
body.dark .btn-cancel:hover {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
body:not(.dark) .btn-cancel:hover {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* Animation für den Seiteneintritt */
|
||||
@keyframes slideInUp {
|
||||
from {
|
||||
transform: translateY(20px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.form-container {
|
||||
animation: slideInUp 0.5s ease forwards;
|
||||
}
|
||||
|
||||
/* Animation für Hover-Effekte */
|
||||
.input-animation {
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.input-animation:focus {
|
||||
transform: scale(1.01);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4 py-8 animate-fadeIn">
|
||||
<div class="max-w-3xl mx-auto">
|
||||
<!-- Titel mit Animation -->
|
||||
<div class="text-center mb-8 animate-pulse">
|
||||
<h1 class="text-3xl font-bold mb-2 mystical-glow gradient-text">
|
||||
Mindmap bearbeiten
|
||||
</h1>
|
||||
<p class="opacity-80">Aktualisiere die Details deiner Mindmap</p>
|
||||
</div>
|
||||
|
||||
<div class="form-container">
|
||||
<div class="form-header">
|
||||
<h2 class="text-xl font-semibold">Mindmap-Details</h2>
|
||||
</div>
|
||||
|
||||
<div class="form-body">
|
||||
<form action="{{ url_for('edit_mindmap', mindmap_id=mindmap.id) }}" method="POST">
|
||||
<div class="form-group">
|
||||
<label for="name" class="form-label">Name der Mindmap</label>
|
||||
<input type="text" id="name" name="name" class="form-input input-animation" required
|
||||
placeholder="z.B. Meine Philosophie-Mindmap" value="{{ mindmap.name }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="description" class="form-label">Beschreibung</label>
|
||||
<textarea id="description" name="description" class="form-textarea input-animation"
|
||||
placeholder="Worum geht es in dieser Mindmap?">{{ mindmap.description }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="form-switch">
|
||||
<input type="checkbox" id="is_private" name="is_private" {% if mindmap.is_private %}checked{% endif %}>
|
||||
<label for="is_private"></label>
|
||||
<span>Private Mindmap (nur für dich sichtbar)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between mt-6">
|
||||
<a href="{{ url_for('mindmap', mindmap_id=mindmap.id) }}" class="btn-cancel">
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
Zurück
|
||||
</a>
|
||||
<button type="submit" class="btn-submit">
|
||||
<i class="fas fa-save"></i>
|
||||
Änderungen speichern
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Mindmap-Editor -->
|
||||
<div class="mt-8">
|
||||
<h3 class="text-xl font-semibold mb-4">Mindmap bearbeiten</h3>
|
||||
<div class="mindmap-container">
|
||||
<div id="cy" class="w-full h-[600px] rounded-xl border"
|
||||
x-bind:class="darkMode ? 'border-gray-700' : 'border-gray-200'">
|
||||
</div>
|
||||
</div>
|
||||
<!-- Bearbeitungshinweise -->
|
||||
<div class="mt-4 text-sm opacity-80">
|
||||
<p><i class="fas fa-info-circle mr-2"></i>Klicke auf Knoten zum Bearbeiten, ziehe sie zum Neuanordnen oder nutze die Toolbar für weitere Funktionen.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tipps-Sektion -->
|
||||
<div class="mt-8 p-5 rounded-lg border animate-fadeIn"
|
||||
x-bind:class="darkMode ? 'bg-slate-800/40 border-slate-700/50' : 'bg-white border-slate-200'">
|
||||
<h3 class="text-xl font-semibold mb-3"
|
||||
x-bind:class="darkMode ? 'text-white' : 'text-gray-800'">
|
||||
<i class="fa-solid fa-lightbulb text-yellow-400 mr-2"></i>Tipps zum Bearbeiten einer Mindmap
|
||||
</h3>
|
||||
<div x-bind:class="darkMode ? 'text-gray-300' : 'text-gray-600'">
|
||||
<ul class="list-disc pl-5 space-y-2">
|
||||
<li>Überprüfe, ob der Name noch zum aktuellen Inhalt passt</li>
|
||||
<li>Aktualisiere die Beschreibung, um neue Aspekte zu berücksichtigen</li>
|
||||
<li>Entscheide, ob die Sichtbarkeitseinstellungen noch passend sind</li>
|
||||
<li>Nutze aussagekräftige Namen für bessere Auffindbarkeit</li>
|
||||
<li>Behalte die Konsistenz mit verknüpften Konzepten im Auge</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.26.0/cytoscape.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape-cose-bilkent/4.1.0/cytoscape-cose-bilkent.min.js"></script>
|
||||
<script nonce="{{ csp_nonce }}">
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Einfache Animationen für die Eingabefelder
|
||||
const inputs = document.querySelectorAll('.input-animation');
|
||||
|
||||
inputs.forEach(input => {
|
||||
// Subtile Skalierung bei Fokus
|
||||
input.addEventListener('focus', function() {
|
||||
this.style.transform = 'scale(1.01)';
|
||||
this.style.boxShadow = '0 4px 12px rgba(124, 58, 237, 0.15)';
|
||||
});
|
||||
|
||||
input.addEventListener('blur', function() {
|
||||
this.style.transform = 'scale(1)';
|
||||
this.style.boxShadow = 'none';
|
||||
});
|
||||
});
|
||||
|
||||
// Formular-Absenden-Animation
|
||||
const form = document.querySelector('form');
|
||||
form.addEventListener('submit', function(e) {
|
||||
const submitBtn = this.querySelector('.btn-submit');
|
||||
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Wird gespeichert...';
|
||||
submitBtn.disabled = true;
|
||||
});
|
||||
|
||||
// Mindmap initialisieren
|
||||
const mindmap = new MindMap.Visualization('cy', {
|
||||
enableEditing: true,
|
||||
apiEndpoint: '/api/mindmap/{{ mindmap.id }}',
|
||||
onNodeClick: function(nodeData) {
|
||||
console.log("Knoten ausgewählt:", nodeData);
|
||||
},
|
||||
onChange: function(data) {
|
||||
// Automatisches Speichern bei Änderungen
|
||||
fetch('/api/mindmap/{{ mindmap.id }}/update', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': '{{ csrf_token() }}'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
}).then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Netzwerkfehler beim Speichern');
|
||||
}
|
||||
console.log('Änderungen gespeichert');
|
||||
}).catch(error => {
|
||||
console.error('Fehler beim Speichern:', error);
|
||||
alert('Fehler beim Speichern der Änderungen');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Formularfelder mit Mindmap verbinden
|
||||
const nameInput = document.getElementById('name');
|
||||
const descriptionInput = document.getElementById('description');
|
||||
|
||||
// Aktualisiere Mindmap wenn sich die Eingaben ändern
|
||||
nameInput.addEventListener('input', function() {
|
||||
if (mindmap.cy) {
|
||||
const rootNode = mindmap.cy.$('#root');
|
||||
if (rootNode.length > 0) {
|
||||
rootNode.data('name', this.value || 'Mindmap');
|
||||
mindmap.saveToServer();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Initialisiere die Mindmap mit existierenden Daten
|
||||
mindmap.initialize().then(() => {
|
||||
console.log("Mindmap-Editor initialisiert");
|
||||
|
||||
// Lade existierende Daten
|
||||
fetch('/api/mindmap/{{ mindmap.id }}/data')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
mindmap.loadData(data);
|
||||
console.log("Mindmap-Daten geladen");
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("Fehler beim Laden der Mindmap-Daten:", error);
|
||||
alert("Fehler beim Laden der Mindmap");
|
||||
});
|
||||
}).catch(error => {
|
||||
console.error("Fehler bei der Initialisierung des Editors:", error);
|
||||
});
|
||||
|
||||
// Autosave-Status Anzeige
|
||||
const statusIndicator = document.createElement('div');
|
||||
statusIndicator.className = 'fixed bottom-4 right-4 px-4 py-2 rounded-full text-sm transition-all duration-300';
|
||||
document.body.appendChild(statusIndicator);
|
||||
|
||||
// Zeige Speicherstatus
|
||||
function showStatus(message, isError = false) {
|
||||
statusIndicator.textContent = message;
|
||||
statusIndicator.className = `fixed bottom-4 right-4 px-4 py-2 rounded-full text-sm transition-all duration-300 ${
|
||||
isError
|
||||
? 'bg-red-500 text-white'
|
||||
: 'bg-green-500 text-white'
|
||||
}`;
|
||||
setTimeout(() => {
|
||||
statusIndicator.className = 'fixed bottom-4 right-4 px-4 py-2 rounded-full text-sm transition-all duration-300 opacity-0';
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// Event-Listener für Speicherstatus
|
||||
document.addEventListener('mindmapSaved', () => {
|
||||
showStatus('Änderungen gespeichert');
|
||||
});
|
||||
|
||||
document.addEventListener('mindmapError', (event) => {
|
||||
showStatus(event.detail.message, true);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -39,35 +39,6 @@
|
||||
animation: textReveal 1s cubic-bezier(0.77, 0, 0.18, 1) forwards;
|
||||
}
|
||||
|
||||
/* Marker-Animation für den Text */
|
||||
@keyframes markerAnimation {
|
||||
0% { width: 0; opacity: 0; }
|
||||
20% { width: 100%; opacity: 0.7; }
|
||||
80% { width: 100%; opacity: 0.7; }
|
||||
100% { width: 0; opacity: 0; }
|
||||
}
|
||||
|
||||
.marker-animation {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.marker-animation::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 6px;
|
||||
width: 0;
|
||||
background: linear-gradient(to right, rgba(109, 40, 217, 0.3), rgba(139, 92, 246, 0.6), rgba(109, 40, 217, 0.3));
|
||||
border-radius: 3px;
|
||||
opacity: 0;
|
||||
animation: markerAnimation 2.5s ease-in-out forwards;
|
||||
}
|
||||
|
||||
.marker-animation-delay::after {
|
||||
animation-delay: 1.5s;
|
||||
}
|
||||
|
||||
.delay-1 { animation-delay: 0.2s; }
|
||||
.delay-2 { animation-delay: 0.4s; }
|
||||
.delay-3 { animation-delay: 0.6s; }
|
||||
@@ -100,20 +71,16 @@
|
||||
|
||||
/* Chat section styles */
|
||||
.embedded-chat {
|
||||
height: 500px;
|
||||
height: 350px;
|
||||
border-radius: 1rem;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 5px 10px -5px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.dark .embedded-chat {
|
||||
background-color: rgba(17, 24, 39, 0.7);
|
||||
border-color: rgba(109, 40, 217, 0.2);
|
||||
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.3), 0 5px 10px -5px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.embedded-chat {
|
||||
@@ -122,118 +89,9 @@
|
||||
}
|
||||
|
||||
#embedded-chat-messages {
|
||||
flex: 1;
|
||||
height: 250px;
|
||||
overflow-y: auto;
|
||||
padding: 1.25rem;
|
||||
min-height: 320px;
|
||||
}
|
||||
|
||||
.chat-input-container {
|
||||
padding: 1.25rem;
|
||||
border-top: 1px solid;
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.dark .chat-input-container {
|
||||
background-color: rgba(17, 24, 39, 0.6);
|
||||
border-color: rgba(75, 85, 99, 0.4);
|
||||
}
|
||||
|
||||
.mystical-input {
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
border: 1px solid rgba(209, 213, 219, 0.5);
|
||||
color: #4B5563;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
outline: none;
|
||||
transition: all 0.3s ease;
|
||||
width: 100%;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.dark .mystical-input {
|
||||
background-color: rgba(31, 41, 55, 0.7);
|
||||
border-color: rgba(75, 85, 99, 0.4);
|
||||
color: #E5E7EB;
|
||||
}
|
||||
|
||||
.mystical-input:focus {
|
||||
border-color: rgba(139, 92, 246, 0.5);
|
||||
box-shadow: 0 0 0 2px rgba(139, 92, 246, 0.2);
|
||||
}
|
||||
|
||||
.dark .mystical-input:focus {
|
||||
border-color: rgba(139, 92, 246, 0.5);
|
||||
box-shadow: 0 0 0 2px rgba(139, 92, 246, 0.3);
|
||||
}
|
||||
|
||||
/* Verbesserte Lesbarkeit für Chat-Nachrichten */
|
||||
.chat-message {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.chat-bubble {
|
||||
padding: 1rem;
|
||||
border-radius: 0.75rem;
|
||||
max-width: 85%;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.assistant-bubble {
|
||||
background-color: rgba(243, 244, 246, 0.95);
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.dark .assistant-bubble {
|
||||
background-color: rgba(31, 41, 55, 0.95);
|
||||
color: #E5E7EB;
|
||||
}
|
||||
|
||||
.user-bubble {
|
||||
background-color: rgba(139, 92, 246, 0.15);
|
||||
color: #4B5563;
|
||||
}
|
||||
|
||||
.dark .user-bubble {
|
||||
background-color: rgba(124, 58, 237, 0.3);
|
||||
color: #E5E7EB;
|
||||
}
|
||||
|
||||
/* Beispiel-Buttons verbessert */
|
||||
.quick-query-container {
|
||||
margin-top: 0.75rem;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.quick-query-btn {
|
||||
font-size: 0.8rem;
|
||||
padding: 0.4rem 0.75rem;
|
||||
border-radius: 2rem;
|
||||
background-color: rgba(243, 244, 246, 0.8);
|
||||
color: #4B5563;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
white-space: nowrap;
|
||||
border: 1px solid rgba(209, 213, 219, 0.5);
|
||||
}
|
||||
|
||||
.dark .quick-query-btn {
|
||||
background-color: rgba(55, 65, 81, 0.8);
|
||||
color: #E5E7EB;
|
||||
border-color: rgba(75, 85, 99, 0.4);
|
||||
}
|
||||
|
||||
.quick-query-btn:hover {
|
||||
background-color: rgba(229, 231, 235, 0.9);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.dark .quick-query-btn:hover {
|
||||
background-color: rgba(75, 85, 99, 0.9);
|
||||
}
|
||||
|
||||
/* Chat typing indicator */
|
||||
@@ -273,9 +131,16 @@
|
||||
<div class="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||
<div class="text-center mb-16">
|
||||
<h1 class="hero-heading mb-8 text-gray-900 dark:text-white">
|
||||
<div class="overflow-hidden flex justify-center gap-6">
|
||||
<span class="relative inline-block text-reveal marker-animation">Wissen.</span>
|
||||
<span class="relative inline-block text-reveal delay-1 marker-animation marker-animation-delay">Vernetzen.</span>
|
||||
<div class="overflow-hidden">
|
||||
<span class="gradient-text inline-block text-reveal">Wissen</span>
|
||||
</div>
|
||||
<div class="overflow-hidden mt-2">
|
||||
<span class="inline-block text-reveal delay-1">neu</span>
|
||||
</div>
|
||||
<div class="mt-2 relative overflow-hidden">
|
||||
<span class="relative inline-block text-reveal delay-2">vernetzen
|
||||
<div class="absolute -bottom-2 left-0 right-0 h-1 bg-gradient-to-r from-purple-500/0 via-purple-500/70 to-purple-500/0 rounded-full"></div>
|
||||
</span>
|
||||
</div>
|
||||
</h1>
|
||||
<div class="overflow-hidden">
|
||||
@@ -314,12 +179,6 @@
|
||||
<div class="text-center">
|
||||
<div class="text-3xl font-bold gradient-text mb-2 animate-float">Systades</div>
|
||||
<div class="text-lg text-gray-700 dark:text-gray-300">WISSEN VERNETZEN</div>
|
||||
<!-- Animierte Pfeilspitze -->
|
||||
<div class="mt-6 flex justify-center">
|
||||
<svg width="20" height="12" viewBox="0 0 20 12" fill="none" xmlns="http://www.w3.org/2000/svg" class="text-white animate-bounce-slow">
|
||||
<path d="M10 12L0 2L2 0L10 8L18 0L20 2L10 12Z" fill="currentColor" fill-opacity="0.7"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -412,14 +271,14 @@
|
||||
</div>
|
||||
|
||||
<!-- Chat Messages -->
|
||||
<div id="embedded-chat-messages" class="border-b-0">
|
||||
<div id="embedded-chat-messages" class="border-b border-gray-200 dark:border-gray-700">
|
||||
<!-- Assistant Message -->
|
||||
<div class="chat-message flex">
|
||||
<div class="w-9 h-9 rounded-full bg-gradient-to-r from-purple-600 to-indigo-600 flex items-center justify-center text-white mr-3 flex-shrink-0">
|
||||
<div class="mb-4 flex">
|
||||
<div class="w-8 h-8 rounded-full bg-gradient-to-r from-purple-600 to-indigo-600 flex items-center justify-center text-white mr-2 flex-shrink-0">
|
||||
<i class="fa-solid fa-robot text-sm"></i>
|
||||
</div>
|
||||
<div class="chat-bubble assistant-bubble">
|
||||
<div class="markdown-content">
|
||||
<div class="bg-gray-100 dark:bg-gray-800 rounded-lg p-3 max-w-[80%]">
|
||||
<div class="text-gray-700 dark:text-gray-300 markdown-content">
|
||||
<p>Hallo! Ich bin dein Systades-Assistent. Wie kann ich dir heute helfen?</p>
|
||||
<p>Du kannst mir Fragen zu:</p>
|
||||
<ul>
|
||||
@@ -433,24 +292,24 @@
|
||||
</div>
|
||||
|
||||
<!-- User Message -->
|
||||
<div class="chat-message flex justify-end">
|
||||
<div class="chat-bubble user-bubble">
|
||||
<p>
|
||||
<div class="mb-4 flex justify-end">
|
||||
<div class="bg-purple-100 dark:bg-purple-900/30 rounded-lg p-3 max-w-[80%]">
|
||||
<p class="text-gray-800 dark:text-gray-200">
|
||||
Kann ich mit deiner Hilfe eine Mindmap zum Thema Künstliche Intelligenz erstellen?
|
||||
</p>
|
||||
</div>
|
||||
<div class="w-9 h-9 rounded-full bg-gray-300 dark:bg-gray-600 flex items-center justify-center text-gray-700 dark:text-gray-300 ml-3 flex-shrink-0">
|
||||
<div class="w-8 h-8 rounded-full bg-gray-300 dark:bg-gray-600 flex items-center justify-center text-gray-700 dark:text-gray-300 ml-2 flex-shrink-0">
|
||||
<i class="fa-solid fa-user text-sm"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Assistant Response -->
|
||||
<div class="chat-message flex" id="demo-ai-response">
|
||||
<div class="w-9 h-9 rounded-full bg-gradient-to-r from-purple-600 to-indigo-600 flex items-center justify-center text-white mr-3 flex-shrink-0">
|
||||
<div class="mb-4 flex" id="demo-ai-response">
|
||||
<div class="w-8 h-8 rounded-full bg-gradient-to-r from-purple-600 to-indigo-600 flex items-center justify-center text-white mr-2 flex-shrink-0">
|
||||
<i class="fa-solid fa-robot text-sm"></i>
|
||||
</div>
|
||||
<div class="chat-bubble assistant-bubble">
|
||||
<div class="markdown-content">
|
||||
<div class="bg-gray-100 dark:bg-gray-800 rounded-lg p-3 max-w-[80%]">
|
||||
<div class="text-gray-700 dark:text-gray-300 markdown-content">
|
||||
<p>Ja, natürlich! Ich kann dir dabei helfen, eine Mindmap zum Thema <strong>Künstliche Intelligenz</strong> zu erstellen.</p>
|
||||
<p>Du kannst wie folgt vorgehen:</p>
|
||||
<ol>
|
||||
@@ -466,19 +325,19 @@
|
||||
</div>
|
||||
|
||||
<!-- Chat Input -->
|
||||
<div class="chat-input-container">
|
||||
<div class="p-4">
|
||||
<div class="flex">
|
||||
<input type="text" placeholder="Stelle eine Frage..." class="mystical-input flex-grow" disabled>
|
||||
<button class="ml-3 bg-gradient-to-r from-purple-600 to-indigo-600 text-white px-3 py-2 rounded-lg disabled:opacity-50 flex-shrink-0 hover:shadow-md transition-all duration-200" disabled>
|
||||
<button class="ml-2 bg-gradient-to-r from-purple-600 to-indigo-600 text-white p-2 rounded-lg disabled:opacity-50" disabled>
|
||||
<i class="fa-solid fa-paper-plane"></i>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Quick Queries -->
|
||||
<div class="quick-query-container">
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400 mr-1">Beispiele:</span>
|
||||
<button data-question="Was sind die wichtigsten Grundlagen der Künstlichen Intelligenz?" class="quick-query-btn hover:shadow-sm">KI-Grundlagen</button>
|
||||
<button data-question="Wie kann ich eine Mindmap zum Thema Neuronale Netzwerke erstellen?" class="quick-query-btn hover:shadow-sm">Mindmap erstellen</button>
|
||||
<button data-question="Zeige mir alle verfügbaren Kategorien in der Datenbank" class="quick-query-btn hover:shadow-sm">Datenbank durchsuchen</button>
|
||||
<div class="mt-3 flex flex-wrap gap-2">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400 mr-1">Beispiele:</span>
|
||||
<button data-question="Was sind die wichtigsten Grundlagen der Künstlichen Intelligenz?" class="quick-query-btn text-xs px-2 py-1 rounded-full bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600 cursor-pointer transition-colors">KI-Grundlagen</button>
|
||||
<button data-question="Wie kann ich eine Mindmap zum Thema Neuronale Netzwerke erstellen?" class="quick-query-btn text-xs px-2 py-1 rounded-full bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600 cursor-pointer transition-colors">Mindmap erstellen</button>
|
||||
<button data-question="Zeige mir alle verfügbaren Kategorien in der Datenbank" class="quick-query-btn text-xs px-2 py-1 rounded-full bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600 cursor-pointer transition-colors">Datenbank durchsuchen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,228 +1,234 @@
|
||||
{% extends "base.html" %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Interaktive Mindmap</title>
|
||||
|
||||
{% block title %}Interaktive Mindmap{% endblock %}
|
||||
<!-- Cytoscape.js -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.26.0/cytoscape.min.js"></script>
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
/* Grundlegendes Layout */
|
||||
.mindmap-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: calc(100vh - 64px);
|
||||
background: linear-gradient(135deg, #1a1f2e 0%, #0f172a 100%);
|
||||
overflow: hidden;
|
||||
}
|
||||
<!-- Socket.IO -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.7.2/socket.io.min.js"></script>
|
||||
|
||||
/* Hauptcontainer für die Mindmap */
|
||||
#cy {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: transparent;
|
||||
}
|
||||
<!-- Feather Icons (optional) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
|
||||
|
||||
/* Header-Bereich */
|
||||
.mindmap-header {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 1.5rem;
|
||||
background: rgba(15, 23, 42, 0.8);
|
||||
backdrop-filter: blur(10px);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
z-index: 10;
|
||||
}
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.mindmap-title {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
margin: 0;
|
||||
background: linear-gradient(90deg, #60a5fa, #8b5cf6);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background-color: #f9fafb;
|
||||
color: #111827;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Kontrollpanel */
|
||||
.control-panel {
|
||||
position: absolute;
|
||||
right: 2rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: rgba(15, 23, 42, 0.9);
|
||||
border-radius: 1rem;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
z-index: 10;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.control-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.75rem 1rem;
|
||||
margin: 0.5rem 0;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.header {
|
||||
background-color: #1f2937;
|
||||
color: white;
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.control-button:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
transform: translateX(-5px);
|
||||
}
|
||||
.header h1 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.control-button i {
|
||||
margin-right: 0.75rem;
|
||||
}
|
||||
.toolbar {
|
||||
background-color: #f3f4f6;
|
||||
padding: 0.75rem;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
/* Info-Panel */
|
||||
.info-panel {
|
||||
position: absolute;
|
||||
left: 2rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: rgba(15, 23, 42, 0.9);
|
||||
border-radius: 1rem;
|
||||
padding: 1.5rem;
|
||||
width: 300px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
z-index: 10;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
opacity: 0;
|
||||
transform: translateX(-20px);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.btn {
|
||||
background-color: #3b82f6;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.info-panel.visible {
|
||||
opacity: 1;
|
||||
transform: translateY(-50%) translateX(0);
|
||||
}
|
||||
.btn:hover {
|
||||
background-color: #2563eb;
|
||||
}
|
||||
|
||||
.info-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
.btn-secondary {
|
||||
background-color: #6b7280;
|
||||
}
|
||||
|
||||
.info-content {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.btn-secondary:hover {
|
||||
background-color: #4b5563;
|
||||
}
|
||||
|
||||
/* Kategorie-Legende */
|
||||
.category-legend {
|
||||
position: absolute;
|
||||
bottom: 2rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(15, 23, 42, 0.9);
|
||||
border-radius: 1rem;
|
||||
padding: 1rem 2rem;
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
z-index: 10;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
.btn-danger {
|
||||
background-color: #ef4444;
|
||||
}
|
||||
|
||||
.category-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.btn-danger:hover {
|
||||
background-color: #dc2626;
|
||||
}
|
||||
|
||||
.category-color {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
.search-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
/* Animationen */
|
||||
@keyframes pulse {
|
||||
0% { transform: scale(1); }
|
||||
50% { transform: scale(1.05); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
.search-input {
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.pulse {
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
#cy {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
{% block content %}
|
||||
<div class="mindmap-container">
|
||||
<!-- Header -->
|
||||
<div class="mindmap-header">
|
||||
<h1 class="mindmap-title">Interaktive Wissenslandkarte</h1>
|
||||
.category-filters {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
padding: 0.75rem;
|
||||
background-color: #ffffff;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.category-filter {
|
||||
border: none;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.25rem 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.category-filter:not(.active) {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.category-filter:hover:not(.active) {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.footer {
|
||||
background-color: #f3f4f6;
|
||||
padding: 0.75rem;
|
||||
text-align: center;
|
||||
font-size: 0.75rem;
|
||||
color: #6b7280;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
/* Kontextmenü Styling */
|
||||
#context-menu {
|
||||
position: absolute;
|
||||
background-color: white;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 0.25rem;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#context-menu .menu-item {
|
||||
padding: 0.5rem 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#context-menu .menu-item:hover {
|
||||
background-color: #f3f4f6;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header class="header">
|
||||
<h1>Interaktive Mindmap</h1>
|
||||
<div class="search-container">
|
||||
<input type="text" id="search-mindmap" class="search-input" placeholder="Suchen...">
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="toolbar">
|
||||
<button id="addNode" class="btn">
|
||||
<i data-feather="plus-circle"></i>
|
||||
Knoten hinzufügen
|
||||
</button>
|
||||
<button id="addEdge" class="btn">
|
||||
<i data-feather="git-branch"></i>
|
||||
Verbindung erstellen
|
||||
</button>
|
||||
<button id="editNode" class="btn btn-secondary">
|
||||
<i data-feather="edit-2"></i>
|
||||
Knoten bearbeiten
|
||||
</button>
|
||||
<button id="deleteNode" class="btn btn-danger">
|
||||
<i data-feather="trash-2"></i>
|
||||
Knoten löschen
|
||||
</button>
|
||||
<button id="deleteEdge" class="btn btn-danger">
|
||||
<i data-feather="scissors"></i>
|
||||
Verbindung löschen
|
||||
</button>
|
||||
<button id="reLayout" class="btn btn-secondary">
|
||||
<i data-feather="refresh-cw"></i>
|
||||
Layout neu anordnen
|
||||
</button>
|
||||
<button id="exportMindmap" class="btn btn-secondary">
|
||||
<i data-feather="download"></i>
|
||||
Exportieren
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="category-filters" class="category-filters">
|
||||
<!-- Wird dynamisch befüllt -->
|
||||
</div>
|
||||
|
||||
<div id="cy"></div>
|
||||
|
||||
<footer class="footer">
|
||||
Mindmap-Anwendung © 2023
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- Hauptvisualisierung -->
|
||||
<div id="cy"></div>
|
||||
<!-- Unsere Mindmap JS -->
|
||||
<script src="{{ url_for('static', filename='js/mindmap.js') }}"></script>
|
||||
|
||||
<!-- Kontrollpanel -->
|
||||
<div class="control-panel">
|
||||
<button class="control-button" id="zoom-in">
|
||||
<i class="fas fa-search-plus"></i> Einzoomen
|
||||
</button>
|
||||
<button class="control-button" id="zoom-out">
|
||||
<i class="fas fa-search-minus"></i> Auszoomen
|
||||
</button>
|
||||
<button class="control-button" id="reset-view">
|
||||
<i class="fas fa-compress-arrows-alt"></i> Ansicht zurücksetzen
|
||||
</button>
|
||||
<button class="control-button" id="toggle-legend">
|
||||
<i class="fas fa-layer-group"></i> Legende ein/aus
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Info-Panel -->
|
||||
<div class="info-panel" id="node-info">
|
||||
<h3 class="info-title">Knoteninformationen</h3>
|
||||
<div class="info-content">
|
||||
<p>Wählen Sie einen Knoten aus, um Details anzuzeigen.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Kategorie-Legende -->
|
||||
<div class="category-legend">
|
||||
<div class="category-item">
|
||||
<span class="category-color" style="background: #60a5fa;"></span>
|
||||
Philosophie
|
||||
</div>
|
||||
<div class="category-item">
|
||||
<span class="category-color" style="background: #8b5cf6;"></span>
|
||||
Wissenschaft
|
||||
</div>
|
||||
<div class="category-item">
|
||||
<span class="category-color" style="background: #10b981;"></span>
|
||||
Technologie
|
||||
</div>
|
||||
<div class="category-item">
|
||||
<span class="category-color" style="background: #f59e0b;"></span>
|
||||
Künste
|
||||
</div>
|
||||
<div class="category-item">
|
||||
<span class="category-color" style="background: #ef4444;"></span>
|
||||
Psychologie
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script src="{{ url_for('static', filename='js/cytoscape.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/mindmap-init.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/update_mindmap.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/mindmap-interaction.js') }}"></script>
|
||||
{% endblock %}
|
||||
<!-- Icons initialisieren -->
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (typeof feather !== 'undefined') {
|
||||
feather.replace();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -4,53 +4,17 @@
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
/* Gradient Text Styles */
|
||||
.text-gradient-purple-blue {
|
||||
background: linear-gradient(135deg, #b38fff, #58a9ff);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.text-gradient-blue-cyan {
|
||||
background: linear-gradient(135deg, #58a9ff, #38bdf8);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.text-gradient-cyan-teal {
|
||||
background: linear-gradient(135deg, #38bdf8, #14b8a6);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.text-gradient-teal-green {
|
||||
background: linear-gradient(135deg, #14b8a6, #22c55e);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.text-gradient-green-yellow {
|
||||
background: linear-gradient(135deg, #22c55e, #eab308);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
/* Grundstile für das Profil mit verbessertem Glasmorphismus */
|
||||
.profile-container {
|
||||
background: rgba(24, 28, 45, 0.75);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-radius: 24px;
|
||||
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.35),
|
||||
0 8px 16px rgba(179, 143, 255, 0.1);
|
||||
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.35);
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.profile-container:hover {
|
||||
@@ -225,64 +189,294 @@
|
||||
overflow-x: auto;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(179, 143, 255, 0.5) rgba(255, 255, 255, 0.05);
|
||||
padding: 0.5rem;
|
||||
background: rgba(24, 28, 45, 0.4);
|
||||
border-radius: 16px 16px 0 0;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.profile-tab {
|
||||
padding: 0.75rem 1.25rem;
|
||||
padding: 1rem 1.5rem;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
border-bottom: 3px solid transparent;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
border-radius: 12px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.profile-tab:hover {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.profile-tab.active {
|
||||
color: #b38fff;
|
||||
border-bottom: 3px solid #b38fff;
|
||||
background: rgba(179, 143, 255, 0.1);
|
||||
}
|
||||
|
||||
/* Aktivitäten und Beiträge */
|
||||
.activity-feed {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.activity-card {
|
||||
background: rgba(32, 36, 55, 0.7);
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
backdrop-filter: blur(15px);
|
||||
-webkit-backdrop-filter: blur(15px);
|
||||
}
|
||||
|
||||
.activity-card:hover {
|
||||
transform: translateY(-3px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.25), 0 0 15px rgba(179, 143, 255, 0.15);
|
||||
}
|
||||
|
||||
.activity-header {
|
||||
padding: 1.25rem 1.5rem;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.activity-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
}
|
||||
|
||||
.activity-date {
|
||||
font-size: 0.85rem;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.activity-content {
|
||||
padding: 1.5rem;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
font-size: 1.05rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.activity-footer {
|
||||
padding: 1rem 1.5rem;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.activity-reactions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.reaction-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 12px;
|
||||
font-size: 0.9rem;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: none;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.reaction-button:hover {
|
||||
background: rgba(179, 143, 255, 0.15);
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Statistik-Elemente - verkleinert */
|
||||
.stat-item {
|
||||
padding: 0.75rem !important;
|
||||
.reaction-button.active {
|
||||
background: rgba(179, 143, 255, 0.2);
|
||||
color: #b38fff;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 8px !important;
|
||||
height: 8px !important;
|
||||
margin-bottom: 0.3rem !important;
|
||||
.activity-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1.2rem !important;
|
||||
margin-bottom: 0.2rem !important;
|
||||
.action-button {
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 12px;
|
||||
font-size: 0.9rem;
|
||||
background: rgba(179, 143, 255, 0.1);
|
||||
color: #b38fff;
|
||||
border: 1px solid rgba(179, 143, 255, 0.25);
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.7rem !important;
|
||||
.action-button:hover {
|
||||
background: rgba(179, 143, 255, 0.2);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1), 0 0 10px rgba(179, 143, 255, 0.2);
|
||||
}
|
||||
|
||||
/* Persönliche Mindmap Container */
|
||||
.personal-mindmap-container {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 1rem;
|
||||
position: relative;
|
||||
/* Verbesserte Einstellungskarten */
|
||||
.settings-card {
|
||||
background: rgba(32, 36, 55, 0.7);
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 2rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
margin-bottom: 1.5rem;
|
||||
backdrop-filter: blur(15px);
|
||||
-webkit-backdrop-filter: blur(15px);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.dark .personal-mindmap-container {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
.settings-card:hover {
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.2), 0 0 15px rgba(179, 143, 255, 0.15);
|
||||
}
|
||||
|
||||
.settings-card-header {
|
||||
padding: 1.25rem 1.5rem;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
}
|
||||
|
||||
.settings-card-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.settings-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.settings-label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
|
||||
.settings-input {
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
background: rgba(24, 28, 45, 0.6);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
font-size: 1rem;
|
||||
transition: all 0.3s ease;
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.settings-input:focus {
|
||||
border-color: rgba(179, 143, 255, 0.4);
|
||||
box-shadow: 0 0 0 3px rgba(179, 143, 255, 0.15);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Light Mode Anpassungen */
|
||||
html.light .profile-container,
|
||||
html.light .profile-tabs,
|
||||
html.light .activity-card,
|
||||
html.light .settings-card {
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
html.light .avatar-container {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border: 3px solid rgba(126, 63, 242, 0.3);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1), 0 0 15px rgba(126, 63, 242, 0.15);
|
||||
}
|
||||
|
||||
html.light .user-info h1 {
|
||||
background: linear-gradient(135deg, #7e3ff2, #3282f6);
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
html.light .user-bio,
|
||||
html.light .activity-content {
|
||||
color: #1a202c;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
html.light .user-meta span {
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
html.light .stat-item,
|
||||
html.light .settings-input {
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
html.light .stat-value {
|
||||
background: linear-gradient(135deg, #7e3ff2, #3282f6);
|
||||
}
|
||||
|
||||
html.light .stat-label {
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
html.light .profile-tab {
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
html.light .profile-tab:hover {
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
color: #1a202c;
|
||||
}
|
||||
|
||||
html.light .profile-tab.active {
|
||||
color: #7e3ff2;
|
||||
border-bottom: 3px solid #7e3ff2;
|
||||
background: rgba(126, 63, 242, 0.08);
|
||||
}
|
||||
|
||||
html.light .activity-title,
|
||||
html.light .settings-card-header,
|
||||
html.light .settings-label {
|
||||
color: #1a202c;
|
||||
}
|
||||
|
||||
html.light .activity-date {
|
||||
color: #718096;
|
||||
}
|
||||
|
||||
html.light .activity-footer {
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
html.light .reaction-button {
|
||||
color: #4a5568;
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
html.light .reaction-button:hover {
|
||||
background: rgba(126, 63, 242, 0.1);
|
||||
color: #7e3ff2;
|
||||
}
|
||||
|
||||
html.light .reaction-button.active {
|
||||
background: rgba(126, 63, 242, 0.15);
|
||||
color: #7e3ff2;
|
||||
}
|
||||
|
||||
html.light .action-button {
|
||||
background: rgba(126, 63, 242, 0.1);
|
||||
color: #7e3ff2;
|
||||
border: 1px solid rgba(126, 63, 242, 0.2);
|
||||
}
|
||||
|
||||
html.light .action-button:hover {
|
||||
background: rgba(126, 63, 242, 0.2);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
@@ -294,23 +488,7 @@
|
||||
<!-- User Info Section -->
|
||||
<div class="profile-header">
|
||||
<div class="avatar-container">
|
||||
{% if user.avatar %}
|
||||
<img src="{{ user.avatar }}" alt="Profilbild" class="avatar">
|
||||
{% else %}
|
||||
<div class="avatar default-avatar">
|
||||
<svg width="100%" height="100%" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="100" cy="100" r="98" fill="url(#gradient)" stroke="#7C3AED" stroke-width="4"/>
|
||||
<circle cx="100" cy="80" r="36" fill="white"/>
|
||||
<path d="M100 140C77.9086 140 60 157.909 60 180H140C140 157.909 122.091 140 100 140Z" fill="white"/>
|
||||
<defs>
|
||||
<linearGradient id="gradient" x1="0" y1="0" x2="200" y2="200" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#8B5CF6"/>
|
||||
<stop offset="1" stop-color="#3B82F6"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
{% endif %}
|
||||
<img src="{{ user.avatar if user.avatar else url_for('static', filename='img/default-avatar.png') }}" alt="Profilbild" class="avatar">
|
||||
<div class="avatar-edit">
|
||||
<i class="fas fa-camera"></i>
|
||||
</div>
|
||||
@@ -327,60 +505,50 @@
|
||||
</div>
|
||||
|
||||
<!-- Statistiken -->
|
||||
<div class="profile-stats grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4 mt-6">
|
||||
<div class="profile-stats">
|
||||
<!-- Gedanken -->
|
||||
<div class="stat-item bg-opacity-70 backdrop-blur-md rounded-xl p-4 flex flex-col items-center justify-center transition-all duration-300 hover:transform hover:scale-105">
|
||||
<div class="stat-icon w-12 h-12 rounded-full flex items-center justify-center bg-gradient-to-br from-purple-500/20 to-blue-500/20 mb-3">
|
||||
<i class="fas fa-lightbulb text-xl text-gradient-purple-blue"></i>
|
||||
<div class="stat-item">
|
||||
<div class="stat-icon">
|
||||
<i class="fas fa-lightbulb"></i>
|
||||
</div>
|
||||
<div class="stat-value text-2xl font-bold mb-1 bg-gradient-to-r from-purple-400 to-blue-400 bg-clip-text text-transparent">
|
||||
{{ stats.thought_count if stats and stats.thought_count else 0 }}
|
||||
</div>
|
||||
<div class="stat-label text-sm text-gray-400">Gedanken</div>
|
||||
<div class="stat-value">{{ stats.thought_count if stats and stats.thought_count else 0 }}</div>
|
||||
<div class="stat-label">Gedanken</div>
|
||||
</div>
|
||||
|
||||
<!-- Verbindungen -->
|
||||
<div class="stat-item bg-opacity-70 backdrop-blur-md rounded-xl p-4 flex flex-col items-center justify-center transition-all duration-300 hover:transform hover:scale-105">
|
||||
<div class="stat-icon w-12 h-12 rounded-full flex items-center justify-center bg-gradient-to-br from-blue-500/20 to-cyan-500/20 mb-3">
|
||||
<i class="fas fa-project-diagram text-xl text-gradient-blue-cyan"></i>
|
||||
<div class="stat-item">
|
||||
<div class="stat-icon">
|
||||
<i class="fas fa-project-diagram"></i>
|
||||
</div>
|
||||
<div class="stat-value text-2xl font-bold mb-1 bg-gradient-to-r from-blue-400 to-cyan-400 bg-clip-text text-transparent">
|
||||
{{ stats.connections_count if stats and stats.connections_count else 0 }}
|
||||
</div>
|
||||
<div class="stat-label text-sm text-gray-400">Verbindungen</div>
|
||||
<div class="stat-value">{{ stats.connections_count if stats and stats.connections_count else 0 }}</div>
|
||||
<div class="stat-label">Verbindungen</div>
|
||||
</div>
|
||||
|
||||
<!-- Follower -->
|
||||
<div class="stat-item bg-opacity-70 backdrop-blur-md rounded-xl p-4 flex flex-col items-center justify-center transition-all duration-300 hover:transform hover:scale-105">
|
||||
<div class="stat-icon w-12 h-12 rounded-full flex items-center justify-center bg-gradient-to-br from-cyan-500/20 to-teal-500/20 mb-3">
|
||||
<i class="fas fa-users text-xl text-gradient-cyan-teal"></i>
|
||||
<div class="stat-item">
|
||||
<div class="stat-icon">
|
||||
<i class="fas fa-users"></i>
|
||||
</div>
|
||||
<div class="stat-value text-2xl font-bold mb-1 bg-gradient-to-r from-cyan-400 to-teal-400 bg-clip-text text-transparent">
|
||||
{{ stats.followers_count if stats and stats.followers_count else 0 }}
|
||||
</div>
|
||||
<div class="stat-label text-sm text-gray-400">Follower</div>
|
||||
<div class="stat-value">{{ stats.followers_count if stats and stats.followers_count else 0 }}</div>
|
||||
<div class="stat-label">Follower</div>
|
||||
</div>
|
||||
|
||||
<!-- Beiträge -->
|
||||
<div class="stat-item bg-opacity-70 backdrop-blur-md rounded-xl p-4 flex flex-col items-center justify-center transition-all duration-300 hover:transform hover:scale-105">
|
||||
<div class="stat-icon w-12 h-12 rounded-full flex items-center justify-center bg-gradient-to-br from-teal-500/20 to-green-500/20 mb-3">
|
||||
<i class="fas fa-comment-dots text-xl text-gradient-teal-green"></i>
|
||||
<div class="stat-item">
|
||||
<div class="stat-icon">
|
||||
<i class="fas fa-comment-dots"></i>
|
||||
</div>
|
||||
<div class="stat-value text-2xl font-bold mb-1 bg-gradient-to-r from-teal-400 to-green-400 bg-clip-text text-transparent">
|
||||
{{ stats.contributions_count if stats and stats.contributions_count else 0 }}
|
||||
</div>
|
||||
<div class="stat-label text-sm text-gray-400">Beiträge</div>
|
||||
<div class="stat-value">{{ stats.contributions_count if stats and stats.contributions_count else 0 }}</div>
|
||||
<div class="stat-label">Beiträge</div>
|
||||
</div>
|
||||
|
||||
<!-- Bewertung -->
|
||||
<div class="stat-item bg-opacity-70 backdrop-blur-md rounded-xl p-4 flex flex-col items-center justify-center transition-all duration-300 hover:transform hover:scale-105">
|
||||
<div class="stat-icon w-12 h-12 rounded-full flex items-center justify-center bg-gradient-to-br from-green-500/20 to-yellow-500/20 mb-3">
|
||||
<i class="fas fa-star text-xl text-gradient-green-yellow"></i>
|
||||
<div class="stat-item">
|
||||
<div class="stat-icon">
|
||||
<i class="fas fa-star"></i>
|
||||
</div>
|
||||
<div class="stat-value text-2xl font-bold mb-1 bg-gradient-to-r from-green-400 to-yellow-400 bg-clip-text text-transparent">
|
||||
{{ stats.rating if stats and stats.rating else '0.0' }}
|
||||
</div>
|
||||
<div class="stat-label text-sm text-gray-400">Bewertung</div>
|
||||
<div class="stat-value">{{ stats.rating if stats and stats.rating else '0.0' }}</div>
|
||||
<div class="stat-label">Bewertung</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -391,7 +559,6 @@
|
||||
<div class="profile-tabs">
|
||||
<div class="profile-tab active" data-tab="activity">Aktivitäten</div>
|
||||
<div class="profile-tab" data-tab="thoughts">Gedanken</div>
|
||||
<div class="profile-tab" data-tab="mindmaps">Mindmaps</div>
|
||||
<div class="profile-tab" data-tab="collections">Sammlungen</div>
|
||||
<div class="profile-tab" data-tab="connections">Verbindungen</div>
|
||||
<div class="profile-tab" data-tab="settings">Einstellungen</div>
|
||||
@@ -445,87 +612,21 @@
|
||||
<div class="tab-content hidden" id="thoughts-tab">
|
||||
<div id="thoughts-container">
|
||||
{% if thoughts %}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{% for thought in thoughts %}
|
||||
<div class="thought-item bg-opacity-70 rounded-xl overflow-hidden border transition-all duration-300 hover:transform hover:scale-105 hover:shadow-lg"
|
||||
x-bind:class="darkMode ? 'bg-gray-800/80 border-gray-700/60' : 'bg-white/90 border-gray-200/60'">
|
||||
<div class="p-5 thought-border" data-color="{{ thought.color_code|default('#B39DDB') }}">
|
||||
<h3 class="text-xl font-bold mb-2"
|
||||
x-bind:class="darkMode ? 'text-purple-300' : 'text-purple-700'">{{ thought.title }}</h3>
|
||||
<p class="mb-4 text-sm"
|
||||
x-bind:class="darkMode ? 'text-gray-300' : 'text-gray-600'">
|
||||
{{ thought.abstract or thought.content[:150] ~ '...' }}
|
||||
</p>
|
||||
<div class="flex justify-between items-center text-xs"
|
||||
x-bind:class="darkMode ? 'text-gray-400' : 'text-gray-500'">
|
||||
<span>{{ thought.created_at.strftime('%d.%m.%Y') }}</span>
|
||||
<span>{{ thought.branch }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-3 border-t flex justify-between items-center"
|
||||
x-bind:class="darkMode ? 'bg-gray-900/80 border-gray-700/60' : 'bg-gray-50/80 border-gray-200/60'">
|
||||
<a href="{{ url_for('get_thought', thought_id=thought.id) }}" class="transition-colors"
|
||||
x-bind:class="darkMode ? 'text-purple-400 hover:text-purple-300' : 'text-purple-600 hover:text-purple-500'">
|
||||
<i class="fas fa-eye mr-1"></i> Ansehen
|
||||
</a>
|
||||
<a href="{{ url_for('update_thought', thought_id=thought.id) }}" class="transition-colors"
|
||||
x-bind:class="darkMode ? 'text-blue-400 hover:text-blue-300' : 'text-blue-600 hover:text-blue-500'">
|
||||
<i class="fas fa-edit mr-1"></i> Bearbeiten
|
||||
</a>
|
||||
<div class="thought-item">
|
||||
<h3>{{ thought.title }}</h3>
|
||||
<p>{{ thought.content }}</p>
|
||||
<div class="thought-meta">
|
||||
<span>{{ thought.date }}</span>
|
||||
<span>{{ thought.category }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-12">
|
||||
<i class="fas fa-lightbulb text-5xl text-gray-400 mb-4"></i>
|
||||
<p class="text-gray-500">Noch keine Gedanken erstellt</p>
|
||||
<a href="{{ url_for('add_thought') }}" class="mt-4 inline-block px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors">Ersten Gedanken erstellen</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mindmaps-Tab -->
|
||||
<div class="tab-content hidden" id="mindmaps-tab">
|
||||
<div id="mindmaps-container">
|
||||
{% if user_mindmaps %}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{% for mindmap in user_mindmaps %}
|
||||
<div class="mindmap-item bg-opacity-70 rounded-xl overflow-hidden border transition-all duration-300 hover:transform hover:scale-105 hover:shadow-lg"
|
||||
x-bind:class="darkMode ? 'bg-gray-800/80 border-gray-700/60' : 'bg-white/90 border-gray-200/60'">
|
||||
<div class="p-5">
|
||||
<h3 class="text-xl font-bold mb-2"
|
||||
x-bind:class="darkMode ? 'text-purple-400' : 'text-purple-700'">{{ mindmap.name }}</h3>
|
||||
<p class="mb-4 text-sm"
|
||||
x-bind:class="darkMode ? 'text-gray-300' : 'text-gray-600'">{{ mindmap.description }}</p>
|
||||
<div class="flex justify-between items-center text-xs"
|
||||
x-bind:class="darkMode ? 'text-gray-400' : 'text-gray-500'">
|
||||
<span>Erstellt: {{ mindmap.created_at.strftime('%d.%m.%Y') }}</span>
|
||||
<span>Zuletzt bearbeitet: {{ mindmap.last_modified.strftime('%d.%m.%Y') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-3 border-t flex justify-between"
|
||||
x-bind:class="darkMode ? 'bg-gray-900/80 border-gray-700/60' : 'bg-gray-50/80 border-gray-200/60'">
|
||||
<a href="{{ url_for('mindmap') }}?id={{ mindmap.id }}" class="transition-colors"
|
||||
x-bind:class="darkMode ? 'text-purple-400 hover:text-purple-300' : 'text-purple-600 hover:text-purple-500'">
|
||||
<i class="fas fa-eye mr-1"></i> Anzeigen
|
||||
</a>
|
||||
<a href="{{ url_for('edit_mindmap', mindmap_id=mindmap.id) }}" class="transition-colors"
|
||||
x-bind:class="darkMode ? 'text-blue-400 hover:text-blue-300' : 'text-blue-600 hover:text-blue-500'">
|
||||
<i class="fas fa-edit mr-1"></i> Bearbeiten
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-12">
|
||||
<i class="fas fa-project-diagram text-5xl text-gray-400 mb-4"></i>
|
||||
<p class="text-gray-500">Noch keine Mindmaps erstellt</p>
|
||||
<a href="{{ url_for('create_mindmap') }}" class="mt-4 inline-block px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors">
|
||||
Erste Mindmap erstellen
|
||||
</a>
|
||||
<a href="{{ url_for('get_thought') }}" class="mt-4 inline-block px-4 py-2 bg-purple-600 text-white rounded-lg">Ersten Gedanken erstellen</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -548,7 +649,7 @@
|
||||
<div class="text-center py-12">
|
||||
<i class="fas fa-folder-open text-5xl text-gray-400 mb-4"></i>
|
||||
<p class="text-gray-500">Noch keine Sammlungen erstellt</p>
|
||||
<a href="{{ url_for('profile') }}" class="mt-4 inline-block px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors">Zurück zum Profil</a>
|
||||
<a href="{{ url_for('create_collection') }}" class="mt-4 inline-block px-4 py-2 bg-purple-600 text-white rounded-lg">Erste Sammlung erstellen</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -582,7 +683,7 @@
|
||||
<div class="text-center py-12">
|
||||
<i class="fas fa-project-diagram text-5xl text-gray-400 mb-4"></i>
|
||||
<p class="text-gray-500">Noch keine Verbindungen erstellt</p>
|
||||
<a href="{{ url_for('mindmap') }}" class="mt-4 inline-block px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors">Verbindungen in der Mindmap erstellen</a>
|
||||
<a href="{{ url_for('mindmap') }}" class="mt-4 inline-block px-4 py-2 bg-purple-600 text-white rounded-lg">Verbindungen in der Mindmap erstellen</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -668,21 +769,7 @@
|
||||
|
||||
// Entsprechenden Tab-Inhalt anzeigen
|
||||
const tabId = this.getAttribute('data-tab');
|
||||
const tabContent = document.getElementById(`${tabId}-tab`);
|
||||
|
||||
if (tabContent) {
|
||||
tabContent.classList.remove('hidden');
|
||||
|
||||
// Animation für Tab-Inhalt
|
||||
tabContent.style.opacity = '0';
|
||||
tabContent.style.transform = 'translateY(10px)';
|
||||
|
||||
setTimeout(() => {
|
||||
tabContent.style.transition = 'all 0.3s ease';
|
||||
tabContent.style.opacity = '1';
|
||||
tabContent.style.transform = 'translateY(0)';
|
||||
}, 50);
|
||||
}
|
||||
document.getElementById(`${tabId}-tab`).classList.remove('hidden');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -708,280 +795,6 @@
|
||||
countElement.textContent = count;
|
||||
});
|
||||
});
|
||||
|
||||
// Profilbearbeitung
|
||||
const editProfileBtn = document.querySelector('.edit-profile-btn');
|
||||
|
||||
if (editProfileBtn) {
|
||||
editProfileBtn.addEventListener('click', function() {
|
||||
// Zum Einstellungstab wechseln
|
||||
const settingsTab = document.querySelector('[data-tab="settings"]');
|
||||
if (settingsTab) {
|
||||
settingsTab.click();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Avatar-Bearbeitung
|
||||
const avatarEditBtn = document.querySelector('.avatar-edit');
|
||||
if (avatarEditBtn) {
|
||||
avatarEditBtn.addEventListener('click', function() {
|
||||
// Dateiauwahl öffnen
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.accept = 'image/*';
|
||||
fileInput.style.display = 'none';
|
||||
document.body.appendChild(fileInput);
|
||||
|
||||
fileInput.click();
|
||||
|
||||
fileInput.addEventListener('change', function() {
|
||||
if (this.files && this.files[0]) {
|
||||
// Anzeigen des gewählten Bildes
|
||||
const avatarImg = document.querySelector('.avatar');
|
||||
|
||||
// FileReader zum Einlesen des Bildes
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
// Vorschau anzeigen
|
||||
avatarImg.src = e.target.result;
|
||||
|
||||
// Avatar-URL im Einstellungsbereich speichern
|
||||
const avatarUrlInput = document.createElement('input');
|
||||
avatarUrlInput.type = 'hidden';
|
||||
avatarUrlInput.name = 'avatar_url';
|
||||
avatarUrlInput.id = 'avatar_url';
|
||||
avatarUrlInput.value = e.target.result;
|
||||
|
||||
// Entferne vorhandenes Input, falls vorhanden
|
||||
const existingInput = document.getElementById('avatar_url');
|
||||
if (existingInput) {
|
||||
existingInput.remove();
|
||||
}
|
||||
|
||||
// Zum Formular hinzufügen
|
||||
const settingsForm = document.querySelector('.settings-card');
|
||||
if (settingsForm) {
|
||||
settingsForm.appendChild(avatarUrlInput);
|
||||
}
|
||||
|
||||
// Erfolgsmeldung anzeigen
|
||||
showNotification('Avatar wurde aktualisiert! Bitte speichere deine Änderungen.', 'success');
|
||||
};
|
||||
|
||||
reader.readAsDataURL(this.files[0]);
|
||||
}
|
||||
|
||||
// Input entfernen
|
||||
document.body.removeChild(fileInput);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Einstellungen-Formular-Handling
|
||||
const saveSettingsBtn = document.querySelectorAll('.settings-card .profile-action-btn.primary');
|
||||
|
||||
saveSettingsBtn.forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
const isPasswordUpdate = this.textContent.includes('Passwort');
|
||||
|
||||
// Passwort-Update
|
||||
if (isPasswordUpdate) {
|
||||
const currentPassword = document.getElementById('password').value;
|
||||
const newPassword = document.getElementById('password_confirm').value;
|
||||
|
||||
if (!currentPassword || !newPassword) {
|
||||
showNotification('Bitte fülle alle Passwortfelder aus', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// AJAX-Anfrage senden
|
||||
const formData = new FormData();
|
||||
formData.append('action', 'update_password');
|
||||
formData.append('current_password', currentPassword);
|
||||
formData.append('new_password', newPassword);
|
||||
formData.append('confirm_password', newPassword);
|
||||
|
||||
// Visuelle Rückmeldung
|
||||
const originalText = this.innerHTML;
|
||||
this.innerHTML = '<i class="fas fa-circle-notch fa-spin mr-1"></i> Speichern...';
|
||||
this.disabled = true;
|
||||
|
||||
fetch('{{ url_for("settings") }}', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.catch(error => {
|
||||
console.error('Fehler beim Aktualisieren des Passworts:', error);
|
||||
return { success: false, message: 'Netzwerkfehler. Bitte versuche es erneut.' };
|
||||
})
|
||||
.then(data => {
|
||||
this.innerHTML = originalText;
|
||||
this.disabled = false;
|
||||
|
||||
if (data && data.success) {
|
||||
showNotification('Passwort erfolgreich aktualisiert!', 'success');
|
||||
document.getElementById('password').value = '';
|
||||
document.getElementById('password_confirm').value = '';
|
||||
} else {
|
||||
showNotification(data?.message || 'Fehler beim Aktualisieren des Passworts', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
// Profil-Update
|
||||
else {
|
||||
// Sammle Daten aus den Eingabefeldern
|
||||
const formData = new FormData();
|
||||
formData.append('action', 'update_profile');
|
||||
formData.append('bio', document.getElementById('bio').value || '');
|
||||
formData.append('location', document.getElementById('location').value || '');
|
||||
formData.append('website', document.getElementById('website').value || '');
|
||||
|
||||
// Avatar hinzufügen, falls vorhanden
|
||||
const avatarUrlInput = document.getElementById('avatar_url');
|
||||
if (avatarUrlInput) {
|
||||
formData.append('avatar_url', avatarUrlInput.value);
|
||||
}
|
||||
|
||||
// Visuelle Rückmeldung
|
||||
const originalText = this.innerHTML;
|
||||
this.innerHTML = '<i class="fas fa-circle-notch fa-spin mr-1"></i> Speichern...';
|
||||
this.disabled = true;
|
||||
|
||||
// AJAX-Anfrage senden
|
||||
fetch('{{ url_for("settings") }}', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Fehler beim Speichern');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Fehler beim Speichern der Profileinstellungen:', error);
|
||||
return { success: false, message: 'Netzwerkfehler. Bitte versuche es erneut.' };
|
||||
})
|
||||
.then(data => {
|
||||
this.innerHTML = originalText;
|
||||
this.disabled = false;
|
||||
|
||||
if (data && data.success) {
|
||||
// Erfolgsanimation
|
||||
showNotification('Profil erfolgreich aktualisiert!', 'success');
|
||||
|
||||
// UI aktualisieren ohne Neuladen
|
||||
const bioElement = document.querySelector('.user-bio');
|
||||
const locationElement = document.querySelector('.user-meta span:first-child');
|
||||
|
||||
if (bioElement) {
|
||||
bioElement.textContent = document.getElementById('bio').value || 'Keine Bio vorhanden. Klicke auf bearbeiten, um eine hinzuzufügen.';
|
||||
}
|
||||
|
||||
if (locationElement) {
|
||||
const location = document.getElementById('location').value;
|
||||
locationElement.innerHTML = `<i class="fas fa-map-marker-alt"></i> ${location || 'Kein Standort angegeben'}`;
|
||||
}
|
||||
} else {
|
||||
showNotification(data?.message || 'Fehler beim Aktualisieren des Profils', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Gedanken-Karten mit Hover-Effekten und Border-Farben
|
||||
const thoughtItems = document.querySelectorAll('.thought-item');
|
||||
thoughtItems.forEach(item => {
|
||||
// Hover-Effekte
|
||||
item.addEventListener('mouseenter', () => {
|
||||
item.style.transform = 'translateY(-5px)';
|
||||
item.style.boxShadow = '0 12px 30px rgba(0, 0, 0, 0.15)';
|
||||
});
|
||||
|
||||
item.addEventListener('mouseleave', () => {
|
||||
item.style.transform = 'translateY(0)';
|
||||
item.style.boxShadow = 'none';
|
||||
});
|
||||
|
||||
// Border-Farben anwenden
|
||||
const borderElem = item.querySelector('.thought-border');
|
||||
if (borderElem && borderElem.dataset.color) {
|
||||
borderElem.style.borderLeftColor = borderElem.dataset.color;
|
||||
}
|
||||
});
|
||||
|
||||
// Mindmap-Karten mit Hover-Effekten
|
||||
const mindmapItems = document.querySelectorAll('.mindmap-item');
|
||||
mindmapItems.forEach(item => {
|
||||
item.addEventListener('mouseenter', () => {
|
||||
item.style.transform = 'translateY(-5px)';
|
||||
item.style.boxShadow = '0 12px 30px rgba(0, 0, 0, 0.15)';
|
||||
});
|
||||
|
||||
item.addEventListener('mouseleave', () => {
|
||||
item.style.transform = 'translateY(0)';
|
||||
item.style.boxShadow = 'none';
|
||||
});
|
||||
});
|
||||
|
||||
// Benachrichtigungsfunktion
|
||||
function showNotification(message, type = 'info') {
|
||||
// Bestehende Benachrichtigung entfernen
|
||||
const existingNotification = document.getElementById('notification');
|
||||
if (existingNotification) {
|
||||
existingNotification.remove();
|
||||
}
|
||||
|
||||
// Neue Benachrichtigung erstellen
|
||||
const notification = document.createElement('div');
|
||||
notification.id = 'notification';
|
||||
notification.className = `fixed top-4 right-4 p-4 rounded-lg shadow-lg z-50 flex items-center transform transition-all duration-300 translate-y-0 opacity-0`;
|
||||
|
||||
// Typ-basierte Stile
|
||||
if (type === 'success') {
|
||||
notification.classList.add('bg-green-500', 'text-white');
|
||||
notification.innerHTML = `<i class="fas fa-check-circle mr-2"></i> ${message}`;
|
||||
} else if (type === 'error') {
|
||||
notification.classList.add('bg-red-500', 'text-white');
|
||||
notification.innerHTML = `<i class="fas fa-exclamation-circle mr-2"></i> ${message}`;
|
||||
} else {
|
||||
notification.classList.add('bg-blue-500', 'text-white');
|
||||
notification.innerHTML = `<i class="fas fa-info-circle mr-2"></i> ${message}`;
|
||||
}
|
||||
|
||||
// Close-Button
|
||||
const closeBtn = document.createElement('button');
|
||||
closeBtn.className = 'ml-4 text-white opacity-75 hover:opacity-100';
|
||||
closeBtn.innerHTML = '<i class="fas fa-times"></i>';
|
||||
closeBtn.addEventListener('click', () => {
|
||||
notification.classList.add('opacity-0', 'translate-y-[-10px]');
|
||||
setTimeout(() => notification.remove(), 300);
|
||||
});
|
||||
|
||||
notification.appendChild(closeBtn);
|
||||
document.body.appendChild(notification);
|
||||
|
||||
// Animation starten
|
||||
setTimeout(() => {
|
||||
notification.classList.remove('opacity-0');
|
||||
notification.classList.add('opacity-100');
|
||||
}, 10);
|
||||
|
||||
// Automatisch ausblenden nach 5 Sekunden
|
||||
setTimeout(() => {
|
||||
if (document.body.contains(notification)) {
|
||||
notification.classList.add('opacity-0', 'translate-y-[-10px]');
|
||||
setTimeout(() => {
|
||||
if (document.body.contains(notification)) {
|
||||
notification.remove();
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,75 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Einfaches Profil{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4 py-10">
|
||||
<div class="bg-gray-800 bg-opacity-70 rounded-lg p-6 mb-6">
|
||||
<h1 class="text-3xl font-bold text-purple-400 mb-4">Hallo, {{ user.username }}</h1>
|
||||
<div class="text-gray-300 mb-4">
|
||||
<p>E-Mail: {{ user.email }}</p>
|
||||
<p>Mitglied seit: {{ user.created_at.strftime('%d.%m.%Y') }}</p>
|
||||
</div>
|
||||
|
||||
<h2 class="text-xl font-semibold text-purple-300 mt-6 mb-3">Deine Mindmaps</h2>
|
||||
{% if user_mindmaps %}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{% for mindmap in user_mindmaps %}
|
||||
<div class="bg-gray-700 bg-opacity-50 p-4 rounded-lg">
|
||||
<h3 class="text-lg font-medium text-purple-400 mb-2">{{ mindmap.name }}</h3>
|
||||
<p class="text-gray-300 text-sm mb-3">{{ mindmap.description }}</p>
|
||||
<div class="flex justify-between text-xs text-gray-400">
|
||||
<span>Erstellt: {{ mindmap.created_at.strftime('%d.%m.%Y') }}</span>
|
||||
</div>
|
||||
<div class="mt-4 flex justify-between">
|
||||
<a href="{{ url_for('mindmap') }}?id={{ mindmap.id }}" class="text-purple-400 hover:text-purple-300">
|
||||
<i class="fas fa-eye mr-1"></i> Anzeigen
|
||||
</a>
|
||||
<a href="{{ url_for('edit_mindmap', mindmap_id=mindmap.id) }}" class="text-blue-400 hover:text-blue-300">
|
||||
<i class="fas fa-edit mr-1"></i> Bearbeiten
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-6">
|
||||
<p class="text-gray-400">Du hast noch keine Mindmaps erstellt</p>
|
||||
<a href="{{ url_for('create_mindmap') }}" class="mt-3 inline-block px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700">
|
||||
Erste Mindmap erstellen
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<h2 class="text-xl font-semibold text-purple-300 mt-8 mb-3">Deine Gedanken</h2>
|
||||
{% if thoughts %}
|
||||
<div class="space-y-4">
|
||||
{% for thought in thoughts %}
|
||||
<div class="bg-gray-700 bg-opacity-50 p-4 rounded-lg">
|
||||
<h3 class="text-lg font-medium text-purple-400 mb-2">{{ thought.title }}</h3>
|
||||
<p class="text-gray-300 text-sm mb-2">
|
||||
{{ thought.abstract[:150] ~ '...' if thought.abstract and thought.abstract|length > 150 else thought.abstract }}
|
||||
</p>
|
||||
<div class="flex justify-between text-xs text-gray-400">
|
||||
<span>Erstellt: {{ thought.created_at.strftime('%d.%m.%Y') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-6">
|
||||
<p class="text-gray-400">Du hast noch keine Gedanken erstellt</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="mt-8 flex justify-between">
|
||||
<a href="{{ url_for('index') }}" class="px-4 py-2 bg-gray-700 text-white rounded-lg hover:bg-gray-600">
|
||||
Zurück zur Startseite
|
||||
</a>
|
||||
<a href="{{ url_for('logout') }}" class="px-4 py-2 bg-red-700 text-white rounded-lg hover:bg-red-600">
|
||||
Abmelden
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,391 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ mindmap.name }} - Mindmap{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
/* Container für die Mindmap mit verbesserten Glaseffekten */
|
||||
#cy {
|
||||
width: 100%;
|
||||
height: calc(100vh - 14rem);
|
||||
background-color: rgba(15, 23, 42, 0.3);
|
||||
border-radius: 1rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body:not(.dark) #cy {
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
/* Info-Panel */
|
||||
.node-info-panel {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
width: 300px;
|
||||
max-height: calc(100vh - 16rem);
|
||||
overflow-y: auto;
|
||||
border-radius: 1rem;
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
pointer-events: none;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.node-info-panel.visible {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
body.dark .node-info-panel {
|
||||
background-color: rgba(15, 23, 42, 0.8);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
body:not(.dark) .node-info-panel {
|
||||
background-color: rgba(255, 255, 255, 0.85);
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
/* Toolbar */
|
||||
.mindmap-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
body.dark .mindmap-toolbar {
|
||||
background-color: rgba(15, 23, 42, 0.6);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
body:not(.dark) .mindmap-toolbar {
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.mindmap-action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
body.dark .mindmap-action-btn {
|
||||
background-color: rgba(30, 41, 59, 0.7);
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
body:not(.dark) .mindmap-action-btn {
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
color: #1e293b;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
body.dark .mindmap-action-btn:hover {
|
||||
background-color: rgba(51, 65, 85, 0.8);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
body:not(.dark) .mindmap-action-btn:hover {
|
||||
background-color: rgba(243, 244, 246, 0.9);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Node-Link Styles */
|
||||
.node-link {
|
||||
display: inline-block;
|
||||
padding: 0.35rem 0.75rem;
|
||||
margin: 0.25rem;
|
||||
border-radius: 1rem;
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
white-space: nowrap;
|
||||
color: white;
|
||||
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.node-link:hover {
|
||||
transform: translateY(-2px);
|
||||
filter: brightness(1.1);
|
||||
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
/* Notes */
|
||||
.note-container {
|
||||
position: absolute;
|
||||
width: 250px;
|
||||
cursor: move;
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
transition: all 0.3s ease;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.note-container.visible {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
/* Note-Werkzeuge */
|
||||
.note-tools {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.note-tool {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 0.7rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.note-tool:hover {
|
||||
opacity: 1;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Color Palette */
|
||||
.color-palette {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
flex-wrap: wrap;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.color-swatch {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.color-swatch:hover {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
/* Notizen-Editor */
|
||||
.note-content {
|
||||
min-height: 100px;
|
||||
padding: 0.75rem;
|
||||
outline: none;
|
||||
width: 100%;
|
||||
font-size: 0.9rem;
|
||||
resize: both;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* Kontext-Menü */
|
||||
#context-menu {
|
||||
min-width: 180px;
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
body.dark #context-menu {
|
||||
background-color: rgba(15, 23, 42, 0.95);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
body:not(.dark) #context-menu {
|
||||
background-color: rgba(255, 255, 255, 0.95);
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.1);
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
padding: 0.75rem 1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
body.dark .menu-item:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
body:not(.dark) .menu-item:hover {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* Dialogfenster */
|
||||
.dialog-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.dialog-content {
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
border-radius: 1rem;
|
||||
padding: 1.5rem;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
body.dark .dialog-content {
|
||||
background-color: rgba(15, 23, 42, 0.95);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
body:not(.dark) .dialog-content {
|
||||
background-color: rgba(255, 255, 255, 0.95);
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
color: #1e293b;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto max-w-7xl px-4 py-6">
|
||||
<!-- Header -->
|
||||
<div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-6 gap-4">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold gradient-text">{{ mindmap.name }}</h1>
|
||||
<p class="opacity-80 mt-1">{{ mindmap.description }}</p>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3 flex-wrap">
|
||||
{% if mindmap.user_id == current_user.id %}
|
||||
<a href="{{ url_for('edit_mindmap', mindmap_id=mindmap.id) }}" class="mindmap-action-btn">
|
||||
<i class="fas fa-edit"></i>
|
||||
<span>Bearbeiten</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<a href="{{ url_for('profile') }}" class="mindmap-action-btn">
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
<span>Zurück</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toolbar -->
|
||||
<div class="mindmap-toolbar mb-4">
|
||||
<button id="fit-btn" class="mindmap-action-btn">
|
||||
<i class="fas fa-expand"></i>
|
||||
<span>Alles anzeigen</span>
|
||||
</button>
|
||||
<button id="reset-btn" class="mindmap-action-btn">
|
||||
<i class="fas fa-redo"></i>
|
||||
<span>Layout zurücksetzen</span>
|
||||
</button>
|
||||
<button id="toggle-labels-btn" class="mindmap-action-btn">
|
||||
<i class="fas fa-tags"></i>
|
||||
<span>Labels ein/aus</span>
|
||||
</button>
|
||||
<button id="add-note-btn" class="mindmap-action-btn">
|
||||
<i class="fas fa-sticky-note"></i>
|
||||
<span>Notiz hinzufügen</span>
|
||||
</button>
|
||||
<button id="save-layout-btn" class="mindmap-action-btn">
|
||||
<i class="fas fa-save"></i>
|
||||
<span>Layout speichern</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Mindmap Container mit Positionsindikator -->
|
||||
<div class="relative rounded-xl overflow-hidden border transition-all duration-300"
|
||||
x-bind:class="darkMode ? 'border-gray-700/50' : 'border-gray-300/50'">
|
||||
<div id="cy"></div>
|
||||
|
||||
<!-- Informationsanzeige für ausgewählten Knoten -->
|
||||
<div id="node-info-panel" class="node-info-panel p-4">
|
||||
<h3 class="text-xl font-bold gradient-text mb-2">Knotendetails</h3>
|
||||
<p id="node-description" class="mb-4 opacity-80"></p>
|
||||
|
||||
<h4 class="font-semibold mb-2 opacity-90">Verbundene Knoten</h4>
|
||||
<div id="connected-nodes" class="flex flex-wrap mb-4"></div>
|
||||
|
||||
<div class="flex flex-wrap gap-2 mt-4">
|
||||
<button id="add-thought-btn" class="mindmap-action-btn">
|
||||
<i class="fas fa-lightbulb"></i>
|
||||
<span>Gedanke hinzufügen</span>
|
||||
</button>
|
||||
<button id="view-thoughts-btn" class="mindmap-action-btn">
|
||||
<i class="fas fa-list"></i>
|
||||
<span>Gedanken anzeigen</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script src="{{ url_for('static', filename='js/cytoscape.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/mindmap-init.js') }}"></script>
|
||||
<script nonce="{{ csp_nonce }}">
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Benutzer-Mindmap-ID für die API-Anfragen
|
||||
const mindmapId = {{ mindmap.id }};
|
||||
|
||||
// Erstellt eine neue MindMap-Instanz für die Benutzer-Mindmap
|
||||
window.userMindmap = new MindMap('#cy', {
|
||||
editable: true,
|
||||
isUserLoggedIn: true,
|
||||
isPublicMap: false,
|
||||
userMindmapId: mindmapId,
|
||||
fitViewOnInit: true,
|
||||
callbacks: {
|
||||
onLoad: function() {
|
||||
console.log('Benutzerdefinierte Mindmap wurde geladen');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Event-Listener für Notiz-Button
|
||||
document.getElementById('add-note-btn').addEventListener('click', function() {
|
||||
// Erstellt eine neue Notiz in der Mitte des Viewports
|
||||
const position = window.userMindmap.cy.pan();
|
||||
|
||||
window.userMindmap.showAddNoteDialog({
|
||||
x: position.x,
|
||||
y: position.y
|
||||
});
|
||||
});
|
||||
|
||||
// Event-Listener für Layout-Speichern-Button
|
||||
document.getElementById('save-layout-btn').addEventListener('click', function() {
|
||||
window.userMindmap.saveLayout();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
65
update_db.py
65
update_db.py
@@ -1,65 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
import sqlite3
|
||||
import sys
|
||||
|
||||
# Bestimme den absoluten Pfad zur Datenbank
|
||||
basedir = os.path.abspath(os.path.dirname(__file__))
|
||||
db_path = os.path.join(basedir, 'database', 'systades.db')
|
||||
|
||||
def update_user_table():
|
||||
"""Aktualisiert die User-Tabelle mit den fehlenden Spalten"""
|
||||
|
||||
# Überprüfe, ob die Datenbankdatei existiert
|
||||
if not os.path.exists(db_path):
|
||||
print(f"Datenbank nicht gefunden unter: {db_path}")
|
||||
return False
|
||||
|
||||
# Verbindung zur Datenbank herstellen
|
||||
try:
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Überprüfe, ob die neuen Spalten bereits existieren
|
||||
cursor.execute("PRAGMA table_info(user)")
|
||||
columns = [info[1] for info in cursor.fetchall()]
|
||||
|
||||
# Neue Spalten, die hinzugefügt werden müssen
|
||||
new_columns = {
|
||||
'bio': 'TEXT',
|
||||
'location': 'VARCHAR(100)',
|
||||
'website': 'VARCHAR(200)',
|
||||
'avatar': 'VARCHAR(200)',
|
||||
'last_login': 'DATETIME'
|
||||
}
|
||||
|
||||
# Spalten hinzufügen, die noch nicht existieren
|
||||
for col_name, col_type in new_columns.items():
|
||||
if col_name not in columns:
|
||||
print(f"Füge Spalte '{col_name}' zur User-Tabelle hinzu...")
|
||||
cursor.execute(f"ALTER TABLE user ADD COLUMN {col_name} {col_type}")
|
||||
|
||||
# Änderungen speichern
|
||||
conn.commit()
|
||||
print("User-Tabelle erfolgreich aktualisiert!")
|
||||
return True
|
||||
|
||||
except sqlite3.Error as e:
|
||||
print(f"Fehler bei der Datenbankaktualisierung: {e}")
|
||||
return False
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Führe die Aktualisierung durch
|
||||
success = update_user_table()
|
||||
|
||||
if success:
|
||||
print("Die Datenbank wurde erfolgreich aktualisiert.")
|
||||
sys.exit(0)
|
||||
else:
|
||||
print("Es gab ein Problem bei der Datenbankaktualisierung.")
|
||||
sys.exit(1)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -55,7 +55,7 @@ def create_user(username, email, password, is_admin=False):
|
||||
user = User(
|
||||
username=username,
|
||||
email=email,
|
||||
role='admin' if is_admin else 'user',
|
||||
is_admin=is_admin,
|
||||
created_at=datetime.utcnow()
|
||||
)
|
||||
user.set_password(password)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
home = C:\Program Files\Python313
|
||||
include-system-site-packages = false
|
||||
version = 3.13.3
|
||||
executable = C:\Users\firem\Desktop\111\Systades\website\.venv\Scripts\python.exe
|
||||
command = C:\Users\firem\Desktop\111\Systades\website\.venv\Scripts\python.exe -m venv C:\Users\firem\Desktop\111\Systades\website\venv
|
||||
executable = C:\Program Files\Python313\python.exe
|
||||
command = C:\Program Files\Python313\python.exe -m venv C:\Users\TTOMCZA.EMEA\Dev\website\venv
|
||||
|
||||
58
windows_setup.bat
Normal file
58
windows_setup.bat
Normal file
@@ -0,0 +1,58 @@
|
||||
@echo off
|
||||
echo Mindmap Projekt - Windows Setup
|
||||
echo ==============================
|
||||
echo.
|
||||
|
||||
REM Prüfen, ob Python installiert ist
|
||||
python --version >nul 2>&1
|
||||
if %errorlevel% neq 0 (
|
||||
echo Python ist nicht installiert oder nicht im PATH.
|
||||
echo Bitte installiere Python 3.11 von https://www.python.org/downloads/
|
||||
echo und stelle sicher, dass "Add Python to PATH" während der Installation aktiviert ist.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo Erstelle virtuelle Umgebung...
|
||||
python -m venv venv
|
||||
if %errorlevel% neq 0 (
|
||||
echo Fehler beim Erstellen der virtuellen Umgebung.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo Aktiviere virtuelle Umgebung...
|
||||
call venv\Scripts\activate.bat
|
||||
if %errorlevel% neq 0 (
|
||||
echo Fehler beim Aktivieren der virtuellen Umgebung.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo Aktualisiere pip...
|
||||
python -m pip install --upgrade pip
|
||||
if %errorlevel% neq 0 (
|
||||
echo Warnung: Pip konnte nicht aktualisiert werden. Fahre trotzdem fort.
|
||||
)
|
||||
|
||||
echo Installiere Abhängigkeiten...
|
||||
pip install -r requirements.txt
|
||||
if %errorlevel% neq 0 (
|
||||
echo Fehler beim Installieren der Abhängigkeiten.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo.
|
||||
echo Setup abgeschlossen!
|
||||
echo.
|
||||
echo Zum Starten des Servers:
|
||||
echo 1. Führe "venv\Scripts\activate.bat" aus
|
||||
echo 2. Führe "python TOOLS.py db:rebuild" aus (Nur beim ersten Mal oder zum Zurücksetzen der Datenbank)
|
||||
echo 3. Führe "python TOOLS.py user:admin" aus (Erstellt einen Admin-Benutzer: admin/admin)
|
||||
echo 4. Führe "python TOOLS.py server:run" aus
|
||||
echo.
|
||||
echo Die Anwendung ist dann unter http://localhost:5000 erreichbar.
|
||||
echo.
|
||||
|
||||
pause
|
||||
Reference in New Issue
Block a user