diff --git a/__pycache__/app.cpython-313.pyc b/__pycache__/app.cpython-313.pyc index 87792dd..6bee820 100644 Binary files a/__pycache__/app.cpython-313.pyc and b/__pycache__/app.cpython-313.pyc differ diff --git a/__pycache__/models.cpython-313.pyc b/__pycache__/models.cpython-313.pyc index cbe731e..d61fab9 100644 Binary files a/__pycache__/models.cpython-313.pyc and b/__pycache__/models.cpython-313.pyc differ diff --git a/app.py b/app.py index 3a71f7a..9aa310d 100644 --- a/app.py +++ b/app.py @@ -17,6 +17,9 @@ import secrets from sqlalchemy.sql import func from openai import OpenAI from dotenv import load_dotenv +from flask_cors import CORS +from flask_socketio import SocketIO, emit +from flask_wtf.csrf import CSRFProtect # Modelle importieren 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_TRACK_MODIFICATIONS'] = False 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 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 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(): """Erstellt die Standardkategorien für die Mindmap""" # Hauptkategorien @@ -1453,7 +1464,7 @@ if __name__ == '__main__': with app.app_context(): # Make sure tables exist 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') def refresh_mindmap(): @@ -1500,4 +1511,23 @@ def refresh_mindmap(): return jsonify({ 'success': False, 'error': 'Datenbankverbindung konnte nicht hergestellt werden' - }), 500 \ No newline at end of file + }), 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 \ No newline at end of file diff --git a/database/systades.db b/database/systades.db index 7c7d3e5..8a04d1c 100644 Binary files a/database/systades.db and b/database/systades.db differ diff --git a/init_db.py b/init_db.py index 4a46282..5e34b97 100644 --- a/init_db.py +++ b/init_db.py @@ -3,254 +3,242 @@ 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 +from models import Category, UserMindmap, UserMindmapNode, MindmapNote, NodeRelationship import os +import sqlite3 +from flask import Flask +from flask_sqlalchemy import SQLAlchemy +from datetime import datetime -def init_database(): - """Initialisiert die Datenbank mit Beispieldaten.""" - with app.app_context(): - # Datenbank löschen und neu erstellen - if os.path.exists(db_path): - 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!") +# Erstelle eine temporäre Flask-App, um die Datenbank zu initialisieren +app = Flask(__name__) +app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///systades.db' +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False +db.init_app(app) def init_db(): - """Alias für Kompatibilität mit älteren Scripts.""" - init_database() + with app.app_context(): + 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__': - init_database() + init_db() print("Datenbank wurde erfolgreich initialisiert!") print("Sie können die Anwendung jetzt mit 'python app.py' starten") print("Anmelden mit:") diff --git a/models.py b/models.py index 2b2e926..4f3b78e 100644 --- a/models.py +++ b/models.py @@ -6,6 +6,8 @@ from flask_login import UserMixin from datetime import datetime from werkzeug.security import generate_password_hash, check_password_hash from enum import Enum +import uuid as uuid_pkg +import os db = SQLAlchemy() @@ -43,30 +45,28 @@ user_thought_bookmark = db.Table('user_thought_bookmark', 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) username = db.Column(db.String(80), unique=True, nullable=False) email = db.Column(db.String(120), unique=True, nullable=False) - password_hash = db.Column(db.String(128)) - is_admin = db.Column(db.Boolean, default=False) + password = db.Column(db.String(512), nullable=False) created_at = db.Column(db.DateTime, default=datetime.utcnow) - last_login = db.Column(db.DateTime) - avatar = db.Column(db.String(200)) - bio = db.Column(db.Text) + is_active = db.Column(db.Boolean, default=True) + role = db.Column(db.String(20), default="user") # 'user', 'admin', 'moderator' - # Beziehungen - thoughts = db.relationship('Thought', backref='author', lazy=True) - comments = db.relationship('Comment', backref='author', lazy=True) - user_mindmaps = db.relationship('UserMindmap', backref='user', 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')) + # Relationships + threads = db.relationship('Thread', backref='creator', lazy=True) + messages = db.relationship('Message', backref='author', lazy=True) + projects = db.relationship('Project', backref='owner', lazy=True) + def __repr__(self): + return f'' + def set_password(self, password): - self.password_hash = generate_password_hash(password) + self.password = generate_password_hash(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): """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])) nodes = db.relationship('MindMapNode', backref='category', lazy=True) + def __repr__(self): + return f'' + class MindMapNode(db.Model): """Öffentliche Mindmap-Knoten, die für alle Benutzer sichtbar sind""" 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_by_id = db.Column(db.Integer, db.ForeignKey('user.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) parents = db.relationship( 'MindMapNode', @@ -111,6 +116,20 @@ class MindMapNode(db.Model): # Beziehung zum Ersteller created_by = db.relationship('User', backref='created_nodes') + def __repr__(self): + return f'' + + 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): """Benutzerspezifische Mindmap, die vom Benutzer personalisierbar ist""" 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) user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) created_at = db.Column(db.DateTime, default=datetime.utcnow) - last_modified = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) \ No newline at end of file + 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'' + +# 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'' + +# 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'' + +# 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'' \ No newline at end of file diff --git a/static/js/mindmap.html b/static/js/mindmap.html new file mode 100644 index 0000000..b17eb06 --- /dev/null +++ b/static/js/mindmap.html @@ -0,0 +1,234 @@ + + + + + + Interaktive Mindmap + + + + + + + + + + + + + +
+
+

