From 0852ea070b01f42a607ba277839fff364bcb7a79 Mon Sep 17 00:00:00 2001 From: Till Tomczak Date: Mon, 28 Apr 2025 15:21:11 +0200 Subject: [PATCH] Add Flask-CORS and SocketIO for real-time updates, refactor database handling to use a temporary Flask app; improve error handling with @app.errorhandler decorators. --- __pycache__/app.cpython-313.pyc | Bin 67747 -> 69218 bytes __pycache__/models.cpython-313.pyc | Bin 16976 -> 22744 bytes app.py | 34 +- database/systades.db | Bin 106496 -> 122880 bytes init_db.py | 472 ++++++------ models.py | 112 ++- static/js/mindmap.html | 234 ++++++ static/js/mindmap.js | 659 +++++++++++++++++ static/neural-network-background.js | 288 ++++---- templates/mindmap.html | 1056 ++++++--------------------- templates/profile.html | 2 +- 11 files changed, 1633 insertions(+), 1224 deletions(-) create mode 100644 static/js/mindmap.html create mode 100644 static/js/mindmap.js diff --git a/__pycache__/app.cpython-313.pyc b/__pycache__/app.cpython-313.pyc index 87792dddd43113ec06daa3c64dd941f17b5f4f48..6bee820f42876557d9f0c26913a25239adfb78df 100644 GIT binary patch delta 13890 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 jVMb2={gN!%M>yYxkboqh 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 delta 3635 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 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 7c7d3e5b1cf33ae2ee130ebc7b92ac85bcdccc89..8a04d1cd31dfe9793a92ac0f8e01b25fd9244157 100644 GIT binary patch delta 855 zcmZoTz}9epeS);0B?AM491z2R-b5W^R!auGHqDJGv-uhMHwmod6R=P)G`2D{03tne zb0cHZ=AClecgit(D{ApGy<^}@<3G>8j8~e^l)IfPoZ|$03P&rO3F~8)78Yfucg!~! zo-!ry|CrbqyuHzZF_&q3S2SZ8%k~s^#swTIJPgSU-1eMDI2+mPSTC_KvGy|UW8A@z z%y*PG7~&+>QxhAljhcca*~O)$8C$AL5|eUL3ySiyQj<%dl#Fwbt7C|(LWrZ2kE;Sy zaC(3*qqIOtW=T$}LYQNavxj4lhN0>7DqlvG>1r{IV#1{bDTyViDe;LV3NAq1A)daj z(;s>B&1NHQgr1qeN+ND{@FRjCS|ej%>z zu0hi;hB2yyL+y*tOwm*ba&>bJa`kg|4TeRff(B5WOS8#B3X%rg!C{b_T3no%o(iR8 zQ9=MJIQ>HqqsjC+VT|I_pL#LMut8%7m}FmK)|w|%D^qqm|K7kdf=UmE{;{$;$8z+>pY)^4#T)-j6%e0Sy?*d;vuL940?k;Y7&Lf~*Y{ sSeRIQnf7gLoWrzzUl1b?GZO>D_A{Z3`&hTXb7CxGVdUM+c;LT107DElY5)KL 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 %} -
-{% 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 %}