Compare commits
230 Commits
tills-bran
...
37c457ca3f
| Author | SHA1 | Date | |
|---|---|---|---|
| 37c457ca3f | |||
| 936d983cb3 | |||
| 9ed9adfeaf | |||
| 9c1475844c | |||
| 310d0af0d1 | |||
| cab8d28aeb | |||
| 9dc44f94f6 | |||
| 5b9ae85453 | |||
| 302d5213ef | |||
| bb3211ab3d | |||
| 2a246ee063 | |||
| fc8861c73c | |||
| 8c49e7396e | |||
| 8e3c81fd06 | |||
| f18d23cfea | |||
| d3405a7031 | |||
| 5793902e47 | |||
| e73ccd7e80 | |||
| e6784b712d | |||
| 35b5f321d4 | |||
| b68f65cc76 | |||
| 3a2f721f63 | |||
| 5933195196 | |||
| beccfa25a6 | |||
| bc5cef3ba8 | |||
| b867af9c8b | |||
| ee04432a49 | |||
| bbcee7f610 | |||
| 1eb47fc230 | |||
| 2921c5a824 | |||
| c98e238841 | |||
| af30a208ca | |||
| 2e2f35ccc1 | |||
| fd293e53e1 | |||
| 2b19cb000b | |||
| 3aefe6c5e6 | |||
| c7b87dc643 | |||
| dc96252013 | |||
| ab56f44ae9 | |||
| 61124f5266 | |||
| fab8d10f03 | |||
| dec30e4681 | |||
| a1bd999c6a | |||
| b1d33ce643 | |||
| 293f877017 | |||
| e86d0b0f90 | |||
| 059fd167d6 | |||
| 256d38e140 | |||
| 4b75489631 | |||
| cb95c78276 | |||
| 00cb100467 | |||
| 8c66461dc8 | |||
| 566f84fc0c | |||
| 07eae42ba3 | |||
| 0a1bebd862 | |||
| 59b79b3466 | |||
| 6f5526b648 | |||
| 21148f0c0e | |||
| ba6cac32a9 | |||
| be767e9f27 | |||
| 6aaf073ffb | |||
| b6080f96cf | |||
| 9ebf4b7abd | |||
| 5d35983f15 | |||
| 7278ece2b8 | |||
| f677e98795 | |||
| 40c3f6d9b4 | |||
| 9939db731b | |||
| d0f32a8355 | |||
| 02d1801fc9 | |||
| c51a8e23ca | |||
| 1600647bc4 | |||
| 82d03f6c48 | |||
| d1352286b7 | |||
| e7b3374c53 | |||
| 4bf046c657 | |||
| 892a1212d9 | |||
| 8440b7c30d | |||
| 74c2783b1a | |||
| fcd82eb5c9 | |||
| c654986f65 | |||
| f4ab617c59 | |||
| 9c36179f29 | |||
| f292cf1ce5 | |||
| 3a20ea0282 | |||
| 44986bfa23 | |||
| 41195a44cb | |||
| e1cd23230d | |||
| 77095e91b6 | |||
| 6322e046c5 | |||
| 2584bae149 | |||
| c0bd7a3986 | |||
| dec4a57b89 | |||
| 6a3b3a81c1 | |||
| 629813c486 | |||
| 7cb2bf1ed0 | |||
| ed1d41d316 | |||
| fe3cf81bc7 | |||
| 2e68ae30b8 | |||
| 858fdf5c44 | |||
| 4948f3ad2a | |||
| 52954e51f1 | |||
| 14f1356551 | |||
| 44c7183e97 | |||
| d99cae4956 | |||
| 3ae5f2527c | |||
| 412dabd5c1 | |||
| 5ade301f80 | |||
| 118f8ed132 | |||
| 121f46df01 | |||
| 4b0613eb6b | |||
| dd172d8596 | |||
| 653b3abe91 | |||
| ec50886145 | |||
| c888dcc452 | |||
| acceec4352 | |||
| f093a6211c | |||
| 58a5ea00bd | |||
| aeb829e36a | |||
| 49e5e19b7c | |||
| 903e095b66 | |||
| 2d083f5c0a | |||
| cbe8dc3bd0 | |||
| 7c1533c20d | |||
| c285b7d8dc | |||
| 21ddd38e13 | |||
| 1cf7bfbf76 | |||
| 40b28134fc | |||
| d5fababd49 | |||
| 7c742debdf | |||
| 4a4271a23c | |||
| c1038b479f | |||
| cd0083544a | |||
| a03bec2dff | |||
| 997479581d | |||
| 8153390e35 | |||
| bfa155628e | |||
| 700a8a3b89 | |||
| 808481ffe7 | |||
| e2c8cfaacf | |||
| 78e37fa717 | |||
| b2cf50626a | |||
| 7f48526315 | |||
| 84f8a6bf31 | |||
| 7003c89447 | |||
| d0821db983 | |||
| f0c4c514c4 | |||
| 304a399b85 | |||
| a5396c0d6e | |||
| 9cc4e70cba | |||
| a8cac08d30 | |||
| 42a7485ce1 | |||
| 54a5ccc224 | |||
| a99f82d4cf | |||
| 699127f41f | |||
| e8d356a27a | |||
| daf2704253 | |||
| 084059449f | |||
| c9bbc6ff25 | |||
| 742e3fda20 | |||
| 54aa246b79 | |||
| 505fb9aa47 | |||
| e4e6541b8c | |||
| e724181915 | |||
| 460c3f987e | |||
| 7f33dea278 | |||
| 726d9c9c70 | |||
| 81170fbd3d | |||
| eff3fda1ca | |||
| d49b266d96 | |||
| 34a08c4a6a | |||
| 7918de1723 | |||
| a0e4cd2208 | |||
| 2199d6007c | |||
| 7fb9452d09 | |||
| 1f3e60efde | |||
| 5e97381c8f | |||
| 4c402423c0 | |||
| 6d2595e3a6 | |||
| 29b44e5c52 | |||
| 693e542d5f | |||
| 4c3e476338 | |||
| 613c38ccb2 | |||
| 91fdd43fe0 | |||
| f36dd5ffaa | |||
| 2e1c3ce8b0 | |||
| d80c4c9aec | |||
| 3b0bea959c | |||
| cb3bfe0e6a | |||
| fd63810845 | |||
| 883973fe7b | |||
| 027e632856 | |||
| 406289e54f | |||
| 71b33e6cec | |||
| c74d3164bb | |||
| 4982cddeef | |||
| 631619ccb4 | |||
| f9881b678d | |||
| 259ce3cf69 | |||
| 9f4743eaea | |||
| de0f837cfd | |||
| 1c49ddfb19 | |||
| 46c16e5f01 | |||
| 84667bca00 | |||
| 779449559d | |||
| 721a10e861 | |||
| a431873ca2 | |||
| e4ab1e1bb5 | |||
| f69356473b | |||
| 38ac13e87c | |||
| 0afb8cb6e2 | |||
| 5d282d2108 | |||
| 4aba72efa2 | |||
| 89476d5353 | |||
| 0f7a33340a | |||
| 73501e7cda | |||
| 9f8eba6736 | |||
| b6bf9f387d | |||
| d9fe1f8efc | |||
| fd7bc59851 | |||
| 55f1f87509 | |||
| 03f8761312 | |||
| 506748fda7 | |||
| 6d069f68cd | |||
| 4310239a7a | |||
| e9fe907af0 | |||
| 0c69d9aba3 | |||
| 6da85cdece | |||
| a073b09115 | |||
| f1f4870989 |
17
.env
17
.env
@@ -1,2 +1,15 @@
|
|||||||
SECRET_KEY=eed9298856dc9363cd32778265780d6904ba24e6a6b815a2cc382bcdd767ea7b
|
# MindMap Umgebungsvariablen
|
||||||
OPENAI_API_KEY=sk-dein-openai-api-schluessel-hier
|
# 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
|
||||||
|
|
||||||
|
# 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
|
||||||
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
logs/app.log
|
||||||
33
Dockerfile
Normal file
33
Dockerfile
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# 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.
Binary file not shown.
Binary file not shown.
2526
app.py.bak
Normal file
2526
app.py.bak
Normal file
File diff suppressed because it is too large
Load Diff
BIN
backup/archiv_0.1.zip
Normal file
BIN
backup/archiv_0.1.zip
Normal file
Binary file not shown.
Binary file not shown.
17
docker-compose.yml
Normal file
17
docker-compose.yml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
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,14 @@
|
|||||||
# Kopiere diese Datei zu .env und passe die Werte an
|
# Kopiere diese Datei zu .env und passe die Werte an
|
||||||
|
|
||||||
# Flask
|
# Flask
|
||||||
SECRET_KEY=dein-geheimer-schluessel-hier
|
FLASK_APP=app.py
|
||||||
|
FLASK_ENV=development
|
||||||
|
SECRET_KEY=mein-sicherer-schluessel-fuer-entwicklung
|
||||||
|
|
||||||
# OpenAI API
|
# OpenAI API
|
||||||
OPENAI_API_KEY=sk-dein-openai-api-schluessel-hier
|
OPENAI_API_KEY=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
|
||||||
|
|
||||||
# Datenbank
|
# Datenbank
|
||||||
# Bei Bedarf kann hier eine andere Datenbank-URL angegeben werden
|
# Bei Bedarf kann hier eine andere Datenbank-URL angegeben werden
|
||||||
# Der Pfad wird relativ zum Projektverzeichnis angegeben
|
# Der Pfad wird relativ zum Projektverzeichnis angegeben
|
||||||
# SQLALCHEMY_DATABASE_URI=sqlite:////absoluter/pfad/zu/database/systades.db
|
SQLALCHEMY_DATABASE_URI=sqlite:///database/systades.db
|
||||||
130
init_db.py
130
init_db.py
@@ -1,19 +1,29 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from app import app, initialize_database, db_path
|
|
||||||
from models import db, User, Thought, Comment, MindMapNode, ThoughtRelation, ThoughtRating, RelationType
|
|
||||||
from models import Category, UserMindmap, UserMindmapNode, MindmapNote
|
|
||||||
import os
|
import os
|
||||||
import sqlite3
|
import sqlite3
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Pfad zur Datenbank
|
||||||
|
basedir = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
db_path = os.path.join(basedir, 'database', 'systades.db')
|
||||||
|
|
||||||
|
# Stelle sicher, dass das Verzeichnis existiert
|
||||||
|
db_dir = os.path.dirname(db_path)
|
||||||
|
os.makedirs(db_dir, exist_ok=True)
|
||||||
|
|
||||||
# Erstelle eine temporäre Flask-App, um die Datenbank zu initialisieren
|
# Erstelle eine temporäre Flask-App, um die Datenbank zu initialisieren
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///systades.db'
|
app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{db_path}'
|
||||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||||
|
|
||||||
|
# Importiere die Modelle nach der App-Initialisierung
|
||||||
|
from models import db, User, Thought, Comment, MindMapNode, ThoughtRelation, ThoughtRating, RelationType
|
||||||
|
from models import Category, UserMindmap, UserMindmapNode, MindmapNote
|
||||||
|
|
||||||
db.init_app(app)
|
db.init_app(app)
|
||||||
|
|
||||||
def init_db():
|
def init_db():
|
||||||
@@ -69,45 +79,111 @@ def create_default_users():
|
|||||||
|
|
||||||
def create_default_categories():
|
def create_default_categories():
|
||||||
"""Erstellt die Standardkategorien für die Mindmap"""
|
"""Erstellt die Standardkategorien für die Mindmap"""
|
||||||
categories = [
|
# Hauptkategorien
|
||||||
|
main_categories = [
|
||||||
{
|
{
|
||||||
'name': 'Konzept',
|
"name": "Philosophie",
|
||||||
'description': 'Abstrakte Ideen und theoretische Konzepte',
|
"description": "Philosophisches Denken und Konzepte",
|
||||||
'color_code': '#6366f1',
|
"color_code": "#9F7AEA",
|
||||||
'icon': 'lightbulb'
|
"icon": "fa-brain"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Technologie',
|
"name": "Wissenschaft",
|
||||||
'description': 'Hardware, Software, Tools und Plattformen',
|
"description": "Wissenschaftliche Disziplinen und Erkenntnisse",
|
||||||
'color_code': '#10b981',
|
"color_code": "#60A5FA",
|
||||||
'icon': 'cpu'
|
"icon": "fa-flask"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Prozess',
|
"name": "Technologie",
|
||||||
'description': 'Workflows, Methodologien und Vorgehensweisen',
|
"description": "Technologische Entwicklungen und Anwendungen",
|
||||||
'color_code': '#f59e0b',
|
"color_code": "#10B981",
|
||||||
'icon': 'git-branch'
|
"icon": "fa-microchip"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Person',
|
"name": "Künste",
|
||||||
'description': 'Personen, Teams und Organisationen',
|
"description": "Künstlerische Ausdrucksformen und Werke",
|
||||||
'color_code': '#ec4899',
|
"color_code": "#F59E0B",
|
||||||
'icon': 'user'
|
"icon": "fa-palette"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Dokument',
|
"name": "Psychologie",
|
||||||
'description': 'Dokumentationen, Referenzen und Ressourcen',
|
"description": "Mentale Prozesse und Verhaltensweisen",
|
||||||
'color_code': '#3b82f6',
|
"color_code": "#EF4444",
|
||||||
'icon': 'file-text'
|
"icon": "fa-brain"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
for cat_data in categories:
|
# Hauptkategorien erstellen
|
||||||
|
category_map = {}
|
||||||
|
for cat_data in main_categories:
|
||||||
category = Category(**cat_data)
|
category = Category(**cat_data)
|
||||||
db.session.add(category)
|
db.session.add(category)
|
||||||
|
db.session.flush() # ID generieren
|
||||||
|
category_map[cat_data["name"]] = category
|
||||||
|
|
||||||
|
# Unterkategorien für Philosophie
|
||||||
|
philosophy_subcategories = [
|
||||||
|
{"name": "Ethik", "description": "Moralische Grundsätze", "icon": "fa-balance-scale", "color_code": "#8B5CF6"},
|
||||||
|
{"name": "Logik", "description": "Gesetze des Denkens", "icon": "fa-project-diagram", "color_code": "#8B5CF6"},
|
||||||
|
{"name": "Erkenntnistheorie", "description": "Natur des Wissens", "icon": "fa-lightbulb", "color_code": "#8B5CF6"}
|
||||||
|
]
|
||||||
|
|
||||||
|
# Unterkategorien für Wissenschaft
|
||||||
|
science_subcategories = [
|
||||||
|
{"name": "Physik", "description": "Studie der Materie und Energie", "icon": "fa-atom", "color_code": "#3B82F6"},
|
||||||
|
{"name": "Biologie", "description": "Studie des Lebens", "icon": "fa-dna", "color_code": "#3B82F6"},
|
||||||
|
{"name": "Mathematik", "description": "Studie der Zahlen und Strukturen", "icon": "fa-square-root-alt", "color_code": "#3B82F6"}
|
||||||
|
]
|
||||||
|
|
||||||
|
# Unterkategorien für Technologie
|
||||||
|
tech_subcategories = [
|
||||||
|
{"name": "Software", "description": "Computerprogramme und Anwendungen", "icon": "fa-code", "color_code": "#059669"},
|
||||||
|
{"name": "Hardware", "description": "Physische Komponenten der Technik", "icon": "fa-microchip", "color_code": "#059669"},
|
||||||
|
{"name": "Internet", "description": "Globales Netzwerk und Web", "icon": "fa-globe", "color_code": "#059669"}
|
||||||
|
]
|
||||||
|
|
||||||
|
# Unterkategorien für Künste
|
||||||
|
arts_subcategories = [
|
||||||
|
{"name": "Musik", "description": "Klangkunst", "icon": "fa-music", "color_code": "#D97706"},
|
||||||
|
{"name": "Literatur", "description": "Geschriebene Kunst", "icon": "fa-book", "color_code": "#D97706"},
|
||||||
|
{"name": "Bildende Kunst", "description": "Visuelle Kunst", "icon": "fa-paint-brush", "color_code": "#D97706"}
|
||||||
|
]
|
||||||
|
|
||||||
|
# Unterkategorien für Psychologie
|
||||||
|
psychology_subcategories = [
|
||||||
|
{"name": "Kognition", "description": "Gedächtnisprozesse und Denken", "icon": "fa-brain", "color_code": "#DC2626"},
|
||||||
|
{"name": "Emotionen", "description": "Gefühle und emotionale Prozesse", "icon": "fa-heart", "color_code": "#DC2626"},
|
||||||
|
{"name": "Verhalten", "description": "Beobachtbares Verhalten und Reaktionen", "icon": "fa-user", "color_code": "#DC2626"}
|
||||||
|
]
|
||||||
|
|
||||||
|
# Alle Unterkategorien zu ihren Hauptkategorien hinzufügen
|
||||||
|
for subcat_data in philosophy_subcategories:
|
||||||
|
subcat = Category(**subcat_data)
|
||||||
|
subcat.parent_id = category_map["Philosophie"].id
|
||||||
|
db.session.add(subcat)
|
||||||
|
|
||||||
|
for subcat_data in science_subcategories:
|
||||||
|
subcat = Category(**subcat_data)
|
||||||
|
subcat.parent_id = category_map["Wissenschaft"].id
|
||||||
|
db.session.add(subcat)
|
||||||
|
|
||||||
|
for subcat_data in tech_subcategories:
|
||||||
|
subcat = Category(**subcat_data)
|
||||||
|
subcat.parent_id = category_map["Technologie"].id
|
||||||
|
db.session.add(subcat)
|
||||||
|
|
||||||
|
for subcat_data in arts_subcategories:
|
||||||
|
subcat = Category(**subcat_data)
|
||||||
|
subcat.parent_id = category_map["Künste"].id
|
||||||
|
db.session.add(subcat)
|
||||||
|
|
||||||
|
for subcat_data in psychology_subcategories:
|
||||||
|
subcat = Category(**subcat_data)
|
||||||
|
subcat.parent_id = category_map["Psychologie"].id
|
||||||
|
db.session.add(subcat)
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
print(f"{len(categories)} Kategorien wurden erstellt.")
|
print(f"{len(main_categories)} Hauptkategorien und {len(philosophy_subcategories + science_subcategories + tech_subcategories + arts_subcategories + psychology_subcategories)} Unterkategorien wurden erstellt.")
|
||||||
|
|
||||||
def create_sample_mindmap():
|
def create_sample_mindmap():
|
||||||
"""Erstellt eine Beispiel-Mindmap mit Knoten und Beziehungen"""
|
"""Erstellt eine Beispiel-Mindmap mit Knoten und Beziehungen"""
|
||||||
|
|||||||
1219
logs/app.log
Normal file
1219
logs/app.log
Normal file
File diff suppressed because it is too large
Load Diff
BIN
migrations/__pycache__/env.cpython-313.pyc
Normal file
BIN
migrations/__pycache__/env.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
38
migrations/versions/add_mindmap_shares_table.py
Normal file
38
migrations/versions/add_mindmap_shares_table.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
"""add mindmap shares table
|
||||||
|
|
||||||
|
Revision ID: add_mindmap_shares
|
||||||
|
Revises: add_missing_user_fields
|
||||||
|
Create Date: 2025-05-10 23:20:00.000000
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import sqlite
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'add_mindmap_shares'
|
||||||
|
down_revision = 'add_missing_user_fields'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# Erstelle PermissionType Enum
|
||||||
|
op.create_table('mindmap_share',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('mindmap_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('shared_by_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('shared_with_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('permission_type', sa.String(20), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('last_accessed', sa.DateTime(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['mindmap_id'], ['user_mindmap.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['shared_by_id'], ['user.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['shared_with_id'], ['user.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('mindmap_id', 'shared_with_id', name='unique_mindmap_share')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
op.drop_table('mindmap_share')
|
||||||
40
migrations/versions/add_missing_user_fields.py
Normal file
40
migrations/versions/add_missing_user_fields.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
"""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 ###
|
||||||
85
models.py
85
models.py
@@ -53,11 +53,20 @@ class User(db.Model, UserMixin):
|
|||||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
is_active = db.Column(db.Boolean, default=True)
|
is_active = db.Column(db.Boolean, default=True)
|
||||||
role = db.Column(db.String(20), default="user") # 'user', 'admin', 'moderator'
|
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
|
# Relationships
|
||||||
threads = db.relationship('Thread', backref='creator', lazy=True)
|
threads = db.relationship('Thread', backref='creator', lazy=True)
|
||||||
messages = db.relationship('Message', backref='author', lazy=True)
|
messages = db.relationship('Message', backref='author', lazy=True)
|
||||||
projects = db.relationship('Project', backref='owner', 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):
|
def __repr__(self):
|
||||||
return f'<User {self.username}>'
|
return f'<User {self.username}>'
|
||||||
@@ -67,6 +76,14 @@ class User(db.Model, UserMixin):
|
|||||||
|
|
||||||
def check_password(self, password):
|
def check_password(self, password):
|
||||||
return check_password_hash(self.password, 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):
|
class Category(db.Model):
|
||||||
"""Wissenschaftliche Kategorien für die Gliederung der öffentlichen Mindmap"""
|
"""Wissenschaftliche Kategorien für die Gliederung der öffentlichen Mindmap"""
|
||||||
@@ -305,4 +322,70 @@ class Document(db.Model):
|
|||||||
file_size = db.Column(db.Integer, nullable=True)
|
file_size = db.Column(db.Integer, nullable=True)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<Document {self.title}>'
|
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}>'
|
||||||
|
|
||||||
|
# Berechtigungstypen für Mindmap-Freigaben
|
||||||
|
class PermissionType(Enum):
|
||||||
|
READ = "Nur-Lesen"
|
||||||
|
EDIT = "Bearbeiten"
|
||||||
|
ADMIN = "Administrator"
|
||||||
|
|
||||||
|
# Freigabemodell für Mindmaps
|
||||||
|
class MindmapShare(db.Model):
|
||||||
|
"""Speichert Informationen über freigegebene Mindmaps und Berechtigungen"""
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
mindmap_id = db.Column(db.Integer, db.ForeignKey('user_mindmap.id'), nullable=False)
|
||||||
|
shared_by_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||||
|
shared_with_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||||
|
permission_type = db.Column(db.Enum(PermissionType), nullable=False, default=PermissionType.READ)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
last_accessed = db.Column(db.DateTime, nullable=True)
|
||||||
|
|
||||||
|
# Beziehungen
|
||||||
|
mindmap = db.relationship('UserMindmap', backref=db.backref('shares', lazy='dynamic'))
|
||||||
|
shared_by = db.relationship('User', foreign_keys=[shared_by_id], backref=db.backref('shared_mindmaps', lazy='dynamic'))
|
||||||
|
shared_with = db.relationship('User', foreign_keys=[shared_with_id], backref=db.backref('accessible_mindmaps', lazy='dynamic'))
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
db.UniqueConstraint('mindmap_id', 'shared_with_id', name='unique_mindmap_share'),
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<MindmapShare: {self.mindmap_id} - {self.shared_with_id} - {self.permission_type.name}>'
|
||||||
53
start.sh
Normal file
53
start.sh
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
#!/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
static/541AABD7-4E37-44ED-B491-1459C8C19699.PNG
Normal file
1
static/541AABD7-4E37-44ED-B491-1459C8C19699.PNG
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
/* ChatGPT Assistent Styles - Verbesserte Version */
|
/* ChatGPT Assistent Styles - Verbesserte Version */
|
||||||
#chatgpt-assistant {
|
#chatgpt-assistant {
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: 'Inter', sans-serif;
|
||||||
|
bottom: 5.5rem;
|
||||||
|
z-index: 100;
|
||||||
|
max-height: 85vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
#assistant-chat {
|
#assistant-chat {
|
||||||
@@ -10,6 +13,15 @@
|
|||||||
border-radius: 0.75rem;
|
border-radius: 0.75rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
max-width: calc(100vw - 2rem);
|
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 {
|
#assistant-toggle {
|
||||||
@@ -22,11 +34,6 @@
|
|||||||
transform: scale(1.1) rotate(10deg);
|
transform: scale(1.1) rotate(10deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
#assistant-history {
|
|
||||||
scrollbar-width: thin;
|
|
||||||
scrollbar-color: rgba(156, 163, 175, 0.5) transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
#assistant-history::-webkit-scrollbar {
|
#assistant-history::-webkit-scrollbar {
|
||||||
width: 6px;
|
width: 6px;
|
||||||
}
|
}
|
||||||
@@ -142,14 +149,21 @@
|
|||||||
.typing-indicator span {
|
.typing-indicator span {
|
||||||
height: 8px;
|
height: 8px;
|
||||||
width: 8px;
|
width: 8px;
|
||||||
background-color: #888;
|
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 0 2px;
|
margin: 0 2px;
|
||||||
opacity: 0.4;
|
opacity: 0.6;
|
||||||
animation: bounce 1.4s infinite ease-in-out;
|
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(1) { animation-delay: 0s; }
|
||||||
.typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
|
.typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
|
||||||
.typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
|
.typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
|
||||||
@@ -173,11 +187,12 @@
|
|||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
#assistant-chat {
|
#assistant-chat {
|
||||||
width: calc(100vw - 2rem) !important;
|
width: calc(100vw - 2rem) !important;
|
||||||
|
max-height: 65vh !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
#chatgpt-assistant {
|
#chatgpt-assistant {
|
||||||
right: 1rem;
|
right: 1rem;
|
||||||
bottom: 1rem;
|
bottom: 6rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,4 +215,38 @@ main {
|
|||||||
|
|
||||||
footer {
|
footer {
|
||||||
flex-shrink: 0;
|
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-bg: #f9fafb;
|
||||||
--light-text: #1e293b;
|
--light-text: #1e293b;
|
||||||
--light-heading: #0f172a;
|
--light-heading: #0f172a;
|
||||||
--light-primary: #3b82f6;
|
--light-primary: #7c3aed;
|
||||||
--light-primary-hover: #4f46e5;
|
--light-primary-hover: #6d28d9;
|
||||||
--light-secondary: #6b7280;
|
--light-secondary: #6b7280;
|
||||||
--light-border: #e5e7eb;
|
--light-border: #e5e7eb;
|
||||||
--light-card-bg: rgba(255, 255, 255, 0.92);
|
--light-card-bg: rgba(255, 255, 255, 0.92);
|
||||||
@@ -68,18 +68,37 @@ body {
|
|||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dark Mode */
|
/* Strikte Trennung: Dark Mode */
|
||||||
html.dark body {
|
html.dark body,
|
||||||
|
body.dark {
|
||||||
background-color: var(--bg-primary-dark);
|
background-color: var(--bg-primary-dark);
|
||||||
color: var(--text-primary-dark);
|
color: var(--text-primary-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Light Mode */
|
/* Strikte Trennung: Light Mode */
|
||||||
|
html:not(.dark) body,
|
||||||
body:not(.dark) {
|
body:not(.dark) {
|
||||||
background-color: var(--light-bg);
|
background-color: var(--light-bg);
|
||||||
color: var(--light-text);
|
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 */
|
/* Typography */
|
||||||
h1, h2, h3, h4, h5, h6 {
|
h1, h2, h3, h4, h5, h6 {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -388,7 +407,7 @@ html.dark ::-webkit-scrollbar-thumb:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.section-heading {
|
.section-heading {
|
||||||
font-size: 1.5rem;
|
font-size: 1.75rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -455,22 +474,60 @@ body:not(.dark) a:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Light Mode Buttons */
|
/* 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,
|
||||||
body:not(.dark) button:not(.toggle) {
|
body:not(.dark) .btn-primary,
|
||||||
background-color: var(--light-primary);
|
body:not(.dark) .btn-secondary,
|
||||||
color: white;
|
body:not(.dark) .btn-success,
|
||||||
border: none;
|
body:not(.dark) .btn-danger,
|
||||||
box-shadow: var(--light-shadow);
|
body:not(.dark) .btn-warning,
|
||||||
border-radius: 0.375rem;
|
body:not(.dark) .btn-info {
|
||||||
padding: 0.5rem 1rem;
|
color: white !important;
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body:not(.dark) .btn:hover,
|
body:not(.dark) .btn:hover,
|
||||||
body:not(.dark) button:not(.toggle):hover {
|
body:not(.dark) button:not(.toggle):hover {
|
||||||
background-color: var(--light-primary-hover);
|
background: linear-gradient(135deg, #7c3aed, #6d28d9);
|
||||||
transform: translateY(-2px);
|
transform: translateY(-1px);
|
||||||
box-shadow: 0 6px 10px rgba(0, 0, 0, 0.1);
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Light Mode Cards und Panels */
|
/* Light Mode Cards und Panels */
|
||||||
@@ -523,4 +580,489 @@ body:not(.dark) .navbar {
|
|||||||
background-color: var(--light-navbar-bg);
|
background-color: var(--light-navbar-bg);
|
||||||
box-shadow: var(--light-shadow);
|
box-shadow: var(--light-shadow);
|
||||||
border-bottom: 1px solid var(--light-border);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
437
static/css/mindmap.css
Normal file
437
static/css/mindmap.css
Normal file
@@ -0,0 +1,437 @@
|
|||||||
|
/* Mindmap Container Styles */
|
||||||
|
.mindmap-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 600px;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cytoscape Container für die Hauptmindmap */
|
||||||
|
#cy {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: transparent;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Subpage Styles - Identisches Design wie Hauptmindmap */
|
||||||
|
.mindmap-subpage {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
z-index: 10;
|
||||||
|
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Subpage Header */
|
||||||
|
.subpage-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .subpage-header {
|
||||||
|
background: rgba(30, 41, 59, 0.8);
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Zurück-Button */
|
||||||
|
.back-button {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Subpage Titel */
|
||||||
|
.subpage-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: white;
|
||||||
|
margin: 0;
|
||||||
|
background: linear-gradient(90deg, #60a5fa, #8b5cf6);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Subpage Cytoscape Container */
|
||||||
|
.subpage-cy-container {
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100% - 72px);
|
||||||
|
overflow: hidden;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toolbar für Zoom-Kontrollen */
|
||||||
|
.mindmap-toolbar {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
background: rgba(30, 41, 59, 0.8);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
|
||||||
|
z-index: 20;
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mindmap-toolbar button {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: none;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
color: white;
|
||||||
|
border-radius: 6px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mindmap-toolbar button:hover {
|
||||||
|
background: rgba(139, 92, 246, 0.5);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mindmap-toolbar button i {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mindmap Header */
|
||||||
|
.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;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark Mode spezifische Stile */
|
||||||
|
.dark .mindmap-subpage {
|
||||||
|
background: linear-gradient(135deg, #0f172a 0%, #0c1221 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix für Zoom-Buttons */
|
||||||
|
body.dark .mindmap-toolbar button {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .mindmap-toolbar button {
|
||||||
|
background: rgba(30, 41, 59, 0.2);
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Kontext-Menü-Anpassungen */
|
||||||
|
.context-menu {
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Kategorien-Panel */
|
||||||
|
.categories-panel {
|
||||||
|
position: absolute;
|
||||||
|
top: 80px;
|
||||||
|
left: 20px;
|
||||||
|
width: 300px;
|
||||||
|
max-height: calc(100vh - 120px);
|
||||||
|
background: rgba(15, 23, 42, 0.95);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||||
|
z-index: 1000;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
transform: translateX(-320px);
|
||||||
|
transition: transform 0.3s ease-in-out;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.categories-panel.visible {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.categories-panel h3 {
|
||||||
|
color: white;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin: 4px 0;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-item:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
transform: translateX(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-color {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-right: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-name {
|
||||||
|
flex-grow: 1;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-count {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
11
static/img/default-avatar.svg
Normal file
11
static/img/default-avatar.svg
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 583 B |
@@ -1,25 +1,54 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
"""
|
||||||
|
Generate favicon.ico from SVG using cairosvg and PIL
|
||||||
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import io
|
||||||
|
from cairosvg import svg2png
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
import cairosvg
|
|
||||||
|
|
||||||
# Pfad zum SVG-Favicon
|
# Verzeichnis dieses Skripts
|
||||||
svg_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'favicon.svg')
|
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
# 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')
|
|
||||||
|
|
||||||
# SVG zu PNG konvertieren
|
def svg_to_ico(svg_path, ico_path, sizes=[16, 32, 48, 64, 128, 256]):
|
||||||
cairosvg.svg2png(url=svg_path, write_to=png_path, output_width=512, output_height=512)
|
"""Convert SVG to multi-size ICO file"""
|
||||||
|
img_io = io.BytesIO()
|
||||||
|
|
||||||
|
# Höchste Auflösung für Zwischenspeicherung
|
||||||
|
max_size = max(sizes)
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
# 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!")
|
||||||
|
|
||||||
# PNG zu ICO konvertieren
|
# Ursprüngliches Favicon konvertieren
|
||||||
img = Image.open(png_path)
|
svg_to_ico(
|
||||||
img.save(ico_path, sizes=[(16, 16), (32, 32), (48, 48), (64, 64), (128, 128)])
|
os.path.join(CURRENT_DIR, 'favicon.svg'),
|
||||||
|
os.path.join(CURRENT_DIR, 'favicon.ico')
|
||||||
|
)
|
||||||
|
|
||||||
print(f"Favicon erfolgreich erstellt: {ico_path}")
|
# Neues Neuron-Favicon konvertieren
|
||||||
|
svg_to_ico(
|
||||||
# Optional: PNG-Datei löschen, wenn nur ICO benötigt wird
|
os.path.join(CURRENT_DIR, 'neuron-favicon.svg'),
|
||||||
# os.remove(png_path)
|
os.path.join(CURRENT_DIR, 'neuron-favicon.ico')
|
||||||
|
)
|
||||||
29
static/img/neuron-favicon.svg
Normal file
29
static/img/neuron-favicon.svg
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
59
static/img/neuron-logo.svg
Normal file
59
static/img/neuron-logo.svg
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 3.0 KiB |
214
static/js/mindmap-init.js
Normal file
214
static/js/mindmap-init.js
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
/**
|
||||||
|
* Mindmap Initialisierung und Event-Handling
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Warte auf die Cytoscape-Instanz
|
||||||
|
document.addEventListener('mindmap-loaded', function() {
|
||||||
|
const cy = window.cy;
|
||||||
|
if (!cy) return;
|
||||||
|
|
||||||
|
// Event-Listener für Knoten-Klicks
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Zoom-Controls
|
||||||
|
document.getElementById('zoomIn')?.addEventListener('click', () => {
|
||||||
|
cy.zoom({
|
||||||
|
level: cy.zoom() * 1.2,
|
||||||
|
renderedPosition: { x: cy.width() / 2, y: cy.height() / 2 }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('zoomOut')?.addEventListener('click', () => {
|
||||||
|
cy.zoom({
|
||||||
|
level: cy.zoom() / 1.2,
|
||||||
|
renderedPosition: { x: cy.width() / 2, y: cy.height() / 2 }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('resetView')?.addEventListener('click', () => {
|
||||||
|
cy.fit();
|
||||||
|
resetSelection(cy);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Legend-Toggle
|
||||||
|
document.getElementById('toggleLegend')?.addEventListener('click', () => {
|
||||||
|
const legend = document.getElementById('categoryLegend');
|
||||||
|
if (legend) {
|
||||||
|
isLegendVisible = !isLegendVisible;
|
||||||
|
legend.style.display = isLegendVisible ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keyboard-Controls
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === '+' || e.key === '=') {
|
||||||
|
cy.zoom({
|
||||||
|
level: cy.zoom() * 1.2,
|
||||||
|
renderedPosition: { x: cy.width() / 2, y: cy.height() / 2 }
|
||||||
|
});
|
||||||
|
} else if (e.key === '-' || e.key === '_') {
|
||||||
|
cy.zoom({
|
||||||
|
level: cy.zoom() / 1.2,
|
||||||
|
renderedPosition: { x: cy.width() / 2, y: cy.height() / 2 }
|
||||||
|
});
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
resetSelection(cy);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aktualisiert das Info-Panel mit Knoteninformationen
|
||||||
|
* @param {Object} node - Der ausgewählte Knoten
|
||||||
|
*/
|
||||||
|
function updateInfoPanel(node) {
|
||||||
|
const infoPanel = document.getElementById('infoPanel');
|
||||||
|
if (!infoPanel) return;
|
||||||
|
|
||||||
|
const data = node.data();
|
||||||
|
const connectedNodes = node.neighborhood('node');
|
||||||
|
|
||||||
|
let html = `
|
||||||
|
<h3>${data.label || data.name}</h3>
|
||||||
|
<p class="category">${data.category || 'Keine Kategorie'}</p>
|
||||||
|
${data.description ? `<p class="description">${data.description}</p>` : ''}
|
||||||
|
<div class="connections">
|
||||||
|
<h4>Verbindungen (${connectedNodes.length})</h4>
|
||||||
|
<ul>
|
||||||
|
`;
|
||||||
|
|
||||||
|
connectedNodes.forEach(connectedNode => {
|
||||||
|
const connectedData = connectedNode.data();
|
||||||
|
html += `
|
||||||
|
<li style="color: ${connectedData.color || '#60a5fa'}">
|
||||||
|
${connectedData.label || connectedData.name}
|
||||||
|
</li>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
html += `
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
infoPanel.innerHTML = html;
|
||||||
|
infoPanel.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aktualisiert die Seitenleiste mit Knoteninformationen
|
||||||
|
* @param {Object} node - Der ausgewählte Knoten
|
||||||
|
*/
|
||||||
|
function updateSidebar(node) {
|
||||||
|
const sidebar = document.getElementById('sidebar');
|
||||||
|
if (!sidebar) return;
|
||||||
|
|
||||||
|
const data = node.data();
|
||||||
|
const connectedNodes = node.neighborhood('node');
|
||||||
|
|
||||||
|
let html = `
|
||||||
|
<div class="node-details">
|
||||||
|
<h3>${data.label || data.name}</h3>
|
||||||
|
<p class="category">${data.category || 'Keine Kategorie'}</p>
|
||||||
|
${data.description ? `<p class="description">${data.description}</p>` : ''}
|
||||||
|
<div class="connections">
|
||||||
|
<h4>Verbindungen (${connectedNodes.length})</h4>
|
||||||
|
<ul>
|
||||||
|
`;
|
||||||
|
|
||||||
|
connectedNodes.forEach(connectedNode => {
|
||||||
|
const connectedData = connectedNode.data();
|
||||||
|
html += `
|
||||||
|
<li style="color: ${connectedData.color || '#60a5fa'}">
|
||||||
|
${connectedData.label || connectedData.name}
|
||||||
|
</li>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
html += `
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
sidebar.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setzt die Auswahl zurück
|
||||||
|
* @param {Object} cy - Cytoscape-Instanz
|
||||||
|
*/
|
||||||
|
function resetSelection(cy) {
|
||||||
|
window.mindmapInstance.selectedNode = null;
|
||||||
|
|
||||||
|
// Alle Hervorhebungen zurücksetzen
|
||||||
|
cy.nodes().forEach(node => {
|
||||||
|
node.removeStyle();
|
||||||
|
node.connectedEdges().removeStyle();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Info-Panel ausblenden
|
||||||
|
const infoPanel = document.getElementById('infoPanel');
|
||||||
|
if (infoPanel) {
|
||||||
|
infoPanel.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seitenleiste leeren
|
||||||
|
const sidebar = document.getElementById('sidebar');
|
||||||
|
if (sidebar) {
|
||||||
|
sidebar.innerHTML = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,234 +0,0 @@
|
|||||||
<!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>
|
|
||||||
|
|
||||||
<!-- Cytoscape.js -->
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.26.0/cytoscape.min.js"></script>
|
|
||||||
|
|
||||||
<!-- Socket.IO -->
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.7.2/socket.io.min.js"></script>
|
|
||||||
|
|
||||||
<!-- Feather Icons (optional) -->
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
||||||
background-color: #f9fafb;
|
|
||||||
color: #111827;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100vh;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
background-color: #1f2937;
|
|
||||||
color: white;
|
|
||||||
padding: 1rem;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header h1 {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar {
|
|
||||||
background-color: #f3f4f6;
|
|
||||||
padding: 0.75rem;
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
border-bottom: 1px solid #e5e7eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn:hover {
|
|
||||||
background-color: #2563eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary {
|
|
||||||
background-color: #6b7280;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary:hover {
|
|
||||||
background-color: #4b5563;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-danger {
|
|
||||||
background-color: #ef4444;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-danger:hover {
|
|
||||||
background-color: #dc2626;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-container {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
margin-left: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-input {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 300px;
|
|
||||||
padding: 0.5rem;
|
|
||||||
border: 1px solid #d1d5db;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#cy {
|
|
||||||
flex: 1;
|
|
||||||
width: 100%;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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>
|
|
||||||
|
|
||||||
<!-- Unsere Mindmap JS -->
|
|
||||||
<script src="../js/mindmap.js"></script>
|
|
||||||
|
|
||||||
<!-- Icons initialisieren -->
|
|
||||||
<script>
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
if (typeof feather !== 'undefined') {
|
|
||||||
feather.replace();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,749 +0,0 @@
|
|||||||
/**
|
|
||||||
* Mindmap.js - Interaktive Mind-Map Implementierung
|
|
||||||
* - Cytoscape.js für Graph-Rendering
|
|
||||||
* - Fetch API für REST-Zugriffe
|
|
||||||
* - Socket.IO für Echtzeit-Synchronisation
|
|
||||||
*/
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
/* 1. Initialisierung und Grundkonfiguration */
|
|
||||||
const cy = cytoscape({
|
|
||||||
container: document.getElementById('cy'),
|
|
||||||
style: [
|
|
||||||
{
|
|
||||||
selector: 'node',
|
|
||||||
style: {
|
|
||||||
'label': 'data(name)',
|
|
||||||
'text-valign': 'center',
|
|
||||||
'color': '#fff',
|
|
||||||
'background-color': 'data(color)',
|
|
||||||
'width': 45,
|
|
||||||
'height': 45,
|
|
||||||
'font-size': 11,
|
|
||||||
'text-outline-width': 1,
|
|
||||||
'text-outline-color': '#000',
|
|
||||||
'text-outline-opacity': 0.5,
|
|
||||||
'text-wrap': 'wrap',
|
|
||||||
'text-max-width': 80
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
selector: 'node[icon]',
|
|
||||||
style: {
|
|
||||||
'background-image': function(ele) {
|
|
||||||
return `static/img/icons/${ele.data('icon')}.svg`;
|
|
||||||
},
|
|
||||||
'background-width': '60%',
|
|
||||||
'background-height': '60%',
|
|
||||||
'background-position-x': '50%',
|
|
||||||
'background-position-y': '40%',
|
|
||||||
'text-margin-y': 10
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
selector: 'edge',
|
|
||||||
style: {
|
|
||||||
'width': 2,
|
|
||||||
'line-color': '#888',
|
|
||||||
'target-arrow-shape': 'triangle',
|
|
||||||
'curve-style': 'bezier',
|
|
||||||
'target-arrow-color': '#888'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
selector: ':selected',
|
|
||||||
style: {
|
|
||||||
'border-width': 3,
|
|
||||||
'border-color': '#f8f32b'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
layout: {
|
|
||||||
name: 'breadthfirst',
|
|
||||||
directed: true,
|
|
||||||
padding: 30,
|
|
||||||
spacingFactor: 1.2
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/* 2. Hilfs-Funktionen für API-Zugriffe */
|
|
||||||
const get = async endpoint => {
|
|
||||||
try {
|
|
||||||
const response = await fetch(endpoint);
|
|
||||||
if (!response.ok) {
|
|
||||||
console.error(`API-Fehler: ${endpoint} antwortet mit Status ${response.status}`);
|
|
||||||
return []; // Leeres Array zurückgeben bei Fehlern
|
|
||||||
}
|
|
||||||
return await response.json();
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Fehler beim Abrufen von ${endpoint}:`, error);
|
|
||||||
return []; // Leeres Array zurückgeben bei Netzwerkfehlern
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const post = async (endpoint, body) => {
|
|
||||||
try {
|
|
||||||
const response = await fetch(endpoint, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(body)
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
console.error(`API-Fehler: ${endpoint} antwortet mit Status ${response.status}`);
|
|
||||||
return {}; // Leeres Objekt zurückgeben bei Fehlern
|
|
||||||
}
|
|
||||||
return await response.json();
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Fehler beim POST zu ${endpoint}:`, error);
|
|
||||||
return {}; // Leeres Objekt zurückgeben bei Netzwerkfehlern
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const del = async endpoint => {
|
|
||||||
try {
|
|
||||||
const response = await fetch(endpoint, { method: 'DELETE' });
|
|
||||||
if (!response.ok) {
|
|
||||||
console.error(`API-Fehler: ${endpoint} antwortet mit Status ${response.status}`);
|
|
||||||
return {}; // Leeres Objekt zurückgeben bei Fehlern
|
|
||||||
}
|
|
||||||
return await response.json();
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Fehler beim DELETE zu ${endpoint}:`, error);
|
|
||||||
return {}; // Leeres Objekt zurückgeben bei Netzwerkfehlern
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/* 3. Kategorien laden für Style-Informationen */
|
|
||||||
let categories = await get('/api/categories');
|
|
||||||
|
|
||||||
/* 4. Daten laden und Rendering */
|
|
||||||
const loadMindmap = async () => {
|
|
||||||
try {
|
|
||||||
// Nodes und Beziehungen parallel laden
|
|
||||||
const [nodes, relationships] = await Promise.all([
|
|
||||||
get('/api/mind_map_nodes'),
|
|
||||||
get('/api/node_relationships')
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Graph leeren (für Reload-Fälle)
|
|
||||||
cy.elements().remove();
|
|
||||||
|
|
||||||
// Überprüfen, ob nodes ein Array ist, wenn nicht, setze es auf ein leeres Array
|
|
||||||
const nodesArray = Array.isArray(nodes) ? nodes : [];
|
|
||||||
|
|
||||||
// Knoten zum Graph hinzufügen
|
|
||||||
cy.add(
|
|
||||||
nodesArray.map(node => {
|
|
||||||
// Kategorie-Informationen für Styling abrufen
|
|
||||||
const category = categories.find(c => c.id === node.category_id) || {};
|
|
||||||
|
|
||||||
return {
|
|
||||||
data: {
|
|
||||||
id: node.id.toString(),
|
|
||||||
name: node.name,
|
|
||||||
description: node.description,
|
|
||||||
color: node.color_code || category.color_code || '#6b7280',
|
|
||||||
icon: node.icon || category.icon,
|
|
||||||
category_id: node.category_id
|
|
||||||
},
|
|
||||||
position: node.x && node.y ? { x: node.x, y: node.y } : undefined
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Überprüfen, ob relationships ein Array ist, wenn nicht, setze es auf ein leeres Array
|
|
||||||
const relationshipsArray = Array.isArray(relationships) ? relationships : [];
|
|
||||||
|
|
||||||
// Kanten zum Graph hinzufügen
|
|
||||||
cy.add(
|
|
||||||
relationshipsArray.map(rel => ({
|
|
||||||
data: {
|
|
||||||
id: `${rel.parent_id}_${rel.child_id}`,
|
|
||||||
source: rel.parent_id.toString(),
|
|
||||||
target: rel.child_id.toString()
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
|
|
||||||
// Wenn keine Knoten geladen wurden, Fallback-Knoten erstellen
|
|
||||||
if (nodesArray.length === 0) {
|
|
||||||
// Mindestens einen Standardknoten hinzufügen
|
|
||||||
cy.add({
|
|
||||||
data: {
|
|
||||||
id: 'fallback-1',
|
|
||||||
name: 'Mindmap',
|
|
||||||
description: 'Erstellen Sie hier Ihre eigene Mindmap',
|
|
||||||
color: '#3b82f6',
|
|
||||||
icon: 'help-circle'
|
|
||||||
},
|
|
||||||
position: { x: 300, y: 200 }
|
|
||||||
});
|
|
||||||
|
|
||||||
// Erfolgsmeldung anzeigen
|
|
||||||
console.log('Mindmap erfolgreich initialisiert mit Fallback-Knoten');
|
|
||||||
|
|
||||||
// Info-Meldung für Benutzer anzeigen
|
|
||||||
const infoBox = document.createElement('div');
|
|
||||||
infoBox.classList.add('info-message');
|
|
||||||
infoBox.style.position = 'absolute';
|
|
||||||
infoBox.style.top = '50%';
|
|
||||||
infoBox.style.left = '50%';
|
|
||||||
infoBox.style.transform = 'translate(-50%, -50%)';
|
|
||||||
infoBox.style.padding = '15px 20px';
|
|
||||||
infoBox.style.backgroundColor = 'rgba(59, 130, 246, 0.9)';
|
|
||||||
infoBox.style.color = 'white';
|
|
||||||
infoBox.style.borderRadius = '8px';
|
|
||||||
infoBox.style.zIndex = '5';
|
|
||||||
infoBox.style.maxWidth = '80%';
|
|
||||||
infoBox.style.textAlign = 'center';
|
|
||||||
infoBox.style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)';
|
|
||||||
infoBox.innerHTML = 'Mindmap erfolgreich initialisiert.<br>Verwenden Sie die Werkzeugleiste, um Knoten hinzuzufügen.';
|
|
||||||
|
|
||||||
document.getElementById('cy').appendChild(infoBox);
|
|
||||||
|
|
||||||
// Meldung nach 5 Sekunden ausblenden
|
|
||||||
setTimeout(() => {
|
|
||||||
infoBox.style.opacity = '0';
|
|
||||||
infoBox.style.transition = 'opacity 0.5s ease';
|
|
||||||
setTimeout(() => {
|
|
||||||
if (infoBox.parentNode) {
|
|
||||||
infoBox.parentNode.removeChild(infoBox);
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
}, 5000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Layout anwenden wenn keine Positionsdaten vorhanden
|
|
||||||
const nodesWithoutPosition = cy.nodes().filter(node =>
|
|
||||||
!node.position() || (node.position().x === 0 && node.position().y === 0)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (nodesWithoutPosition.length > 0) {
|
|
||||||
cy.layout({
|
|
||||||
name: 'breadthfirst',
|
|
||||||
directed: true,
|
|
||||||
padding: 30,
|
|
||||||
spacingFactor: 1.2
|
|
||||||
}).run();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tooltip-Funktionalität
|
|
||||||
cy.nodes().unbind('mouseover').bind('mouseover', (event) => {
|
|
||||||
const node = event.target;
|
|
||||||
const description = node.data('description');
|
|
||||||
|
|
||||||
if (description) {
|
|
||||||
const tooltip = document.getElementById('node-tooltip') ||
|
|
||||||
document.createElement('div');
|
|
||||||
|
|
||||||
if (!tooltip.id) {
|
|
||||||
tooltip.id = 'node-tooltip';
|
|
||||||
tooltip.style.position = 'absolute';
|
|
||||||
tooltip.style.backgroundColor = '#333';
|
|
||||||
tooltip.style.color = '#fff';
|
|
||||||
tooltip.style.padding = '8px';
|
|
||||||
tooltip.style.borderRadius = '4px';
|
|
||||||
tooltip.style.maxWidth = '250px';
|
|
||||||
tooltip.style.zIndex = 10;
|
|
||||||
tooltip.style.pointerEvents = 'none';
|
|
||||||
tooltip.style.transition = 'opacity 0.2s';
|
|
||||||
tooltip.style.boxShadow = '0 2px 10px rgba(0,0,0,0.3)';
|
|
||||||
document.body.appendChild(tooltip);
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderedPosition = node.renderedPosition();
|
|
||||||
const containerRect = cy.container().getBoundingClientRect();
|
|
||||||
|
|
||||||
tooltip.innerHTML = description;
|
|
||||||
tooltip.style.left = (containerRect.left + renderedPosition.x + 25) + 'px';
|
|
||||||
tooltip.style.top = (containerRect.top + renderedPosition.y - 15) + 'px';
|
|
||||||
tooltip.style.opacity = '1';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.nodes().unbind('mouseout').bind('mouseout', () => {
|
|
||||||
const tooltip = document.getElementById('node-tooltip');
|
|
||||||
if (tooltip) {
|
|
||||||
tooltip.style.opacity = '0';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Fehler beim Laden der Mindmap:', error);
|
|
||||||
alert('Die Mindmap konnte nicht geladen werden. Bitte prüfen Sie die Konsole für Details.');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initial laden
|
|
||||||
await loadMindmap();
|
|
||||||
|
|
||||||
/* 5. Socket.IO für Echtzeit-Synchronisation */
|
|
||||||
const socket = io();
|
|
||||||
|
|
||||||
socket.on('node_added', async (node) => {
|
|
||||||
// Kategorie-Informationen für Styling abrufen
|
|
||||||
const category = categories.find(c => c.id === node.category_id) || {};
|
|
||||||
|
|
||||||
cy.add({
|
|
||||||
data: {
|
|
||||||
id: node.id.toString(),
|
|
||||||
name: node.name,
|
|
||||||
description: node.description,
|
|
||||||
color: node.color_code || category.color_code || '#6b7280',
|
|
||||||
icon: node.icon || category.icon,
|
|
||||||
category_id: node.category_id
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Layout neu anwenden, wenn nötig
|
|
||||||
if (!node.x || !node.y) {
|
|
||||||
cy.layout({ name: 'breadthfirst', directed: true, padding: 30 }).run();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('node_updated', (node) => {
|
|
||||||
const cyNode = cy.$id(node.id.toString());
|
|
||||||
if (cyNode.length > 0) {
|
|
||||||
// Kategorie-Informationen für Styling abrufen
|
|
||||||
const category = categories.find(c => c.id === node.category_id) || {};
|
|
||||||
|
|
||||||
cyNode.data({
|
|
||||||
name: node.name,
|
|
||||||
description: node.description,
|
|
||||||
color: node.color_code || category.color_code || '#6b7280',
|
|
||||||
icon: node.icon || category.icon,
|
|
||||||
category_id: node.category_id
|
|
||||||
});
|
|
||||||
|
|
||||||
if (node.x && node.y) {
|
|
||||||
cyNode.position({ x: node.x, y: node.y });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('node_deleted', (nodeId) => {
|
|
||||||
const cyNode = cy.$id(nodeId.toString());
|
|
||||||
if (cyNode.length > 0) {
|
|
||||||
cy.remove(cyNode);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('relationship_added', (rel) => {
|
|
||||||
cy.add({
|
|
||||||
data: {
|
|
||||||
id: `${rel.parent_id}_${rel.child_id}`,
|
|
||||||
source: rel.parent_id.toString(),
|
|
||||||
target: rel.child_id.toString()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('relationship_deleted', (rel) => {
|
|
||||||
const edgeId = `${rel.parent_id}_${rel.child_id}`;
|
|
||||||
const cyEdge = cy.$id(edgeId);
|
|
||||||
if (cyEdge.length > 0) {
|
|
||||||
cy.remove(cyEdge);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('category_updated', async () => {
|
|
||||||
// Kategorien neu laden
|
|
||||||
categories = await get('/api/categories');
|
|
||||||
// Nodes aktualisieren, die diese Kategorie verwenden
|
|
||||||
cy.nodes().forEach(node => {
|
|
||||||
const categoryId = node.data('category_id');
|
|
||||||
if (categoryId) {
|
|
||||||
const category = categories.find(c => c.id === categoryId);
|
|
||||||
if (category) {
|
|
||||||
node.data('color', node.data('color_code') || category.color_code);
|
|
||||||
node.data('icon', node.data('icon') || category.icon);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
/* 6. UI-Interaktionen */
|
|
||||||
// Knoten hinzufügen
|
|
||||||
const btnAddNode = document.getElementById('addNode');
|
|
||||||
if (btnAddNode) {
|
|
||||||
btnAddNode.addEventListener('click', async () => {
|
|
||||||
const name = prompt('Knotenname eingeben:');
|
|
||||||
if (!name) return;
|
|
||||||
|
|
||||||
const description = prompt('Beschreibung (optional):');
|
|
||||||
|
|
||||||
// Kategorie auswählen
|
|
||||||
let categoryId = null;
|
|
||||||
if (categories.length > 0) {
|
|
||||||
const categoryOptions = categories.map((c, i) => `${i}: ${c.name}`).join('\n');
|
|
||||||
const categoryChoice = prompt(
|
|
||||||
`Kategorie auswählen (Nummer eingeben):\n${categoryOptions}`,
|
|
||||||
'0'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (categoryChoice !== null) {
|
|
||||||
const index = parseInt(categoryChoice, 10);
|
|
||||||
if (!isNaN(index) && index >= 0 && index < categories.length) {
|
|
||||||
categoryId = categories[index].id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Knoten erstellen
|
|
||||||
await post('/api/mind_map_node', {
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
category_id: categoryId
|
|
||||||
});
|
|
||||||
// Darstellung wird durch Socket.IO Event übernommen
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verbindung hinzufügen
|
|
||||||
const btnAddEdge = document.getElementById('addEdge');
|
|
||||||
if (btnAddEdge) {
|
|
||||||
btnAddEdge.addEventListener('click', async () => {
|
|
||||||
const sel = cy.$('node:selected');
|
|
||||||
if (sel.length !== 2) {
|
|
||||||
alert('Bitte genau zwei Knoten auswählen (Parent → Child)');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [parent, child] = sel.map(node => node.id());
|
|
||||||
await post('/api/node_relationship', {
|
|
||||||
parent_id: parent,
|
|
||||||
child_id: child
|
|
||||||
});
|
|
||||||
// Darstellung wird durch Socket.IO Event übernommen
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Knoten bearbeiten
|
|
||||||
const btnEditNode = document.getElementById('editNode');
|
|
||||||
if (btnEditNode) {
|
|
||||||
btnEditNode.addEventListener('click', async () => {
|
|
||||||
const sel = cy.$('node:selected');
|
|
||||||
if (sel.length !== 1) {
|
|
||||||
alert('Bitte genau einen Knoten auswählen');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const node = sel[0];
|
|
||||||
const nodeData = node.data();
|
|
||||||
|
|
||||||
const name = prompt('Knotenname:', nodeData.name);
|
|
||||||
if (!name) return;
|
|
||||||
|
|
||||||
const description = prompt('Beschreibung:', nodeData.description || '');
|
|
||||||
|
|
||||||
// Kategorie auswählen
|
|
||||||
let categoryId = nodeData.category_id;
|
|
||||||
if (categories.length > 0) {
|
|
||||||
const categoryOptions = categories.map((c, i) => `${i}: ${c.name}`).join('\n');
|
|
||||||
const categoryChoice = prompt(
|
|
||||||
`Kategorie auswählen (Nummer eingeben):\n${categoryOptions}`,
|
|
||||||
categories.findIndex(c => c.id === categoryId).toString()
|
|
||||||
);
|
|
||||||
|
|
||||||
if (categoryChoice !== null) {
|
|
||||||
const index = parseInt(categoryChoice, 10);
|
|
||||||
if (!isNaN(index) && index >= 0 && index < categories.length) {
|
|
||||||
categoryId = categories[index].id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Knoten aktualisieren
|
|
||||||
await post(`/api/mind_map_node/${nodeData.id}`, {
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
category_id: categoryId
|
|
||||||
});
|
|
||||||
// Darstellung wird durch Socket.IO Event übernommen
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Knoten löschen
|
|
||||||
const btnDeleteNode = document.getElementById('deleteNode');
|
|
||||||
if (btnDeleteNode) {
|
|
||||||
btnDeleteNode.addEventListener('click', async () => {
|
|
||||||
const sel = cy.$('node:selected');
|
|
||||||
if (sel.length !== 1) {
|
|
||||||
alert('Bitte genau einen Knoten auswählen');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (confirm('Sind Sie sicher, dass Sie diesen Knoten löschen möchten?')) {
|
|
||||||
const nodeId = sel[0].id();
|
|
||||||
await del(`/api/mind_map_node/${nodeId}`);
|
|
||||||
// Darstellung wird durch Socket.IO Event übernommen
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verbindung löschen
|
|
||||||
const btnDeleteEdge = document.getElementById('deleteEdge');
|
|
||||||
if (btnDeleteEdge) {
|
|
||||||
btnDeleteEdge.addEventListener('click', async () => {
|
|
||||||
const sel = cy.$('edge:selected');
|
|
||||||
if (sel.length !== 1) {
|
|
||||||
alert('Bitte genau eine Verbindung auswählen');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (confirm('Sind Sie sicher, dass Sie diese Verbindung löschen möchten?')) {
|
|
||||||
const edge = sel[0];
|
|
||||||
const parentId = edge.source().id();
|
|
||||||
const childId = edge.target().id();
|
|
||||||
|
|
||||||
await del(`/api/node_relationship/${parentId}/${childId}`);
|
|
||||||
// Darstellung wird durch Socket.IO Event übernommen
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Layout aktualisieren
|
|
||||||
const btnReLayout = document.getElementById('reLayout');
|
|
||||||
if (btnReLayout) {
|
|
||||||
btnReLayout.addEventListener('click', () => {
|
|
||||||
cy.layout({
|
|
||||||
name: 'breadthfirst',
|
|
||||||
directed: true,
|
|
||||||
padding: 30,
|
|
||||||
spacingFactor: 1.2
|
|
||||||
}).run();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 7. Position speichern bei Drag & Drop */
|
|
||||||
cy.on('dragfree', 'node', async (e) => {
|
|
||||||
const node = e.target;
|
|
||||||
const position = node.position();
|
|
||||||
|
|
||||||
await post(`/api/mind_map_node/${node.id()}/position`, {
|
|
||||||
x: Math.round(position.x),
|
|
||||||
y: Math.round(position.y)
|
|
||||||
});
|
|
||||||
|
|
||||||
// Andere Benutzer erhalten die Position über den node_updated Event
|
|
||||||
});
|
|
||||||
|
|
||||||
/* 8. Kontextmenü (optional) */
|
|
||||||
const setupContextMenu = () => {
|
|
||||||
cy.on('cxttap', 'node', function(e) {
|
|
||||||
const node = e.target;
|
|
||||||
const nodeData = node.data();
|
|
||||||
|
|
||||||
// Position des Kontextmenüs berechnen
|
|
||||||
const renderedPosition = node.renderedPosition();
|
|
||||||
const containerRect = cy.container().getBoundingClientRect();
|
|
||||||
const menuX = containerRect.left + renderedPosition.x;
|
|
||||||
const menuY = containerRect.top + renderedPosition.y;
|
|
||||||
|
|
||||||
// Kontextmenü erstellen oder aktualisieren
|
|
||||||
let contextMenu = document.getElementById('context-menu');
|
|
||||||
if (!contextMenu) {
|
|
||||||
contextMenu = document.createElement('div');
|
|
||||||
contextMenu.id = 'context-menu';
|
|
||||||
contextMenu.style.position = 'absolute';
|
|
||||||
contextMenu.style.backgroundColor = '#fff';
|
|
||||||
contextMenu.style.border = '1px solid #ccc';
|
|
||||||
contextMenu.style.borderRadius = '4px';
|
|
||||||
contextMenu.style.padding = '5px 0';
|
|
||||||
contextMenu.style.boxShadow = '0 2px 10px rgba(0,0,0,0.2)';
|
|
||||||
contextMenu.style.zIndex = 1000;
|
|
||||||
document.body.appendChild(contextMenu);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Menüinhalte
|
|
||||||
contextMenu.innerHTML = `
|
|
||||||
<div class="menu-item" data-action="edit">Knoten bearbeiten</div>
|
|
||||||
<div class="menu-item" data-action="connect">Verbindung erstellen</div>
|
|
||||||
<div class="menu-item" data-action="delete">Knoten löschen</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Styling für Menüpunkte
|
|
||||||
const menuItems = contextMenu.querySelectorAll('.menu-item');
|
|
||||||
menuItems.forEach(item => {
|
|
||||||
item.style.padding = '8px 20px';
|
|
||||||
item.style.cursor = 'pointer';
|
|
||||||
item.style.fontSize = '14px';
|
|
||||||
|
|
||||||
item.addEventListener('mouseover', function() {
|
|
||||||
this.style.backgroundColor = '#f0f0f0';
|
|
||||||
});
|
|
||||||
|
|
||||||
item.addEventListener('mouseout', function() {
|
|
||||||
this.style.backgroundColor = 'transparent';
|
|
||||||
});
|
|
||||||
|
|
||||||
// Event-Handler
|
|
||||||
item.addEventListener('click', async function() {
|
|
||||||
const action = this.getAttribute('data-action');
|
|
||||||
|
|
||||||
switch(action) {
|
|
||||||
case 'edit':
|
|
||||||
// Knoten bearbeiten (gleiche Logik wie beim Edit-Button)
|
|
||||||
const name = prompt('Knotenname:', nodeData.name);
|
|
||||||
if (name) {
|
|
||||||
const description = prompt('Beschreibung:', nodeData.description || '');
|
|
||||||
await post(`/api/mind_map_node/${nodeData.id}`, { name, description });
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'connect':
|
|
||||||
// Modus zum Verbinden aktivieren
|
|
||||||
cy.nodes().unselect();
|
|
||||||
node.select();
|
|
||||||
alert('Wählen Sie nun einen zweiten Knoten aus, um eine Verbindung zu erstellen');
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'delete':
|
|
||||||
if (confirm('Sind Sie sicher, dass Sie diesen Knoten löschen möchten?')) {
|
|
||||||
await del(`/api/mind_map_node/${nodeData.id}`);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Menü schließen
|
|
||||||
contextMenu.style.display = 'none';
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Menü positionieren und anzeigen
|
|
||||||
contextMenu.style.left = menuX + 'px';
|
|
||||||
contextMenu.style.top = menuY + 'px';
|
|
||||||
contextMenu.style.display = 'block';
|
|
||||||
|
|
||||||
// Event-Listener zum Schließen des Menüs
|
|
||||||
const closeMenu = function() {
|
|
||||||
if (contextMenu) {
|
|
||||||
contextMenu.style.display = 'none';
|
|
||||||
}
|
|
||||||
document.removeEventListener('click', closeMenu);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Verzögerung, um den aktuellen Click nicht zu erfassen
|
|
||||||
setTimeout(() => {
|
|
||||||
document.addEventListener('click', closeMenu);
|
|
||||||
}, 0);
|
|
||||||
|
|
||||||
e.preventDefault();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Kontextmenü aktivieren (optional)
|
|
||||||
// setupContextMenu();
|
|
||||||
|
|
||||||
/* 9. Export-Funktion (optional) */
|
|
||||||
const btnExport = document.getElementById('exportMindmap');
|
|
||||||
if (btnExport) {
|
|
||||||
btnExport.addEventListener('click', () => {
|
|
||||||
const elements = cy.json().elements;
|
|
||||||
const exportData = {
|
|
||||||
nodes: elements.nodes.map(n => ({
|
|
||||||
id: n.data.id,
|
|
||||||
name: n.data.name,
|
|
||||||
description: n.data.description,
|
|
||||||
category_id: n.data.category_id,
|
|
||||||
x: Math.round(n.position?.x || 0),
|
|
||||||
y: Math.round(n.position?.y || 0)
|
|
||||||
})),
|
|
||||||
relationships: elements.edges.map(e => ({
|
|
||||||
parent_id: e.data.source,
|
|
||||||
child_id: e.data.target
|
|
||||||
}))
|
|
||||||
};
|
|
||||||
|
|
||||||
const blob = new Blob([JSON.stringify(exportData, null, 2)], {type: 'application/json'});
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = url;
|
|
||||||
a.download = 'mindmap_export.json';
|
|
||||||
document.body.appendChild(a);
|
|
||||||
a.click();
|
|
||||||
document.body.removeChild(a);
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 10. Filter-Funktion nach Kategorien (optional) */
|
|
||||||
const setupCategoryFilters = () => {
|
|
||||||
const filterContainer = document.getElementById('category-filters');
|
|
||||||
if (!filterContainer || !categories.length) return;
|
|
||||||
|
|
||||||
filterContainer.innerHTML = '';
|
|
||||||
|
|
||||||
// "Alle anzeigen" Option
|
|
||||||
const allBtn = document.createElement('button');
|
|
||||||
allBtn.innerText = 'Alle Kategorien';
|
|
||||||
allBtn.className = 'category-filter active';
|
|
||||||
allBtn.onclick = () => {
|
|
||||||
document.querySelectorAll('.category-filter').forEach(btn => btn.classList.remove('active'));
|
|
||||||
allBtn.classList.add('active');
|
|
||||||
cy.nodes().removeClass('filtered').show();
|
|
||||||
cy.edges().show();
|
|
||||||
};
|
|
||||||
filterContainer.appendChild(allBtn);
|
|
||||||
|
|
||||||
// Filter-Button pro Kategorie
|
|
||||||
categories.forEach(category => {
|
|
||||||
const btn = document.createElement('button');
|
|
||||||
btn.innerText = category.name;
|
|
||||||
btn.className = 'category-filter';
|
|
||||||
btn.style.backgroundColor = category.color_code;
|
|
||||||
btn.style.color = '#fff';
|
|
||||||
btn.onclick = () => {
|
|
||||||
document.querySelectorAll('.category-filter').forEach(btn => btn.classList.remove('active'));
|
|
||||||
btn.classList.add('active');
|
|
||||||
|
|
||||||
const matchingNodes = cy.nodes().filter(node => node.data('category_id') === category.id);
|
|
||||||
cy.nodes().addClass('filtered').hide();
|
|
||||||
matchingNodes.removeClass('filtered').show();
|
|
||||||
|
|
||||||
// Verbindungen zu/von diesen Knoten anzeigen
|
|
||||||
cy.edges().hide();
|
|
||||||
matchingNodes.connectedEdges().show();
|
|
||||||
};
|
|
||||||
filterContainer.appendChild(btn);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Filter-Funktionalität aktivieren (optional)
|
|
||||||
// setupCategoryFilters();
|
|
||||||
|
|
||||||
/* 11. Suchfunktion (optional) */
|
|
||||||
const searchInput = document.getElementById('search-mindmap');
|
|
||||||
if (searchInput) {
|
|
||||||
searchInput.addEventListener('input', (e) => {
|
|
||||||
const searchTerm = e.target.value.toLowerCase();
|
|
||||||
|
|
||||||
if (!searchTerm) {
|
|
||||||
cy.nodes().removeClass('search-hidden').show();
|
|
||||||
cy.edges().show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
cy.nodes().forEach(node => {
|
|
||||||
const name = node.data('name').toLowerCase();
|
|
||||||
const description = (node.data('description') || '').toLowerCase();
|
|
||||||
|
|
||||||
if (name.includes(searchTerm) || description.includes(searchTerm)) {
|
|
||||||
node.removeClass('search-hidden').show();
|
|
||||||
node.connectedEdges().show();
|
|
||||||
} else {
|
|
||||||
node.addClass('search-hidden').hide();
|
|
||||||
// Kanten nur verstecken, wenn beide verbundenen Knoten versteckt sind
|
|
||||||
node.connectedEdges().forEach(edge => {
|
|
||||||
const otherNode = edge.source().id() === node.id() ? edge.target() : edge.source();
|
|
||||||
if (otherNode.hasClass('search-hidden')) {
|
|
||||||
edge.hide();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Mindmap erfolgreich initialisiert');
|
|
||||||
})();
|
|
||||||
@@ -247,130 +247,63 @@ class ChatGPTAssistant {
|
|||||||
|
|
||||||
const bubble = document.createElement('div');
|
const bubble = document.createElement('div');
|
||||||
bubble.className = sender === 'user'
|
bubble.className = sender === 'user'
|
||||||
? 'bg-primary-100 dark:bg-primary-900 text-gray-800 dark:text-white rounded-lg py-2 px-3 max-w-[85%]'
|
? 'user-message 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%]';
|
: 'assistant-message rounded-lg py-2 px-3 max-w-[85%]';
|
||||||
|
|
||||||
// Formatierung des Texts (mit Markdown für Assistent-Nachrichten)
|
// Nachrichtentext einfügen, falls Markdown-Parser verfügbar, nutzen
|
||||||
let formattedText = '';
|
if (this.markdownParser) {
|
||||||
|
bubble.innerHTML = this.markdownParser.parse(text);
|
||||||
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 {
|
} else {
|
||||||
// Für Benutzernachrichten einfache Formatierung
|
bubble.textContent = text;
|
||||||
formattedText = text.split('\n').map(line => {
|
|
||||||
if (line.trim() === '') return '<br>';
|
|
||||||
return `<p>${line}</p>`;
|
|
||||||
}).join('');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bubble.innerHTML = formattedText;
|
// 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';
|
||||||
|
});
|
||||||
|
|
||||||
messageEl.appendChild(bubble);
|
messageEl.appendChild(bubble);
|
||||||
|
this.chatHistory.appendChild(messageEl);
|
||||||
|
|
||||||
if (this.chatHistory) {
|
// Scrolle zum Ende des Chat-Verlaufs
|
||||||
this.chatHistory.appendChild(messageEl);
|
this.chatHistory.scrollTop = this.chatHistory.scrollHeight;
|
||||||
|
|
||||||
// Scroll zum Ende des Verlaufs
|
|
||||||
this.chatHistory.scrollTop = this.chatHistory.scrollHeight;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Zeigt Vorschläge als klickbare Pills an
|
* Zeigt Vorschläge für mögliche Fragen an
|
||||||
* @param {string[]} suggestions - Liste von Vorschlägen
|
* @param {Array} suggestions - Array von Vorschlägen
|
||||||
*/
|
*/
|
||||||
showSuggestions(suggestions) {
|
showSuggestions(suggestions) {
|
||||||
if (!this.suggestionArea) return;
|
if (!this.suggestionArea || !suggestions || !suggestions.length) return;
|
||||||
|
|
||||||
// Vorherige Vorschläge entfernen
|
// Vorherige Vorschläge entfernen
|
||||||
this.suggestionArea.innerHTML = '';
|
this.suggestionArea.innerHTML = '';
|
||||||
|
|
||||||
if (suggestions && suggestions.length > 0) {
|
// Neue Vorschläge hinzufügen
|
||||||
suggestions.forEach(suggestion => {
|
suggestions.forEach((text, index) => {
|
||||||
const pill = document.createElement('button');
|
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.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.textContent = suggestion;
|
pill.style.animationDelay = `${index * 0.1}s`;
|
||||||
this.suggestionArea.appendChild(pill);
|
pill.textContent = text;
|
||||||
});
|
this.suggestionArea.appendChild(pill);
|
||||||
|
});
|
||||||
this.suggestionArea.classList.remove('hidden');
|
|
||||||
} else {
|
// Vorschlagsbereich anzeigen
|
||||||
this.suggestionArea.classList.add('hidden');
|
this.suggestionArea.classList.remove('hidden');
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -409,14 +342,27 @@ class ChatGPTAssistant {
|
|||||||
messages: this.messages
|
messages: this.messages
|
||||||
}),
|
}),
|
||||||
cache: 'no-cache', // Kein Cache verwenden
|
cache: 'no-cache', // Kein Cache verwenden
|
||||||
credentials: 'same-origin' // Cookies senden
|
credentials: 'same-origin', // Cookies senden
|
||||||
|
timeout: 60000 // 60 Sekunden Timeout
|
||||||
});
|
});
|
||||||
|
|
||||||
// Ladeindikator entfernen
|
// Ladeindikator entfernen
|
||||||
this.removeLoadingIndicator();
|
this.removeLoadingIndicator();
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Serverfehler: ${response.status} ${response.statusText}`);
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
@@ -442,24 +388,45 @@ class ChatGPTAssistant {
|
|||||||
// Ladeindikator entfernen, falls noch vorhanden
|
// Ladeindikator entfernen, falls noch vorhanden
|
||||||
this.removeLoadingIndicator();
|
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
|
// Fehlermeldung anzeigen oder Wiederholungsversuch starten
|
||||||
if (this.retryCount < this.maxRetries) {
|
if (this.retryCount < this.maxRetries) {
|
||||||
this.retryCount++;
|
this.retryCount++;
|
||||||
this.addMessage('assistant', 'Es gab ein Problem mit der Anfrage. Ich versuche es erneut...');
|
this.addMessage('assistant', `${userFriendlyMessage} Ich versuche es erneut... (Versuch ${this.retryCount}/${this.maxRetries})`);
|
||||||
|
|
||||||
// Kurze Verzögerung vor dem erneuten Versuch
|
// Letzte Benutzernachricht speichern für den Wiederholungsversuch
|
||||||
setTimeout(() => {
|
const lastUserMessageIndex = this.messages.findLastIndex(msg => msg.role === 'user');
|
||||||
// Letzte Benutzernachricht aus dem Messages-Array entfernen
|
if (lastUserMessageIndex >= 0) {
|
||||||
const lastUserMessage = this.messages[this.messages.length - 2].content;
|
const lastUserMessage = this.messages[lastUserMessageIndex].content;
|
||||||
this.messages = this.messages.slice(0, -2); // Entferne Benutzernachricht und Fehlermeldung
|
|
||||||
|
|
||||||
// Erneuter Versand mit gleicher Nachricht
|
// Kurze Verzögerung vor dem erneuten Versuch mit exponentieller Backoff-Strategie
|
||||||
this.inputField.value = lastUserMessage;
|
const retryDelay = 1500 * Math.pow(2, this.retryCount - 1); // 1.5s, 3s, 6s, ...
|
||||||
this.sendMessage();
|
|
||||||
}, 1500);
|
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);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Maximale Anzahl an Wiederholungsversuchen erreicht
|
// 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.');
|
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.retryCount = 0; // Zurücksetzen für die nächste Anfrage
|
this.retryCount = 0; // Zurücksetzen für die nächste Anfrage
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -512,26 +479,33 @@ class ChatGPTAssistant {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Zeigt einen Ladeindikator im Chat an
|
* Zeigt eine Ladeanimation an
|
||||||
*/
|
*/
|
||||||
showLoadingIndicator() {
|
showLoadingIndicator() {
|
||||||
if (!this.chatHistory) return;
|
if (!this.chatHistory) return;
|
||||||
|
|
||||||
// Entferne vorhandenen Ladeindikator (falls vorhanden)
|
// Prüfen, ob bereits ein Ladeindikator angezeigt wird
|
||||||
this.removeLoadingIndicator();
|
if (document.getElementById('assistant-loading-indicator')) return;
|
||||||
|
|
||||||
const loadingEl = document.createElement('div');
|
const loadingEl = document.createElement('div');
|
||||||
loadingEl.id = 'assistant-loading';
|
|
||||||
loadingEl.className = 'flex justify-start';
|
loadingEl.className = 'flex justify-start';
|
||||||
|
loadingEl.id = 'assistant-loading-indicator';
|
||||||
|
|
||||||
const bubble = document.createElement('div');
|
const bubble = document.createElement('div');
|
||||||
bubble.className = 'bg-gray-100 dark:bg-dark-700 text-gray-800 dark:text-white rounded-lg py-2 px-3';
|
bubble.className = 'assistant-message rounded-lg py-3 px-4 max-w-[85%] flex items-center';
|
||||||
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);
|
loadingEl.appendChild(bubble);
|
||||||
this.chatHistory.appendChild(loadingEl);
|
|
||||||
|
|
||||||
// Scroll zum Ende des Verlaufs
|
this.chatHistory.appendChild(loadingEl);
|
||||||
this.chatHistory.scrollTop = this.chatHistory.scrollHeight;
|
this.chatHistory.scrollTop = this.chatHistory.scrollHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -539,7 +513,7 @@ class ChatGPTAssistant {
|
|||||||
* Entfernt den Ladeindikator aus dem Chat
|
* Entfernt den Ladeindikator aus dem Chat
|
||||||
*/
|
*/
|
||||||
removeLoadingIndicator() {
|
removeLoadingIndicator() {
|
||||||
const loadingIndicator = document.getElementById('assistant-loading');
|
const loadingIndicator = document.getElementById('assistant-loading-indicator');
|
||||||
if (loadingIndicator) {
|
if (loadingIndicator) {
|
||||||
loadingIndicator.remove();
|
loadingIndicator.remove();
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,844 +0,0 @@
|
|||||||
/**
|
|
||||||
* MindMap D3.js Modul
|
|
||||||
* Visualisiert die Mindmap mit D3.js
|
|
||||||
*/
|
|
||||||
|
|
||||||
class MindMapVisualization {
|
|
||||||
constructor(containerSelector, options = {}) {
|
|
||||||
this.containerSelector = containerSelector;
|
|
||||||
this.container = d3.select(containerSelector);
|
|
||||||
this.width = options.width || this.container.node().clientWidth || 800;
|
|
||||||
this.height = options.height || 600;
|
|
||||||
this.nodeRadius = options.nodeRadius || 14;
|
|
||||||
this.selectedNodeRadius = options.selectedNodeRadius || 20;
|
|
||||||
this.linkDistance = options.linkDistance || 150;
|
|
||||||
this.chargeStrength = options.chargeStrength || -900;
|
|
||||||
this.centerForce = options.centerForce || 0.15;
|
|
||||||
this.onNodeClick = options.onNodeClick || ((node) => console.log('Node clicked:', node));
|
|
||||||
|
|
||||||
this.nodes = [];
|
|
||||||
this.links = [];
|
|
||||||
this.simulation = null;
|
|
||||||
this.svg = null;
|
|
||||||
this.linkElements = null;
|
|
||||||
this.nodeElements = null;
|
|
||||||
this.textElements = null;
|
|
||||||
this.tooltipEnabled = options.tooltipEnabled !== undefined ? options.tooltipEnabled : true;
|
|
||||||
|
|
||||||
this.mouseoverNode = null;
|
|
||||||
this.selectedNode = null;
|
|
||||||
|
|
||||||
this.zoomFactor = 1;
|
|
||||||
this.tooltipDiv = null;
|
|
||||||
this.isLoading = true;
|
|
||||||
|
|
||||||
// Lade die gemerkten Knoten
|
|
||||||
this.bookmarkedNodes = this.loadBookmarkedNodes();
|
|
||||||
|
|
||||||
// Sicherstellen, dass der Container bereit ist
|
|
||||||
if (this.container.node()) {
|
|
||||||
this.init();
|
|
||||||
this.setupDefaultNodes();
|
|
||||||
|
|
||||||
// Sofortige Datenladung
|
|
||||||
window.setTimeout(() => {
|
|
||||||
this.loadData();
|
|
||||||
}, 100);
|
|
||||||
} else {
|
|
||||||
console.error('Mindmap-Container nicht gefunden:', containerSelector);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Standardknoten als Fallback einrichten, falls die API nicht reagiert
|
|
||||||
setupDefaultNodes() {
|
|
||||||
// Basis-Mindmap mit Hauptthemen
|
|
||||||
const defaultNodes = [
|
|
||||||
{ id: "root", name: "Wissen", description: "Zentrale Wissensbasis", thought_count: 0 },
|
|
||||||
{ id: "philosophy", name: "Philosophie", description: "Philosophisches Denken", thought_count: 0 },
|
|
||||||
{ id: "science", name: "Wissenschaft", description: "Wissenschaftliche Erkenntnisse", thought_count: 0 },
|
|
||||||
{ id: "technology", name: "Technologie", description: "Technologische Entwicklungen", thought_count: 0 },
|
|
||||||
{ id: "arts", name: "Künste", description: "Künstlerische Ausdrucksformen", thought_count: 0 }
|
|
||||||
];
|
|
||||||
|
|
||||||
const defaultLinks = [
|
|
||||||
{ source: "root", target: "philosophy" },
|
|
||||||
{ source: "root", target: "science" },
|
|
||||||
{ source: "root", target: "technology" },
|
|
||||||
{ source: "root", target: "arts" }
|
|
||||||
];
|
|
||||||
|
|
||||||
// Als Fallback verwenden, falls die API fehlschlägt
|
|
||||||
this.defaultNodes = defaultNodes;
|
|
||||||
this.defaultLinks = defaultLinks;
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
// SVG erstellen, wenn noch nicht vorhanden
|
|
||||||
if (!this.svg) {
|
|
||||||
// Container zuerst leeren
|
|
||||||
this.container.html('');
|
|
||||||
|
|
||||||
this.svg = this.container
|
|
||||||
.append('svg')
|
|
||||||
.attr('width', '100%')
|
|
||||||
.attr('height', this.height)
|
|
||||||
.attr('viewBox', `0 0 ${this.width} ${this.height}`)
|
|
||||||
.attr('class', 'mindmap-svg')
|
|
||||||
.call(
|
|
||||||
d3.zoom()
|
|
||||||
.scaleExtent([0.1, 5])
|
|
||||||
.on('zoom', (event) => {
|
|
||||||
this.handleZoom(event.transform);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Hauptgruppe für alles, was zoom-transformierbar ist
|
|
||||||
this.g = this.svg.append('g');
|
|
||||||
|
|
||||||
// Tooltip initialisieren
|
|
||||||
if (!d3.select('body').select('.node-tooltip').size()) {
|
|
||||||
this.tooltipDiv = d3.select('body')
|
|
||||||
.append('div')
|
|
||||||
.attr('class', 'node-tooltip')
|
|
||||||
.style('opacity', 0)
|
|
||||||
.style('position', 'absolute')
|
|
||||||
.style('pointer-events', 'none')
|
|
||||||
.style('background', 'rgba(20, 20, 40, 0.9)')
|
|
||||||
.style('color', '#ffffff')
|
|
||||||
.style('border', '1px solid rgba(160, 80, 255, 0.2)')
|
|
||||||
.style('border-radius', '6px')
|
|
||||||
.style('padding', '8px 12px')
|
|
||||||
.style('font-size', '14px')
|
|
||||||
.style('max-width', '250px')
|
|
||||||
.style('box-shadow', '0 10px 25px rgba(0, 0, 0, 0.5), 0 0 10px rgba(160, 80, 255, 0.2)');
|
|
||||||
} else {
|
|
||||||
this.tooltipDiv = d3.select('body').select('.node-tooltip');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Force-Simulation initialisieren
|
|
||||||
this.simulation = d3.forceSimulation()
|
|
||||||
.force('link', d3.forceLink().id(d => d.id).distance(this.linkDistance))
|
|
||||||
.force('charge', d3.forceManyBody().strength(this.chargeStrength))
|
|
||||||
.force('center', d3.forceCenter(this.width / 2, this.height / 2).strength(this.centerForce))
|
|
||||||
.force('collision', d3.forceCollide().radius(this.nodeRadius * 2));
|
|
||||||
|
|
||||||
// Globale Mindmap-Instanz für externe Zugriffe setzen
|
|
||||||
window.mindmapInstance = this;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleZoom(transform) {
|
|
||||||
this.g.attr('transform', transform);
|
|
||||||
this.zoomFactor = transform.k;
|
|
||||||
|
|
||||||
// Knotengröße anpassen, um bei Zoom lesbar zu bleiben
|
|
||||||
if (this.nodeElements) {
|
|
||||||
this.nodeElements
|
|
||||||
.attr('r', d => (d === this.selectedNode ? this.selectedNodeRadius : this.nodeRadius) / Math.sqrt(transform.k));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Textgröße anpassen
|
|
||||||
if (this.textElements) {
|
|
||||||
this.textElements
|
|
||||||
.style('font-size', `${12 / Math.sqrt(transform.k)}px`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadData() {
|
|
||||||
try {
|
|
||||||
// Ladeindikator anzeigen
|
|
||||||
this.showLoading();
|
|
||||||
|
|
||||||
// Verwende sofort die Standarddaten für eine schnelle erste Anzeige
|
|
||||||
this.nodes = [...this.defaultNodes];
|
|
||||||
this.links = [...this.defaultLinks];
|
|
||||||
|
|
||||||
// Visualisierung sofort aktualisieren
|
|
||||||
this.isLoading = false;
|
|
||||||
this.updateVisualization();
|
|
||||||
|
|
||||||
// Status auf bereit setzen - don't wait for API
|
|
||||||
this.container.attr('data-status', 'ready');
|
|
||||||
|
|
||||||
// API-Aufruf mit kürzerem Timeout im Hintergrund durchführen
|
|
||||||
try {
|
|
||||||
const controller = new AbortController();
|
|
||||||
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 Sekunden Timeout
|
|
||||||
|
|
||||||
const response = await fetch('/api/mindmap', {
|
|
||||||
signal: controller.signal,
|
|
||||||
headers: {
|
|
||||||
'Cache-Control': 'no-cache',
|
|
||||||
'Pragma': 'no-cache'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
console.warn(`HTTP Fehler: ${response.status}, versuche erneute Verbindung`);
|
|
||||||
|
|
||||||
// Bei Verbindungsfehler versuchen, die Verbindung neu herzustellen
|
|
||||||
const retryResponse = await fetch('/api/refresh-mindmap', {
|
|
||||||
headers: {
|
|
||||||
'Cache-Control': 'no-cache',
|
|
||||||
'Pragma': 'no-cache'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!retryResponse.ok) {
|
|
||||||
throw new Error(`Retry failed with status: ${retryResponse.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const retryData = await retryResponse.json();
|
|
||||||
|
|
||||||
if (!retryData.success || !retryData.nodes || retryData.nodes.length === 0) {
|
|
||||||
console.warn('Keine Mindmap-Daten nach Neuversuch, verwende weiterhin Standard-Daten.');
|
|
||||||
return; // Keep using default data
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flache Liste von Knoten und Verbindungen erstellen
|
|
||||||
this.nodes = [];
|
|
||||||
this.links = [];
|
|
||||||
|
|
||||||
// Knoten direkt übernehmen
|
|
||||||
retryData.nodes.forEach(node => {
|
|
||||||
this.nodes.push({
|
|
||||||
id: node.id,
|
|
||||||
name: node.name,
|
|
||||||
description: node.description || '',
|
|
||||||
thought_count: node.thought_count || 0,
|
|
||||||
color: this.generateColorFromString(node.name),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Verbindungen hinzufügen
|
|
||||||
if (node.connections && node.connections.length > 0) {
|
|
||||||
node.connections.forEach(conn => {
|
|
||||||
this.links.push({
|
|
||||||
source: node.id,
|
|
||||||
target: conn.target
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Visualisierung aktualisieren mit den tatsächlichen Daten
|
|
||||||
this.updateVisualization();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (!data || !data.nodes || data.nodes.length === 0) {
|
|
||||||
console.warn('Keine Mindmap-Daten vorhanden, verwende weiterhin Standard-Daten.');
|
|
||||||
return; // Keep using default data
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flache Liste von Knoten und Verbindungen erstellen
|
|
||||||
this.nodes = [];
|
|
||||||
this.links = [];
|
|
||||||
this.processHierarchicalData(data.nodes);
|
|
||||||
|
|
||||||
// Visualisierung aktualisieren mit den tatsächlichen Daten
|
|
||||||
this.updateVisualization();
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Fehler beim Laden der Mindmap-Daten, verwende Standarddaten:', error);
|
|
||||||
// Already using default data, no action needed
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Kritischer Fehler bei der Mindmap-Darstellung:', error);
|
|
||||||
this.showError('Fehler beim Laden der Mindmap-Daten. Bitte laden Sie die Seite neu.');
|
|
||||||
this.container.attr('data-status', 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
showLoading() {
|
|
||||||
// Element nur leeren, wenn es noch kein SVG enthält
|
|
||||||
if (!this.container.select('svg').size()) {
|
|
||||||
this.container.html(`
|
|
||||||
<div class="flex justify-center items-center h-full">
|
|
||||||
<div class="text-center">
|
|
||||||
<div class="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-primary-400 mx-auto mb-4"></div>
|
|
||||||
<p class="text-lg text-white">Mindmap wird geladen...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
processHierarchicalData(hierarchicalNodes, parentId = null) {
|
|
||||||
hierarchicalNodes.forEach(node => {
|
|
||||||
// Knoten hinzufügen, wenn noch nicht vorhanden
|
|
||||||
if (!this.nodes.find(n => n.id === node.id)) {
|
|
||||||
this.nodes.push({
|
|
||||||
id: node.id,
|
|
||||||
name: node.name,
|
|
||||||
description: node.description || '',
|
|
||||||
thought_count: node.thought_count || 0,
|
|
||||||
color: this.generateColorFromString(node.name),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verbindung zum Elternknoten hinzufügen
|
|
||||||
if (parentId !== null) {
|
|
||||||
this.links.push({
|
|
||||||
source: parentId,
|
|
||||||
target: node.id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rekursiv für Kindknoten aufrufen
|
|
||||||
if (node.children && node.children.length > 0) {
|
|
||||||
this.processHierarchicalData(node.children, node.id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
generateColorFromString(str) {
|
|
||||||
// Erzeugt eine deterministische Farbe basierend auf dem String
|
|
||||||
let hash = 0;
|
|
||||||
for (let i = 0; i < str.length; i++) {
|
|
||||||
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verwende deterministische Farbe aus unserem Farbschema
|
|
||||||
const colors = [
|
|
||||||
'#4080ff', // primary-400
|
|
||||||
'#a040ff', // secondary-400
|
|
||||||
'#205cf5', // primary-500
|
|
||||||
'#8020f5', // secondary-500
|
|
||||||
'#1040e0', // primary-600
|
|
||||||
'#6010e0', // secondary-600
|
|
||||||
];
|
|
||||||
|
|
||||||
return colors[Math.abs(hash) % colors.length];
|
|
||||||
}
|
|
||||||
|
|
||||||
updateVisualization() {
|
|
||||||
// Starte die Visualisierung nur, wenn nicht mehr im Ladezustand
|
|
||||||
if (this.isLoading) return;
|
|
||||||
|
|
||||||
// Container leeren, wenn Diagramm neu erstellt wird
|
|
||||||
if (!this.svg) {
|
|
||||||
this.container.html('');
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Performance-Optimierung: Deaktiviere Transition während des Datenladens
|
|
||||||
const useTransitions = false;
|
|
||||||
|
|
||||||
// Links (Edges) erstellen
|
|
||||||
this.linkElements = this.g.selectAll('.link')
|
|
||||||
.data(this.links)
|
|
||||||
.join(
|
|
||||||
enter => enter.append('line')
|
|
||||||
.attr('class', 'link')
|
|
||||||
.attr('stroke', '#ffffff30')
|
|
||||||
.attr('stroke-width', 2)
|
|
||||||
.attr('stroke-dasharray', d => d.relation_type === 'contradicts' ? '5,5' : null)
|
|
||||||
.attr('marker-end', d => d.relation_type === 'builds_upon' ? 'url(#arrowhead)' : null),
|
|
||||||
update => update
|
|
||||||
.attr('stroke', '#ffffff30')
|
|
||||||
.attr('stroke-dasharray', d => d.relation_type === 'contradicts' ? '5,5' : null)
|
|
||||||
.attr('marker-end', d => d.relation_type === 'builds_upon' ? 'url(#arrowhead)' : null),
|
|
||||||
exit => exit.remove()
|
|
||||||
);
|
|
||||||
|
|
||||||
// Pfeilspitze für gerichtete Beziehungen hinzufügen (falls noch nicht vorhanden)
|
|
||||||
if (!this.svg.select('defs').node()) {
|
|
||||||
const defs = this.svg.append('defs');
|
|
||||||
defs.append('marker')
|
|
||||||
.attr('id', 'arrowhead')
|
|
||||||
.attr('viewBox', '0 -5 10 10')
|
|
||||||
.attr('refX', 20)
|
|
||||||
.attr('refY', 0)
|
|
||||||
.attr('orient', 'auto')
|
|
||||||
.attr('markerWidth', 6)
|
|
||||||
.attr('markerHeight', 6)
|
|
||||||
.append('path')
|
|
||||||
.attr('d', 'M0,-5L10,0L0,5')
|
|
||||||
.attr('fill', '#ffffff50');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simplified Effekte definieren, falls noch nicht vorhanden
|
|
||||||
if (!this.svg.select('#glow').node()) {
|
|
||||||
const defs = this.svg.select('defs').size() ? this.svg.select('defs') : this.svg.append('defs');
|
|
||||||
|
|
||||||
// Glow-Effekt für Knoten
|
|
||||||
const filter = defs.append('filter')
|
|
||||||
.attr('id', 'glow')
|
|
||||||
.attr('x', '-50%')
|
|
||||||
.attr('y', '-50%')
|
|
||||||
.attr('width', '200%')
|
|
||||||
.attr('height', '200%');
|
|
||||||
|
|
||||||
filter.append('feGaussianBlur')
|
|
||||||
.attr('stdDeviation', '1')
|
|
||||||
.attr('result', 'blur');
|
|
||||||
|
|
||||||
filter.append('feComposite')
|
|
||||||
.attr('in', 'SourceGraphic')
|
|
||||||
.attr('in2', 'blur')
|
|
||||||
.attr('operator', 'over');
|
|
||||||
|
|
||||||
// Blur-Effekt für Schatten
|
|
||||||
const blurFilter = defs.append('filter')
|
|
||||||
.attr('id', 'blur')
|
|
||||||
.attr('x', '-50%')
|
|
||||||
.attr('y', '-50%')
|
|
||||||
.attr('width', '200%')
|
|
||||||
.attr('height', '200%');
|
|
||||||
|
|
||||||
blurFilter.append('feGaussianBlur')
|
|
||||||
.attr('stdDeviation', '1');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Knoten-Gruppe erstellen/aktualisieren
|
|
||||||
const nodeGroups = this.g.selectAll('.node-group')
|
|
||||||
.data(this.nodes)
|
|
||||||
.join(
|
|
||||||
enter => {
|
|
||||||
const group = enter.append('g')
|
|
||||||
.attr('class', 'node-group')
|
|
||||||
.call(d3.drag()
|
|
||||||
.on('start', (event, d) => this.dragStarted(event, d))
|
|
||||||
.on('drag', (event, d) => this.dragged(event, d))
|
|
||||||
.on('end', (event, d) => this.dragEnded(event, d)));
|
|
||||||
|
|
||||||
// Hintergrundschatten für besseren Kontrast
|
|
||||||
group.append('circle')
|
|
||||||
.attr('class', 'node-shadow')
|
|
||||||
.attr('r', d => this.nodeRadius * 1.2)
|
|
||||||
.attr('fill', 'rgba(0, 0, 0, 0.3)')
|
|
||||||
.attr('filter', 'url(#blur)');
|
|
||||||
|
|
||||||
// Kreis für jeden Knoten
|
|
||||||
group.append('circle')
|
|
||||||
.attr('class', d => `node ${this.isNodeBookmarked(d.id) ? 'bookmarked' : ''}`)
|
|
||||||
.attr('r', this.nodeRadius)
|
|
||||||
.attr('fill', d => d.color || this.generateColorFromString(d.name))
|
|
||||||
.attr('stroke', d => this.isNodeBookmarked(d.id) ? '#FFD700' : '#ffffff50')
|
|
||||||
.attr('stroke-width', d => this.isNodeBookmarked(d.id) ? 3 : 2)
|
|
||||||
.attr('filter', 'url(#glow)');
|
|
||||||
|
|
||||||
// Text-Label mit besserem Kontrast
|
|
||||||
group.append('text')
|
|
||||||
.attr('class', 'node-label')
|
|
||||||
.attr('dy', '0.35em')
|
|
||||||
.attr('text-anchor', 'middle')
|
|
||||||
.attr('fill', '#ffffff')
|
|
||||||
.attr('stroke', 'rgba(0, 0, 0, 0.4)')
|
|
||||||
.attr('stroke-width', '0.7px')
|
|
||||||
.attr('paint-order', 'stroke')
|
|
||||||
.style('font-size', '12px')
|
|
||||||
.style('font-weight', '500')
|
|
||||||
.style('pointer-events', 'none')
|
|
||||||
.text(d => d.name.length > 12 ? d.name.slice(0, 10) + '...' : d.name);
|
|
||||||
|
|
||||||
// Interaktivität hinzufügen
|
|
||||||
group
|
|
||||||
.on('mouseover', (event, d) => this.nodeMouseover(event, d))
|
|
||||||
.on('mouseout', (event, d) => this.nodeMouseout(event, d))
|
|
||||||
.on('click', (event, d) => this.nodeClicked(event, d));
|
|
||||||
|
|
||||||
return group;
|
|
||||||
},
|
|
||||||
update => {
|
|
||||||
// Knoten aktualisieren
|
|
||||||
update.select('.node')
|
|
||||||
.attr('class', d => `node ${this.isNodeBookmarked(d.id) ? 'bookmarked' : ''}`)
|
|
||||||
.attr('fill', d => d.color || this.generateColorFromString(d.name))
|
|
||||||
.attr('stroke', d => this.isNodeBookmarked(d.id) ? '#FFD700' : '#ffffff50')
|
|
||||||
.attr('stroke-width', d => this.isNodeBookmarked(d.id) ? 3 : 2);
|
|
||||||
|
|
||||||
// Text aktualisieren
|
|
||||||
update.select('.node-label')
|
|
||||||
.text(d => d.name.length > 12 ? d.name.slice(0, 10) + '...' : d.name);
|
|
||||||
|
|
||||||
return update;
|
|
||||||
},
|
|
||||||
exit => exit.remove()
|
|
||||||
);
|
|
||||||
|
|
||||||
// Einzelne Elemente für direkten Zugriff speichern
|
|
||||||
this.nodeElements = this.g.selectAll('.node');
|
|
||||||
this.textElements = this.g.selectAll('.node-label');
|
|
||||||
|
|
||||||
// Performance-Optimierung: Weniger Simulationsschritte für schnellere Stabilisierung
|
|
||||||
this.simulation
|
|
||||||
.nodes(this.nodes)
|
|
||||||
.on('tick', () => this.ticked())
|
|
||||||
.alpha(0.3) // Reduzierter Wert für schnellere Stabilisierung
|
|
||||||
.alphaDecay(0.05); // Erhöhter Wert für schnellere Stabilisierung
|
|
||||||
|
|
||||||
this.simulation.force('link')
|
|
||||||
.links(this.links);
|
|
||||||
|
|
||||||
// Simulation neu starten
|
|
||||||
this.simulation.restart();
|
|
||||||
|
|
||||||
// Update connection counts
|
|
||||||
this.updateConnectionCounts();
|
|
||||||
}
|
|
||||||
|
|
||||||
ticked() {
|
|
||||||
// Linienpositionen aktualisieren
|
|
||||||
this.linkElements
|
|
||||||
.attr('x1', d => d.source.x)
|
|
||||||
.attr('y1', d => d.source.y)
|
|
||||||
.attr('x2', d => d.target.x)
|
|
||||||
.attr('y2', d => d.target.y);
|
|
||||||
|
|
||||||
// Knotenpositionen aktualisieren
|
|
||||||
this.g.selectAll('.node-group')
|
|
||||||
.attr('transform', d => `translate(${d.x}, ${d.y})`);
|
|
||||||
}
|
|
||||||
|
|
||||||
dragStarted(event, d) {
|
|
||||||
if (!event.active) this.simulation.alphaTarget(0.3).restart();
|
|
||||||
d.fx = d.x;
|
|
||||||
d.fy = d.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
dragged(event, d) {
|
|
||||||
d.fx = event.x;
|
|
||||||
d.fy = event.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
dragEnded(event, d) {
|
|
||||||
if (!event.active) this.simulation.alphaTarget(0);
|
|
||||||
d.fx = null;
|
|
||||||
d.fy = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
nodeMouseover(event, d) {
|
|
||||||
this.mouseoverNode = d;
|
|
||||||
|
|
||||||
// Tooltip anzeigen
|
|
||||||
if (this.tooltipEnabled) {
|
|
||||||
const isBookmarked = this.isNodeBookmarked(d.id);
|
|
||||||
const tooltipContent = `
|
|
||||||
<div class="p-2">
|
|
||||||
<strong>${d.name}</strong>
|
|
||||||
${d.description ? `<p class="text-sm text-gray-200 mt-1">${d.description}</p>` : ''}
|
|
||||||
<div class="text-xs text-gray-300 mt-1">
|
|
||||||
Gedanken: ${d.thought_count}
|
|
||||||
</div>
|
|
||||||
<div class="mt-2">
|
|
||||||
<button id="bookmark-button" class="px-2 py-1 text-xs rounded bg-gray-700 hover:bg-gray-600 text-white"
|
|
||||||
data-nodeid="${d.id}">
|
|
||||||
${isBookmarked ? '<i class="fas fa-bookmark mr-1"></i> Gemerkt' : '<i class="far fa-bookmark mr-1"></i> Merken'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
this.tooltipDiv
|
|
||||||
.html(tooltipContent)
|
|
||||||
.style('left', (event.pageX + 10) + 'px')
|
|
||||||
.style('top', (event.pageY - 10) + 'px')
|
|
||||||
.transition()
|
|
||||||
.duration(200)
|
|
||||||
.style('opacity', 1);
|
|
||||||
|
|
||||||
// Event-Listener für den Bookmark-Button hinzufügen
|
|
||||||
document.getElementById('bookmark-button').addEventListener('click', (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
const nodeId = e.currentTarget.getAttribute('data-nodeid');
|
|
||||||
const isNowBookmarked = this.toggleBookmark(nodeId);
|
|
||||||
|
|
||||||
// Button-Text aktualisieren
|
|
||||||
if (isNowBookmarked) {
|
|
||||||
e.currentTarget.innerHTML = '<i class="fas fa-bookmark mr-1"></i> Gemerkt';
|
|
||||||
} else {
|
|
||||||
e.currentTarget.innerHTML = '<i class="far fa-bookmark mr-1"></i> Merken';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Knoten visuell hervorheben
|
|
||||||
d3.select(event.currentTarget).select('circle')
|
|
||||||
.transition()
|
|
||||||
.duration(200)
|
|
||||||
.attr('r', this.nodeRadius * 1.2)
|
|
||||||
.attr('stroke', this.isNodeBookmarked(d.id) ? '#FFD700' : '#ffffff');
|
|
||||||
}
|
|
||||||
|
|
||||||
nodeMouseout(event, d) {
|
|
||||||
this.mouseoverNode = null;
|
|
||||||
|
|
||||||
// Tooltip ausblenden
|
|
||||||
if (this.tooltipEnabled) {
|
|
||||||
this.tooltipDiv
|
|
||||||
.transition()
|
|
||||||
.duration(200)
|
|
||||||
.style('opacity', 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Knoten-Stil zurücksetzen, wenn nicht ausgewählt
|
|
||||||
const nodeElement = d3.select(event.currentTarget).select('circle');
|
|
||||||
if (d !== this.selectedNode) {
|
|
||||||
const isBookmarked = this.isNodeBookmarked(d.id);
|
|
||||||
nodeElement
|
|
||||||
.transition()
|
|
||||||
.duration(200)
|
|
||||||
.attr('r', this.nodeRadius)
|
|
||||||
.attr('stroke', isBookmarked ? '#FFD700' : '#ffffff50')
|
|
||||||
.attr('stroke-width', isBookmarked ? 3 : 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
nodeClicked(event, d) {
|
|
||||||
// Frühere Auswahl zurücksetzen
|
|
||||||
if (this.selectedNode && this.selectedNode !== d) {
|
|
||||||
this.g.selectAll('.node')
|
|
||||||
.filter(n => n === this.selectedNode)
|
|
||||||
.transition()
|
|
||||||
.duration(200)
|
|
||||||
.attr('r', this.nodeRadius)
|
|
||||||
.attr('stroke', '#ffffff50');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Neue Auswahl hervorheben
|
|
||||||
if (this.selectedNode !== d) {
|
|
||||||
this.selectedNode = d;
|
|
||||||
d3.select(event.currentTarget).select('circle')
|
|
||||||
.transition()
|
|
||||||
.duration(200)
|
|
||||||
.attr('r', this.selectedNodeRadius)
|
|
||||||
.attr('stroke', '#ffffff');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Callback mit Node-Daten aufrufen
|
|
||||||
this.onNodeClick(d);
|
|
||||||
}
|
|
||||||
|
|
||||||
showError(message) {
|
|
||||||
this.container.html(`
|
|
||||||
<div class="w-full text-center p-6">
|
|
||||||
<div class="mb-4 text-red-500">
|
|
||||||
<i class="fas fa-exclamation-triangle text-4xl"></i>
|
|
||||||
</div>
|
|
||||||
<p class="text-lg text-gray-200">${message}</p>
|
|
||||||
</div>
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fokussiert die Ansicht auf einen bestimmten Knoten
|
|
||||||
focusNode(nodeId) {
|
|
||||||
const node = this.nodes.find(n => n.id === nodeId);
|
|
||||||
if (!node) return;
|
|
||||||
|
|
||||||
// Simuliere einen Klick auf den Knoten
|
|
||||||
const nodeElement = this.g.selectAll('.node-group')
|
|
||||||
.filter(d => d.id === nodeId);
|
|
||||||
|
|
||||||
nodeElement.dispatch('click');
|
|
||||||
|
|
||||||
// Zentriere den Knoten in der Ansicht
|
|
||||||
const transform = d3.zoomIdentity
|
|
||||||
.translate(this.width / 2, this.height / 2)
|
|
||||||
.scale(1.2)
|
|
||||||
.translate(-node.x, -node.y);
|
|
||||||
|
|
||||||
this.svg.transition()
|
|
||||||
.duration(750)
|
|
||||||
.call(
|
|
||||||
d3.zoom().transform,
|
|
||||||
transform
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filtert die Mindmap basierend auf einem Suchbegriff
|
|
||||||
filterBySearchTerm(searchTerm) {
|
|
||||||
if (!searchTerm || searchTerm.trim() === '') {
|
|
||||||
// Alle Knoten anzeigen
|
|
||||||
this.g.selectAll('.node-group')
|
|
||||||
.style('opacity', 1)
|
|
||||||
.style('pointer-events', 'all');
|
|
||||||
|
|
||||||
this.g.selectAll('.link')
|
|
||||||
.style('opacity', 1);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const searchLower = searchTerm.toLowerCase();
|
|
||||||
const matchingNodes = this.nodes.filter(node =>
|
|
||||||
node.name.toLowerCase().includes(searchLower) ||
|
|
||||||
(node.description && node.description.toLowerCase().includes(searchLower))
|
|
||||||
);
|
|
||||||
|
|
||||||
const matchingNodeIds = new Set(matchingNodes.map(n => n.id));
|
|
||||||
|
|
||||||
// Passende Knoten hervorheben, andere ausblenden
|
|
||||||
this.g.selectAll('.node-group')
|
|
||||||
.style('opacity', d => matchingNodeIds.has(d.id) ? 1 : 0.2)
|
|
||||||
.style('pointer-events', d => matchingNodeIds.has(d.id) ? 'all' : 'none');
|
|
||||||
|
|
||||||
// Verbindungen zwischen passenden Knoten hervorheben
|
|
||||||
this.g.selectAll('.link')
|
|
||||||
.style('opacity', d =>
|
|
||||||
matchingNodeIds.has(d.source.id) && matchingNodeIds.has(d.target.id) ? 1 : 0.1
|
|
||||||
);
|
|
||||||
|
|
||||||
// Auf den ersten passenden Knoten fokussieren, wenn vorhanden
|
|
||||||
if (matchingNodes.length > 0) {
|
|
||||||
this.focusNode(matchingNodes[0].id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the thought_count property for each node based on existing connections
|
|
||||||
*/
|
|
||||||
updateConnectionCounts() {
|
|
||||||
// Reset all counts first
|
|
||||||
this.nodes.forEach(node => {
|
|
||||||
// Initialize thought_count if it doesn't exist
|
|
||||||
if (typeof node.thought_count !== 'number') {
|
|
||||||
node.thought_count = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Count connections for this node
|
|
||||||
const connectedNodes = this.getConnectedNodes(node);
|
|
||||||
node.thought_count = connectedNodes.length;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update UI to show counts
|
|
||||||
this.updateNodeLabels();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the visual representation of node labels to include connection counts
|
|
||||||
*/
|
|
||||||
updateNodeLabels() {
|
|
||||||
if (!this.textElements) return;
|
|
||||||
|
|
||||||
this.textElements.text(d => {
|
|
||||||
if (d.thought_count > 0) {
|
|
||||||
return `${d.name} (${d.thought_count})`;
|
|
||||||
}
|
|
||||||
return d.name;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a new connection between nodes and updates the counts
|
|
||||||
*/
|
|
||||||
addConnection(sourceNode, targetNode) {
|
|
||||||
if (!sourceNode || !targetNode) return false;
|
|
||||||
|
|
||||||
// Check if connection already exists
|
|
||||||
if (this.isConnected(sourceNode, targetNode)) return false;
|
|
||||||
|
|
||||||
// Add new connection
|
|
||||||
this.links.push({
|
|
||||||
source: sourceNode.id,
|
|
||||||
target: targetNode.id
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update counts
|
|
||||||
this.updateConnectionCounts();
|
|
||||||
|
|
||||||
// Update visualization
|
|
||||||
this.updateVisualization();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lädt gemerkete Knoten aus dem LocalStorage
|
|
||||||
loadBookmarkedNodes() {
|
|
||||||
try {
|
|
||||||
const bookmarked = localStorage.getItem('bookmarkedNodes');
|
|
||||||
return bookmarked ? JSON.parse(bookmarked) : [];
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Fehler beim Laden der gemerkten Knoten:', error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Speichert gemerkete Knoten im LocalStorage
|
|
||||||
saveBookmarkedNodes() {
|
|
||||||
try {
|
|
||||||
localStorage.setItem('bookmarkedNodes', JSON.stringify(this.bookmarkedNodes));
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Fehler beim Speichern der gemerkten Knoten:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prüft, ob ein Knoten gemerkt ist
|
|
||||||
isNodeBookmarked(nodeId) {
|
|
||||||
return this.bookmarkedNodes.includes(nodeId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merkt einen Knoten oder hebt die Markierung auf
|
|
||||||
toggleBookmark(nodeId) {
|
|
||||||
const index = this.bookmarkedNodes.indexOf(nodeId);
|
|
||||||
if (index === -1) {
|
|
||||||
// Node hinzufügen
|
|
||||||
this.bookmarkedNodes.push(nodeId);
|
|
||||||
this.updateNodeAppearance(nodeId, true);
|
|
||||||
} else {
|
|
||||||
// Node entfernen
|
|
||||||
this.bookmarkedNodes.splice(index, 1);
|
|
||||||
this.updateNodeAppearance(nodeId, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Änderungen speichern
|
|
||||||
this.saveBookmarkedNodes();
|
|
||||||
|
|
||||||
// Event auslösen für andere Komponenten
|
|
||||||
const event = new CustomEvent('nodeBookmarkToggled', {
|
|
||||||
detail: {
|
|
||||||
nodeId: nodeId,
|
|
||||||
isBookmarked: index === -1
|
|
||||||
}
|
|
||||||
});
|
|
||||||
document.dispatchEvent(event);
|
|
||||||
|
|
||||||
return index === -1; // true wenn jetzt gemerkt, false wenn Markierung aufgehoben
|
|
||||||
}
|
|
||||||
|
|
||||||
// Aktualisiert das Aussehen eines Knotens basierend auf Bookmark-Status
|
|
||||||
updateNodeAppearance(nodeId, isBookmarked) {
|
|
||||||
this.g.selectAll('.node-group')
|
|
||||||
.filter(d => d.id === nodeId)
|
|
||||||
.select('.node')
|
|
||||||
.classed('bookmarked', isBookmarked)
|
|
||||||
.attr('stroke', isBookmarked ? '#FFD700' : '#ffffff50')
|
|
||||||
.attr('stroke-width', isBookmarked ? 3 : 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Aktualisiert das Aussehen aller gemerkten Knoten
|
|
||||||
updateAllBookmarkedNodes() {
|
|
||||||
this.g.selectAll('.node-group')
|
|
||||||
.each((d) => {
|
|
||||||
const isBookmarked = this.isNodeBookmarked(d.id);
|
|
||||||
this.updateNodeAppearance(d.id, isBookmarked);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gibt alle direkt verbundenen Knoten eines Knotens zurück
|
|
||||||
* @param {Object} node - Der Knoten, für den die Verbindungen gesucht werden
|
|
||||||
* @returns {Array} Array der verbundenen Knotenobjekte
|
|
||||||
*/
|
|
||||||
getConnectedNodes(node) {
|
|
||||||
if (!node || !this.links || !this.nodes) return [];
|
|
||||||
const nodeId = node.id;
|
|
||||||
const connectedIds = new Set();
|
|
||||||
this.links.forEach(link => {
|
|
||||||
if (link.source === nodeId || (link.source && link.source.id === nodeId)) {
|
|
||||||
connectedIds.add(link.target.id ? link.target.id : link.target);
|
|
||||||
}
|
|
||||||
if (link.target === nodeId || (link.target && link.target.id === nodeId)) {
|
|
||||||
connectedIds.add(link.source.id ? link.source.id : link.source);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return this.nodes.filter(n => connectedIds.has(n.id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exportiere die Klasse für die Verwendung in anderen Modulen
|
|
||||||
window.MindMapVisualization = MindMapVisualization;
|
|
||||||
2724
static/js/update_mindmap.js
Normal file
2724
static/js/update_mindmap.js
Normal file
File diff suppressed because it is too large
Load Diff
1078
static/js/update_mindmap.js.bak
Normal file
1078
static/js/update_mindmap.js.bak
Normal file
File diff suppressed because it is too large
Load Diff
BIN
static/js/update_mindmap.js.new
Normal file
BIN
static/js/update_mindmap.js.new
Normal file
Binary file not shown.
1078
static/js/update_mindmap.js.original
Normal file
1078
static/js/update_mindmap.js.original
Normal file
File diff suppressed because it is too large
Load Diff
1904
static/mindmap.js
1904
static/mindmap.js
File diff suppressed because it is too large
Load Diff
@@ -18,11 +18,11 @@ class NeuralNetworkBackground {
|
|||||||
|
|
||||||
// Standardkonfiguration mit subtileren Werten
|
// Standardkonfiguration mit subtileren Werten
|
||||||
this.config = {
|
this.config = {
|
||||||
nodeCount: 50, // Weniger Knoten
|
nodeCount: 10, // Weniger Knoten
|
||||||
nodeSize: 1.2, // Kleinere Knoten
|
nodeSize: 1.2, // Kleinere Knoten
|
||||||
connectionDistance: 150, // Reduzierte Verbindungsdistanz
|
connectionDistance: 150, // Reduzierte Verbindungsdistanz
|
||||||
connectionOpacity: 0.3, // Sanftere Verbindungslinien
|
connectionOpacity: 0.3, // Sanftere Verbindungslinien
|
||||||
clusterCount: 2, // Weniger Cluster
|
clusterCount: 7, // Weniger Cluster
|
||||||
clusterRadius: 380, // Größerer Cluster-Radius für mehr Verteilung
|
clusterRadius: 380, // Größerer Cluster-Radius für mehr Verteilung
|
||||||
animationSpeed: 0.25, // Langsamere Animation
|
animationSpeed: 0.25, // Langsamere Animation
|
||||||
flowDensity: 0.05, // Deutlich weniger Flussanimationen
|
flowDensity: 0.05, // Deutlich weniger Flussanimationen
|
||||||
@@ -1106,4 +1106,66 @@ window.addEventListener('beforeunload', () => {
|
|||||||
if (window.neuralNetworkBackground) {
|
if (window.neuralNetworkBackground) {
|
||||||
window.neuralNetworkBackground.destroy();
|
window.neuralNetworkBackground.destroy();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function applyNeuralNetworkStyle(cy) {
|
||||||
|
cy.style()
|
||||||
|
.selector('node')
|
||||||
|
.style({
|
||||||
|
'label': 'data(label)',
|
||||||
|
'text-valign': 'center',
|
||||||
|
'text-halign': 'center',
|
||||||
|
'color': 'data(fontColor)',
|
||||||
|
'text-outline-width': 2,
|
||||||
|
'text-outline-color': 'rgba(0,0,0,0.8)',
|
||||||
|
'text-outline-opacity': 0.9,
|
||||||
|
'font-size': 'data(fontSize)',
|
||||||
|
'font-weight': '500',
|
||||||
|
'text-margin-y': 8,
|
||||||
|
'width': function(ele) {
|
||||||
|
if (ele.data('isCenter')) return 120;
|
||||||
|
return ele.data('neuronSize') ? ele.data('neuronSize') * 10 : 80;
|
||||||
|
},
|
||||||
|
'height': function(ele) {
|
||||||
|
if (ele.data('isCenter')) return 120;
|
||||||
|
return ele.data('neuronSize') ? ele.data('neuronSize') * 10 : 80;
|
||||||
|
},
|
||||||
|
'background-color': 'data(color)',
|
||||||
|
'background-opacity': 0.9,
|
||||||
|
'border-width': 2,
|
||||||
|
'border-color': '#ffffff',
|
||||||
|
'border-opacity': 0.8,
|
||||||
|
'shape': 'ellipse',
|
||||||
|
'transition-property': 'background-color, background-opacity, border-width',
|
||||||
|
'transition-duration': '0.3s',
|
||||||
|
'transition-timing-function': 'ease-in-out'
|
||||||
|
})
|
||||||
|
.selector('edge')
|
||||||
|
.style({
|
||||||
|
'width': function(ele) {
|
||||||
|
return ele.data('strength') ? ele.data('strength') * 3 : 1;
|
||||||
|
},
|
||||||
|
'curve-style': 'bezier',
|
||||||
|
'line-color': function(ele) {
|
||||||
|
const sourceColor = ele.source().data('color');
|
||||||
|
return sourceColor || '#8a8aaa';
|
||||||
|
},
|
||||||
|
'line-opacity': function(ele) {
|
||||||
|
return ele.data('strength') ? ele.data('strength') * 0.8 : 0.4;
|
||||||
|
},
|
||||||
|
'line-style': function(ele) {
|
||||||
|
const strength = ele.data('strength');
|
||||||
|
if (!strength) return 'solid';
|
||||||
|
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%',
|
||||||
|
'transition-property': 'line-opacity, width',
|
||||||
|
'transition-duration': '0.3s',
|
||||||
|
'transition-timing-function': 'ease-in-out'
|
||||||
|
})
|
||||||
|
.update();
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
46
templates/admin/update_database.html
Normal file
46
templates/admin/update_database.html
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
{% 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,8 +6,7 @@
|
|||||||
<title>Systades - {% block title %}{% endblock %}</title>
|
<title>Systades - {% block title %}{% endblock %}</title>
|
||||||
|
|
||||||
<!-- Favicon -->
|
<!-- Favicon -->
|
||||||
<link rel="icon" href="{{ url_for('static', filename='img/favicon.svg') }}" type="image/svg+xml">
|
<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.ico') }}" sizes="any">
|
|
||||||
|
|
||||||
<!-- Meta Tags -->
|
<!-- Meta Tags -->
|
||||||
<meta name="description" content="Eine interaktive Plattform zum Visualisieren, Erforschen und Teilen von Wissen">
|
<meta name="description" content="Eine interaktive Plattform zum Visualisieren, Erforschen und Teilen von Wissen">
|
||||||
@@ -59,6 +58,20 @@
|
|||||||
800: '#0e1220',
|
800: '#0e1220',
|
||||||
900: '#0a0e19'
|
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'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -69,8 +82,8 @@
|
|||||||
<link href="{{ url_for('static', filename='fonts/inter.css') }}" rel="stylesheet">
|
<link href="{{ url_for('static', filename='fonts/inter.css') }}" rel="stylesheet">
|
||||||
<link href="{{ url_for('static', filename='fonts/jetbrains-mono.css') }}" rel="stylesheet">
|
<link href="{{ url_for('static', filename='fonts/jetbrains-mono.css') }}" rel="stylesheet">
|
||||||
|
|
||||||
<!-- Icons - Self-hosted Font Awesome -->
|
<!-- Font Awesome vom CDN -->
|
||||||
<link href="{{ url_for('static', filename='css/all.min.css') }}" rel="stylesheet">
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" rel="stylesheet">
|
||||||
|
|
||||||
<!-- Assistent CSS -->
|
<!-- Assistent CSS -->
|
||||||
<link href="{{ url_for('static', filename='css/assistant.css') }}" rel="stylesheet">
|
<link href="{{ url_for('static', filename='css/assistant.css') }}" rel="stylesheet">
|
||||||
@@ -87,6 +100,9 @@
|
|||||||
<!-- Neural Network Background CSS -->
|
<!-- Neural Network Background CSS -->
|
||||||
<link href="{{ url_for('static', filename='css/neural-network-background.css') }}" rel="stylesheet">
|
<link href="{{ url_for('static', filename='css/neural-network-background.css') }}" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- Mindmap CSS -->
|
||||||
|
<link href="{{ url_for('static', filename='css/mindmap.css', v='1.0.1') }}" rel="stylesheet">
|
||||||
|
|
||||||
<!-- D3.js für Visualisierungen -->
|
<!-- D3.js für Visualisierungen -->
|
||||||
<script src="https://d3js.org/d3.v7.min.js"></script>
|
<script src="https://d3js.org/d3.v7.min.js"></script>
|
||||||
|
|
||||||
@@ -95,12 +111,6 @@
|
|||||||
|
|
||||||
<!-- ChatGPT Assistant -->
|
<!-- ChatGPT Assistant -->
|
||||||
<script src="{{ url_for('static', filename='js/modules/chatgpt-assistant.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/modules/chatgpt-assistant.js') }}"></script>
|
||||||
|
|
||||||
<!-- MindMap Visualization Module -->
|
|
||||||
<script src="{{ url_for('static', filename='js/modules/mindmap.js') }}"></script>
|
|
||||||
|
|
||||||
<!-- MindMap Page Module -->
|
|
||||||
<script src="{{ url_for('static', filename='js/modules/mindmap-page.js') }}"></script>
|
|
||||||
|
|
||||||
<!-- Neural Network Background Script -->
|
<!-- Neural Network Background Script -->
|
||||||
<script src="{{ url_for('static', filename='neural-network-background.js') }}"></script>
|
<script src="{{ url_for('static', filename='neural-network-background.js') }}"></script>
|
||||||
@@ -111,18 +121,20 @@
|
|||||||
<!-- Seitenspezifische Styles -->
|
<!-- Seitenspezifische Styles -->
|
||||||
{% block extra_css %}{% endblock %}
|
{% block extra_css %}{% endblock %}
|
||||||
|
|
||||||
<!-- Custom dark mode styles -->
|
<!-- Custom dark/light mode styles -->
|
||||||
<!-- ► ► Farb‑Token strikt getrennt ◄ ◄ -->
|
<!-- ► ► Farb‑Token strikt getrennt ◄ ◄ -->
|
||||||
<style>
|
<style>
|
||||||
/* Light‑Mode */
|
/* Light‑Mode */
|
||||||
:root {
|
:root {
|
||||||
--bg-primary:#f4f6fa;
|
--bg-primary:#f8fafc;
|
||||||
--bg-secondary:#e9ecf3;
|
--bg-secondary:#f1f5f9;
|
||||||
--text-primary:#232837;
|
--text-primary:#232837;
|
||||||
--text-secondary:#475569;
|
--text-secondary:#475569;
|
||||||
--accent-primary:#7c3aed;
|
--accent-primary:#7c3aed;
|
||||||
--accent-secondary:#8b5cf6;
|
--accent-secondary:#8b5cf6;
|
||||||
--glow-effect:0 0 8px rgba(139,92,246,.08);
|
--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‑Mode */
|
||||||
.dark {
|
.dark {
|
||||||
@@ -136,7 +148,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply min-h-screen bg-[color:var(--bg-primary)] text-[color:var(--text-primary)] transition-colors duration-300;
|
@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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Utilities */
|
/* Utilities */
|
||||||
@@ -149,6 +162,151 @@
|
|||||||
.glass-navbar { @apply glass-morphism border backdrop-blur-xl; }
|
.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); }
|
.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); }
|
.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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body data-page="{{ request.endpoint }}" class="relative overflow-x-hidden dark bg-gray-900 text-white" x-data="{
|
<body data-page="{{ request.endpoint }}" class="relative overflow-x-hidden dark bg-gray-900 text-white" x-data="{
|
||||||
@@ -158,6 +316,17 @@
|
|||||||
showSettingsModal: false,
|
showSettingsModal: false,
|
||||||
|
|
||||||
init() {
|
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();
|
this.fetchDarkModeFromSession();
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -167,7 +336,7 @@
|
|||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
this.darkMode = data.darkMode === 'true';
|
this.darkMode = data.darkMode === 'true';
|
||||||
document.querySelector('html').classList.toggle('dark', this.darkMode);
|
this.applyDarkMode();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
@@ -175,10 +344,17 @@
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
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() {
|
toggleDarkMode() {
|
||||||
this.darkMode = !this.darkMode;
|
this.darkMode = !this.darkMode;
|
||||||
document.querySelector('html').classList.toggle('dark', this.darkMode);
|
this.applyDarkMode();
|
||||||
|
|
||||||
|
// Server über Änderung informieren
|
||||||
fetch('/api/set_dark_mode', {
|
fetch('/api/set_dark_mode', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -189,12 +365,10 @@
|
|||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
localStorage.setItem('darkMode', this.darkMode ? 'dark' : 'light');
|
// Event auslösen für andere Komponenten
|
||||||
document.dispatchEvent(new CustomEvent('darkModeToggled', {
|
document.dispatchEvent(new CustomEvent('darkModeToggled', {
|
||||||
detail: { isDark: this.darkMode }
|
detail: { isDark: this.darkMode }
|
||||||
}));
|
}));
|
||||||
} else {
|
|
||||||
console.error('Fehler beim Speichern der Dark Mode-Einstellung:', data.error);
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
@@ -210,6 +384,7 @@
|
|||||||
<div class="container mx-auto flex justify-between items-center">
|
<div class="container mx-auto flex justify-between items-center">
|
||||||
<!-- Logo -->
|
<!-- Logo -->
|
||||||
<a href="{{ url_for('index') }}" class="flex items-center group">
|
<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>
|
<span class="text-2xl font-bold gradient-text transform transition-transform group-hover:scale-105">Systades</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
@@ -241,7 +416,7 @@
|
|||||||
class="nav-link flex items-center"
|
class="nav-link flex items-center"
|
||||||
x-bind:class="darkMode
|
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-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/30 to-indigo-500/30 text-gray-800 font-medium px-4 py-2 rounded-xl hover:shadow-md 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'">
|
||||||
<i class="fa-solid fa-robot mr-2"></i>KI-Chat
|
<i class="fa-solid fa-robot mr-2"></i>KI-Chat
|
||||||
</button>
|
</button>
|
||||||
{% if current_user.is_authenticated %}
|
{% if current_user.is_authenticated %}
|
||||||
@@ -257,25 +432,14 @@
|
|||||||
|
|
||||||
<!-- Rechte Seite -->
|
<!-- Rechte Seite -->
|
||||||
<div class="flex items-center space-x-4">
|
<div class="flex items-center space-x-4">
|
||||||
<!-- Dark Mode Toggle Switch -->
|
<!-- Dark/Light Mode Schalter -->
|
||||||
<div class="flex items-center cursor-pointer" @click="toggleDarkMode">
|
<button
|
||||||
<div class="relative w-12 h-6">
|
@click="toggleDarkMode()"
|
||||||
<input type="checkbox" id="darkModeToggle" class="sr-only" x-model="darkMode">
|
class="theme-toggle relative w-12 h-6 rounded-full transition-all duration-300 flex items-center overflow-hidden"
|
||||||
<div class="block w-12 h-6 rounded-full transition-colors duration-300"
|
aria-label="Dark Mode umschalten"
|
||||||
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"
|
<span class="sr-only" x-text="darkMode ? 'Zum Light Mode wechseln' : 'Zum Dark Mode wechseln'"></span>
|
||||||
x-bind:class="darkMode ? 'bg-purple-600 transform translate-x-6' : 'bg-white'"></div>
|
</button>
|
||||||
</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 -->
|
<!-- Profil-Link oder Login -->
|
||||||
{% if current_user.is_authenticated %}
|
{% if current_user.is_authenticated %}
|
||||||
<div class="relative" x-data="{ open: false }">
|
<div class="relative" x-data="{ open: false }">
|
||||||
@@ -289,12 +453,21 @@
|
|||||||
{% if current_user.avatar %}
|
{% if current_user.avatar %}
|
||||||
<img src="{{ current_user.avatar }}" alt="{{ current_user.username }}" class="w-full h-full object-cover">
|
<img src="{{ current_user.avatar }}" alt="{{ current_user.username }}" class="w-full h-full object-cover">
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ current_user.username[0].upper() }}
|
<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>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<span class="text-sm hidden lg:block">{{ current_user.username }}</span>
|
<span class="hidden md:block">{{ current_user.username }}</span>
|
||||||
<i class="fa-solid fa-chevron-down text-xs hidden lg:block transition-transform duration-200"
|
<i class="fas fa-chevron-down text-xs opacity-60 ml-1.5"></i>
|
||||||
x-bind:class="open ? 'transform rotate-180' : ''"></i>
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Dropdown-Menü -->
|
<!-- Dropdown-Menü -->
|
||||||
@@ -412,7 +585,7 @@
|
|||||||
class="block w-full text-left py-3.5 px-4 rounded-xl transition-all duration-200 flex items-center"
|
class="block w-full text-left py-3.5 px-4 rounded-xl transition-all duration-200 flex items-center"
|
||||||
x-bind:class="darkMode
|
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/30 to-blue-500/30 text-white hover:from-purple-600/40 hover:to-blue-500/40'
|
||||||
: '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'">
|
: 'bg-gradient-to-r from-purple-600 to-blue-500 text-white hover:from-purple-600/90 hover:to-blue-500/90'">
|
||||||
<i class="fa-solid fa-robot w-5 mr-3"></i>KI-Chat
|
<i class="fa-solid fa-robot w-5 mr-3"></i>KI-Chat
|
||||||
</button>
|
</button>
|
||||||
{% if current_user.is_authenticated %}
|
{% if current_user.is_authenticated %}
|
||||||
@@ -478,6 +651,10 @@
|
|||||||
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
||||||
Mindmap
|
Mindmap
|
||||||
</a>
|
</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 %}
|
{% if current_user.is_authenticated %}
|
||||||
<a href="{{ url_for('profile') }}" class="text-sm transition-all duration-200"
|
<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'">
|
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
||||||
@@ -557,58 +734,286 @@
|
|||||||
|
|
||||||
<!-- Hilfsscripts -->
|
<!-- Hilfsscripts -->
|
||||||
{% block scripts %}{% endblock %}
|
{% block scripts %}{% endblock %}
|
||||||
|
{% block extra_js %}{% endblock %}
|
||||||
|
|
||||||
<!-- KI-Chat Initialisierung -->
|
<!-- ChatGPT Initialisierung -->
|
||||||
<script>
|
<script>
|
||||||
// Initialisiere den ChatGPT-Assistenten direkt, um sicherzustellen,
|
// Prüfe, ob ChatGPTAssistant bereits existiert
|
||||||
// dass er auf jeder Seite verfügbar ist, selbst wenn MindMap nicht geladen ist
|
if (typeof ChatGPTAssistant === 'undefined') {
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
class ChatGPTAssistant {
|
||||||
// Prüfen, ob der Assistent bereits durch MindMap initialisiert wurde
|
constructor() {
|
||||||
if (!window.MindMap || !window.MindMap.assistant) {
|
this.chatContainer = null;
|
||||||
console.log('KI-Assistent wird direkt initialisiert...');
|
this.messages = [];
|
||||||
const assistant = new ChatGPTAssistant();
|
this.isOpen = false;
|
||||||
assistant.init();
|
}
|
||||||
|
|
||||||
// Speichere in window.MindMap, falls es existiert, oder erstelle es
|
init() {
|
||||||
if (!window.MindMap) {
|
// Chat-Container erstellen, falls noch nicht vorhanden
|
||||||
window.MindMap = {};
|
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;
|
||||||
}
|
}
|
||||||
window.MindMap.assistant = assistant;
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
// Initialisiere den ChatGPT-Assistenten direkt
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Prüfen, ob der Assistent bereits durch MindMap initialisiert wurde
|
||||||
|
if (!window.MindMap || !window.MindMap.assistant) {
|
||||||
|
console.log('KI-Assistent wird direkt initialisiert...');
|
||||||
|
const assistant = new ChatGPTAssistant();
|
||||||
|
assistant.init();
|
||||||
|
|
||||||
|
// Speichere in window.MindMap, falls es existiert, oder erstelle es
|
||||||
|
if (!window.MindMap) {
|
||||||
|
window.MindMap = {};
|
||||||
|
}
|
||||||
|
window.MindMap.assistant = assistant;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Dark/Light-Mode persistent und robust -->
|
<!-- Dark/Light-Mode vereinheitlicht -->
|
||||||
<script>
|
<script>
|
||||||
(function() {
|
// Globaler Zugriff für externe Skripte
|
||||||
function applyMode(mode) {
|
window.MindMap = window.MindMap || {};
|
||||||
if (mode === 'dark') {
|
|
||||||
document.documentElement.classList.add('dark');
|
// Funktion zum Anwenden des Dark Mode, strikt getrennt
|
||||||
localStorage.setItem('colorMode', 'dark');
|
function applyDarkModeClasses(isDarkMode) {
|
||||||
} else {
|
if (isDarkMode) {
|
||||||
document.documentElement.classList.remove('dark');
|
document.documentElement.classList.add('dark');
|
||||||
localStorage.setItem('colorMode', 'light');
|
document.body.classList.add('dark');
|
||||||
}
|
localStorage.setItem('colorMode', 'dark');
|
||||||
}
|
|
||||||
// Beim Laden: Präferenz aus localStorage oder System übernehmen
|
|
||||||
const stored = localStorage.getItem('colorMode');
|
|
||||||
if (stored === 'dark' || stored === 'light') {
|
|
||||||
applyMode(stored);
|
|
||||||
} else {
|
} else {
|
||||||
// Systempräferenz als Fallback
|
document.documentElement.classList.remove('dark');
|
||||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
document.body.classList.remove('dark');
|
||||||
applyMode(prefersDark ? 'dark' : 'light');
|
localStorage.setItem('colorMode', 'light');
|
||||||
}
|
}
|
||||||
// Umschalter für alle Mode-Toggles
|
|
||||||
window.toggleColorMode = function() {
|
// Alpine.js darkMode-Variable aktualisieren, falls zutreffend
|
||||||
const isDark = document.documentElement.classList.contains('dark');
|
const appEl = document.querySelector('body');
|
||||||
applyMode(isDark ? 'light' : 'dark');
|
if (appEl && appEl.__x) {
|
||||||
};
|
appEl.__x.$data.darkMode = isDarkMode;
|
||||||
// Optional: globales Event für andere Skripte
|
}
|
||||||
window.addEventListener('storage', function(e) {
|
|
||||||
if (e.key === 'colorMode') applyMode(e.newValue);
|
// 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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
})();
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
192
templates/community/category.html
Normal file
192
templates/community/category.html
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
{% 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 %}
|
||||||
344
templates/community/edit_post.html
Normal file
344
templates/community/edit_post.html
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
{% 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 %}
|
||||||
125
templates/community/index.html
Normal file
125
templates/community/index.html
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
{% 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 %}
|
||||||
355
templates/community/new_post.html
Normal file
355
templates/community/new_post.html
Normal file
@@ -0,0 +1,355 @@
|
|||||||
|
{% 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 %}
|
||||||
511
templates/community/post.html
Normal file
511
templates/community/post.html
Normal file
@@ -0,0 +1,511 @@
|
|||||||
|
{% 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 %}
|
||||||
137
templates/community/preview.html
Normal file
137
templates/community/preview.html
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
{% 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 %}
|
||||||
365
templates/create_mindmap.html
Normal file
365
templates/create_mindmap.html
Normal file
@@ -0,0 +1,365 @@
|
|||||||
|
{% 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 %}
|
||||||
525
templates/edit_mindmap.html
Normal file
525
templates/edit_mindmap.html
Normal file
@@ -0,0 +1,525 @@
|
|||||||
|
{% 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 id="edit-mindmap-form">
|
||||||
|
<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('my_account') }}" class="btn-cancel"> {# Zurück zur Kontoübersicht geändert #}
|
||||||
|
<i class="fas fa-arrow-left"></i>
|
||||||
|
Zurück
|
||||||
|
</a>
|
||||||
|
<button type="button" id="save-mindmap-details-btn" class="btn-submit"> {# type="button" und ID hinzugefügt #}
|
||||||
|
<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-Logik für Metadaten
|
||||||
|
const editMindmapForm = document.getElementById('edit-mindmap-form');
|
||||||
|
const saveDetailsBtn = document.getElementById('save-mindmap-details-btn');
|
||||||
|
|
||||||
|
if (saveDetailsBtn && editMindmapForm) {
|
||||||
|
saveDetailsBtn.addEventListener('click', async function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const nameInput = document.getElementById('name');
|
||||||
|
const descriptionInput = document.getElementById('description');
|
||||||
|
const isPrivateInput = document.getElementById('is_private');
|
||||||
|
|
||||||
|
const mindmapId = "{{ mindmap.id }}"; // Sicherstellen, dass mindmap.id hier verfügbar ist
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
name: nameInput.value,
|
||||||
|
description: descriptionInput.value,
|
||||||
|
is_private: isPrivateInput.checked
|
||||||
|
// Die 'data' (Knoten/Kanten) wird separat vom Cytoscape-Editor gehandhabt
|
||||||
|
};
|
||||||
|
|
||||||
|
saveDetailsBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Wird gespeichert...';
|
||||||
|
saveDetailsBtn.disabled = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/mindmaps/${mindmapId}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const result = await response.json();
|
||||||
|
showStatus('Metadaten erfolgreich gespeichert!', false);
|
||||||
|
// Optional: Weiterleitung oder Aktualisierung der Seiteninhalte
|
||||||
|
// window.location.href = "{{ url_for('my_account') }}";
|
||||||
|
} else {
|
||||||
|
const errorData = await response.json();
|
||||||
|
console.error('Fehler beim Speichern der Metadaten:', errorData);
|
||||||
|
showStatus(`Fehler: ${errorData.error || response.statusText}`, true);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Netzwerkfehler oder anderer Fehler:', error);
|
||||||
|
showStatus('Speichern fehlgeschlagen. Netzwerkproblem?', true);
|
||||||
|
} finally {
|
||||||
|
saveDetailsBtn.innerHTML = '<i class="fas fa-save"></i> Änderungen speichern';
|
||||||
|
saveDetailsBtn.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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(dataFromCytoscape) {
|
||||||
|
// Automatisches Speichern bei Änderungen der Mindmap-Struktur
|
||||||
|
// Die Metadaten (Name, Beschreibung, is_private) werden separat über das Formular oben gespeichert.
|
||||||
|
// Diese onChange Funktion kümmert sich nur um die Strukturdaten (Knoten/Kanten).
|
||||||
|
const mindmapId = "{{ mindmap.id }}";
|
||||||
|
|
||||||
|
// Debounce-Funktion, um API-Aufrufe zu limitieren
|
||||||
|
let debounceTimer;
|
||||||
|
const debounceSaveStructure = (currentMindmapData) => {
|
||||||
|
clearTimeout(debounceTimer);
|
||||||
|
debounceTimer = setTimeout(() => {
|
||||||
|
// Der Backend-Endpunkt PUT /api/mindmaps/<id> erwartet ein Objekt,
|
||||||
|
// das die zu aktualisierenden Felder enthält. Für die Struktur ist das 'data'.
|
||||||
|
const payload = {
|
||||||
|
data: currentMindmapData // Dies sind die von Cytoscape gelieferten Strukturdaten
|
||||||
|
};
|
||||||
|
|
||||||
|
// showStatus('Speichere Struktur...', false); // Status wird jetzt über Event gehandhabt
|
||||||
|
fetch(`/api/mindmaps/${mindmapId}`, { // Endpunkt angepasst
|
||||||
|
method: 'PUT', // Methode zu PUT geändert
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload) // Sende die Mindmap-Daten als { data: ... }
|
||||||
|
}).then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
response.json().then(err => {
|
||||||
|
console.error('Fehler beim Speichern der Struktur:', err);
|
||||||
|
document.dispatchEvent(new CustomEvent('mindmapError', { detail: { message: `Struktur: ${err.message || err.error || 'Speicherfehler'}` } }));
|
||||||
|
}).catch(() => {
|
||||||
|
console.error('Fehler beim Speichern der Struktur, Status:', response.statusText);
|
||||||
|
document.dispatchEvent(new CustomEvent('mindmapError', { detail: { message: `Struktur: ${response.statusText}` } }));
|
||||||
|
});
|
||||||
|
// throw new Error('Netzwerkfehler beim Speichern der Struktur'); // Wird schon behandelt
|
||||||
|
return; // Verhindere weitere Verarbeitung bei Fehler
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
}).then(responseData => {
|
||||||
|
if (responseData) { // Nur wenn response.ok war
|
||||||
|
console.log('Mindmap-Struktur erfolgreich gespeichert:', responseData);
|
||||||
|
// Die responseData von einem PUT könnte die aktualisierte Mindmap oder nur eine Erfolgsmeldung sein.
|
||||||
|
// Annahme: { message: "Mindmap updated successfully", mindmap: { ... } } oder ähnlich
|
||||||
|
document.dispatchEvent(new CustomEvent('mindmapSaved', { detail: { message: 'Struktur aktualisiert!' }}));
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('Netzwerkfehler oder anderer Fehler beim Speichern der Struktur:', error);
|
||||||
|
// Vermeide doppelte Fehlermeldung, falls schon durch !response.ok behandelt
|
||||||
|
if (!document.querySelector('.bg-red-500')) { // Prüft, ob schon eine Fehlermeldung angezeigt wird
|
||||||
|
document.dispatchEvent(new CustomEvent('mindmapError', { detail: { message: 'Struktur: Netzwerkfehler' } }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 1500); // Speichern 1.5 Sekunden nach der letzten Änderung
|
||||||
|
};
|
||||||
|
|
||||||
|
debounceSaveStructure(dataFromCytoscape); // Aufruf der Debounce-Funktion mit Cytoscape-Daten
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Die Verknüpfung der Formularfelder (Name, Beschreibung) mit dem Cytoscape Root-Knoten wird entfernt,
|
||||||
|
// da die Metadaten nun über das separate Formular oben gespeichert werden und nicht mehr direkt
|
||||||
|
// die Cytoscape-Daten manipulieren sollen. Die Logik für mindmap.saveToServer() wurde entfernt,
|
||||||
|
// da das Speichern jetzt über den onChange Handler mit PUT /api/mindmaps/<id> erfolgt.
|
||||||
|
// const nameInput = document.getElementById('name'); // Bereits oben deklariert für Metadaten
|
||||||
|
// nameInput.removeEventListener('input', ...); // Event Listener muss hier nicht entfernt werden, da er nicht neu hinzugefügt wird.
|
||||||
|
|
||||||
|
// Initialisiere die Mindmap mit existierenden Daten
|
||||||
|
mindmap.initialize().then(() => {
|
||||||
|
console.log("Mindmap-Editor initialisiert");
|
||||||
|
const mindmapId = "{{ mindmap.id }}";
|
||||||
|
|
||||||
|
// Lade existierende Daten für die Mindmap-Struktur
|
||||||
|
fetch(`/api/mindmaps/${mindmapId}`, { // Endpunkt für GET angepasst
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
response.json().then(err => {
|
||||||
|
showStatus(`Fehler beim Laden: ${err.message || err.error || response.statusText}`, true);
|
||||||
|
}).catch(() => {
|
||||||
|
showStatus(`Fehler beim Laden: ${response.statusText}`, true);
|
||||||
|
});
|
||||||
|
throw new Error(`Netzwerkantwort war nicht ok: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(mindmapDataFromServer => {
|
||||||
|
// Die API GET /api/mindmaps/<id> gibt ein Objekt zurück, das { id, name, description, is_private, data, ... } enthält.
|
||||||
|
// Wir brauchen nur den 'data'-Teil (Struktur) für Cytoscape.
|
||||||
|
// Die Metadaten (name, description, is_private) werden bereits serverseitig in die Formularfelder gerendert.
|
||||||
|
if (mindmapDataFromServer && mindmapDataFromServer.data) {
|
||||||
|
mindmap.loadData(mindmapDataFromServer.data); // Lade nur die Strukturdaten
|
||||||
|
console.log("Mindmap-Strukturdaten geladen:", mindmapDataFromServer.data);
|
||||||
|
showStatus("Mindmap geladen.", false);
|
||||||
|
} else {
|
||||||
|
console.error("Fehler: Mindmap-Daten (Struktur) nicht im erwarteten Format:", mindmapDataFromServer);
|
||||||
|
showStatus("Fehler: Mindmap-Struktur konnte nicht geladen werden (Formatfehler).", true);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error("Fehler beim Laden der Mindmap-Strukturdaten:", error);
|
||||||
|
if (!document.querySelector('.bg-red-500')) { // Prüft, ob schon eine Fehlermeldung angezeigt wird
|
||||||
|
showStatus("Laden der Struktur fehlgeschlagen.", true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).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', (event) => {
|
||||||
|
const message = event.detail && event.detail.message ? event.detail.message : 'Erfolgreich gespeichert!';
|
||||||
|
showStatus(message, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('mindmapError', (event) => {
|
||||||
|
showStatus(event.detail.message, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
48
templates/errors/400.html
Normal file
48
templates/errors/400.html
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}400 - Ungültige Anfrage{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mx-auto max-w-4xl px-4 py-8">
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl p-8 border border-gray-200 dark:border-gray-700">
|
||||||
|
<div class="text-center mb-8">
|
||||||
|
<div class="text-6xl font-bold text-red-500 mb-4">400</div>
|
||||||
|
<h1 class="text-3xl font-bold mb-2">Ungültige Anfrage</h1>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400">Die Anfrage konnte nicht verarbeitet werden.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-8 p-4 border border-red-200 dark:border-red-900 bg-red-50 dark:bg-red-900/20 rounded-lg">
|
||||||
|
<div class="flex items-start">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<svg class="h-5 w-5 text-red-500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="ml-3">
|
||||||
|
<h3 class="text-sm font-medium text-red-800 dark:text-red-400">Fehlerbeschreibung</h3>
|
||||||
|
<div class="mt-2 text-sm text-red-700 dark:text-red-300">
|
||||||
|
{% if error %}
|
||||||
|
<p>{{ error }}</p>
|
||||||
|
{% else %}
|
||||||
|
<p>Die Anfrage enthält ungültige oder fehlerhafte Daten und konnte nicht verarbeitet werden.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<p class="mb-4 text-gray-600 dark:text-gray-400">Hier sind einige Dinge, die Sie versuchen können:</p>
|
||||||
|
<ul class="list-disc list-inside text-left max-w-md mx-auto mb-6 text-gray-600 dark:text-gray-400">
|
||||||
|
<li>Überprüfen Sie Ihre Eingaben auf Fehler.</li>
|
||||||
|
<li>Stellen Sie sicher, dass Sie die richtigen Daten übermittelt haben.</li>
|
||||||
|
<li>Versuchen Sie, die Seite neu zu laden.</li>
|
||||||
|
<li>Kehren Sie zur Startseite zurück und versuchen Sie es erneut.</li>
|
||||||
|
</ul>
|
||||||
|
<a href="{{ url_for('index') }}" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||||
|
Zurück zur Startseite
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -39,6 +39,35 @@
|
|||||||
animation: textReveal 1s cubic-bezier(0.77, 0, 0.18, 1) forwards;
|
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-1 { animation-delay: 0.2s; }
|
||||||
.delay-2 { animation-delay: 0.4s; }
|
.delay-2 { animation-delay: 0.4s; }
|
||||||
.delay-3 { animation-delay: 0.6s; }
|
.delay-3 { animation-delay: 0.6s; }
|
||||||
@@ -71,16 +100,20 @@
|
|||||||
|
|
||||||
/* Chat section styles */
|
/* Chat section styles */
|
||||||
.embedded-chat {
|
.embedded-chat {
|
||||||
height: 350px;
|
height: 500px;
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
border: 1px solid;
|
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 {
|
.dark .embedded-chat {
|
||||||
background-color: rgba(17, 24, 39, 0.7);
|
background-color: rgba(17, 24, 39, 0.7);
|
||||||
border-color: rgba(109, 40, 217, 0.2);
|
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 {
|
.embedded-chat {
|
||||||
@@ -89,9 +122,118 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#embedded-chat-messages {
|
#embedded-chat-messages {
|
||||||
height: 250px;
|
flex: 1;
|
||||||
overflow-y: auto;
|
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;
|
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 */
|
/* Chat typing indicator */
|
||||||
@@ -131,16 +273,9 @@
|
|||||||
<div class="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
<div class="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||||
<div class="text-center mb-16">
|
<div class="text-center mb-16">
|
||||||
<h1 class="hero-heading mb-8 text-gray-900 dark:text-white">
|
<h1 class="hero-heading mb-8 text-gray-900 dark:text-white">
|
||||||
<div class="overflow-hidden">
|
<div class="overflow-hidden flex justify-center gap-6">
|
||||||
<span class="gradient-text inline-block text-reveal">Wissen</span>
|
<span class="relative inline-block text-reveal marker-animation">Wissen.</span>
|
||||||
</div>
|
<span class="relative inline-block text-reveal delay-1 marker-animation marker-animation-delay">Vernetzen.</span>
|
||||||
<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>
|
</div>
|
||||||
</h1>
|
</h1>
|
||||||
<div class="overflow-hidden">
|
<div class="overflow-hidden">
|
||||||
@@ -179,6 +314,12 @@
|
|||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<div class="text-3xl font-bold gradient-text mb-2 animate-float">Systades</div>
|
<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>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -271,14 +412,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Chat Messages -->
|
<!-- Chat Messages -->
|
||||||
<div id="embedded-chat-messages" class="border-b border-gray-200 dark:border-gray-700">
|
<div id="embedded-chat-messages" class="border-b-0">
|
||||||
<!-- Assistant Message -->
|
<!-- Assistant Message -->
|
||||||
<div class="mb-4 flex">
|
<div class="chat-message 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">
|
<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">
|
||||||
<i class="fa-solid fa-robot text-sm"></i>
|
<i class="fa-solid fa-robot text-sm"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-gray-100 dark:bg-gray-800 rounded-lg p-3 max-w-[80%]">
|
<div class="chat-bubble assistant-bubble">
|
||||||
<div class="text-gray-700 dark:text-gray-300 markdown-content">
|
<div class="markdown-content">
|
||||||
<p>Hallo! Ich bin dein Systades-Assistent. Wie kann ich dir heute helfen?</p>
|
<p>Hallo! Ich bin dein Systades-Assistent. Wie kann ich dir heute helfen?</p>
|
||||||
<p>Du kannst mir Fragen zu:</p>
|
<p>Du kannst mir Fragen zu:</p>
|
||||||
<ul>
|
<ul>
|
||||||
@@ -292,24 +433,24 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- User Message -->
|
<!-- User Message -->
|
||||||
<div class="mb-4 flex justify-end">
|
<div class="chat-message flex justify-end">
|
||||||
<div class="bg-purple-100 dark:bg-purple-900/30 rounded-lg p-3 max-w-[80%]">
|
<div class="chat-bubble user-bubble">
|
||||||
<p class="text-gray-800 dark:text-gray-200">
|
<p>
|
||||||
Kann ich mit deiner Hilfe eine Mindmap zum Thema Künstliche Intelligenz erstellen?
|
Kann ich mit deiner Hilfe eine Mindmap zum Thema Künstliche Intelligenz erstellen?
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<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">
|
<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">
|
||||||
<i class="fa-solid fa-user text-sm"></i>
|
<i class="fa-solid fa-user text-sm"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Assistant Response -->
|
<!-- Assistant Response -->
|
||||||
<div class="mb-4 flex" id="demo-ai-response">
|
<div class="chat-message 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">
|
<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">
|
||||||
<i class="fa-solid fa-robot text-sm"></i>
|
<i class="fa-solid fa-robot text-sm"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-gray-100 dark:bg-gray-800 rounded-lg p-3 max-w-[80%]">
|
<div class="chat-bubble assistant-bubble">
|
||||||
<div class="text-gray-700 dark:text-gray-300 markdown-content">
|
<div class="markdown-content">
|
||||||
<p>Ja, natürlich! Ich kann dir dabei helfen, eine Mindmap zum Thema <strong>Künstliche Intelligenz</strong> zu erstellen.</p>
|
<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>
|
<p>Du kannst wie folgt vorgehen:</p>
|
||||||
<ol>
|
<ol>
|
||||||
@@ -325,19 +466,19 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Chat Input -->
|
<!-- Chat Input -->
|
||||||
<div class="p-4">
|
<div class="chat-input-container">
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<input type="text" placeholder="Stelle eine Frage..." class="mystical-input flex-grow" disabled>
|
<input type="text" placeholder="Stelle eine Frage..." class="mystical-input flex-grow" 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>
|
<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>
|
||||||
<i class="fa-solid fa-paper-plane"></i>
|
<i class="fa-solid fa-paper-plane"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<!-- Quick Queries -->
|
<!-- Quick Queries -->
|
||||||
<div class="mt-3 flex flex-wrap gap-2">
|
<div class="quick-query-container">
|
||||||
<span class="text-xs text-gray-500 dark:text-gray-400 mr-1">Beispiele:</span>
|
<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 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="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 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="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 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>
|
<button data-question="Zeige mir alle verfügbaren Kategorien in der Datenbank" class="quick-query-btn hover:shadow-sm">Datenbank durchsuchen</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,234 +1,705 @@
|
|||||||
<!DOCTYPE html>
|
{% extends "base.html" %}
|
||||||
<html lang="de">
|
|
||||||
<head>
|
{% block title %}Interaktive Mindmap{% endblock %}
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
{% block extra_css %}
|
||||||
<title>Interaktive Mindmap</title>
|
<style>
|
||||||
|
/* Grundlegendes Layout */
|
||||||
|
.mindmap-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100vh - 64px);
|
||||||
|
background: linear-gradient(135deg, #1a1f2e 0%, #0f172a 100%);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hauptcontainer für die Mindmap */
|
||||||
|
#cy {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Zoom-Toolbar */
|
||||||
|
.mindmap-toolbar {
|
||||||
|
position: absolute;
|
||||||
|
top: 80px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
background: rgba(30, 41, 59, 0.8);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
|
||||||
|
z-index: 10;
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
<!-- Cytoscape.js -->
|
.mindmap-toolbar button {
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.26.0/cytoscape.min.js"></script>
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: none;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
color: white;
|
||||||
|
border-radius: 6px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
<!-- Socket.IO -->
|
.mindmap-toolbar button:hover {
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.7.2/socket.io.min.js"></script>
|
background: rgba(139, 92, 246, 0.5);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
<!-- Feather Icons (optional) -->
|
.mindmap-toolbar button i {
|
||||||
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Aktionsmenü im Header */
|
||||||
|
.mindmap-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
color: white;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button.primary {
|
||||||
|
background: rgba(139, 92, 246, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button.primary:hover {
|
||||||
|
background: rgba(139, 92, 246, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button.danger {
|
||||||
|
background: rgba(220, 38, 38, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button.danger:hover {
|
||||||
|
background: rgba(220, 38, 38, 0.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-button:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
transform: translateX(-5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-button i {
|
||||||
|
margin-right: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CRUD Panel */
|
||||||
|
.crud-panel {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 2rem;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background: rgba(15, 23, 42, 0.9);
|
||||||
|
border-radius: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||||
|
z-index: 10;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.crud-button {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 4rem;
|
||||||
|
height: 4rem;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crud-button:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.crud-button i {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crud-button span {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crud-button.create {
|
||||||
|
background: rgba(16, 185, 129, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.crud-button.create:hover {
|
||||||
|
background: rgba(16, 185, 129, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.crud-button.edit {
|
||||||
|
background: rgba(245, 158, 11, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.crud-button.edit:hover {
|
||||||
|
background: rgba(245, 158, 11, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.crud-button.delete {
|
||||||
|
background: rgba(220, 38, 38, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.crud-button.delete:hover {
|
||||||
|
background: rgba(220, 38, 38, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.crud-button.save {
|
||||||
|
background: rgba(59, 130, 246, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.crud-button.save:hover {
|
||||||
|
background: rgba(59, 130, 246, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-panel.visible {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(-50%) translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-content {
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-color {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animationen */
|
||||||
|
@keyframes pulse {
|
||||||
|
0% { transform: scale(1); }
|
||||||
|
50% { transform: scale(1.05); }
|
||||||
|
100% { transform: scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.pulse {
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
<style>
|
/* Ladeanzeige */
|
||||||
* {
|
.loader {
|
||||||
box-sizing: border-box;
|
border: 4px solid rgba(255, 255, 255, 0.1);
|
||||||
margin: 0;
|
border-radius: 50%;
|
||||||
padding: 0;
|
border-top: 4px solid #60a5fa;
|
||||||
}
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
body {
|
animation: spin 1s linear infinite;
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
position: absolute;
|
||||||
background-color: #f9fafb;
|
top: 50%;
|
||||||
color: #111827;
|
left: 50%;
|
||||||
line-height: 1.5;
|
margin-top: -20px;
|
||||||
}
|
margin-left: -20px;
|
||||||
|
z-index: 5;
|
||||||
.container {
|
}
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
@keyframes spin {
|
||||||
height: 100vh;
|
0% { transform: rotate(0deg); }
|
||||||
width: 100%;
|
100% { transform: rotate(360deg); }
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
/* Status-Meldung */
|
||||||
background-color: #1f2937;
|
.status-message {
|
||||||
color: white;
|
position: absolute;
|
||||||
padding: 1rem;
|
top: 50%;
|
||||||
display: flex;
|
left: 50%;
|
||||||
justify-content: space-between;
|
transform: translate(-50%, -50%);
|
||||||
align-items: center;
|
background: rgba(15, 23, 42, 0.9);
|
||||||
}
|
padding: 1rem 2rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
.header h1 {
|
color: white;
|
||||||
font-size: 1.5rem;
|
font-size: 1rem;
|
||||||
font-weight: 500;
|
z-index: 15;
|
||||||
}
|
text-align: center;
|
||||||
|
max-width: 80%;
|
||||||
.toolbar {
|
}
|
||||||
background-color: #f3f4f6;
|
|
||||||
padding: 0.75rem;
|
/* Bearbeitungsmodus-Hinweis */
|
||||||
display: flex;
|
.edit-mode-indicator {
|
||||||
gap: 0.5rem;
|
position: fixed;
|
||||||
border-bottom: 1px solid #e5e7eb;
|
bottom: 1rem;
|
||||||
}
|
left: 1rem;
|
||||||
|
background: rgba(245, 158, 11, 0.8);
|
||||||
.btn {
|
color: white;
|
||||||
background-color: #3b82f6;
|
padding: 0.5rem 1rem;
|
||||||
color: white;
|
border-radius: 0.5rem;
|
||||||
border: none;
|
font-size: 0.9rem;
|
||||||
border-radius: 0.25rem;
|
z-index: 1000;
|
||||||
padding: 0.5rem 1rem;
|
display: none;
|
||||||
font-size: 0.875rem;
|
}
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.2s;
|
.edit-mode-indicator.active {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn:hover {
|
/* Kontext-Menü */
|
||||||
background-color: #2563eb;
|
.context-menu {
|
||||||
}
|
position: absolute;
|
||||||
|
background: rgba(30, 41, 59, 0.95);
|
||||||
.btn-secondary {
|
border-radius: 8px;
|
||||||
background-color: #6b7280;
|
padding: 8px 0;
|
||||||
}
|
min-width: 160px;
|
||||||
|
z-index: 2000;
|
||||||
.btn-secondary:hover {
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||||
background-color: #4b5563;
|
backdrop-filter: blur(8px);
|
||||||
}
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
.btn-danger {
|
|
||||||
background-color: #ef4444;
|
.context-menu-item {
|
||||||
}
|
padding: 8px 16px;
|
||||||
|
color: white;
|
||||||
.btn-danger:hover {
|
cursor: pointer;
|
||||||
background-color: #dc2626;
|
transition: background-color 0.2s ease;
|
||||||
}
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
.search-container {
|
gap: 8px;
|
||||||
flex: 1;
|
}
|
||||||
display: flex;
|
|
||||||
margin-left: 1rem;
|
.context-menu-item:hover {
|
||||||
}
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
.search-input {
|
</style>
|
||||||
width: 100%;
|
{% endblock %}
|
||||||
max-width: 300px;
|
|
||||||
padding: 0.5rem;
|
{% block content %}
|
||||||
border: 1px solid #d1d5db;
|
<div class="mindmap-container">
|
||||||
border-radius: 0.25rem;
|
<div id="cy"></div>
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
<!-- Zoom-Toolbar für Hauptmindmap -->
|
||||||
|
<div class="mindmap-toolbar">
|
||||||
#cy {
|
<button id="zoomIn" title="Vergrößern">
|
||||||
flex: 1;
|
<i class="fas fa-plus"></i>
|
||||||
width: 100%;
|
</button>
|
||||||
position: relative;
|
<button id="zoomOut" title="Verkleinern">
|
||||||
}
|
<i class="fas fa-minus"></i>
|
||||||
|
</button>
|
||||||
.category-filters {
|
<button id="resetView" title="Ansicht zurücksetzen">
|
||||||
display: flex;
|
<i class="fas fa-expand"></i>
|
||||||
gap: 0.5rem;
|
</button>
|
||||||
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>
|
</div>
|
||||||
|
|
||||||
<!-- Unsere Mindmap JS -->
|
<div class="mindmap-header">
|
||||||
<script src="{{ url_for('static', filename='js/mindmap.js') }}"></script>
|
<h1 class="mindmap-title">Wissenslandschaft</h1>
|
||||||
|
<div class="mindmap-actions">
|
||||||
|
<button class="action-button" id="toggleCategories">
|
||||||
|
<i class="fas fa-tags"></i> Kategorien
|
||||||
|
</button>
|
||||||
|
<button class="action-button primary" id="startEdit">
|
||||||
|
<i class="fas fa-edit"></i> Bearbeiten
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Info-Panel für Knotendetails -->
|
||||||
|
<div id="infoPanel" class="info-panel">
|
||||||
|
<h3 class="info-title">Knotendetails</h3>
|
||||||
|
<div class="info-content"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Icons initialisieren -->
|
<div id="categoryLegend" class="category-legend"></div>
|
||||||
<script>
|
</div>
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
{% endblock %}
|
||||||
if (typeof feather !== 'undefined') {
|
|
||||||
feather.replace();
|
{% block extra_js %}
|
||||||
|
<!-- Cytoscape und Erweiterungen -->
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- Unsere JavaScript-Dateien -->
|
||||||
|
<script src="{{ url_for('static', filename='js/update_mindmap.js', v='1.0.1') }}"></script>
|
||||||
|
|
||||||
|
<!-- Initialisierung -->
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
console.log('DOMContentLoaded Event ausgelöst');
|
||||||
|
|
||||||
|
const cyContainer = document.getElementById('cy');
|
||||||
|
const loader = document.getElementById('loader');
|
||||||
|
const statusMessage = document.getElementById('statusMessage');
|
||||||
|
const crudPanel = document.getElementById('crudPanel');
|
||||||
|
const editModeIndicator = document.getElementById('editModeIndicator');
|
||||||
|
|
||||||
|
// CRUD Buttons
|
||||||
|
const createNodeBtn = document.getElementById('createNode');
|
||||||
|
const createEdgeBtn = document.getElementById('createEdge');
|
||||||
|
const editNodeBtn = document.getElementById('editNode');
|
||||||
|
const deleteElementBtn = document.getElementById('deleteElement');
|
||||||
|
const saveMindmapBtn = document.getElementById('saveMindmap');
|
||||||
|
|
||||||
|
// Header Action Buttons
|
||||||
|
const toggleEditModeBtn = document.getElementById('toggleEditMode');
|
||||||
|
const saveChangesBtn = document.getElementById('saveChanges');
|
||||||
|
const cancelEditBtn = document.getElementById('cancelEdit');
|
||||||
|
|
||||||
|
let isEditMode = false;
|
||||||
|
let selectedElement = null;
|
||||||
|
|
||||||
|
if (cyContainer) {
|
||||||
|
console.log('Container gefunden:', cyContainer);
|
||||||
|
|
||||||
|
// Loader und Statusmeldung anzeigen, nur wenn die Elemente existieren
|
||||||
|
if (loader) loader.style.display = 'block';
|
||||||
|
if (statusMessage) {
|
||||||
|
statusMessage.textContent = 'Lade Mindmap...';
|
||||||
|
statusMessage.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prüfen, ob Cytoscape vorhanden ist
|
||||||
|
if (typeof cytoscape !== 'undefined') {
|
||||||
|
console.log('Cytoscape ist verfügbar');
|
||||||
|
|
||||||
|
// Initialisieren der Mindmap
|
||||||
|
initializeMindmap().then(() => {
|
||||||
|
// Erfolg: Loader und Statusmeldung ausblenden
|
||||||
|
if (loader) loader.style.display = 'none';
|
||||||
|
if (statusMessage) statusMessage.style.display = 'none';
|
||||||
|
|
||||||
|
// Event-Listener für Knotenauswahl
|
||||||
|
window.cy.on('select', 'node', function(event) {
|
||||||
|
selectedElement = event.target;
|
||||||
|
if (editNodeBtn) editNodeBtn.disabled = false;
|
||||||
|
if (deleteElementBtn) deleteElementBtn.disabled = false;
|
||||||
|
|
||||||
|
// Knotendetails im Info-Panel anzeigen
|
||||||
|
showNodeInfo(selectedElement);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.cy.on('select', 'edge', function(event) {
|
||||||
|
selectedElement = event.target;
|
||||||
|
if (editNodeBtn) editNodeBtn.disabled = true;
|
||||||
|
if (deleteElementBtn) deleteElementBtn.disabled = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
window.cy.on('unselect', function() {
|
||||||
|
selectedElement = null;
|
||||||
|
if (editNodeBtn) editNodeBtn.disabled = true;
|
||||||
|
if (deleteElementBtn) deleteElementBtn.disabled = true;
|
||||||
|
|
||||||
|
// Info-Panel ausblenden
|
||||||
|
hideNodeInfo();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Rechtsklick-Menü
|
||||||
|
window.cy.on('cxttap', 'node', function(event) {
|
||||||
|
// Kontextmenü für Knoten anzeigen
|
||||||
|
if (isEditMode) {
|
||||||
|
const node = event.target;
|
||||||
|
const position = event.renderedPosition;
|
||||||
|
showNodeContextMenu(node, {
|
||||||
|
x: event.originalEvent.clientX,
|
||||||
|
y: event.originalEvent.clientY
|
||||||
|
});
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.cy.on('cxttap', function(event) {
|
||||||
|
// Kontextmenü zum Hinzufügen eines Knotens
|
||||||
|
if (isEditMode && event.target === window.cy) {
|
||||||
|
showAddNodeMenu({
|
||||||
|
x: event.originalEvent.clientX,
|
||||||
|
y: event.originalEvent.clientY
|
||||||
|
});
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).catch(error => {
|
||||||
|
// Fehler: Fehlermeldung anzeigen
|
||||||
|
console.error('Mindmap-Initialisierung fehlgeschlagen', error);
|
||||||
|
if (loader) loader.style.display = 'none';
|
||||||
|
if (statusMessage) {
|
||||||
|
statusMessage.textContent = 'Mindmap konnte nicht initialisiert werden: ' + error.message;
|
||||||
|
statusMessage.style.backgroundColor = 'rgba(220, 38, 38, 0.9)';
|
||||||
|
statusMessage.style.display = 'block';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error('Cytoscape ist nicht verfügbar');
|
||||||
|
if (loader) loader.style.display = 'none';
|
||||||
|
if (statusMessage) {
|
||||||
|
statusMessage.textContent = 'Cytoscape-Bibliothek konnte nicht geladen werden';
|
||||||
|
statusMessage.style.backgroundColor = 'rgba(220, 38, 38, 0.9)';
|
||||||
|
statusMessage.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('Container #cy nicht gefunden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bearbeitungsmodus umschalten (nur wenn alle erforderlichen Elemente existieren)
|
||||||
|
if (toggleEditModeBtn && crudPanel && editModeIndicator && saveChangesBtn && cancelEditBtn && window.cy) {
|
||||||
|
toggleEditModeBtn.addEventListener('click', function() {
|
||||||
|
isEditMode = !isEditMode;
|
||||||
|
|
||||||
|
if (isEditMode) {
|
||||||
|
// Bearbeitungsmodus aktivieren
|
||||||
|
crudPanel.style.display = 'flex';
|
||||||
|
editModeIndicator.classList.add('active');
|
||||||
|
toggleEditModeBtn.style.display = 'none';
|
||||||
|
saveChangesBtn.style.display = 'inline-flex';
|
||||||
|
cancelEditBtn.style.display = 'inline-flex';
|
||||||
|
window.cy.container().classList.add('editing-mode');
|
||||||
|
|
||||||
|
// Aktiviere Knotenbewegung (dragging)
|
||||||
|
window.cy.nodes().unlock();
|
||||||
|
} else {
|
||||||
|
// Bearbeitungsmodus deaktivieren
|
||||||
|
crudPanel.style.display = 'none';
|
||||||
|
editModeIndicator.classList.remove('active');
|
||||||
|
toggleEditModeBtn.style.display = 'inline-flex';
|
||||||
|
saveChangesBtn.style.display = 'none';
|
||||||
|
cancelEditBtn.style.display = 'none';
|
||||||
|
window.cy.container().classList.remove('editing-mode');
|
||||||
|
|
||||||
|
// Deaktiviere Knotenbewegung
|
||||||
|
window.cy.nodes().lock();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
}
|
||||||
</body>
|
|
||||||
</html>
|
// Änderungen speichern
|
||||||
|
if (saveChangesBtn && window.cy) {
|
||||||
|
saveChangesBtn.addEventListener('click', function() {
|
||||||
|
saveMindmapChanges(window.cy);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bearbeitungsmodus abbrechen
|
||||||
|
if (cancelEditBtn && crudPanel && editModeIndicator && toggleEditModeBtn && saveChangesBtn && loader && statusMessage) {
|
||||||
|
cancelEditBtn.addEventListener('click', function() {
|
||||||
|
if (confirm('Möchten Sie den Bearbeitungsmodus wirklich verlassen? Nicht gespeicherte Änderungen gehen verloren.')) {
|
||||||
|
isEditMode = false;
|
||||||
|
crudPanel.style.display = 'none';
|
||||||
|
editModeIndicator.classList.remove('active');
|
||||||
|
toggleEditModeBtn.style.display = 'inline-flex';
|
||||||
|
saveChangesBtn.style.display = 'none';
|
||||||
|
cancelEditBtn.style.display = 'none';
|
||||||
|
window.cy.container().classList.remove('editing-mode');
|
||||||
|
|
||||||
|
// Neuinitialisierung der Mindmap
|
||||||
|
initializeMindmap().then(() => {
|
||||||
|
if (loader) loader.style.display = 'none';
|
||||||
|
if (statusMessage) statusMessage.style.display = 'none';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// CRUD-Funktionen
|
||||||
|
if (createNodeBtn && window.cy) {
|
||||||
|
createNodeBtn.addEventListener('click', function() {
|
||||||
|
if (isEditMode) {
|
||||||
|
addNewNode(window.cy);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (createEdgeBtn && window.cy) {
|
||||||
|
createEdgeBtn.addEventListener('click', function() {
|
||||||
|
if (isEditMode) {
|
||||||
|
enableEdgeCreationMode(window.cy);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (editNodeBtn) {
|
||||||
|
editNodeBtn.addEventListener('click', function() {
|
||||||
|
if (isEditMode && selectedElement && selectedElement.isNode()) {
|
||||||
|
editNodeProperties(selectedElement);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deleteElementBtn) {
|
||||||
|
deleteElementBtn.addEventListener('click', function() {
|
||||||
|
if (isEditMode && selectedElement) {
|
||||||
|
if (selectedElement.isNode()) {
|
||||||
|
deleteNode(selectedElement);
|
||||||
|
} else if (selectedElement.isEdge()) {
|
||||||
|
if (confirm('Möchten Sie diese Verbindung wirklich löschen?')) {
|
||||||
|
selectedElement.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (saveMindmapBtn && window.cy) {
|
||||||
|
saveMindmapBtn.addEventListener('click', function() {
|
||||||
|
if (isEditMode) {
|
||||||
|
saveMindmapChanges(window.cy);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Funktionen für Zoom-Buttons und Reset
|
||||||
|
const zoomInBtn = document.getElementById('zoomIn');
|
||||||
|
const zoomOutBtn = document.getElementById('zoomOut');
|
||||||
|
const resetViewBtn = document.getElementById('resetView');
|
||||||
|
|
||||||
|
if (zoomInBtn && window.cy) {
|
||||||
|
zoomInBtn.addEventListener('click', function() {
|
||||||
|
if (window.cy) window.cy.zoom(window.cy.zoom() * 1.2);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (zoomOutBtn && window.cy) {
|
||||||
|
zoomOutBtn.addEventListener('click', function() {
|
||||||
|
if (window.cy) window.cy.zoom(window.cy.zoom() * 0.8);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resetViewBtn && window.cy) {
|
||||||
|
resetViewBtn.addEventListener('click', function() {
|
||||||
|
if (window.cy) window.cy.fit();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -84,6 +84,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Meine erstellten Mindmaps -->
|
||||||
|
<div class="mb-12">
|
||||||
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
<h2 class="text-xl font-bold text-gray-800 dark:text-white flex items-center">
|
||||||
|
<i class="fas fa-brain mr-3 text-green-500"></i>
|
||||||
|
Meine erstellten Mindmaps
|
||||||
|
</h2>
|
||||||
|
<button id="create-mindmap-btn" class="px-4 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors flex items-center">
|
||||||
|
<i class="fas fa-plus mr-2"></i> Neue Mindmap erstellen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="user-mindmaps-container" class="space-y-4">
|
||||||
|
<!-- Hier werden die Mindmaps des Benutzers geladen -->
|
||||||
|
<p class="text-gray-600 dark:text-gray-400">Lade Mindmaps...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Gemerkte Inhalte -->
|
<!-- Gemerkte Inhalte -->
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 mb-8">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 mb-8">
|
||||||
@@ -123,6 +140,431 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal zum Erstellen einer neuen Mindmap -->
|
||||||
|
<div id="create-mindmap-modal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full flex items-center justify-center hidden z-50">
|
||||||
|
<div class="relative mx-auto p-5 border w-full max-w-md shadow-lg rounded-md bg-white dark:bg-gray-800">
|
||||||
|
<div class="mt-3 text-center">
|
||||||
|
<h3 class="text-lg leading-6 font-medium text-gray-900 dark:text-white">Neue Mindmap erstellen</h3>
|
||||||
|
<div class="mt-2 px-7 py-3">
|
||||||
|
<form id="create-mindmap-form">
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="mindmap-name" class="block text-sm font-medium text-gray-700 dark:text-gray-300 text-left">Name</label>
|
||||||
|
<input type="text" name="name" id="mindmap-name" class="mt-1 block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm dark:bg-gray-700 dark:text-white" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="mindmap-description" class="block text-sm font-medium text-gray-700 dark:text-gray-300 text-left">Beschreibung (optional)</label>
|
||||||
|
<textarea name="description" id="mindmap-description" rows="3" class="mt-1 block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm dark:bg-gray-700 dark:text-white"></textarea>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="items-center px-4 py-3">
|
||||||
|
<button id="submit-create-mindmap" class="px-4 py-2 bg-green-500 text-white text-base font-medium rounded-md w-full shadow-sm hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-green-300">
|
||||||
|
Erstellen
|
||||||
|
</button>
|
||||||
|
<button id="cancel-create-mindmap" class="mt-2 px-4 py-2 bg-gray-300 text-gray-800 dark:bg-gray-600 dark:text-gray-200 text-base font-medium rounded-md w-full shadow-sm hover:bg-gray-400 dark:hover:bg-gray-500 focus:outline-none focus:ring-2 focus:ring-gray-300">
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- JavaScript für persönliche Mindmap und CRUD -->
|
||||||
|
<script>
|
||||||
|
</script>
|
||||||
|
<script nonce="{{ csp_nonce }}">
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Modal-Logik
|
||||||
|
const createMindmapBtn = document.getElementById('create-mindmap-btn');
|
||||||
|
const createMindmapModal = document.getElementById('create-mindmap-modal');
|
||||||
|
const cancelCreateMindmapBtn = document.getElementById('cancel-create-mindmap');
|
||||||
|
const submitCreateMindmapBtn = document.getElementById('submit-create-mindmap');
|
||||||
|
const createMindmapForm = document.getElementById('create-mindmap-form');
|
||||||
|
|
||||||
|
if (createMindmapBtn) {
|
||||||
|
createMindmapBtn.addEventListener('click', () => {
|
||||||
|
createMindmapModal.classList.remove('hidden');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cancelCreateMindmapBtn) {
|
||||||
|
cancelCreateMindmapBtn.addEventListener('click', () => {
|
||||||
|
createMindmapModal.classList.add('hidden');
|
||||||
|
createMindmapForm.reset();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schließen bei Klick außerhalb des Modals
|
||||||
|
if (createMindmapModal) {
|
||||||
|
createMindmapModal.addEventListener('click', (event) => {
|
||||||
|
if (event.target === createMindmapModal) {
|
||||||
|
createMindmapModal.classList.add('hidden');
|
||||||
|
createMindmapForm.reset();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Funktion zum Anzeigen von Benachrichtigungen
|
||||||
|
function showNotification(message, type = 'success') {
|
||||||
|
const notificationArea = document.getElementById('notification-area') || createNotificationArea();
|
||||||
|
const notificationId = `notif-${Date.now()}`;
|
||||||
|
constbgColor = type === 'success' ? 'bg-green-500' : (type === 'error' ? 'bg-red-500' : 'bg-blue-500');
|
||||||
|
|
||||||
|
const notificationElement = `
|
||||||
|
<div id="${notificationId}" class="p-4 mb-4 text-sm text-white rounded-lg ${bgColor} animate-fadeIn" role="alert">
|
||||||
|
<span class="font-medium">${type.charAt(0).toUpperCase() + type.slice(1)}:</span> ${message}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
notificationArea.insertAdjacentHTML('beforeend', notificationElement);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const el = document.getElementById(notificationId);
|
||||||
|
if (el) {
|
||||||
|
el.classList.add('animate-fadeOut');
|
||||||
|
setTimeout(() => el.remove(), 500);
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createNotificationArea() {
|
||||||
|
const area = document.createElement('div');
|
||||||
|
area.id = 'notification-area';
|
||||||
|
area.className = 'fixed top-5 right-5 z-50 w-auto max-w-sm';
|
||||||
|
document.body.appendChild(area);
|
||||||
|
// Add some basic animation styles
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.textContent = `
|
||||||
|
.animate-fadeIn { animation: fadeIn 0.5s ease-out; }
|
||||||
|
.animate-fadeOut { animation: fadeOut 0.5s ease-in forwards; }
|
||||||
|
@keyframes fadeIn { from { opacity: 0; transform: translateY(-20px); } to { opacity: 1; transform: translateY(0); } }
|
||||||
|
@keyframes fadeOut { from { opacity: 1; transform: translateY(0); } to { opacity: 0; transform: translateY(-20px); } }
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
return area;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// CRUD-Funktionen für UserMindmaps
|
||||||
|
const mindmapsContainer = document.getElementById('user-mindmaps-container');
|
||||||
|
|
||||||
|
async function fetchUserMindmaps() {
|
||||||
|
if (!mindmapsContainer) return;
|
||||||
|
mindmapsContainer.innerHTML = '<p class="text-gray-600 dark:text-gray-400">Lade Mindmaps...</p>';
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/mindmaps');
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
const mindmaps = await response.json();
|
||||||
|
renderMindmaps(mindmaps);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Laden der Mindmaps:', error);
|
||||||
|
mindmapsContainer.innerHTML = '<p class="text-red-500">Fehler beim Laden der Mindmaps.</p>';
|
||||||
|
showNotification('Fehler beim Laden der Mindmaps.', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMindmaps(mindmaps) {
|
||||||
|
if (!mindmapsContainer) return;
|
||||||
|
if (mindmaps.length === 0) {
|
||||||
|
mindmapsContainer.innerHTML = '<p class="text-gray-600 dark:text-gray-400">Du hast noch keine eigenen Mindmaps erstellt.</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mindmapsContainer.innerHTML = ''; // Container leeren
|
||||||
|
const ul = document.createElement('ul');
|
||||||
|
ul.className = 'space-y-3';
|
||||||
|
|
||||||
|
mindmaps.forEach(mindmap => {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.className = 'p-4 rounded-xl bg-white/80 dark:bg-gray-800/80 shadow-sm hover:shadow-md transition-all flex justify-between items-center';
|
||||||
|
|
||||||
|
const mindmapLink = document.createElement('a');
|
||||||
|
mindmapLink.href = `/user_mindmap/${mindmap.id}`;
|
||||||
|
mindmapLink.className = 'flex-grow';
|
||||||
|
|
||||||
|
const textDiv = document.createElement('div');
|
||||||
|
const nameH3 = document.createElement('h3');
|
||||||
|
nameH3.className = 'font-semibold text-gray-900 dark:text-white';
|
||||||
|
nameH3.textContent = mindmap.name;
|
||||||
|
textDiv.appendChild(nameH3);
|
||||||
|
|
||||||
|
if (mindmap.description) {
|
||||||
|
const descP = document.createElement('p');
|
||||||
|
descP.className = 'text-sm text-gray-600 dark:text-gray-400';
|
||||||
|
descP.textContent = mindmap.description;
|
||||||
|
textDiv.appendChild(descP);
|
||||||
|
}
|
||||||
|
mindmapLink.appendChild(textDiv);
|
||||||
|
li.appendChild(mindmapLink);
|
||||||
|
|
||||||
|
const actionsDiv = document.createElement('div');
|
||||||
|
actionsDiv.className = 'flex space-x-2 ml-4';
|
||||||
|
|
||||||
|
const editButton = document.createElement('a');
|
||||||
|
editButton.href = `/edit_mindmap/${mindmap.id}`; // oder JavaScript-basiertes Editieren
|
||||||
|
editButton.className = 'px-3 py-1 bg-blue-500 text-white rounded hover:bg-blue-600 text-sm flex items-center';
|
||||||
|
editButton.innerHTML = '<i class="fas fa-edit mr-1"></i> Bearbeiten';
|
||||||
|
// Hier könnte auch ein Event-Listener für ein Modal zum Bearbeiten hinzugefügt werden
|
||||||
|
// editButton.addEventListener('click', (e) => { e.preventDefault(); openEditModal(mindmap); });
|
||||||
|
actionsDiv.appendChild(editButton);
|
||||||
|
|
||||||
|
const deleteButton = document.createElement('button');
|
||||||
|
deleteButton.className = 'px-3 py-1 bg-red-500 text-white rounded hover:bg-red-600 text-sm flex items-center delete-mindmap-btn';
|
||||||
|
deleteButton.innerHTML = '<i class="fas fa-trash mr-1"></i> Löschen';
|
||||||
|
deleteButton.dataset.mindmapId = mindmap.id;
|
||||||
|
actionsDiv.appendChild(deleteButton);
|
||||||
|
|
||||||
|
li.appendChild(actionsDiv);
|
||||||
|
ul.appendChild(li);
|
||||||
|
});
|
||||||
|
mindmapsContainer.appendChild(ul);
|
||||||
|
|
||||||
|
// Event Listener für Löschen-Buttons hinzufügen
|
||||||
|
document.querySelectorAll('.delete-mindmap-btn').forEach(button => {
|
||||||
|
button.addEventListener('click', async (event) => {
|
||||||
|
const mindmapId = event.currentTarget.dataset.mindmapId;
|
||||||
|
if (confirm('Bist du sicher, dass du diese Mindmap löschen möchtest?')) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/mindmaps/${mindmapId}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
showNotification('Mindmap erfolgreich gelöscht.', 'success');
|
||||||
|
fetchUserMindmaps(); // Liste aktualisieren
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Löschen der Mindmap:', error);
|
||||||
|
showNotification(`Fehler beim Löschen: ${error.message}`, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (submitCreateMindmapBtn) {
|
||||||
|
submitCreateMindmapBtn.addEventListener('click', async () => {
|
||||||
|
const name = document.getElementById('mindmap-name').value;
|
||||||
|
const description = document.getElementById('mindmap-description').value;
|
||||||
|
|
||||||
|
if (!name.trim()) {
|
||||||
|
showNotification('Der Name der Mindmap darf nicht leer sein.', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/mindmaps', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ name, description, is_private: false }), // is_private standardmäßig auf false setzen
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
const newMindmap = await response.json();
|
||||||
|
showNotification(`Mindmap "${newMindmap.name}" erfolgreich erstellt. Weiterleitung...`, 'success');
|
||||||
|
createMindmapModal.classList.add('hidden');
|
||||||
|
createMindmapForm.reset();
|
||||||
|
// fetchUserMindmaps(); // Liste wird auf der neuen Seite ohnehin neu geladen oder ist nicht direkt sichtbar.
|
||||||
|
// Weiterleitung zur Bearbeitungsseite der neuen Mindmap
|
||||||
|
window.location.href = `/edit_mindmap/${newMindmap.id}`;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Erstellen der Mindmap:', error);
|
||||||
|
showNotification(`Fehler beim Erstellen: ${error.message}`, 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initiale Ladefunktion für Mindmaps
|
||||||
|
fetchUserMindmaps();
|
||||||
|
|
||||||
|
// Bestehendes Skript für Bookmarks etc.
|
||||||
|
// Lade gespeicherte Bookmarks aus dem LocalStorage
|
||||||
|
function loadBookmarkedNodes() {
|
||||||
|
try {
|
||||||
|
const bookmarked = localStorage.getItem('bookmarkedNodes');
|
||||||
|
return bookmarked ? JSON.parse(bookmarked) : [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Laden der gemerkten Knoten:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bookmarkedNodeIds = loadBookmarkedNodes();
|
||||||
|
|
||||||
|
// Prüfe, ob es gemerkte Knoten gibt
|
||||||
|
if (bookmarkedNodeIds && bookmarkedNodeIds.length > 0) {
|
||||||
|
// Verstecke die Leer-Nachricht
|
||||||
|
const emptyMindmapMessage = document.getElementById('empty-mindmap-message');
|
||||||
|
if (emptyMindmapMessage) {
|
||||||
|
emptyMindmapMessage.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialisiere die persönliche Mindmap
|
||||||
|
const personalMindmapContainer = document.getElementById('personal-mindmap');
|
||||||
|
if (personalMindmapContainer && typeof MindMapVisualization !== 'undefined') {
|
||||||
|
const personalMindmap = new MindMapVisualization('#personal-mindmap', {
|
||||||
|
width: personalMindmapContainer.clientWidth,
|
||||||
|
height: 400,
|
||||||
|
nodeRadius: 18,
|
||||||
|
selectedNodeRadius: 22,
|
||||||
|
linkDistance: 120,
|
||||||
|
chargeStrength: -800,
|
||||||
|
centerForce: 0.1,
|
||||||
|
tooltipEnabled: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Lade Daten für die Mindmap
|
||||||
|
window.setTimeout(() => {
|
||||||
|
if (window.mindmapInstance) {
|
||||||
|
const nodes = window.mindmapInstance.nodes.filter(node =>
|
||||||
|
bookmarkedNodeIds.includes(node.id)
|
||||||
|
);
|
||||||
|
const links = window.mindmapInstance.links.filter(link =>
|
||||||
|
bookmarkedNodeIds.includes(link.source.id || link.source) &&
|
||||||
|
bookmarkedNodeIds.includes(link.target.id || link.target)
|
||||||
|
);
|
||||||
|
personalMindmap.nodes = nodes;
|
||||||
|
personalMindmap.links = links;
|
||||||
|
personalMindmap.isLoading = false;
|
||||||
|
personalMindmap.updateVisualization();
|
||||||
|
} else {
|
||||||
|
if (emptyMindmapMessage) emptyMindmapMessage.style.display = 'flex';
|
||||||
|
}
|
||||||
|
}, 800);
|
||||||
|
}
|
||||||
|
loadBookmarkedContent(bookmarkedNodeIds);
|
||||||
|
} else {
|
||||||
|
// Zeige Leerzustand an
|
||||||
|
const areasContainer = document.getElementById('bookmarked-areas-container');
|
||||||
|
const thoughtsContainer = document.getElementById('bookmarked-thoughts-container');
|
||||||
|
|
||||||
|
if (areasContainer) {
|
||||||
|
areasContainer.innerHTML = `
|
||||||
|
<div class="empty-state">
|
||||||
|
<div class="text-4xl mb-2 opacity-20">
|
||||||
|
<i class="fas fa-folder-open"></i>
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400">Keine gemerkten Wissensbereiche</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (thoughtsContainer) {
|
||||||
|
thoughtsContainer.innerHTML = `
|
||||||
|
<div class="empty-state">
|
||||||
|
<div class="text-4xl mb-2 opacity-20">
|
||||||
|
<i class="fas fa-lightbulb"></i>
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400">Keine gemerkten Gedanken</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Funktion zum Laden der gemerkten Inhalte (bleibt größtenteils gleich)
|
||||||
|
function loadBookmarkedContent(nodeIds) {
|
||||||
|
if (!nodeIds || nodeIds.length === 0) return;
|
||||||
|
|
||||||
|
const areasContainer = document.getElementById('bookmarked-areas-container');
|
||||||
|
const thoughtsContainer = document.getElementById('bookmarked-thoughts-container');
|
||||||
|
|
||||||
|
const colors = ['purple', 'blue', 'green', 'indigo', 'amber'];
|
||||||
|
|
||||||
|
if (areasContainer) areasContainer.innerHTML = '';
|
||||||
|
if (thoughtsContainer) thoughtsContainer.innerHTML = '';
|
||||||
|
|
||||||
|
const areaTemplates = [
|
||||||
|
{ name: 'Philosophie', description: 'Grundlagen philosophischen Denkens', count: 24 },
|
||||||
|
{ name: 'Wissenschaft', description: 'Wissenschaftliche Methoden und Erkenntnisse', count: 42 },
|
||||||
|
{ name: 'Technologie', description: 'Zukunftsweisende Technologien', count: 36 },
|
||||||
|
{ name: 'Kunst', description: 'Künstlerische Ausdrucksformen', count: 18 },
|
||||||
|
{ name: 'Psychologie', description: 'Menschliches Verhalten verstehen', count: 30 }
|
||||||
|
];
|
||||||
|
|
||||||
|
const thoughtTemplates = [
|
||||||
|
{ title: 'Quantenphysik und Bewusstsein', author: 'Maria Schmidt', date: '12.04.2023' },
|
||||||
|
{ title: 'Ethik in der künstlichen Intelligenz', author: 'Thomas Weber', date: '23.02.2023' },
|
||||||
|
{ title: 'Die Rolle der Kunst in der Gesellschaft', author: 'Lena Müller', date: '05.06.2023' },
|
||||||
|
{ title: 'Nachhaltige Entwicklung im 21. Jahrhundert', author: 'Michael Bauer', date: '18.08.2023' },
|
||||||
|
{ title: 'Kognitive Verzerrungen im Alltag', author: 'Sophie Klein', date: '30.09.2023' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const areaCount = Math.min(nodeIds.length, 5);
|
||||||
|
|
||||||
|
if (areasContainer && areaCount > 0) {
|
||||||
|
for (let i = 0; i < areaCount; i++) {
|
||||||
|
const area = areaTemplates[i];
|
||||||
|
const colorClass = colors[i % colors.length];
|
||||||
|
areasContainer.innerHTML += `
|
||||||
|
<a href="{{ url_for('mindmap') }}" class="bookmark-item block p-4 rounded-xl bg-white/80 dark:bg-gray-800/80 shadow-sm hover:shadow-md transition-all">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="w-10 h-10 rounded-lg bg-${colorClass}-100 dark:bg-${colorClass}-900/30 flex items-center justify-center mr-3">
|
||||||
|
<i class="fas fa-bookmark text-${colorClass}-500"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow">
|
||||||
|
<h3 class="font-semibold text-gray-900 dark:text-white">${area.name}</h3>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-400">${area.description}</p>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
${area.count} Einträge
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
} else if (areasContainer) {
|
||||||
|
areasContainer.innerHTML = `
|
||||||
|
<div class="empty-state">
|
||||||
|
<div class="text-4xl mb-2 opacity-20">
|
||||||
|
<i class="fas fa-folder-open"></i>
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400">Keine gemerkten Wissensbereiche</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const thoughtCount = Math.min(nodeIds.length, 5);
|
||||||
|
|
||||||
|
if (thoughtsContainer && thoughtCount > 0) {
|
||||||
|
for (let i = 0; i < thoughtCount; i++) {
|
||||||
|
const thought = thoughtTemplates[i];
|
||||||
|
const colorClass = colors[(i + 2) % colors.length];
|
||||||
|
thoughtsContainer.innerHTML += `
|
||||||
|
<a href="{{ url_for('mindmap') }}" class="bookmark-item block p-4 rounded-xl bg-white/80 dark:bg-gray-800/80 shadow-sm hover:shadow-md transition-all">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="w-10 h-10 rounded-lg bg-${colorClass}-100 dark:bg-${colorClass}-900/30 flex items-center justify-center mr-3">
|
||||||
|
<i class="fas fa-lightbulb text-${colorClass}-500"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow">
|
||||||
|
<h3 class="font-semibold text-gray-900 dark:text-white">${thought.title}</h3>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-400">Von ${thought.author} • ${thought.date}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
} else if (thoughtsContainer) {
|
||||||
|
thoughtsContainer.innerHTML = `
|
||||||
|
<div class="empty-state">
|
||||||
|
<div class="text-4xl mb-2 opacity-20">
|
||||||
|
<i class="fas fa-lightbulb"></i>
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400">Keine gemerkten Gedanken</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
<!-- JavaScript für persönliche Mindmap -->
|
<!-- JavaScript für persönliche Mindmap -->
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
75
templates/simple_profile.html
Normal file
75
templates/simple_profile.html
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
{% 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 %}
|
||||||
1579
templates/user_mindmap.html
Normal file
1579
templates/user_mindmap.html
Normal file
File diff suppressed because it is too large
Load Diff
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.
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.
38
utils/check_db.py
Normal file
38
utils/check_db.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import sqlite3
|
||||||
|
|
||||||
|
def check_mindmap_nodes():
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect('database/systades.db')
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Check if the table exists
|
||||||
|
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='mind_map_node';")
|
||||||
|
table_exists = cursor.fetchone()
|
||||||
|
|
||||||
|
if not table_exists:
|
||||||
|
print("Die Tabelle 'mind_map_node' existiert nicht!")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check for the "Wissen" node
|
||||||
|
cursor.execute("SELECT * FROM mind_map_node WHERE name = 'Wissen';")
|
||||||
|
wissen_node = cursor.fetchone()
|
||||||
|
|
||||||
|
if wissen_node:
|
||||||
|
print(f"'Wissen'-Knoten gefunden: {wissen_node}")
|
||||||
|
else:
|
||||||
|
print("'Wissen'-Knoten NICHT gefunden!")
|
||||||
|
|
||||||
|
# Get all nodes
|
||||||
|
cursor.execute("SELECT id, name FROM mind_map_node LIMIT 10;")
|
||||||
|
nodes = cursor.fetchall()
|
||||||
|
|
||||||
|
print(f"\nVorhandene Knoten (max. 10):")
|
||||||
|
for node in nodes:
|
||||||
|
print(f" - {node}")
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Fehler: {e}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
check_mindmap_nodes()
|
||||||
@@ -11,19 +11,33 @@ import importlib.util
|
|||||||
parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
sys.path.insert(0, parent_dir)
|
sys.path.insert(0, parent_dir)
|
||||||
|
|
||||||
from app import app, db_path
|
# Direkt den Datenbankpfad berechnen, statt ihn aus app.py zu importieren
|
||||||
|
def get_db_path():
|
||||||
|
"""Berechnet den absoluten Pfad zur Datenbank"""
|
||||||
|
basedir = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
|
||||||
|
return os.path.join(basedir, 'database', 'systades.db')
|
||||||
|
|
||||||
|
# Import models direkt
|
||||||
from models import db
|
from models import db
|
||||||
|
|
||||||
def ensure_db_dir():
|
def ensure_db_dir():
|
||||||
"""Make sure the database directory exists."""
|
"""Make sure the database directory exists."""
|
||||||
|
db_path = get_db_path()
|
||||||
os.makedirs(os.path.dirname(db_path), exist_ok=True)
|
os.makedirs(os.path.dirname(db_path), exist_ok=True)
|
||||||
|
|
||||||
def fix_database_schema():
|
def fix_database_schema():
|
||||||
"""Fix the database schema by adding missing columns."""
|
"""Fix the database schema by adding missing columns."""
|
||||||
|
# Import Flask-App erst innerhalb der Funktion
|
||||||
|
from flask import Flask
|
||||||
|
from app import app
|
||||||
|
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
# Ensure directory exists
|
# Ensure directory exists
|
||||||
ensure_db_dir()
|
ensure_db_dir()
|
||||||
|
|
||||||
|
# Get database path
|
||||||
|
db_path = get_db_path()
|
||||||
|
|
||||||
# Check if database exists, create tables if needed
|
# Check if database exists, create tables if needed
|
||||||
if not os.path.exists(db_path):
|
if not os.path.exists(db_path):
|
||||||
print("Database doesn't exist. Creating all tables from scratch...")
|
print("Database doesn't exist. Creating all tables from scratch...")
|
||||||
|
|||||||
103
utils/db_operations.py
Normal file
103
utils/db_operations.py
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
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()
|
||||||
124
utils/db_test.py
124
utils/db_test.py
@@ -9,9 +9,19 @@ import sqlite3
|
|||||||
parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
sys.path.insert(0, parent_dir)
|
sys.path.insert(0, parent_dir)
|
||||||
|
|
||||||
from app import app, db_path
|
# Vermeidung zirkulärer Importe - importiere nur die Modelle und DB-Objekt
|
||||||
from models import db, User, Thought, MindMapNode, Category
|
from models import db, User, Thought, MindMapNode, Category
|
||||||
|
|
||||||
|
def get_db_path():
|
||||||
|
"""Ermittelt den Pfad zur Datenbankdatei"""
|
||||||
|
db_dir = os.path.join(parent_dir, 'database')
|
||||||
|
if not os.path.exists(db_dir):
|
||||||
|
os.makedirs(db_dir)
|
||||||
|
return os.path.join(db_dir, 'systades.db')
|
||||||
|
|
||||||
|
# Datenbank-Pfad
|
||||||
|
db_path = get_db_path()
|
||||||
|
|
||||||
def test_database_connection():
|
def test_database_connection():
|
||||||
"""Test if the database exists and can be connected to."""
|
"""Test if the database exists and can be connected to."""
|
||||||
try:
|
try:
|
||||||
@@ -37,52 +47,52 @@ def test_database_connection():
|
|||||||
|
|
||||||
def test_models():
|
def test_models():
|
||||||
"""Test if all models are properly defined and can be queried."""
|
"""Test if all models are properly defined and can be queried."""
|
||||||
with app.app_context():
|
# Import app here to avoid circular import
|
||||||
try:
|
from flask import current_app
|
||||||
print("\nTesting User model...")
|
try:
|
||||||
user_count = User.query.count()
|
print("\nTesting User model...")
|
||||||
print(f" Found {user_count} users")
|
user_count = User.query.count()
|
||||||
|
print(f" Found {user_count} users")
|
||||||
print("\nTesting Category model...")
|
|
||||||
category_count = Category.query.count()
|
print("\nTesting Category model...")
|
||||||
print(f" Found {category_count} categories")
|
category_count = Category.query.count()
|
||||||
|
print(f" Found {category_count} categories")
|
||||||
print("\nTesting MindMapNode model...")
|
|
||||||
node_count = MindMapNode.query.count()
|
print("\nTesting MindMapNode model...")
|
||||||
print(f" Found {node_count} mindmap nodes")
|
node_count = MindMapNode.query.count()
|
||||||
|
print(f" Found {node_count} mindmap nodes")
|
||||||
print("\nTesting Thought model...")
|
|
||||||
thought_count = Thought.query.count()
|
print("\nTesting Thought model...")
|
||||||
print(f" Found {thought_count} thoughts")
|
thought_count = Thought.query.count()
|
||||||
|
print(f" Found {thought_count} thoughts")
|
||||||
if user_count == 0:
|
|
||||||
print("\nWARNING: No users found in the database. You might need to create an admin user.")
|
if user_count == 0:
|
||||||
|
print("\nWARNING: No users found in the database. You might need to create an admin user.")
|
||||||
return True
|
|
||||||
except Exception as e:
|
return True
|
||||||
print(f"Error testing models: {e}")
|
except Exception as e:
|
||||||
return False
|
print(f"Error testing models: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
def print_database_stats():
|
def print_database_stats():
|
||||||
"""Print database statistics."""
|
"""Print database statistics."""
|
||||||
with app.app_context():
|
try:
|
||||||
try:
|
stats = []
|
||||||
stats = []
|
stats.append(("Users", User.query.count()))
|
||||||
stats.append(("Users", User.query.count()))
|
stats.append(("Categories", Category.query.count()))
|
||||||
stats.append(("Categories", Category.query.count()))
|
stats.append(("Mindmap Nodes", MindMapNode.query.count()))
|
||||||
stats.append(("Mindmap Nodes", MindMapNode.query.count()))
|
stats.append(("Thoughts", Thought.query.count()))
|
||||||
stats.append(("Thoughts", Thought.query.count()))
|
|
||||||
|
print("\nDatabase Statistics:")
|
||||||
print("\nDatabase Statistics:")
|
print("-" * 40)
|
||||||
print("-" * 40)
|
for name, count in stats:
|
||||||
for name, count in stats:
|
print(f"{name:<20} : {count}")
|
||||||
print(f"{name:<20} : {count}")
|
print("-" * 40)
|
||||||
print("-" * 40)
|
|
||||||
|
return True
|
||||||
return True
|
except Exception as e:
|
||||||
except Exception as e:
|
print(f"Error generating database statistics: {e}")
|
||||||
print(f"Error generating database statistics: {e}")
|
return False
|
||||||
return False
|
|
||||||
|
|
||||||
def run_all_tests():
|
def run_all_tests():
|
||||||
"""Run all database tests."""
|
"""Run all database tests."""
|
||||||
@@ -97,15 +107,18 @@ def run_all_tests():
|
|||||||
if not test_database_connection():
|
if not test_database_connection():
|
||||||
success = False
|
success = False
|
||||||
|
|
||||||
# Test models
|
# Import app here to avoid circular import
|
||||||
print("\n2. Testing database models...")
|
from app import app
|
||||||
if not test_models():
|
with app.app_context():
|
||||||
success = False
|
# Test models
|
||||||
|
print("\n2. Testing database models...")
|
||||||
# Print statistics
|
if not test_models():
|
||||||
print("\n3. Database statistics:")
|
success = False
|
||||||
if not print_database_stats():
|
|
||||||
success = False
|
# Print statistics
|
||||||
|
print("\n3. Database statistics:")
|
||||||
|
if not print_database_stats():
|
||||||
|
success = False
|
||||||
|
|
||||||
print("\n" + "=" * 60)
|
print("\n" + "=" * 60)
|
||||||
if success:
|
if success:
|
||||||
@@ -117,4 +130,7 @@ def run_all_tests():
|
|||||||
return success
|
return success
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
run_all_tests()
|
# Import app here to avoid circular import
|
||||||
|
from app import app
|
||||||
|
with app.app_context():
|
||||||
|
run_all_tests()
|
||||||
BIN
utils/fix_routes.py
Normal file
BIN
utils/fix_routes.py
Normal file
Binary file not shown.
65
utils/update_db.py
Normal file
65
utils/update_db.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
#!/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)
|
||||||
BIN
utils/update_routes.py
Normal file
BIN
utils/update_routes.py
Normal file
Binary file not shown.
@@ -55,7 +55,7 @@ def create_user(username, email, password, is_admin=False):
|
|||||||
user = User(
|
user = User(
|
||||||
username=username,
|
username=username,
|
||||||
email=email,
|
email=email,
|
||||||
is_admin=is_admin,
|
role='admin' if is_admin else 'user',
|
||||||
created_at=datetime.utcnow()
|
created_at=datetime.utcnow()
|
||||||
)
|
)
|
||||||
user.set_password(password)
|
user.set_password(password)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
home = C:\Program Files\Python313
|
home = C:\Program Files\Python313
|
||||||
include-system-site-packages = false
|
include-system-site-packages = false
|
||||||
version = 3.13.3
|
version = 3.13.3
|
||||||
executable = C:\Program Files\Python313\python.exe
|
executable = C:\Users\firem\Desktop\111\Systades\website\.venv\Scripts\python.exe
|
||||||
command = C:\Program Files\Python313\python.exe -m venv C:\Users\TTOMCZA.EMEA\Dev\website\venv
|
command = C:\Users\firem\Desktop\111\Systades\website\.venv\Scripts\python.exe -m venv C:\Users\firem\Desktop\111\Systades\website\venv
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
@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