Interaktive Mindmap

+
+ +
+
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ Mindmap-Anwendung © 2023 +
+
+ + + + + + + + \ No newline at end of file diff --git a/static/js/mindmap.js b/static/js/mindmap.js new file mode 100644 index 0000000..eeab545 --- /dev/null +++ b/static/js/mindmap.js @@ -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 = ` + + + + `; + + // 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'); +})(); \ No newline at end of file diff --git a/static/neural-network-background.js b/static/neural-network-background.js index 06afebd..1bb47ae 100644 --- a/static/neural-network-background.js +++ b/static/neural-network-background.js @@ -69,21 +69,22 @@ class NeuralNetworkBackground { // Konfigurationsobjekt für subtilere, sanftere Neuronen this.config = { - nodeCount: 60, // Weniger Knoten für bessere Leistung und subtileres Aussehen - nodeSize: 2.8, // Kleinere Knoten für dezenteres Erscheinungsbild - nodeVariation: 0.6, // Weniger Varianz für gleichmäßigeres Erscheinungsbild - connectionDistance: 220, // Etwas geringere Verbindungsdistanz - connectionOpacity: 0.15, // Transparentere Verbindungen + nodeCount: 45, // Weniger Knoten für bessere Leistung und subtileres Aussehen + nodeSize: 3.5, // Größere Knoten für bessere Sichtbarkeit + nodeVariation: 0.5, // Weniger Varianz für gleichmäßigeres Erscheinungsbild + connectionDistance: 250, // Größere Verbindungsdistanz + connectionOpacity: 0.22, // Deutlichere Verbindungen animationSpeed: 0.02, // Langsamere Animation für sanftere Bewegung pulseSpeed: 0.002, // Langsameres Pulsieren für subtilere Animation - flowSpeed: 0.6, // Langsamer für sanftere Animation - flowDensity: 0.002, // Deutlich weniger Blitze für subtileres Erscheinungsbild + flowSpeed: 0.6, // Langsamer für bessere Sichtbarkeit + flowDensity: 0.005, // Mehr Blitze gleichzeitig erzeugen flowLength: 0.12, // Kürzere Blitze für dezentere Effekte - maxConnections: 3, // Weniger Verbindungen für aufgeräumteres Erscheinungsbild - clusteringFactor: 0.4, // Moderate Clustering-Stärke - linesFadeDuration: 3500, // Längere Dauer für sanfteres Ein-/Ausblenden von Linien (ms) - linesWidth: 0.6, // Dünnere unterliegende Linien - linesOpacity: 0.25 // Geringere Opazität für Linien + maxConnections: 4, // Mehr Verbindungen pro Neuron + clusteringFactor: 0.45, // Stärkeres Clustering + linesFadeDuration: 4000, // Längere Dauer für sanfteres Ein-/Ausblenden von Linien (ms) + linesWidth: 0.9, // Dickere unterliegende Linien für bessere Sichtbarkeit + linesOpacity: 0.35, // Höhere Opazität für Linien + maxFlowCount: 10 // Maximale Anzahl gleichzeitiger Flüsse }; // Initialize @@ -373,11 +374,10 @@ class NeuralNetworkBackground { const height = this.canvas.height / (window.devicePixelRatio || 1); const now = Date.now(); - // Simulate neural firing with reduced activity + // Setze zunächst alle Neuronen auf inaktiv for (let i = 0; i < this.nodes.length; i++) { - const node = this.nodes[i]; - // Update pulse phase for smoother animation + const node = this.nodes[i]; node.pulsePhase += this.config.pulseSpeed * (1 + (node.connections.length * 0.04)); // Animate node position with gentler movement @@ -394,57 +394,77 @@ class NeuralNetworkBackground { node.y = Math.max(0, Math.min(height, node.y)); } - // Check if node should fire based on reduced firing rate - if (now - node.lastFired > node.firingRate * 1.3) { // 30% langsamere Feuerrate + // Setze alle Knoten standardmäßig auf inaktiv + node.isActive = false; + } + + // Aktiviere Neuronen basierend auf aktiven Flows + for (const flow of this.flows) { + // Aktiviere den Quellknoten (der Flow geht von ihm aus) + if (flow.sourceNodeIdx !== undefined) { + this.nodes[flow.sourceNodeIdx].isActive = true; + this.nodes[flow.sourceNodeIdx].lastFired = now; + } + + // Aktiviere den Zielknoten nur, wenn der Flow weit genug fortgeschritten ist + if (flow.targetNodeIdx !== undefined && flow.progress > 0.9) { + this.nodes[flow.targetNodeIdx].isActive = true; + this.nodes[flow.targetNodeIdx].lastFired = now; + } + } + + // Zufällig neue Flows zwischen Knoten initiieren + if (Math.random() < 0.02) { // 2% Chance in jedem Frame + const randomNodeIdx = Math.floor(Math.random() * this.nodes.length); + const node = this.nodes[randomNodeIdx]; + + // Nur aktivieren, wenn Knoten Verbindungen hat + if (node.connections.length > 0) { node.isActive = true; node.lastFired = now; - node.activationTime = now; // Track when activation started - // Activate connected nodes with probability based on connection strength - for (const connIndex of node.connections) { - // Find the connection - const conn = this.connections.find(c => - (c.from === i && c.to === connIndex) || (c.from === connIndex && c.to === i) - ); + // Wähle eine zufällige Verbindung dieses Knotens + const randomConnIdx = Math.floor(Math.random() * node.connections.length); + const connectedNodeIdx = node.connections[randomConnIdx]; + + // Finde die entsprechende Verbindung + const conn = this.connections.find(c => + (c.from === randomNodeIdx && c.to === connectedNodeIdx) || + (c.from === connectedNodeIdx && c.to === randomNodeIdx) + ); + + if (conn) { + // Markiere die Verbindung als kürzlich aktiviert + conn.lastActivated = now; - if (conn) { - // Mark connection as recently activated - conn.lastActivated = now; + // Stelle sicher, dass die Verbindung sichtbar bleibt + if (conn.fadeState === 'out') { + conn.fadeState = 'visible'; + conn.fadeStartTime = now; + } + + // Verbindung soll schneller aufgebaut werden + if (conn.progress < 1) { + conn.buildSpeed = 0.015 + Math.random() * 0.01; + } + + // Erstelle einen neuen Flow, wenn nicht zu viele existieren + if (this.flows.length < this.config.maxFlowCount) { + // Bestimme die Richtung (vom aktivierten Knoten weg) + const direction = conn.from === randomNodeIdx; - // Wenn eine Verbindung aktiviert wird, verlängere ggf. ihre Sichtbarkeit - if (conn.fadeState === 'out') { - conn.fadeState = 'visible'; - conn.fadeStartTime = now; - } - - // Verbindung soll schneller aufgebaut werden, wenn ein Neuron feuert - if (conn.progress < 1) { - conn.buildSpeed = 0.015 + Math.random() * 0.01; // Schnellerer Aufbau während der Aktivierung - } - - // Reduzierte Wahrscheinlichkeit für neue Flows - if (this.flows.length < 4 && Math.random() < conn.strength * 0.5) { // Reduzierte Wahrscheinlichkeit - this.flows.push({ - connection: conn, - progress: 0, - direction: conn.from === i, // Flow from activated node - length: this.config.flowLength + Math.random() * 0.05, // Geringere Variation - intensity: 0.5 + Math.random() * 0.3, // Geringere Intensität für subtilere Darstellung - creationTime: now, - totalDuration: 1000 + Math.random() * 600 // Längere Dauer für sanftere Animation - }); - } - - // Probability for connected node to activate - if (Math.random() < conn.strength * 0.5) { - this.nodes[connIndex].isActive = true; - this.nodes[connIndex].activationTime = now; - this.nodes[connIndex].lastFired = now - Math.random() * 500; // Slight variation - } + this.flows.push({ + connection: conn, + progress: 0, + direction: direction, + length: this.config.flowLength + Math.random() * 0.05, + creationTime: now, + totalDuration: 1000 + Math.random() * 600, + sourceNodeIdx: direction ? conn.from : conn.to, + targetNodeIdx: direction ? conn.to : conn.from + }); } } - } else if (now - node.lastFired > 400) { // Deactivate after longer period - node.isActive = false; } } @@ -466,60 +486,44 @@ class NeuralNetworkBackground { connection.fadeState = 'out'; connection.fadeStartTime = now; connection.fadeProgress = 1.0; - - // Setze den Fortschritt zurück, damit die Verbindung neu aufgebaut werden kann - if (Math.random() < 0.7) { - connection.progress = 0; - } } } else if (connection.fadeState === 'out') { - // Ausblenden - connection.fadeProgress = Math.max(0.0, 1.0 - (elapsedTime / connection.fadeTotalDuration)); - if (connection.fadeProgress <= 0.0) { - // Setze Verbindung zurück, damit sie wieder eingeblendet werden kann - if (Math.random() < 0.4) { // 40% Chance, direkt wieder einzublenden - connection.fadeState = 'in'; - connection.fadeStartTime = now; - connection.fadeProgress = 0.0; - connection.visibleDuration = 10000 + Math.random() * 15000; // Neue Dauer generieren - - // Setze den Fortschritt zurück, damit die Verbindung neu aufgebaut werden kann - connection.progress = 0; - } else { - // Kurze Pause, bevor die Verbindung wieder erscheint - connection.fadeState = 'hidden'; - connection.fadeStartTime = now; - connection.hiddenDuration = 3000 + Math.random() * 7000; - - // Setze den Fortschritt zurück, damit die Verbindung neu aufgebaut werden kann - connection.progress = 0; - } - } - } else if (connection.fadeState === 'hidden') { - // Verbindung ist unsichtbar, warte auf Wiedereinblendung - if (elapsedTime > connection.hiddenDuration) { + // Ausblenden, aber nie komplett verschwinden + connection.fadeProgress = Math.max(0.1, 1.0 - (elapsedTime / connection.fadeTotalDuration)); + + // Verbindungen bleiben immer minimal sichtbar (nie komplett unsichtbar) + if (connection.fadeProgress <= 0.1) { + // Statt Verbindung komplett zu verstecken, setzen wir sie zurück auf "in" connection.fadeState = 'in'; connection.fadeStartTime = now; - connection.fadeProgress = 0.0; - - // Verbindung wird komplett neu aufgebaut - connection.progress = 0; + connection.fadeProgress = 0.1; // Minimal sichtbar bleiben + connection.visibleDuration = 15000 + Math.random() * 20000; // Längere Sichtbarkeit } + } else if (connection.fadeState === 'hidden') { + // Keine Verbindungen mehr verstecken, stattdessen immer wieder einblenden + connection.fadeState = 'in'; + connection.fadeStartTime = now; + connection.fadeProgress = 0.1; } - // Animierter Verbindungsaufbau: progress inkrementieren, aber nur wenn aktiv + // Verbindungen immer vollständig aufbauen und nicht zurücksetzen if (connection.progress < 1) { - // Verbindung wird nur aufgebaut, wenn sie gerade aktiv ist oder ein Blitz sie aufbaut - const buildingSpeed = connection.buildSpeed || 0.002; // Langsamer Standard-Aufbau + // Konstante Aufbaugeschwindigkeit, unabhängig vom Status + const baseBuildSpeed = 0.003; + let buildSpeed = connection.buildSpeed || baseBuildSpeed; - // Bau die Verbindung auf, wenn sie kürzlich aktiviert wurde + // Wenn kürzlich aktiviert, schneller aufbauen if (now - connection.lastActivated < 2000) { - connection.progress += buildingSpeed; - if (connection.progress > 1) connection.progress = 1; + buildSpeed = Math.max(buildSpeed, 0.006); } - // Zurücksetzen der Aufbaugeschwindigkeit - connection.buildSpeed = 0; + connection.progress += buildSpeed; + + if (connection.progress > 1) { + connection.progress = 1; + // Zurücksetzen der Aufbaugeschwindigkeit + connection.buildSpeed = 0; + } } } @@ -527,7 +531,7 @@ class NeuralNetworkBackground { this.updateFlows(now); // Seltener neue Flows erstellen - if (Math.random() < this.config.flowDensity * 0.8 && this.flows.length < 4) { // Reduzierte Kapazität und Rate + if (Math.random() < this.config.flowDensity && this.flows.length < this.config.maxFlowCount) { this.createNewFlow(now); } @@ -560,6 +564,22 @@ class NeuralNetworkBackground { // Update flow progress flow.progress += this.config.flowSpeed / flow.connection.distance; + // Aktiviere Quell- und Zielknoten basierend auf Flow-Fortschritt + if (flow.sourceNodeIdx !== undefined) { + // Quellknoten immer aktivieren, solange der Flow aktiv ist + this.nodes[flow.sourceNodeIdx].isActive = true; + this.nodes[flow.sourceNodeIdx].lastFired = now; + } + + // Zielknoten erst aktivieren, wenn der Flow ihn erreicht hat + if (flow.targetNodeIdx !== undefined && flow.progress > 0.9) { + this.nodes[flow.targetNodeIdx].isActive = true; + this.nodes[flow.targetNodeIdx].lastFired = now; + } + + // Stellen Sie sicher, dass die Verbindung aktiv bleibt + flow.connection.lastActivated = now; + // Remove completed or expired flows if (flow.progress > 1.0 || flowProgress >= 1.0) { this.flows.splice(i, 1); @@ -1111,11 +1131,11 @@ class NeuralNetworkBackground { // Weniger Funken mit geringerer Vibration const sparks = this.generateSparkPoints(zigzag, 4 + Math.floor(Math.random() * 2)); - // Dezenteres Funkenlicht mit Ein-/Ausblendeffekt - const sparkBaseOpacity = this.isDarkMode ? 0.65 : 0.55; + // Intensiveres Funkenlicht mit dynamischem Ein-/Ausblendeffekt + const sparkBaseOpacity = this.isDarkMode ? 0.75 : 0.65; const sparkBaseColor = this.isDarkMode - ? `rgba(220, 235, 245, ${sparkBaseOpacity * fadeFactor})` - : `rgba(180, 220, 245, ${sparkBaseOpacity * fadeFactor})`; + ? `rgba(230, 240, 250, ${sparkBaseOpacity * fadeFactor})` + : `rgba(190, 230, 250, ${sparkBaseOpacity * fadeFactor})`; for (const spark of sparks) { this.ctx.beginPath(); @@ -1147,34 +1167,36 @@ class NeuralNetworkBackground { this.ctx.fill(); } - // Dezenterer Fortschrittseffekt an der Spitze des Blitzes - if (endProgress >= connProgress - 0.05 && connProgress < 0.95) { + // Deutlicherer und länger anhaltender Fortschrittseffekt an der Spitze des Blitzes + if (endProgress >= connProgress - 0.1 && connProgress < 0.98) { const tipGlow = this.ctx.createRadialGradient( p2.x, p2.y, 0, - p2.x, p2.y, 6 + p2.x, p2.y, 10 ); - tipGlow.addColorStop(0, `rgba(${rgbFlowColor.r}, ${rgbFlowColor.g}, ${rgbFlowColor.b}, ${0.7 * fadeFactor})`); + tipGlow.addColorStop(0, `rgba(${rgbFlowColor.r}, ${rgbFlowColor.g}, ${rgbFlowColor.b}, ${0.85 * fadeFactor})`); + tipGlow.addColorStop(0.5, `rgba(${rgbFlowColor.r}, ${rgbFlowColor.g}, ${rgbFlowColor.b}, ${0.4 * fadeFactor})`); tipGlow.addColorStop(1, `rgba(${rgbFlowColor.r}, ${rgbFlowColor.g}, ${rgbFlowColor.b}, 0)`); this.ctx.fillStyle = tipGlow; this.ctx.beginPath(); - this.ctx.arc(p2.x, p2.y, 6, 0, Math.PI * 2); + this.ctx.arc(p2.x, p2.y, 10, 0, Math.PI * 2); this.ctx.fill(); } - // Sanftere Start- und Endblitz-Fades - if (startProgress < 0.1) { - const startFade = startProgress / 0.1; // 0 bis 1 + // Verstärkter Start- und Endblitz-Fade mit längerer Sichtbarkeit + if (startProgress < 0.15) { + const startFade = startProgress / 0.15; // 0 bis 1 const startGlow = this.ctx.createRadialGradient( p1.x, p1.y, 0, - p1.x, p1.y, 8 * startFade + p1.x, p1.y, 12 * startFade ); - startGlow.addColorStop(0, `rgba(${rgbFlowColor.r}, ${rgbFlowColor.g}, ${rgbFlowColor.b}, ${0.4 * fadeFactor * startFade})`); + startGlow.addColorStop(0, `rgba(${rgbFlowColor.r}, ${rgbFlowColor.g}, ${rgbFlowColor.b}, ${0.6 * fadeFactor * startFade})`); + startGlow.addColorStop(0.7, `rgba(${rgbFlowColor.r}, ${rgbFlowColor.g}, ${rgbFlowColor.b}, ${0.3 * fadeFactor * startFade})`); startGlow.addColorStop(1, `rgba(${rgbFlowColor.r}, ${rgbFlowColor.g}, ${rgbFlowColor.b}, 0)`); this.ctx.fillStyle = startGlow; this.ctx.beginPath(); - this.ctx.arc(p1.x, p1.y, 8 * startFade, 0, Math.PI * 2); + this.ctx.arc(p1.x, p1.y, 12 * startFade, 0, Math.PI * 2); this.ctx.fill(); } @@ -1259,11 +1281,11 @@ class NeuralNetworkBackground { return points; } - // Hilfsfunktion: Erzeuge dezentere Funkenpunkte mit gemäßigter Verteilung - generateSparkPoints(zigzag, sparkCount = 4) { + // Hilfsfunktion: Erzeuge intensivere Funkenpunkte mit dynamischer Verteilung + generateSparkPoints(zigzag, sparkCount = 15) { const sparks = []; - // Weniger Funken - const actualSparkCount = Math.min(sparkCount, zigzag.length); + // Mehr Funken für intensiveren Effekt + const actualSparkCount = Math.min(sparkCount, zigzag.length * 2); // Funken an zufälligen Stellen entlang des Blitzes for (let i = 0; i < actualSparkCount; i++) { @@ -1280,15 +1302,31 @@ class NeuralNetworkBackground { const x = zigzag[segIndex].x + dx * t; const y = zigzag[segIndex].y + dy * t; - // Rechtwinkliger Versatz vom Segment (sanftere Verteilung) - const offsetAngle = segmentAngle + Math.PI/2; - const offsetDistance = Math.random() * 4 - 2; // Geringerer Offset für dezentere Funken + // Dynamischer Versatz für intensivere Funken + const offsetAngle = segmentAngle + (Math.random() * Math.PI - Math.PI/2); + const offsetDistance = Math.random() * 8 - 4; // Größerer Offset für dramatischere Funken + + // Zufällige Größe für variierende Intensität + const baseSize = 3.5 + Math.random() * 3.5; + const sizeVariation = Math.random() * 2.5; sparks.push({ x: x + Math.cos(offsetAngle) * offsetDistance, y: y + Math.sin(offsetAngle) * offsetDistance, - size: 1 + Math.random() * 1.5 // Kleinere Funkengröße für subtilere Effekte + size: baseSize + sizeVariation // Größere und variablere Funkengröße }); + + // Zusätzliche kleinere Funken in der Nähe für einen intensiveren Effekt + if (Math.random() < 0.4) { // 40% Chance für zusätzliche Funken + const subSparkAngle = offsetAngle + (Math.random() * Math.PI/2 - Math.PI/4); + const subDistance = offsetDistance * (0.4 + Math.random() * 0.6); + + sparks.push({ + x: x + Math.cos(subSparkAngle) * subDistance, + y: y + Math.sin(subSparkAngle) * subDistance, + size: (baseSize + sizeVariation) * 0.6 // Kleinere Größe für sekundäre Funken + }); + } } return sparks; diff --git a/templates/mindmap.html b/templates/mindmap.html index 83923a7..fb1dea4 100644 --- a/templates/mindmap.html +++ b/templates/mindmap.html @@ -1,852 +1,234 @@ -{% extends "base.html" %} - -{% block title %}Mindmap{% endblock %} - -{% block extra_css %} - -{% endblock %} - -{% block content %} - -
- -
- - -
-
- -
+ + + +
+
+

