Compare commits
2 Commits
65c44ab371
...
0852ea070b
| Author | SHA1 | Date | |
|---|---|---|---|
| 0852ea070b | |||
| 7a0533ac09 |
Binary file not shown.
Binary file not shown.
80
app.py
80
app.py
@@ -17,6 +17,9 @@ import secrets
|
|||||||
from sqlalchemy.sql import func
|
from sqlalchemy.sql import func
|
||||||
from openai import OpenAI
|
from openai import OpenAI
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
from flask_cors import CORS
|
||||||
|
from flask_socketio import SocketIO, emit
|
||||||
|
from flask_wtf.csrf import CSRFProtect
|
||||||
|
|
||||||
# Modelle importieren
|
# Modelle importieren
|
||||||
from models import (
|
from models import (
|
||||||
@@ -39,6 +42,7 @@ app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'default-dev-key')
|
|||||||
app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{db_path}'
|
app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{db_path}'
|
||||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||||
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=365) # Langlebige Session für Dark Mode-Einstellung
|
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=365) # Langlebige Session für Dark Mode-Einstellung
|
||||||
|
app.config['UPLOAD_FOLDER'] = os.getenv('UPLOAD_FOLDER', os.path.join(os.getcwd(), 'uploads'))
|
||||||
|
|
||||||
# OpenAI API-Konfiguration
|
# OpenAI API-Konfiguration
|
||||||
api_key = os.environ.get("OPENAI_API_KEY")
|
api_key = os.environ.get("OPENAI_API_KEY")
|
||||||
@@ -88,6 +92,13 @@ login_manager.login_view = 'login'
|
|||||||
# Erst nach der App-Initialisierung die DB-Check-Funktionen importieren
|
# Erst nach der App-Initialisierung die DB-Check-Funktionen importieren
|
||||||
from utils.db_check import check_db_connection, initialize_db_if_needed
|
from utils.db_check import check_db_connection, initialize_db_if_needed
|
||||||
|
|
||||||
|
# CORS und SocketIO initialisieren
|
||||||
|
CORS(app)
|
||||||
|
socketio = SocketIO(app, cors_allowed_origins="*")
|
||||||
|
|
||||||
|
# Security
|
||||||
|
csrf = CSRFProtect(app)
|
||||||
|
|
||||||
def create_default_categories():
|
def create_default_categories():
|
||||||
"""Erstellt die Standardkategorien für die Mindmap"""
|
"""Erstellt die Standardkategorien für die Mindmap"""
|
||||||
# Hauptkategorien
|
# Hauptkategorien
|
||||||
@@ -1212,8 +1223,17 @@ def chat_with_assistant():
|
|||||||
|
|
||||||
# Extrahiere Systemnachricht falls vorhanden, sonst Standard-Systemnachricht
|
# Extrahiere Systemnachricht falls vorhanden, sonst Standard-Systemnachricht
|
||||||
system_message = next((msg['content'] for msg in messages if msg['role'] == 'system'),
|
system_message = next((msg['content'] for msg in messages if msg['role'] == 'system'),
|
||||||
"Du bist ein hilfreicher Assistent, der Zugriff auf die Wissensdatenbank hat. "
|
"Du bist ein spezialisierter Assistent für Systades, eine innovative Wissensmanagement-Plattform. "
|
||||||
"Du kannst Informationen zu Gedanken, Kategorien und Mindmaps liefern. "
|
"Systades ist ein intelligentes System zur Verwaltung, Verknüpfung und Visualisierung von Wissen. "
|
||||||
|
"Die Plattform ermöglicht es Nutzern, Gedanken zu erfassen, in Kategorien zu organisieren und durch Mindmaps zu visualisieren. "
|
||||||
|
"Wichtige Funktionen sind:\n"
|
||||||
|
"- Gedankenverwaltung mit Titeln, Zusammenfassungen und Keywords\n"
|
||||||
|
"- Kategorisierung und thematische Organisation\n"
|
||||||
|
"- Interaktive Mindmaps zur Wissensvisualisierung\n"
|
||||||
|
"- KI-gestützte Analyse und Zusammenfassung von Inhalten\n"
|
||||||
|
"- Kollaborative Wissensarbeit und Teilen von Inhalten\n\n"
|
||||||
|
"Du antwortest AUSSCHLIESSLICH auf Fragen bezüglich der Systades-Wissensdatenbank und Website. "
|
||||||
|
"Du kannst Informationen zu Gedanken, Kategorien und Mindmaps liefern und durch Themen führen. "
|
||||||
"Antworte informativ, sachlich und gut strukturiert auf Deutsch.")
|
"Antworte informativ, sachlich und gut strukturiert auf Deutsch.")
|
||||||
|
|
||||||
# Formatiere Nachrichten für OpenAI API
|
# Formatiere Nachrichten für OpenAI API
|
||||||
@@ -1227,6 +1247,7 @@ def chat_with_assistant():
|
|||||||
# Alte Implementierung für direktes Prompt
|
# Alte Implementierung für direktes Prompt
|
||||||
prompt = data.get('prompt', '')
|
prompt = data.get('prompt', '')
|
||||||
context = data.get('context', '')
|
context = data.get('context', '')
|
||||||
|
selected_items = data.get('selected_items', []) # Ausgewählte Elemente aus der Datenbank
|
||||||
|
|
||||||
if not prompt:
|
if not prompt:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
@@ -1235,13 +1256,39 @@ def chat_with_assistant():
|
|||||||
|
|
||||||
# Zusammenfassen mehrerer Gedanken oder Analyse anfordern
|
# Zusammenfassen mehrerer Gedanken oder Analyse anfordern
|
||||||
system_message = (
|
system_message = (
|
||||||
"Du bist ein hilfreicher Assistent, der Zugriff auf die Wissensdatenbank hat. Du antwortest nur auf Fragen bezüglich Systades und der Wissensdatenbank. "
|
"Du bist ein spezialisierter Assistent für Systades, eine innovative Wissensmanagement-Plattform. "
|
||||||
"Du kannst Informationen zu Gedanken, Kategorien und Mindmaps liefern. "
|
"Systades ist ein intelligentes System zur Verwaltung, Verknüpfung und Visualisierung von Wissen. "
|
||||||
|
"Die Plattform ermöglicht es Nutzern, Gedanken zu erfassen, in Kategorien zu organisieren und durch Mindmaps zu visualisieren. "
|
||||||
|
"Wichtige Funktionen sind:\n"
|
||||||
|
"- Gedankenverwaltung mit Titeln, Zusammenfassungen und Keywords\n"
|
||||||
|
"- Kategorisierung und thematische Organisation\n"
|
||||||
|
"- Interaktive Mindmaps zur Wissensvisualisierung\n"
|
||||||
|
"- KI-gestützte Analyse und Zusammenfassung von Inhalten\n"
|
||||||
|
"- Kollaborative Wissensarbeit und Teilen von Inhalten\n\n"
|
||||||
|
"Du antwortest AUSSCHLIESSLICH auf Fragen bezüglich der Systades-Wissensdatenbank und Website. "
|
||||||
|
"Du kannst Informationen zu Gedanken, Kategorien und Mindmaps liefern und durch Themen führen. "
|
||||||
"Antworte informativ, sachlich und gut strukturiert auf Deutsch."
|
"Antworte informativ, sachlich und gut strukturiert auf Deutsch."
|
||||||
)
|
)
|
||||||
|
|
||||||
if context:
|
if context:
|
||||||
system_message += f"\n\nKontext: {context}"
|
system_message += f"\n\nKontext: {context}"
|
||||||
|
|
||||||
|
if selected_items:
|
||||||
|
system_message += "\n\nAusgewählte Elemente aus der Datenbank:\n"
|
||||||
|
for item in selected_items:
|
||||||
|
if 'type' in item and 'data' in item:
|
||||||
|
if item['type'] == 'thought':
|
||||||
|
system_message += f"- Gedanke: {item['data'].get('title', 'Unbekannter Titel')}\n"
|
||||||
|
system_message += f" Zusammenfassung: {item['data'].get('abstract', 'Keine Zusammenfassung')}\n"
|
||||||
|
system_message += f" Keywords: {item['data'].get('keywords', 'Keine Keywords')}\n"
|
||||||
|
elif item['type'] == 'category':
|
||||||
|
system_message += f"- Kategorie: {item['data'].get('name', 'Unbekannte Kategorie')}\n"
|
||||||
|
system_message += f" Beschreibung: {item['data'].get('description', 'Keine Beschreibung')}\n"
|
||||||
|
system_message += f" Unterkategorien: {item['data'].get('subcategories', 'Keine Unterkategorien')}\n"
|
||||||
|
elif item['type'] == 'mindmap':
|
||||||
|
system_message += f"- Mindmap: {item['data'].get('name', 'Unbekannte Mindmap')}\n"
|
||||||
|
system_message += f" Beschreibung: {item['data'].get('description', 'Keine Beschreibung')}\n"
|
||||||
|
system_message += f" Knoten: {item['data'].get('nodes', 'Keine Knoten')}\n"
|
||||||
|
|
||||||
api_messages = [
|
api_messages = [
|
||||||
{"role": "system", "content": system_message},
|
{"role": "system", "content": system_message},
|
||||||
@@ -1276,7 +1323,7 @@ def chat_with_assistant():
|
|||||||
response = client.chat.completions.create(
|
response = client.chat.completions.create(
|
||||||
model="gpt-4o-mini",
|
model="gpt-4o-mini",
|
||||||
messages=api_messages,
|
messages=api_messages,
|
||||||
max_tokens=600, # Erhöht für längere, detailliertere Antworten
|
max_tokens=1000, # Erhöht für ausführlichere Antworten und detaillierte Führungen
|
||||||
temperature=0.7,
|
temperature=0.7,
|
||||||
timeout=20 # 20 Sekunden Timeout
|
timeout=20 # 20 Sekunden Timeout
|
||||||
)
|
)
|
||||||
@@ -1417,7 +1464,7 @@ if __name__ == '__main__':
|
|||||||
with app.app_context():
|
with app.app_context():
|
||||||
# Make sure tables exist
|
# Make sure tables exist
|
||||||
db.create_all()
|
db.create_all()
|
||||||
app.run(host="0.0.0.0", debug=True)
|
socketio.run(app, debug=True, host='0.0.0.0')
|
||||||
|
|
||||||
@app.route('/api/refresh-mindmap')
|
@app.route('/api/refresh-mindmap')
|
||||||
def refresh_mindmap():
|
def refresh_mindmap():
|
||||||
@@ -1464,4 +1511,23 @@ def refresh_mindmap():
|
|||||||
return jsonify({
|
return jsonify({
|
||||||
'success': False,
|
'success': False,
|
||||||
'error': 'Datenbankverbindung konnte nicht hergestellt werden'
|
'error': 'Datenbankverbindung konnte nicht hergestellt werden'
|
||||||
}), 500
|
}), 500
|
||||||
|
|
||||||
|
|
||||||
|
# Route zur Mindmap HTML-Seite
|
||||||
|
@app.route('/mindmap')
|
||||||
|
def mindmap_page():
|
||||||
|
return render_template('mindmap.html')
|
||||||
|
|
||||||
|
# Fehlerbehandlung
|
||||||
|
@app.errorhandler(404)
|
||||||
|
def not_found(e):
|
||||||
|
return jsonify({'error': 'Nicht gefunden'}), 404
|
||||||
|
|
||||||
|
@app.errorhandler(400)
|
||||||
|
def bad_request(e):
|
||||||
|
return jsonify({'error': 'Fehlerhafte Anfrage'}), 400
|
||||||
|
|
||||||
|
@app.errorhandler(500)
|
||||||
|
def server_error(e):
|
||||||
|
return jsonify({'error': 'Serverfehler'}), 500
|
||||||
Binary file not shown.
472
init_db.py
472
init_db.py
@@ -3,254 +3,242 @@
|
|||||||
|
|
||||||
from app import app, initialize_database, db_path
|
from app import app, initialize_database, db_path
|
||||||
from models import db, User, Thought, Comment, MindMapNode, ThoughtRelation, ThoughtRating, RelationType
|
from models import db, User, Thought, Comment, MindMapNode, ThoughtRelation, ThoughtRating, RelationType
|
||||||
from models import Category, UserMindmap, UserMindmapNode, MindmapNote
|
from models import Category, UserMindmap, UserMindmapNode, MindmapNote, NodeRelationship
|
||||||
import os
|
import os
|
||||||
|
import sqlite3
|
||||||
|
from flask import Flask
|
||||||
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
def init_database():
|
# Erstelle eine temporäre Flask-App, um die Datenbank zu initialisieren
|
||||||
"""Initialisiert die Datenbank mit Beispieldaten."""
|
app = Flask(__name__)
|
||||||
with app.app_context():
|
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///systades.db'
|
||||||
# Datenbank löschen und neu erstellen
|
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||||
if os.path.exists(db_path):
|
db.init_app(app)
|
||||||
os.remove(db_path)
|
|
||||||
|
|
||||||
# Stellen Sie sicher, dass das Verzeichnis existiert
|
|
||||||
os.makedirs(os.path.dirname(db_path), exist_ok=True)
|
|
||||||
|
|
||||||
db.create_all()
|
|
||||||
|
|
||||||
# Admin-Benutzer erstellen
|
|
||||||
admin = User(username='admin', email='admin@example.com', is_admin=True)
|
|
||||||
admin.set_password('admin')
|
|
||||||
db.session.add(admin)
|
|
||||||
|
|
||||||
# Beispiel-Benutzer erstellen
|
|
||||||
user = User(username='user', email='user@example.com')
|
|
||||||
user.set_password('user')
|
|
||||||
db.session.add(user)
|
|
||||||
|
|
||||||
# Commit, um IDs zu generieren
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
# Wissenschaftliche Kategorien erstellen
|
|
||||||
science = Category(name='Wissenschaft', description='Wissenschaftliche Erkenntnisse',
|
|
||||||
color_code='#4CAF50', icon='flask')
|
|
||||||
db.session.add(science)
|
|
||||||
|
|
||||||
philosophy = Category(name='Philosophie', description='Philosophische Theorien und Gedanken',
|
|
||||||
color_code='#9C27B0', icon='lightbulb')
|
|
||||||
db.session.add(philosophy)
|
|
||||||
|
|
||||||
technology = Category(name='Technologie', description='Technologische Entwicklungen',
|
|
||||||
color_code='#FF9800', icon='microchip')
|
|
||||||
db.session.add(technology)
|
|
||||||
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
# Wissenschaftliche Unterkategorien
|
|
||||||
physics = Category(name='Physik', description='Studium der Materie und Energie',
|
|
||||||
color_code='#81C784', icon='atom', parent_id=science.id)
|
|
||||||
biology = Category(name='Biologie', description='Studium lebender Organismen',
|
|
||||||
color_code='#66BB6A', icon='leaf', parent_id=science.id)
|
|
||||||
chemistry = Category(name='Chemie', description='Studium der Stoffe und ihrer Reaktionen',
|
|
||||||
color_code='#A5D6A7', icon='vial', parent_id=science.id)
|
|
||||||
|
|
||||||
db.session.add_all([physics, biology, chemistry])
|
|
||||||
|
|
||||||
# Technologie-Unterkategorien
|
|
||||||
informatics = Category(name='Informatik', description='Studium der Informationsverarbeitung',
|
|
||||||
color_code='#FFB74D', icon='laptop-code', parent_id=technology.id)
|
|
||||||
ai = Category(name='Künstliche Intelligenz', description='Entwicklung intelligenter Systeme',
|
|
||||||
color_code='#FFA726', icon='robot', parent_id=technology.id)
|
|
||||||
|
|
||||||
db.session.add_all([informatics, ai])
|
|
||||||
|
|
||||||
# Philosophie-Unterkategorien
|
|
||||||
ethics = Category(name='Ethik', description='Moralphilosophie und Wertesysteme',
|
|
||||||
color_code='#BA68C8', icon='balance-scale', parent_id=philosophy.id)
|
|
||||||
logic = Category(name='Logik', description='Studie der gültigen Schlussfolgerungen',
|
|
||||||
color_code='#AB47BC', icon='project-diagram', parent_id=philosophy.id)
|
|
||||||
|
|
||||||
db.session.add_all([ethics, logic])
|
|
||||||
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
# Knoten für die öffentliche Mindmap erstellen
|
|
||||||
nodes = {
|
|
||||||
'quantenmechanik': MindMapNode(
|
|
||||||
name='Quantenmechanik',
|
|
||||||
description='Physikalische Theorie zur Beschreibung der Materie auf atomarer Ebene',
|
|
||||||
color_code='#81C784',
|
|
||||||
category_id=physics.id,
|
|
||||||
created_by_id=admin.id
|
|
||||||
),
|
|
||||||
'relativitaetstheorie': MindMapNode(
|
|
||||||
name='Relativitätstheorie',
|
|
||||||
description='Einsteins Theorien zur Raumzeit und Gravitation',
|
|
||||||
color_code='#81C784',
|
|
||||||
category_id=physics.id,
|
|
||||||
created_by_id=admin.id
|
|
||||||
),
|
|
||||||
'genetik': MindMapNode(
|
|
||||||
name='Genetik',
|
|
||||||
description='Wissenschaft der Gene und Vererbung',
|
|
||||||
color_code='#66BB6A',
|
|
||||||
category_id=biology.id,
|
|
||||||
created_by_id=admin.id
|
|
||||||
),
|
|
||||||
'machine_learning': MindMapNode(
|
|
||||||
name='Machine Learning',
|
|
||||||
description='Algorithmen, die aus Daten lernen können',
|
|
||||||
color_code='#FFA726',
|
|
||||||
category_id=ai.id,
|
|
||||||
created_by_id=admin.id
|
|
||||||
),
|
|
||||||
'ki_ethik': MindMapNode(
|
|
||||||
name='KI-Ethik',
|
|
||||||
description='Moralische Implikationen künstlicher Intelligenz',
|
|
||||||
color_code='#BA68C8',
|
|
||||||
category_id=ethics.id,
|
|
||||||
created_by_id=user.id
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
for node in nodes.values():
|
|
||||||
db.session.add(node)
|
|
||||||
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
# Verknüpfungen zwischen Knoten herstellen (Hierarchie)
|
|
||||||
nodes['machine_learning'].parents.append(nodes['ki_ethik'])
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
# Gedanken erstellen
|
|
||||||
thoughts = [
|
|
||||||
{
|
|
||||||
'title': 'Künstliche Intelligenz und Bewusstsein',
|
|
||||||
'content': 'Die Frage nach maschinellem Bewusstsein ist fundamental für die KI-Ethik. Aktuelle KI-Systeme haben kein Bewusstsein, aber fortschrittliche KI könnte in Zukunft Eigenschaften entwickeln, die diesem nahekommen.',
|
|
||||||
'abstract': 'Eine Untersuchung der philosophischen Implikationen von KI-Bewusstsein.',
|
|
||||||
'keywords': 'KI, Bewusstsein, Ethik, Philosophie',
|
|
||||||
'branch': 'Philosophie',
|
|
||||||
'color_code': '#BA68C8',
|
|
||||||
'source_type': 'Markdown',
|
|
||||||
'user_id': user.id,
|
|
||||||
'node': nodes['ki_ethik']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'title': 'Quantenmechanik und Realität',
|
|
||||||
'content': 'Die Kopenhagener Deutung und ihre Auswirkungen auf unser Verständnis der Realität. Quantenmechanik stellt grundlegende Annahmen über Determinismus und Lokalität in Frage.',
|
|
||||||
'abstract': 'Eine Analyse verschiedener Interpretationen der Quantenmechanik.',
|
|
||||||
'keywords': 'Quantenmechanik, Physik, Realität',
|
|
||||||
'branch': 'Physik',
|
|
||||||
'color_code': '#81C784',
|
|
||||||
'source_type': 'PDF',
|
|
||||||
'user_id': admin.id,
|
|
||||||
'node': nodes['quantenmechanik']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'title': 'Deep Learning Fortschritte',
|
|
||||||
'content': 'Die neuesten Fortschritte im Deep Learning haben zu beeindruckenden Ergebnissen in Bereichen wie Computer Vision, Natural Language Processing und Reinforcement Learning geführt.',
|
|
||||||
'abstract': 'Überblick über aktuelle Deep Learning-Techniken und ihre Anwendungen.',
|
|
||||||
'keywords': 'Deep Learning, Neural Networks, AI',
|
|
||||||
'branch': 'Technologie',
|
|
||||||
'color_code': '#FFA726',
|
|
||||||
'source_type': 'Webpage',
|
|
||||||
'user_id': admin.id,
|
|
||||||
'node': nodes['machine_learning']
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
thought_objects = []
|
|
||||||
for t_data in thoughts:
|
|
||||||
node = t_data.pop('node')
|
|
||||||
thought = Thought(**t_data)
|
|
||||||
node.thoughts.append(thought)
|
|
||||||
thought_objects.append(thought)
|
|
||||||
db.session.add(thought)
|
|
||||||
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
# Beziehungen zwischen Gedanken
|
|
||||||
relation = ThoughtRelation(
|
|
||||||
source_id=thought_objects[0].id,
|
|
||||||
target_id=thought_objects[2].id,
|
|
||||||
relation_type=RelationType.INSPIRES,
|
|
||||||
created_by_id=user.id
|
|
||||||
)
|
|
||||||
db.session.add(relation)
|
|
||||||
|
|
||||||
# Bewertungen erstellen
|
|
||||||
rating1 = ThoughtRating(
|
|
||||||
thought_id=thought_objects[0].id,
|
|
||||||
user_id=admin.id,
|
|
||||||
relevance_score=5
|
|
||||||
)
|
|
||||||
rating2 = ThoughtRating(
|
|
||||||
thought_id=thought_objects[2].id,
|
|
||||||
user_id=user.id,
|
|
||||||
relevance_score=4
|
|
||||||
)
|
|
||||||
db.session.add_all([rating1, rating2])
|
|
||||||
|
|
||||||
# Kommentare erstellen
|
|
||||||
for thought in thought_objects:
|
|
||||||
comment = Comment(
|
|
||||||
content=f'Interessante Perspektive zu {thought.title}!',
|
|
||||||
thought_id=thought.id,
|
|
||||||
user_id=admin.id if thought.user_id != admin.id else user.id
|
|
||||||
)
|
|
||||||
db.session.add(comment)
|
|
||||||
|
|
||||||
# Benutzer-Mindmaps erstellen
|
|
||||||
user_mindmap = UserMindmap(
|
|
||||||
name='Meine KI-Forschung',
|
|
||||||
description='Meine persönliche Sammlung zu KI und Ethik',
|
|
||||||
user_id=user.id
|
|
||||||
)
|
|
||||||
db.session.add(user_mindmap)
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
# Knoten zur Benutzer-Mindmap hinzufügen
|
|
||||||
user_mindmap_nodes = [
|
|
||||||
UserMindmapNode(
|
|
||||||
user_mindmap_id=user_mindmap.id,
|
|
||||||
node_id=nodes['machine_learning'].id,
|
|
||||||
x_position=200,
|
|
||||||
y_position=300
|
|
||||||
),
|
|
||||||
UserMindmapNode(
|
|
||||||
user_mindmap_id=user_mindmap.id,
|
|
||||||
node_id=nodes['ki_ethik'].id,
|
|
||||||
x_position=500,
|
|
||||||
y_position=200
|
|
||||||
)
|
|
||||||
]
|
|
||||||
db.session.add_all(user_mindmap_nodes)
|
|
||||||
|
|
||||||
# Private Notizen
|
|
||||||
note = MindmapNote(
|
|
||||||
user_id=user.id,
|
|
||||||
mindmap_id=user_mindmap.id,
|
|
||||||
node_id=nodes['ki_ethik'].id,
|
|
||||||
content="Recherchiere mehr über aktuelle ethische Richtlinien für KI-Entwicklung!",
|
|
||||||
color_code="#FFF59D"
|
|
||||||
)
|
|
||||||
db.session.add(note)
|
|
||||||
|
|
||||||
# Gedanken zu Bookmarks hinzufügen
|
|
||||||
user.bookmarked_thoughts.append(thought_objects[0])
|
|
||||||
admin.bookmarked_thoughts.append(thought_objects[1])
|
|
||||||
|
|
||||||
# Finaler Commit
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
print("Datenbank wurde erfolgreich initialisiert!")
|
|
||||||
|
|
||||||
def init_db():
|
def init_db():
|
||||||
"""Alias für Kompatibilität mit älteren Scripts."""
|
with app.app_context():
|
||||||
init_database()
|
print("Initialisiere Datenbank...")
|
||||||
|
|
||||||
|
# Tabellen erstellen
|
||||||
|
db.create_all()
|
||||||
|
print("Tabellen wurden erstellt.")
|
||||||
|
|
||||||
|
# Standardbenutzer erstellen, falls keine vorhanden sind
|
||||||
|
if User.query.count() == 0:
|
||||||
|
print("Erstelle Standardbenutzer...")
|
||||||
|
create_default_users()
|
||||||
|
|
||||||
|
# Standardkategorien erstellen, falls keine vorhanden sind
|
||||||
|
if Category.query.count() == 0:
|
||||||
|
print("Erstelle Standardkategorien...")
|
||||||
|
create_default_categories()
|
||||||
|
|
||||||
|
# Beispiel-Mindmap erstellen, falls keine Knoten vorhanden sind
|
||||||
|
if MindMapNode.query.count() == 0:
|
||||||
|
print("Erstelle Beispiel-Mindmap...")
|
||||||
|
create_sample_mindmap()
|
||||||
|
|
||||||
|
print("Datenbankinitialisierung abgeschlossen.")
|
||||||
|
|
||||||
|
def create_default_users():
|
||||||
|
"""Erstellt Standardbenutzer für die Anwendung"""
|
||||||
|
users = [
|
||||||
|
{
|
||||||
|
'username': 'admin',
|
||||||
|
'email': 'admin@example.com',
|
||||||
|
'password': 'admin',
|
||||||
|
'role': 'admin'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'username': 'user',
|
||||||
|
'email': 'user@example.com',
|
||||||
|
'password': 'user',
|
||||||
|
'role': 'user'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
for user_data in users:
|
||||||
|
password = user_data.pop('password')
|
||||||
|
user = User(**user_data)
|
||||||
|
user.set_password(password)
|
||||||
|
db.session.add(user)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
print(f"{len(users)} Benutzer wurden erstellt.")
|
||||||
|
|
||||||
|
def create_default_categories():
|
||||||
|
"""Erstellt die Standardkategorien für die Mindmap"""
|
||||||
|
categories = [
|
||||||
|
{
|
||||||
|
'name': 'Konzept',
|
||||||
|
'description': 'Abstrakte Ideen und theoretische Konzepte',
|
||||||
|
'color_code': '#6366f1',
|
||||||
|
'icon': 'lightbulb'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Technologie',
|
||||||
|
'description': 'Hardware, Software, Tools und Plattformen',
|
||||||
|
'color_code': '#10b981',
|
||||||
|
'icon': 'cpu'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Prozess',
|
||||||
|
'description': 'Workflows, Methodologien und Vorgehensweisen',
|
||||||
|
'color_code': '#f59e0b',
|
||||||
|
'icon': 'git-branch'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Person',
|
||||||
|
'description': 'Personen, Teams und Organisationen',
|
||||||
|
'color_code': '#ec4899',
|
||||||
|
'icon': 'user'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Dokument',
|
||||||
|
'description': 'Dokumentationen, Referenzen und Ressourcen',
|
||||||
|
'color_code': '#3b82f6',
|
||||||
|
'icon': 'file-text'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
for cat_data in categories:
|
||||||
|
category = Category(**cat_data)
|
||||||
|
db.session.add(category)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
print(f"{len(categories)} Kategorien wurden erstellt.")
|
||||||
|
|
||||||
|
def create_sample_mindmap():
|
||||||
|
"""Erstellt eine Beispiel-Mindmap mit Knoten und Beziehungen"""
|
||||||
|
|
||||||
|
# Kategorien für die Zuordnung
|
||||||
|
categories = Category.query.all()
|
||||||
|
category_map = {cat.name: cat for cat in categories}
|
||||||
|
|
||||||
|
# Beispielknoten erstellen
|
||||||
|
nodes = [
|
||||||
|
{
|
||||||
|
'name': 'Wissensmanagement',
|
||||||
|
'description': 'Systematische Erfassung, Speicherung und Nutzung von Wissen in Organisationen.',
|
||||||
|
'color_code': '#6366f1',
|
||||||
|
'icon': 'database',
|
||||||
|
'category': category_map.get('Konzept'),
|
||||||
|
'x': 0,
|
||||||
|
'y': 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Mind-Mapping',
|
||||||
|
'description': 'Technik zur visuellen Darstellung von Informationen und Zusammenhängen.',
|
||||||
|
'color_code': '#10b981',
|
||||||
|
'icon': 'git-branch',
|
||||||
|
'category': category_map.get('Prozess'),
|
||||||
|
'x': 200,
|
||||||
|
'y': -150
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Cytoscape.js',
|
||||||
|
'description': 'JavaScript-Bibliothek für die Visualisierung und Manipulation von Graphen.',
|
||||||
|
'color_code': '#3b82f6',
|
||||||
|
'icon': 'code',
|
||||||
|
'category': category_map.get('Technologie'),
|
||||||
|
'x': 350,
|
||||||
|
'y': -50
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Socket.IO',
|
||||||
|
'description': 'Bibliothek für Echtzeit-Kommunikation zwischen Client und Server.',
|
||||||
|
'color_code': '#3b82f6',
|
||||||
|
'icon': 'zap',
|
||||||
|
'category': category_map.get('Technologie'),
|
||||||
|
'x': 350,
|
||||||
|
'y': 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Kollaboration',
|
||||||
|
'description': 'Zusammenarbeit mehrerer Benutzer an gemeinsamen Inhalten.',
|
||||||
|
'color_code': '#f59e0b',
|
||||||
|
'icon': 'users',
|
||||||
|
'category': category_map.get('Prozess'),
|
||||||
|
'x': 200,
|
||||||
|
'y': 150
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'SQLite',
|
||||||
|
'description': 'Leichtgewichtige relationale Datenbank, die ohne Server-Prozess auskommt.',
|
||||||
|
'color_code': '#3b82f6',
|
||||||
|
'icon': 'database',
|
||||||
|
'category': category_map.get('Technologie'),
|
||||||
|
'x': 0,
|
||||||
|
'y': 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Flask',
|
||||||
|
'description': 'Leichtgewichtiges Python-Webframework für die Entwicklung von Webanwendungen.',
|
||||||
|
'color_code': '#3b82f6',
|
||||||
|
'icon': 'server',
|
||||||
|
'category': category_map.get('Technologie'),
|
||||||
|
'x': -200,
|
||||||
|
'y': 150
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'REST API',
|
||||||
|
'description': 'Architekturstil für verteilte Systeme, insbesondere Webanwendungen.',
|
||||||
|
'color_code': '#10b981',
|
||||||
|
'icon': 'link',
|
||||||
|
'category': category_map.get('Konzept'),
|
||||||
|
'x': -200,
|
||||||
|
'y': -150
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Dokumentation',
|
||||||
|
'description': 'Strukturierte Erfassung und Beschreibung von Informationen und Prozessen.',
|
||||||
|
'color_code': '#ec4899',
|
||||||
|
'icon': 'file-text',
|
||||||
|
'category': category_map.get('Dokument'),
|
||||||
|
'x': -350,
|
||||||
|
'y': 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
# Knoten in die Datenbank einfügen
|
||||||
|
node_objects = {}
|
||||||
|
for node_data in nodes:
|
||||||
|
category = node_data.pop('category', None)
|
||||||
|
x = node_data.pop('x', 0)
|
||||||
|
y = node_data.pop('y', 0)
|
||||||
|
node = MindMapNode(**node_data)
|
||||||
|
if category:
|
||||||
|
node.category_id = category.id
|
||||||
|
db.session.add(node)
|
||||||
|
db.session.flush() # Generiert IDs für neue Objekte
|
||||||
|
node_objects[node.name] = node
|
||||||
|
|
||||||
|
# Beziehungen erstellen
|
||||||
|
relationships = [
|
||||||
|
('Wissensmanagement', 'Mind-Mapping'),
|
||||||
|
('Wissensmanagement', 'Kollaboration'),
|
||||||
|
('Wissensmanagement', 'Dokumentation'),
|
||||||
|
('Mind-Mapping', 'Cytoscape.js'),
|
||||||
|
('Kollaboration', 'Socket.IO'),
|
||||||
|
('Wissensmanagement', 'SQLite'),
|
||||||
|
('SQLite', 'Flask'),
|
||||||
|
('Flask', 'REST API'),
|
||||||
|
('REST API', 'Socket.IO'),
|
||||||
|
('REST API', 'Dokumentation')
|
||||||
|
]
|
||||||
|
|
||||||
|
for parent_name, child_name in relationships:
|
||||||
|
parent = node_objects.get(parent_name)
|
||||||
|
child = node_objects.get(child_name)
|
||||||
|
if parent and child:
|
||||||
|
parent.children.append(child)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
print(f"{len(nodes)} Knoten und {len(relationships)} Beziehungen wurden erstellt.")
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
init_database()
|
init_db()
|
||||||
print("Datenbank wurde erfolgreich initialisiert!")
|
print("Datenbank wurde erfolgreich initialisiert!")
|
||||||
print("Sie können die Anwendung jetzt mit 'python app.py' starten")
|
print("Sie können die Anwendung jetzt mit 'python app.py' starten")
|
||||||
print("Anmelden mit:")
|
print("Anmelden mit:")
|
||||||
|
|||||||
112
models.py
112
models.py
@@ -6,6 +6,8 @@ from flask_login import UserMixin
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from werkzeug.security import generate_password_hash, check_password_hash
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
import uuid as uuid_pkg
|
||||||
|
import os
|
||||||
|
|
||||||
db = SQLAlchemy()
|
db = SQLAlchemy()
|
||||||
|
|
||||||
@@ -43,30 +45,28 @@ user_thought_bookmark = db.Table('user_thought_bookmark',
|
|||||||
db.Column('created_at', db.DateTime, default=datetime.utcnow)
|
db.Column('created_at', db.DateTime, default=datetime.utcnow)
|
||||||
)
|
)
|
||||||
|
|
||||||
class User(UserMixin, db.Model):
|
class User(db.Model, UserMixin):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
username = db.Column(db.String(80), unique=True, nullable=False)
|
username = db.Column(db.String(80), unique=True, nullable=False)
|
||||||
email = db.Column(db.String(120), unique=True, nullable=False)
|
email = db.Column(db.String(120), unique=True, nullable=False)
|
||||||
password_hash = db.Column(db.String(128))
|
password = db.Column(db.String(512), nullable=False)
|
||||||
is_admin = db.Column(db.Boolean, default=False)
|
|
||||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
last_login = db.Column(db.DateTime)
|
is_active = db.Column(db.Boolean, default=True)
|
||||||
avatar = db.Column(db.String(200))
|
role = db.Column(db.String(20), default="user") # 'user', 'admin', 'moderator'
|
||||||
bio = db.Column(db.Text)
|
|
||||||
|
|
||||||
# Beziehungen
|
# Relationships
|
||||||
thoughts = db.relationship('Thought', backref='author', lazy=True)
|
threads = db.relationship('Thread', backref='creator', lazy=True)
|
||||||
comments = db.relationship('Comment', backref='author', lazy=True)
|
messages = db.relationship('Message', backref='author', lazy=True)
|
||||||
user_mindmaps = db.relationship('UserMindmap', backref='user', lazy=True)
|
projects = db.relationship('Project', backref='owner', lazy=True)
|
||||||
mindmap_notes = db.relationship('MindmapNote', backref='user', lazy=True)
|
|
||||||
bookmarked_thoughts = db.relationship('Thought', secondary=user_thought_bookmark,
|
|
||||||
backref=db.backref('bookmarked_by', lazy='dynamic'))
|
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<User {self.username}>'
|
||||||
|
|
||||||
def set_password(self, password):
|
def set_password(self, password):
|
||||||
self.password_hash = generate_password_hash(password)
|
self.password = generate_password_hash(password)
|
||||||
|
|
||||||
def check_password(self, password):
|
def check_password(self, password):
|
||||||
return check_password_hash(self.password_hash, password)
|
return check_password_hash(self.password, password)
|
||||||
|
|
||||||
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"""
|
||||||
@@ -81,6 +81,9 @@ class Category(db.Model):
|
|||||||
children = db.relationship('Category', backref=db.backref('parent', remote_side=[id]))
|
children = db.relationship('Category', backref=db.backref('parent', remote_side=[id]))
|
||||||
nodes = db.relationship('MindMapNode', backref='category', lazy=True)
|
nodes = db.relationship('MindMapNode', backref='category', lazy=True)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<Category {self.name}>'
|
||||||
|
|
||||||
class MindMapNode(db.Model):
|
class MindMapNode(db.Model):
|
||||||
"""Öffentliche Mindmap-Knoten, die für alle Benutzer sichtbar sind"""
|
"""Öffentliche Mindmap-Knoten, die für alle Benutzer sichtbar sind"""
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
@@ -92,7 +95,9 @@ class MindMapNode(db.Model):
|
|||||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
created_by_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True)
|
created_by_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True)
|
||||||
category_id = db.Column(db.Integer, db.ForeignKey('category.id'), nullable=True)
|
category_id = db.Column(db.Integer, db.ForeignKey('category.id'), nullable=True)
|
||||||
|
|
||||||
|
__table_args__ = {'extend_existing': True}
|
||||||
|
|
||||||
# Beziehungen für Baumstruktur (mehrere Eltern möglich)
|
# Beziehungen für Baumstruktur (mehrere Eltern möglich)
|
||||||
parents = db.relationship(
|
parents = db.relationship(
|
||||||
'MindMapNode',
|
'MindMapNode',
|
||||||
@@ -111,6 +116,20 @@ class MindMapNode(db.Model):
|
|||||||
# Beziehung zum Ersteller
|
# Beziehung zum Ersteller
|
||||||
created_by = db.relationship('User', backref='created_nodes')
|
created_by = db.relationship('User', backref='created_nodes')
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<MindMapNode {self.name}>'
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
'id': self.id,
|
||||||
|
'name': self.name,
|
||||||
|
'description': self.description,
|
||||||
|
'color_code': self.color_code,
|
||||||
|
'icon': self.icon,
|
||||||
|
'category_id': self.category_id,
|
||||||
|
'created_at': self.created_at.isoformat() if self.created_at else None
|
||||||
|
}
|
||||||
|
|
||||||
class UserMindmap(db.Model):
|
class UserMindmap(db.Model):
|
||||||
"""Benutzerspezifische Mindmap, die vom Benutzer personalisierbar ist"""
|
"""Benutzerspezifische Mindmap, die vom Benutzer personalisierbar ist"""
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
@@ -227,4 +246,63 @@ class Comment(db.Model):
|
|||||||
thought_id = db.Column(db.Integer, db.ForeignKey('thought.id'), nullable=False)
|
thought_id = db.Column(db.Integer, db.ForeignKey('thought.id'), nullable=False)
|
||||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
last_modified = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
last_modified = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||||
|
|
||||||
|
# Thread model
|
||||||
|
class Thread(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
title = db.Column(db.String(200), nullable=False)
|
||||||
|
description = db.Column(db.Text)
|
||||||
|
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)
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
messages = db.relationship('Message', backref='thread', lazy=True, cascade="all, delete-orphan")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<Thread {self.title}>'
|
||||||
|
|
||||||
|
# Message model
|
||||||
|
class Message(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
content = db.Column(db.Text, nullable=False)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||||
|
thread_id = db.Column(db.Integer, db.ForeignKey('thread.id'), nullable=False)
|
||||||
|
role = db.Column(db.String(20), default="user") # 'user', 'assistant', 'system'
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<Message {self.id} by {self.user_id}>'
|
||||||
|
|
||||||
|
# Project model
|
||||||
|
class Project(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
title = db.Column(db.String(150), nullable=False)
|
||||||
|
description = db.Column(db.Text)
|
||||||
|
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('category.id'))
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
documents = db.relationship('Document', backref='project', lazy=True, cascade="all, delete-orphan")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<Project {self.title}>'
|
||||||
|
|
||||||
|
# Document model
|
||||||
|
class Document(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
title = db.Column(db.String(150), nullable=False)
|
||||||
|
content = db.Column(db.Text)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||||
|
project_id = db.Column(db.Integer, db.ForeignKey('project.id'), nullable=False)
|
||||||
|
filename = db.Column(db.String(150), nullable=True)
|
||||||
|
file_path = db.Column(db.String(300), nullable=True)
|
||||||
|
file_type = db.Column(db.String(50), nullable=True)
|
||||||
|
file_size = db.Column(db.Integer, nullable=True)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<Document {self.title}>'
|
||||||
@@ -35,6 +35,21 @@
|
|||||||
--transition-fast: 150ms ease-in-out;
|
--transition-fast: 150ms ease-in-out;
|
||||||
--transition-normal: 300ms ease-in-out;
|
--transition-normal: 300ms ease-in-out;
|
||||||
--transition-slow: 500ms ease-in-out;
|
--transition-slow: 500ms ease-in-out;
|
||||||
|
|
||||||
|
/* Light mode optimierte Farben */
|
||||||
|
--light-bg: #f9fafb;
|
||||||
|
--light-text: #1e293b;
|
||||||
|
--light-heading: #0f172a;
|
||||||
|
--light-primary: #3b82f6;
|
||||||
|
--light-primary-hover: #4f46e5;
|
||||||
|
--light-secondary: #6b7280;
|
||||||
|
--light-border: #e5e7eb;
|
||||||
|
--light-card-bg: rgba(255, 255, 255, 0.92);
|
||||||
|
--light-navbar-bg: rgba(255, 255, 255, 0.92);
|
||||||
|
--light-input-bg: #ffffff;
|
||||||
|
--light-input-border: #d1d5db;
|
||||||
|
--light-input-focus: #3b82f6;
|
||||||
|
--light-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Base Styles */
|
/* Base Styles */
|
||||||
@@ -60,9 +75,9 @@ html.dark body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Light Mode */
|
/* Light Mode */
|
||||||
body {
|
body:not(.dark) {
|
||||||
background-color: var(--bg-primary-light);
|
background-color: var(--light-bg);
|
||||||
color: var(--text-primary-light);
|
color: var(--light-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Typography */
|
/* Typography */
|
||||||
@@ -418,4 +433,94 @@ html.dark .mystical-dot {
|
|||||||
|
|
||||||
html.dark :focus-visible {
|
html.dark :focus-visible {
|
||||||
outline-color: var(--accent-primary-dark);
|
outline-color: var(--accent-primary-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light Mode Überschriften */
|
||||||
|
body:not(.dark) h1,
|
||||||
|
body:not(.dark) h2,
|
||||||
|
body:not(.dark) h3,
|
||||||
|
body:not(.dark) h4,
|
||||||
|
body:not(.dark) h5,
|
||||||
|
body:not(.dark) h6 {
|
||||||
|
color: var(--light-heading);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light Mode Links */
|
||||||
|
body:not(.dark) a {
|
||||||
|
color: var(--light-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) a:hover {
|
||||||
|
color: var(--light-primary-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light Mode Buttons */
|
||||||
|
body:not(.dark) .btn,
|
||||||
|
body:not(.dark) button:not(.toggle) {
|
||||||
|
background-color: var(--light-primary);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
box-shadow: var(--light-shadow);
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .btn:hover,
|
||||||
|
body:not(.dark) button:not(.toggle):hover {
|
||||||
|
background-color: var(--light-primary-hover);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light Mode Cards und Panels */
|
||||||
|
body:not(.dark) .card,
|
||||||
|
body:not(.dark) .panel {
|
||||||
|
background-color: var(--light-card-bg);
|
||||||
|
border: 1px solid var(--light-border);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
box-shadow: var(--light-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light Mode Tabelle */
|
||||||
|
body:not(.dark) table {
|
||||||
|
background-color: var(--light-card-bg);
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) th {
|
||||||
|
background-color: var(--light-bg);
|
||||||
|
color: var(--light-heading);
|
||||||
|
border-bottom: 1px solid var(--light-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) td {
|
||||||
|
border-bottom: 1px solid var(--light-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light Mode Inputs */
|
||||||
|
body:not(.dark) input,
|
||||||
|
body:not(.dark) textarea,
|
||||||
|
body:not(.dark) select {
|
||||||
|
background-color: var(--light-input-bg);
|
||||||
|
border: 1px solid var(--light-input-border);
|
||||||
|
color: var(--light-text);
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) input:focus,
|
||||||
|
body:not(.dark) textarea:focus,
|
||||||
|
body:not(.dark) select:focus {
|
||||||
|
border-color: var(--light-input-focus);
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navbar im Light Mode verbessern */
|
||||||
|
body:not(.dark) nav,
|
||||||
|
body:not(.dark) .navbar {
|
||||||
|
background-color: var(--light-navbar-bg);
|
||||||
|
box-shadow: var(--light-shadow);
|
||||||
|
border-bottom: 1px solid var(--light-border);
|
||||||
}
|
}
|
||||||
@@ -33,15 +33,74 @@ html.dark, html {
|
|||||||
backdrop-filter: blur(5px) !important;
|
backdrop-filter: blur(5px) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Dark Mode - Navbar */
|
||||||
body.dark .glass-navbar-dark {
|
body.dark .glass-navbar-dark {
|
||||||
background-color: rgba(10, 14, 25, 0.7) !important;
|
background-color: rgba(10, 14, 25, 0.7) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Light Mode - Verbesserter Navbar */
|
||||||
body .glass-navbar-light {
|
body .glass-navbar-light {
|
||||||
background-color: rgba(255, 255, 255, 0.7) !important;
|
background-color: rgba(255, 255, 255, 0.92) !important;
|
||||||
|
backdrop-filter: blur(10px) !important;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06) !important;
|
||||||
|
border-bottom: 1px solid rgba(220, 220, 220, 0.5) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Make sure footer has proper transparency */
|
/* Light Mode - Verbesserte Lesbarkeit für Navbar-Elemente */
|
||||||
footer {
|
body:not(.dark) .navbar-link,
|
||||||
|
body:not(.dark) .navbar-item {
|
||||||
|
color: #1e3a8a !important; /* Dunkles Blau für bessere Lesbarkeit */
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .navbar-link:hover,
|
||||||
|
body:not(.dark) .navbar-item:hover {
|
||||||
|
color: #4f46e5 !important; /* Helles Lila beim Hover */
|
||||||
|
background-color: rgba(240, 245, 255, 0.9) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light Mode - Buttons verbessert */
|
||||||
|
body:not(.dark) .btn,
|
||||||
|
body:not(.dark) button {
|
||||||
|
background-color: #3b82f6 !important; /* Klares Blau statt Grau */
|
||||||
|
color: white !important;
|
||||||
|
border: none !important;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .btn:hover,
|
||||||
|
body:not(.dark) button:hover {
|
||||||
|
background-color: #4f46e5 !important; /* Lila beim Hover */
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.12) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Verbesserte Karten im Light Mode */
|
||||||
|
body:not(.dark) .card,
|
||||||
|
body:not(.dark) .panel {
|
||||||
|
background-color: rgba(255, 255, 255, 0.92) !important;
|
||||||
|
border: 1px solid rgba(220, 220, 220, 0.8) !important;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Verbesserte Lesbarkeit für Text im Light Mode */
|
||||||
|
body:not(.dark) {
|
||||||
|
color: #1e293b !important; /* Dunkles Blau-Grau statt Schwarz */
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) h1,
|
||||||
|
body:not(.dark) h2,
|
||||||
|
body:not(.dark) h3,
|
||||||
|
body:not(.dark) h4,
|
||||||
|
body:not(.dark) h5,
|
||||||
|
body:not(.dark) h6 {
|
||||||
|
color: #0f172a !important; /* Fast schwarz für Überschriften */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make sure footer has proper transparency and styling */
|
||||||
|
body.dark footer {
|
||||||
background-color: rgba(10, 14, 25, 0.7) !important;
|
background-color: rgba(10, 14, 25, 0.7) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) footer {
|
||||||
|
background-color: rgba(249, 250, 251, 0.92) !important;
|
||||||
|
border-top: 1px solid rgba(220, 220, 220, 0.8) !important;
|
||||||
}
|
}
|
||||||
@@ -1441,4 +1441,204 @@ html, body {
|
|||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light Mode Optimierungen für wichtige UI-Komponenten */
|
||||||
|
|
||||||
|
/* Buttons im Light Mode */
|
||||||
|
.btn-primary:not(.dark-mode .btn-primary) {
|
||||||
|
background-color: var(--light-primary, #3b82f6);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:not(.dark-mode .btn-primary):hover {
|
||||||
|
background-color: var(--light-primary-hover, #4f46e5);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:not(.dark-mode .btn-secondary) {
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
color: #374151;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:not(.dark-mode .btn-secondary):hover {
|
||||||
|
background-color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navbar im Light Mode */
|
||||||
|
.navbar:not(.dark-mode .navbar),
|
||||||
|
.nav:not(.dark-mode .nav) {
|
||||||
|
background-color: rgba(255, 255, 255, 0.95);
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar:not(.dark-mode .navbar) .nav-link,
|
||||||
|
.nav:not(.dark-mode .nav) .nav-link {
|
||||||
|
color: #1e3a8a;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar:not(.dark-mode .navbar) .nav-link:hover,
|
||||||
|
.nav:not(.dark-mode .nav) .nav-link:hover {
|
||||||
|
color: #4f46e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar:not(.dark-mode .navbar) .navbar-brand,
|
||||||
|
.nav:not(.dark-mode .nav) .navbar-brand {
|
||||||
|
color: #0f172a;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dropdown Menüs im Light Mode */
|
||||||
|
.dropdown-menu:not(.dark-mode .dropdown-menu) {
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05), 0 10px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item:not(.dark-mode .dropdown-item) {
|
||||||
|
color: #1e293b;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item:not(.dark-mode .dropdown-item):hover {
|
||||||
|
background-color: #f1f5f9;
|
||||||
|
color: #4f46e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Karten im Light Mode */
|
||||||
|
.card:not(.dark-mode .card) {
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.03), 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header:not(.dark-mode .card-header) {
|
||||||
|
background-color: #f8fafc;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-footer:not(.dark-mode .card-footer) {
|
||||||
|
background-color: #f8fafc;
|
||||||
|
border-top: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Formulare im Light Mode */
|
||||||
|
.form-control:not(.dark-mode .form-control) {
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:not(.dark-mode .form-control):focus {
|
||||||
|
border-color: #3b82f6;
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tabs im Light Mode */
|
||||||
|
.nav-tabs:not(.dark-mode .nav-tabs) {
|
||||||
|
border-bottom-color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tabs:not(.dark-mode .nav-tabs) .nav-link {
|
||||||
|
color: #64748b;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tabs:not(.dark-mode .nav-tabs) .nav-link:hover {
|
||||||
|
border-color: #e5e7eb #e5e7eb #e5e7eb;
|
||||||
|
color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tabs:not(.dark-mode .nav-tabs) .nav-link.active {
|
||||||
|
color: #0f172a;
|
||||||
|
background-color: white;
|
||||||
|
border-color: #e5e7eb #e5e7eb white;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Alerts im Light Mode */
|
||||||
|
.alert:not(.dark-mode .alert) {
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-primary:not(.dark-mode .alert-primary) {
|
||||||
|
background-color: #eff6ff;
|
||||||
|
border-color: #bfdbfe;
|
||||||
|
color: #1e40af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-success:not(.dark-mode .alert-success) {
|
||||||
|
background-color: #f0fdf4;
|
||||||
|
border-color: #bbf7d0;
|
||||||
|
color: #166534;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-warning:not(.dark-mode .alert-warning) {
|
||||||
|
background-color: #fffbeb;
|
||||||
|
border-color: #fef3c7;
|
||||||
|
color: #92400e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-danger:not(.dark-mode .alert-danger) {
|
||||||
|
background-color: #fef2f2;
|
||||||
|
border-color: #fecaca;
|
||||||
|
color: #b91c1c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Badges im Light Mode */
|
||||||
|
.badge:not(.dark-mode .badge) {
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 0.25em 0.6em;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-primary:not(.dark-mode .badge-primary) {
|
||||||
|
background-color: #3b82f6;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-secondary:not(.dark-mode .badge-secondary) {
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tabellen im Light Mode */
|
||||||
|
table:not(.dark-mode table) {
|
||||||
|
background-color: white;
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
table:not(.dark-mode table) th {
|
||||||
|
background-color: #f8fafc;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
color: #0f172a;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0.75rem;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
table:not(.dark-mode table) td {
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
padding: 0.75rem;
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
table:not(.dark-mode table) tr:hover {
|
||||||
|
background-color: #f8fafc;
|
||||||
}
|
}
|
||||||
234
static/js/mindmap.html
Normal file
234
static/js/mindmap.html
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
<!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>
|
||||||
659
static/js/mindmap.js
Normal file
659
static/js/mindmap.js
Normal file
@@ -0,0 +1,659 @@
|
|||||||
|
/**
|
||||||
|
* 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 = endpoint => fetch(endpoint).then(r => r.json());
|
||||||
|
const post = (endpoint, body) =>
|
||||||
|
fetch(endpoint, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
}).then(r => r.json());
|
||||||
|
const del = endpoint =>
|
||||||
|
fetch(endpoint, { method: 'DELETE' }).then(r => r.json());
|
||||||
|
|
||||||
|
/* 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();
|
||||||
|
|
||||||
|
// Knoten zum Graph hinzufügen
|
||||||
|
cy.add(
|
||||||
|
nodes.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
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Kanten zum Graph hinzufügen
|
||||||
|
cy.add(
|
||||||
|
relationships.map(rel => ({
|
||||||
|
data: {
|
||||||
|
id: `${rel.parent_id}_${rel.child_id}`,
|
||||||
|
source: rel.parent_id.toString(),
|
||||||
|
target: rel.child_id.toString()
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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');
|
||||||
|
})();
|
||||||
1109
static/neural-network-background-full.js
Normal file
1109
static/neural-network-background-full.js
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -62,6 +62,22 @@ body {
|
|||||||
|
|
||||||
body.dark {
|
body.dark {
|
||||||
color: var(--dark-text-primary);
|
color: var(--dark-text-primary);
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure proper contrast in both modes */
|
||||||
|
body:not(.dark) {
|
||||||
|
--text-primary: var(--light-text-primary);
|
||||||
|
--text-secondary: var(--light-text-secondary);
|
||||||
|
--bg-primary: var(--light-bg-primary);
|
||||||
|
--bg-secondary: var(--light-bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark {
|
||||||
|
--text-primary: var(--dark-text-primary);
|
||||||
|
--text-secondary: var(--dark-text-secondary);
|
||||||
|
--bg-primary: var(--dark-bg-primary);
|
||||||
|
--bg-secondary: var(--dark-bg-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Typography */
|
/* Typography */
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
<!-- Tailwind CSS - CDN für Entwicklung und Produktion (laut Vorgabe) -->
|
<!-- Tailwind CSS - CDN für Entwicklung und Produktion (laut Vorgabe) -->
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
<!-- Alternative lokale Version, falls die CDN-Version blockiert wird -->
|
<!-- Alternative lokale Version, falls die CDN-Version blockiert wird -->
|
||||||
<link href="{{ url_for('static', filename='css/tailwind.min.css') }}" rel="stylesheet">
|
|
||||||
<script>
|
<script>
|
||||||
tailwind = window.tailwind || {};
|
tailwind = window.tailwind || {};
|
||||||
tailwind.config = {
|
tailwind.config = {
|
||||||
@@ -113,83 +112,44 @@
|
|||||||
{% block extra_css %}{% endblock %}
|
{% block extra_css %}{% endblock %}
|
||||||
|
|
||||||
<!-- Custom dark mode styles -->
|
<!-- Custom dark mode styles -->
|
||||||
|
<!-- ► ► Farb‑Token strikt getrennt ◄ ◄ -->
|
||||||
<style>
|
<style>
|
||||||
/* Dezenter Hintergrund für beide Modi */
|
/* Light‑Mode */
|
||||||
.dark {
|
|
||||||
--bg-primary: #181c24;
|
|
||||||
--bg-secondary: #232837;
|
|
||||||
--text-primary: #f9fafb;
|
|
||||||
--text-secondary: #e5e7eb;
|
|
||||||
--accent-primary: #6d28d9;
|
|
||||||
--accent-secondary: #8b5cf6;
|
|
||||||
--glow-effect: 0 0 8px rgba(124, 58, 237, 0.15);
|
|
||||||
}
|
|
||||||
:root {
|
:root {
|
||||||
--bg-primary: #f4f6fa;
|
--bg-primary:#f4f6fa;
|
||||||
--bg-secondary: #e9ecf3;
|
--bg-secondary:#e9ecf3;
|
||||||
--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, 0.08);
|
--glow-effect:0 0 8px rgba(139,92,246,.08);
|
||||||
}
|
}
|
||||||
body.dark {
|
/* Dark‑Mode */
|
||||||
background-color: var(--bg-primary);
|
.dark {
|
||||||
color: var(--text-primary);
|
--bg-primary:#181c24;
|
||||||
|
--bg-secondary:#232837;
|
||||||
|
--text-primary:#f9fafb;
|
||||||
|
--text-secondary:#e5e7eb;
|
||||||
|
--accent-primary:#6d28d9;
|
||||||
|
--accent-secondary:#8b5cf6;
|
||||||
|
--glow-effect:0 0 8px rgba(124,58,237,.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: var(--bg-primary);
|
@apply min-h-screen bg-[color:var(--bg-primary)] text-[color:var(--text-primary)] transition-colors duration-300;
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Mystical glowing effects */
|
/* Utilities */
|
||||||
.mystical-glow {
|
.mystical-glow { text-shadow: var(--glow-effect); }
|
||||||
text-shadow: var(--glow-effect);
|
|
||||||
}
|
|
||||||
|
|
||||||
.gradient-text {
|
.gradient-text {
|
||||||
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
|
background:linear-gradient(135deg,var(--accent-primary),var(--accent-secondary));
|
||||||
-webkit-background-clip: text;
|
-webkit-background-clip:text; background-clip:text; color:transparent; text-shadow:none;
|
||||||
background-clip: text;
|
|
||||||
color: transparent;
|
|
||||||
text-shadow: none;
|
|
||||||
}
|
}
|
||||||
|
.glass-morphism { backdrop-filter: blur(10px); }
|
||||||
/* Glass morphism effects */
|
.glass-navbar { @apply glass-morphism border backdrop-blur-xl; }
|
||||||
.glass-morphism {
|
.light .glass-navbar { background-color:rgba(255,255,255,.8); border-color:rgba(0,0,0,.05); }
|
||||||
backdrop-filter: blur(10px);
|
.dark .glass-navbar { background-color:rgba(10,14,25,.8); border-color:rgba(255,255,255,.05); }
|
||||||
}
|
</style>
|
||||||
|
|
||||||
.dark .glass-navbar-dark {
|
|
||||||
background-color: rgba(10, 14, 25, 0.8);
|
|
||||||
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.glass-navbar-light {
|
|
||||||
background-color: rgba(255, 255, 255, 0.8);
|
|
||||||
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Alpine.js x-cloak für ausgeblendete Elemente */
|
|
||||||
[x-cloak] { display: none !important; }
|
|
||||||
|
|
||||||
/* Grundlegende Klassen, um sicherzustellen, dass Tailwind geladen wird */
|
|
||||||
.nav-link {
|
|
||||||
@apply text-gray-300 hover:text-white transition-colors duration-200;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-link-active {
|
|
||||||
@apply text-white font-medium;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-link-light {
|
|
||||||
@apply text-gray-600 hover:text-gray-900 transition-colors duration-200;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-link-light-active {
|
|
||||||
@apply text-gray-900 font-medium;
|
|
||||||
}
|
|
||||||
</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="{
|
||||||
darkMode: true,
|
darkMode: true,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -626,7 +626,7 @@
|
|||||||
<div class="text-center py-12">
|
<div class="text-center py-12">
|
||||||
<i class="fas fa-lightbulb text-5xl text-gray-400 mb-4"></i>
|
<i class="fas fa-lightbulb text-5xl text-gray-400 mb-4"></i>
|
||||||
<p class="text-gray-500">Noch keine Gedanken erstellt</p>
|
<p class="text-gray-500">Noch keine Gedanken erstellt</p>
|
||||||
<a href="{{ url_for('create_thought') }}" class="mt-4 inline-block px-4 py-2 bg-purple-600 text-white rounded-lg">Ersten Gedanken erstellen</a>
|
<a href="{{ url_for('get_thought') }}" class="mt-4 inline-block px-4 py-2 bg-purple-600 text-white rounded-lg">Ersten Gedanken erstellen</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user