Interaktive Mindmap

+
+
- - -
-
- -
-
-

Kategorien werden geladen...

-
-
-
- - -
-

Ansicht

-
- - -
-
-
-
- - - {% if current_user.is_authenticated %} -
-
-

Meine Mindmaps

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

Mindmaps werden geladen...

-
-
-
- - - - Neue Mindmap erstellen - +
+
-
- {% endif %} - - -
- - - + +
+ +
+ Mindmap-Anwendung © 2023 +
- -
-
-{% endblock %} - -{% block extra_js %} - - - - - - - - - - - // Kategorien laden - function loadCategories() { - const categoriesContainer = document.getElementById('categories-container'); - const loadingElement = document.getElementById('loading-categories'); - - fetch('/api/categories') - .then(response => { - if (!response.ok) { - throw new Error('Kategorien konnten nicht geladen werden'); - } - return response.json(); - }) - .then(categories => { - // Loading-Anzeige entfernen - if (loadingElement) { - loadingElement.remove(); - } - - // Kategorien rendern - renderCategories(categories, categoriesContainer); - }) - .catch(error => { - console.error('Fehler beim Laden der Kategorien:', error); - if (loadingElement) { - loadingElement.innerHTML = ` -
- -
-

Fehler beim Laden der Kategorien

- `; - } - }); - } - - // Kategorien rekursiv rendern - function renderCategories(categories, container, level = 0) { - categories.forEach(category => { - const categoryElement = document.createElement('div'); - categoryElement.className = 'category-item pl-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700'; - categoryElement.setAttribute('data-category-id', category.id); - categoryElement.style.marginLeft = level > 0 ? `${level * 12}px` : '0'; - - const hasChildren = category.children && category.children.length > 0; - const hasNodes = category.nodes && category.nodes.length > 0; - - categoryElement.innerHTML = ` -
-
- - - - ${category.name} - -
- ${hasNodes ? category.nodes.length : 0} -
- `; - - container.appendChild(categoryElement); - - // Bereich für Knoten und Unterkategorien erstellen - const expandableArea = document.createElement('div'); - expandableArea.className = 'hidden mt-2'; - expandableArea.setAttribute('data-category-expand', category.id); - container.appendChild(expandableArea); - - // Knoten für diese Kategorie anzeigen - if (hasNodes) { - const nodesContainer = document.createElement('div'); - nodesContainer.className = 'node-list pl-4'; - - category.nodes.forEach(node => { - const nodeItem = document.createElement('div'); - nodeItem.className = 'node-item p-2 mb-2'; - nodeItem.style.borderLeft = `3px solid ${node.color_code || '#9F7AEA'}`; - nodeItem.setAttribute('data-node-id', node.id); - - nodeItem.innerHTML = ` -
-
${node.name}
- ${node.thought_count || 0} -
- `; - - nodeItem.addEventListener('click', function(e) { - e.stopPropagation(); - if (window.mindmapInstance && window.mindmapInstance.focusNode) { - window.mindmapInstance.focusNode(node.id); - } - }); - - nodesContainer.appendChild(nodeItem); - }); - - expandableArea.appendChild(nodesContainer); - } - - // Unterkategorien rekursiv rendern - if (hasChildren) { - const childrenContainer = document.createElement('div'); - childrenContainer.className = 'mt-2'; - expandableArea.appendChild(childrenContainer); - - renderCategories(category.children, childrenContainer, level + 1); - } - - // Event-Listener für Aufklappen/Zuklappen - categoryElement.addEventListener('click', function() { - const expandArea = document.querySelector(`[data-category-expand="${category.id}"]`); - const chevron = this.querySelector('.fa-chevron-right'); - - if (expandArea) { - if (expandArea.classList.contains('hidden')) { - expandArea.classList.remove('hidden'); - if (chevron) chevron.style.transform = 'rotate(90deg)'; - } else { - expandArea.classList.add('hidden'); - if (chevron) chevron.style.transform = 'rotate(0)'; - } - } - }); - }); - } - - // Categories filtering - function filterCategoriesInSidebar(searchTerm) { - if (!searchTerm) { - // Show all categories - document.querySelectorAll('.category-item').forEach(el => { - el.style.display = ''; - }); - return; - } - - // Hide/show categories based on search - document.querySelectorAll('.category-item').forEach(el => { - const categoryName = el.querySelector('span').textContent.trim().toLowerCase(); - if (categoryName.includes(searchTerm)) { - el.style.display = ''; - - // Show parent categories - let parent = el.parentElement; - while (parent && !parent.matches('#categories-container')) { - if (parent.hasAttribute('data-category-expand')) { - parent.classList.remove('hidden'); - const parentId = parent.getAttribute('data-category-expand'); - const parentCategory = document.querySelector(`[data-category-id="${parentId}"]`); - if (parentCategory) { - const chevron = parentCategory.querySelector('.fa-chevron-right'); - if (chevron) chevron.style.transform = 'rotate(90deg)'; - } - } - parent = parent.parentElement; - } - } else { - el.style.display = 'none'; + + - -{% if current_user.is_authenticated %} - - -{% endif %} -{% endblock %} \ No newline at end of file + + + \ No newline at end of file diff --git a/templates/profile.html b/templates/profile.html index 9b87b26..a243f45 100644 --- a/templates/profile.html +++ b/templates/profile.html @@ -626,7 +626,7 @@

Noch keine Gedanken erstellt

- Ersten Gedanken erstellen + Ersten Gedanken erstellen
{% endif %}