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.

This commit is contained in:
2025-04-28 15:21:11 +02:00
parent 7a0533ac09
commit 0852ea070b
11 changed files with 1633 additions and 1224 deletions

Binary file not shown.

Binary file not shown.

32
app.py
View File

@@ -17,6 +17,9 @@ import secrets
from sqlalchemy.sql import func from sqlalchemy.sql import func
from openai import OpenAI from openai import OpenAI
from dotenv import load_dotenv from dotenv import load_dotenv
from flask_cors import CORS
from flask_socketio import SocketIO, emit
from flask_wtf.csrf import CSRFProtect
# Modelle importieren # Modelle importieren
from models import ( from models import (
@@ -39,6 +42,7 @@ app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'default-dev-key')
app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{db_path}' app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{db_path}'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=365) # Langlebige Session für Dark Mode-Einstellung app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=365) # Langlebige Session für Dark Mode-Einstellung
app.config['UPLOAD_FOLDER'] = os.getenv('UPLOAD_FOLDER', os.path.join(os.getcwd(), 'uploads'))
# OpenAI API-Konfiguration # OpenAI API-Konfiguration
api_key = os.environ.get("OPENAI_API_KEY") api_key = os.environ.get("OPENAI_API_KEY")
@@ -88,6 +92,13 @@ login_manager.login_view = 'login'
# Erst nach der App-Initialisierung die DB-Check-Funktionen importieren # Erst nach der App-Initialisierung die DB-Check-Funktionen importieren
from utils.db_check import check_db_connection, initialize_db_if_needed from utils.db_check import check_db_connection, initialize_db_if_needed
# CORS und SocketIO initialisieren
CORS(app)
socketio = SocketIO(app, cors_allowed_origins="*")
# Security
csrf = CSRFProtect(app)
def create_default_categories(): def create_default_categories():
"""Erstellt die Standardkategorien für die Mindmap""" """Erstellt die Standardkategorien für die Mindmap"""
# Hauptkategorien # Hauptkategorien
@@ -1453,7 +1464,7 @@ if __name__ == '__main__':
with app.app_context(): with app.app_context():
# Make sure tables exist # Make sure tables exist
db.create_all() db.create_all()
app.run(host="0.0.0.0", debug=True) socketio.run(app, debug=True, host='0.0.0.0')
@app.route('/api/refresh-mindmap') @app.route('/api/refresh-mindmap')
def refresh_mindmap(): def refresh_mindmap():
@@ -1501,3 +1512,22 @@ def refresh_mindmap():
'success': False, 'success': False,
'error': 'Datenbankverbindung konnte nicht hergestellt werden' 'error': 'Datenbankverbindung konnte nicht hergestellt werden'
}), 500 }), 500
# Route zur Mindmap HTML-Seite
@app.route('/mindmap')
def mindmap_page():
return render_template('mindmap.html')
# Fehlerbehandlung
@app.errorhandler(404)
def not_found(e):
return jsonify({'error': 'Nicht gefunden'}), 404
@app.errorhandler(400)
def bad_request(e):
return jsonify({'error': 'Fehlerhafte Anfrage'}), 400
@app.errorhandler(500)
def server_error(e):
return jsonify({'error': 'Serverfehler'}), 500

Binary file not shown.

View File

@@ -3,254 +3,242 @@
from app import app, initialize_database, db_path from app import app, initialize_database, db_path
from models import db, User, Thought, Comment, MindMapNode, ThoughtRelation, ThoughtRating, RelationType from models import db, User, Thought, Comment, MindMapNode, ThoughtRelation, ThoughtRating, RelationType
from models import Category, UserMindmap, UserMindmapNode, MindmapNote from models import Category, UserMindmap, UserMindmapNode, MindmapNote, NodeRelationship
import os import os
import sqlite3
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime
def init_database(): # Erstelle eine temporäre Flask-App, um die Datenbank zu initialisieren
"""Initialisiert die Datenbank mit Beispieldaten.""" app = Flask(__name__)
with app.app_context(): app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///systades.db'
# Datenbank löschen und neu erstellen app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
if os.path.exists(db_path): db.init_app(app)
os.remove(db_path)
# Stellen Sie sicher, dass das Verzeichnis existiert
os.makedirs(os.path.dirname(db_path), exist_ok=True)
db.create_all()
# Admin-Benutzer erstellen
admin = User(username='admin', email='admin@example.com', is_admin=True)
admin.set_password('admin')
db.session.add(admin)
# Beispiel-Benutzer erstellen
user = User(username='user', email='user@example.com')
user.set_password('user')
db.session.add(user)
# Commit, um IDs zu generieren
db.session.commit()
# Wissenschaftliche Kategorien erstellen
science = Category(name='Wissenschaft', description='Wissenschaftliche Erkenntnisse',
color_code='#4CAF50', icon='flask')
db.session.add(science)
philosophy = Category(name='Philosophie', description='Philosophische Theorien und Gedanken',
color_code='#9C27B0', icon='lightbulb')
db.session.add(philosophy)
technology = Category(name='Technologie', description='Technologische Entwicklungen',
color_code='#FF9800', icon='microchip')
db.session.add(technology)
db.session.commit()
# Wissenschaftliche Unterkategorien
physics = Category(name='Physik', description='Studium der Materie und Energie',
color_code='#81C784', icon='atom', parent_id=science.id)
biology = Category(name='Biologie', description='Studium lebender Organismen',
color_code='#66BB6A', icon='leaf', parent_id=science.id)
chemistry = Category(name='Chemie', description='Studium der Stoffe und ihrer Reaktionen',
color_code='#A5D6A7', icon='vial', parent_id=science.id)
db.session.add_all([physics, biology, chemistry])
# Technologie-Unterkategorien
informatics = Category(name='Informatik', description='Studium der Informationsverarbeitung',
color_code='#FFB74D', icon='laptop-code', parent_id=technology.id)
ai = Category(name='Künstliche Intelligenz', description='Entwicklung intelligenter Systeme',
color_code='#FFA726', icon='robot', parent_id=technology.id)
db.session.add_all([informatics, ai])
# Philosophie-Unterkategorien
ethics = Category(name='Ethik', description='Moralphilosophie und Wertesysteme',
color_code='#BA68C8', icon='balance-scale', parent_id=philosophy.id)
logic = Category(name='Logik', description='Studie der gültigen Schlussfolgerungen',
color_code='#AB47BC', icon='project-diagram', parent_id=philosophy.id)
db.session.add_all([ethics, logic])
db.session.commit()
# Knoten für die öffentliche Mindmap erstellen
nodes = {
'quantenmechanik': MindMapNode(
name='Quantenmechanik',
description='Physikalische Theorie zur Beschreibung der Materie auf atomarer Ebene',
color_code='#81C784',
category_id=physics.id,
created_by_id=admin.id
),
'relativitaetstheorie': MindMapNode(
name='Relativitätstheorie',
description='Einsteins Theorien zur Raumzeit und Gravitation',
color_code='#81C784',
category_id=physics.id,
created_by_id=admin.id
),
'genetik': MindMapNode(
name='Genetik',
description='Wissenschaft der Gene und Vererbung',
color_code='#66BB6A',
category_id=biology.id,
created_by_id=admin.id
),
'machine_learning': MindMapNode(
name='Machine Learning',
description='Algorithmen, die aus Daten lernen können',
color_code='#FFA726',
category_id=ai.id,
created_by_id=admin.id
),
'ki_ethik': MindMapNode(
name='KI-Ethik',
description='Moralische Implikationen künstlicher Intelligenz',
color_code='#BA68C8',
category_id=ethics.id,
created_by_id=user.id
)
}
for node in nodes.values():
db.session.add(node)
db.session.commit()
# Verknüpfungen zwischen Knoten herstellen (Hierarchie)
nodes['machine_learning'].parents.append(nodes['ki_ethik'])
db.session.commit()
# Gedanken erstellen
thoughts = [
{
'title': 'Künstliche Intelligenz und Bewusstsein',
'content': 'Die Frage nach maschinellem Bewusstsein ist fundamental für die KI-Ethik. Aktuelle KI-Systeme haben kein Bewusstsein, aber fortschrittliche KI könnte in Zukunft Eigenschaften entwickeln, die diesem nahekommen.',
'abstract': 'Eine Untersuchung der philosophischen Implikationen von KI-Bewusstsein.',
'keywords': 'KI, Bewusstsein, Ethik, Philosophie',
'branch': 'Philosophie',
'color_code': '#BA68C8',
'source_type': 'Markdown',
'user_id': user.id,
'node': nodes['ki_ethik']
},
{
'title': 'Quantenmechanik und Realität',
'content': 'Die Kopenhagener Deutung und ihre Auswirkungen auf unser Verständnis der Realität. Quantenmechanik stellt grundlegende Annahmen über Determinismus und Lokalität in Frage.',
'abstract': 'Eine Analyse verschiedener Interpretationen der Quantenmechanik.',
'keywords': 'Quantenmechanik, Physik, Realität',
'branch': 'Physik',
'color_code': '#81C784',
'source_type': 'PDF',
'user_id': admin.id,
'node': nodes['quantenmechanik']
},
{
'title': 'Deep Learning Fortschritte',
'content': 'Die neuesten Fortschritte im Deep Learning haben zu beeindruckenden Ergebnissen in Bereichen wie Computer Vision, Natural Language Processing und Reinforcement Learning geführt.',
'abstract': 'Überblick über aktuelle Deep Learning-Techniken und ihre Anwendungen.',
'keywords': 'Deep Learning, Neural Networks, AI',
'branch': 'Technologie',
'color_code': '#FFA726',
'source_type': 'Webpage',
'user_id': admin.id,
'node': nodes['machine_learning']
}
]
thought_objects = []
for t_data in thoughts:
node = t_data.pop('node')
thought = Thought(**t_data)
node.thoughts.append(thought)
thought_objects.append(thought)
db.session.add(thought)
db.session.commit()
# Beziehungen zwischen Gedanken
relation = ThoughtRelation(
source_id=thought_objects[0].id,
target_id=thought_objects[2].id,
relation_type=RelationType.INSPIRES,
created_by_id=user.id
)
db.session.add(relation)
# Bewertungen erstellen
rating1 = ThoughtRating(
thought_id=thought_objects[0].id,
user_id=admin.id,
relevance_score=5
)
rating2 = ThoughtRating(
thought_id=thought_objects[2].id,
user_id=user.id,
relevance_score=4
)
db.session.add_all([rating1, rating2])
# Kommentare erstellen
for thought in thought_objects:
comment = Comment(
content=f'Interessante Perspektive zu {thought.title}!',
thought_id=thought.id,
user_id=admin.id if thought.user_id != admin.id else user.id
)
db.session.add(comment)
# Benutzer-Mindmaps erstellen
user_mindmap = UserMindmap(
name='Meine KI-Forschung',
description='Meine persönliche Sammlung zu KI und Ethik',
user_id=user.id
)
db.session.add(user_mindmap)
db.session.commit()
# Knoten zur Benutzer-Mindmap hinzufügen
user_mindmap_nodes = [
UserMindmapNode(
user_mindmap_id=user_mindmap.id,
node_id=nodes['machine_learning'].id,
x_position=200,
y_position=300
),
UserMindmapNode(
user_mindmap_id=user_mindmap.id,
node_id=nodes['ki_ethik'].id,
x_position=500,
y_position=200
)
]
db.session.add_all(user_mindmap_nodes)
# Private Notizen
note = MindmapNote(
user_id=user.id,
mindmap_id=user_mindmap.id,
node_id=nodes['ki_ethik'].id,
content="Recherchiere mehr über aktuelle ethische Richtlinien für KI-Entwicklung!",
color_code="#FFF59D"
)
db.session.add(note)
# Gedanken zu Bookmarks hinzufügen
user.bookmarked_thoughts.append(thought_objects[0])
admin.bookmarked_thoughts.append(thought_objects[1])
# Finaler Commit
db.session.commit()
print("Datenbank wurde erfolgreich initialisiert!")
def init_db(): def init_db():
"""Alias für Kompatibilität mit älteren Scripts.""" with app.app_context():
init_database() print("Initialisiere Datenbank...")
# Tabellen erstellen
db.create_all()
print("Tabellen wurden erstellt.")
# Standardbenutzer erstellen, falls keine vorhanden sind
if User.query.count() == 0:
print("Erstelle Standardbenutzer...")
create_default_users()
# Standardkategorien erstellen, falls keine vorhanden sind
if Category.query.count() == 0:
print("Erstelle Standardkategorien...")
create_default_categories()
# Beispiel-Mindmap erstellen, falls keine Knoten vorhanden sind
if MindMapNode.query.count() == 0:
print("Erstelle Beispiel-Mindmap...")
create_sample_mindmap()
print("Datenbankinitialisierung abgeschlossen.")
def create_default_users():
"""Erstellt Standardbenutzer für die Anwendung"""
users = [
{
'username': 'admin',
'email': 'admin@example.com',
'password': 'admin',
'role': 'admin'
},
{
'username': 'user',
'email': 'user@example.com',
'password': 'user',
'role': 'user'
}
]
for user_data in users:
password = user_data.pop('password')
user = User(**user_data)
user.set_password(password)
db.session.add(user)
db.session.commit()
print(f"{len(users)} Benutzer wurden erstellt.")
def create_default_categories():
"""Erstellt die Standardkategorien für die Mindmap"""
categories = [
{
'name': 'Konzept',
'description': 'Abstrakte Ideen und theoretische Konzepte',
'color_code': '#6366f1',
'icon': 'lightbulb'
},
{
'name': 'Technologie',
'description': 'Hardware, Software, Tools und Plattformen',
'color_code': '#10b981',
'icon': 'cpu'
},
{
'name': 'Prozess',
'description': 'Workflows, Methodologien und Vorgehensweisen',
'color_code': '#f59e0b',
'icon': 'git-branch'
},
{
'name': 'Person',
'description': 'Personen, Teams und Organisationen',
'color_code': '#ec4899',
'icon': 'user'
},
{
'name': 'Dokument',
'description': 'Dokumentationen, Referenzen und Ressourcen',
'color_code': '#3b82f6',
'icon': 'file-text'
}
]
for cat_data in categories:
category = Category(**cat_data)
db.session.add(category)
db.session.commit()
print(f"{len(categories)} Kategorien wurden erstellt.")
def create_sample_mindmap():
"""Erstellt eine Beispiel-Mindmap mit Knoten und Beziehungen"""
# Kategorien für die Zuordnung
categories = Category.query.all()
category_map = {cat.name: cat for cat in categories}
# Beispielknoten erstellen
nodes = [
{
'name': 'Wissensmanagement',
'description': 'Systematische Erfassung, Speicherung und Nutzung von Wissen in Organisationen.',
'color_code': '#6366f1',
'icon': 'database',
'category': category_map.get('Konzept'),
'x': 0,
'y': 0
},
{
'name': 'Mind-Mapping',
'description': 'Technik zur visuellen Darstellung von Informationen und Zusammenhängen.',
'color_code': '#10b981',
'icon': 'git-branch',
'category': category_map.get('Prozess'),
'x': 200,
'y': -150
},
{
'name': 'Cytoscape.js',
'description': 'JavaScript-Bibliothek für die Visualisierung und Manipulation von Graphen.',
'color_code': '#3b82f6',
'icon': 'code',
'category': category_map.get('Technologie'),
'x': 350,
'y': -50
},
{
'name': 'Socket.IO',
'description': 'Bibliothek für Echtzeit-Kommunikation zwischen Client und Server.',
'color_code': '#3b82f6',
'icon': 'zap',
'category': category_map.get('Technologie'),
'x': 350,
'y': 100
},
{
'name': 'Kollaboration',
'description': 'Zusammenarbeit mehrerer Benutzer an gemeinsamen Inhalten.',
'color_code': '#f59e0b',
'icon': 'users',
'category': category_map.get('Prozess'),
'x': 200,
'y': 150
},
{
'name': 'SQLite',
'description': 'Leichtgewichtige relationale Datenbank, die ohne Server-Prozess auskommt.',
'color_code': '#3b82f6',
'icon': 'database',
'category': category_map.get('Technologie'),
'x': 0,
'y': 200
},
{
'name': 'Flask',
'description': 'Leichtgewichtiges Python-Webframework für die Entwicklung von Webanwendungen.',
'color_code': '#3b82f6',
'icon': 'server',
'category': category_map.get('Technologie'),
'x': -200,
'y': 150
},
{
'name': 'REST API',
'description': 'Architekturstil für verteilte Systeme, insbesondere Webanwendungen.',
'color_code': '#10b981',
'icon': 'link',
'category': category_map.get('Konzept'),
'x': -200,
'y': -150
},
{
'name': 'Dokumentation',
'description': 'Strukturierte Erfassung und Beschreibung von Informationen und Prozessen.',
'color_code': '#ec4899',
'icon': 'file-text',
'category': category_map.get('Dokument'),
'x': -350,
'y': 0
}
]
# Knoten in die Datenbank einfügen
node_objects = {}
for node_data in nodes:
category = node_data.pop('category', None)
x = node_data.pop('x', 0)
y = node_data.pop('y', 0)
node = MindMapNode(**node_data)
if category:
node.category_id = category.id
db.session.add(node)
db.session.flush() # Generiert IDs für neue Objekte
node_objects[node.name] = node
# Beziehungen erstellen
relationships = [
('Wissensmanagement', 'Mind-Mapping'),
('Wissensmanagement', 'Kollaboration'),
('Wissensmanagement', 'Dokumentation'),
('Mind-Mapping', 'Cytoscape.js'),
('Kollaboration', 'Socket.IO'),
('Wissensmanagement', 'SQLite'),
('SQLite', 'Flask'),
('Flask', 'REST API'),
('REST API', 'Socket.IO'),
('REST API', 'Dokumentation')
]
for parent_name, child_name in relationships:
parent = node_objects.get(parent_name)
child = node_objects.get(child_name)
if parent and child:
parent.children.append(child)
db.session.commit()
print(f"{len(nodes)} Knoten und {len(relationships)} Beziehungen wurden erstellt.")
if __name__ == '__main__': if __name__ == '__main__':
init_database() init_db()
print("Datenbank wurde erfolgreich initialisiert!") print("Datenbank wurde erfolgreich initialisiert!")
print("Sie können die Anwendung jetzt mit 'python app.py' starten") print("Sie können die Anwendung jetzt mit 'python app.py' starten")
print("Anmelden mit:") print("Anmelden mit:")

108
models.py
View File

@@ -6,6 +6,8 @@ from flask_login import UserMixin
from datetime import datetime from datetime import datetime
from werkzeug.security import generate_password_hash, check_password_hash from werkzeug.security import generate_password_hash, check_password_hash
from enum import Enum from enum import Enum
import uuid as uuid_pkg
import os
db = SQLAlchemy() db = SQLAlchemy()
@@ -43,30 +45,28 @@ user_thought_bookmark = db.Table('user_thought_bookmark',
db.Column('created_at', db.DateTime, default=datetime.utcnow) db.Column('created_at', db.DateTime, default=datetime.utcnow)
) )
class User(UserMixin, db.Model): class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False) username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False) email = db.Column(db.String(120), unique=True, nullable=False)
password_hash = db.Column(db.String(128)) password = db.Column(db.String(512), nullable=False)
is_admin = db.Column(db.Boolean, default=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow) created_at = db.Column(db.DateTime, default=datetime.utcnow)
last_login = db.Column(db.DateTime) is_active = db.Column(db.Boolean, default=True)
avatar = db.Column(db.String(200)) role = db.Column(db.String(20), default="user") # 'user', 'admin', 'moderator'
bio = db.Column(db.Text)
# Beziehungen # Relationships
thoughts = db.relationship('Thought', backref='author', lazy=True) threads = db.relationship('Thread', backref='creator', lazy=True)
comments = db.relationship('Comment', backref='author', lazy=True) messages = db.relationship('Message', backref='author', lazy=True)
user_mindmaps = db.relationship('UserMindmap', backref='user', lazy=True) projects = db.relationship('Project', backref='owner', lazy=True)
mindmap_notes = db.relationship('MindmapNote', backref='user', lazy=True)
bookmarked_thoughts = db.relationship('Thought', secondary=user_thought_bookmark, def __repr__(self):
backref=db.backref('bookmarked_by', lazy='dynamic')) return f'<User {self.username}>'
def set_password(self, password): def set_password(self, password):
self.password_hash = generate_password_hash(password) self.password = generate_password_hash(password)
def check_password(self, password): def check_password(self, password):
return check_password_hash(self.password_hash, password) return check_password_hash(self.password, password)
class Category(db.Model): class Category(db.Model):
"""Wissenschaftliche Kategorien für die Gliederung der öffentlichen Mindmap""" """Wissenschaftliche Kategorien für die Gliederung der öffentlichen Mindmap"""
@@ -81,6 +81,9 @@ class Category(db.Model):
children = db.relationship('Category', backref=db.backref('parent', remote_side=[id])) children = db.relationship('Category', backref=db.backref('parent', remote_side=[id]))
nodes = db.relationship('MindMapNode', backref='category', lazy=True) nodes = db.relationship('MindMapNode', backref='category', lazy=True)
def __repr__(self):
return f'<Category {self.name}>'
class MindMapNode(db.Model): class MindMapNode(db.Model):
"""Öffentliche Mindmap-Knoten, die für alle Benutzer sichtbar sind""" """Öffentliche Mindmap-Knoten, die für alle Benutzer sichtbar sind"""
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
@@ -93,6 +96,8 @@ class MindMapNode(db.Model):
created_by_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True) created_by_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True)
category_id = db.Column(db.Integer, db.ForeignKey('category.id'), nullable=True) category_id = db.Column(db.Integer, db.ForeignKey('category.id'), nullable=True)
__table_args__ = {'extend_existing': True}
# Beziehungen für Baumstruktur (mehrere Eltern möglich) # Beziehungen für Baumstruktur (mehrere Eltern möglich)
parents = db.relationship( parents = db.relationship(
'MindMapNode', 'MindMapNode',
@@ -111,6 +116,20 @@ class MindMapNode(db.Model):
# Beziehung zum Ersteller # Beziehung zum Ersteller
created_by = db.relationship('User', backref='created_nodes') created_by = db.relationship('User', backref='created_nodes')
def __repr__(self):
return f'<MindMapNode {self.name}>'
def to_dict(self):
return {
'id': self.id,
'name': self.name,
'description': self.description,
'color_code': self.color_code,
'icon': self.icon,
'category_id': self.category_id,
'created_at': self.created_at.isoformat() if self.created_at else None
}
class UserMindmap(db.Model): class UserMindmap(db.Model):
"""Benutzerspezifische Mindmap, die vom Benutzer personalisierbar ist""" """Benutzerspezifische Mindmap, die vom Benutzer personalisierbar ist"""
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
@@ -228,3 +247,62 @@ class Comment(db.Model):
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow) created_at = db.Column(db.DateTime, default=datetime.utcnow)
last_modified = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) last_modified = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Thread model
class Thread(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(200), nullable=False)
description = db.Column(db.Text)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
# Relationships
messages = db.relationship('Message', backref='thread', lazy=True, cascade="all, delete-orphan")
def __repr__(self):
return f'<Thread {self.title}>'
# Message model
class Message(db.Model):
id = db.Column(db.Integer, primary_key=True)
content = db.Column(db.Text, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
thread_id = db.Column(db.Integer, db.ForeignKey('thread.id'), nullable=False)
role = db.Column(db.String(20), default="user") # 'user', 'assistant', 'system'
def __repr__(self):
return f'<Message {self.id} by {self.user_id}>'
# Project model
class Project(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(150), nullable=False)
description = db.Column(db.Text)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
category_id = db.Column(db.Integer, db.ForeignKey('category.id'))
# Relationships
documents = db.relationship('Document', backref='project', lazy=True, cascade="all, delete-orphan")
def __repr__(self):
return f'<Project {self.title}>'
# Document model
class Document(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(150), nullable=False)
content = db.Column(db.Text)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
project_id = db.Column(db.Integer, db.ForeignKey('project.id'), nullable=False)
filename = db.Column(db.String(150), nullable=True)
file_path = db.Column(db.String(300), nullable=True)
file_type = db.Column(db.String(50), nullable=True)
file_size = db.Column(db.Integer, nullable=True)
def __repr__(self):
return f'<Document {self.title}>'

234
static/js/mindmap.html Normal file
View File

@@ -0,0 +1,234 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Interaktive Mindmap</title>
<!-- Cytoscape.js -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.26.0/cytoscape.min.js"></script>
<!-- Socket.IO -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.7.2/socket.io.min.js"></script>
<!-- Feather Icons (optional) -->
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f9fafb;
color: #111827;
line-height: 1.5;
}
.container {
display: flex;
flex-direction: column;
height: 100vh;
width: 100%;
}
.header {
background-color: #1f2937;
color: white;
padding: 1rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.header h1 {
font-size: 1.5rem;
font-weight: 500;
}
.toolbar {
background-color: #f3f4f6;
padding: 0.75rem;
display: flex;
gap: 0.5rem;
border-bottom: 1px solid #e5e7eb;
}
.btn {
background-color: #3b82f6;
color: white;
border: none;
border-radius: 0.25rem;
padding: 0.5rem 1rem;
font-size: 0.875rem;
cursor: pointer;
transition: background-color 0.2s;
display: flex;
align-items: center;
gap: 0.5rem;
}
.btn:hover {
background-color: #2563eb;
}
.btn-secondary {
background-color: #6b7280;
}
.btn-secondary:hover {
background-color: #4b5563;
}
.btn-danger {
background-color: #ef4444;
}
.btn-danger:hover {
background-color: #dc2626;
}
.search-container {
flex: 1;
display: flex;
margin-left: 1rem;
}
.search-input {
width: 100%;
max-width: 300px;
padding: 0.5rem;
border: 1px solid #d1d5db;
border-radius: 0.25rem;
font-size: 0.875rem;
}
#cy {
flex: 1;
width: 100%;
position: relative;
}
.category-filters {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
padding: 0.75rem;
background-color: #ffffff;
border-bottom: 1px solid #e5e7eb;
}
.category-filter {
border: none;
border-radius: 0.25rem;
padding: 0.25rem 0.75rem;
font-size: 0.75rem;
cursor: pointer;
transition: opacity 0.2s;
color: white;
}
.category-filter:not(.active) {
opacity: 0.6;
}
.category-filter:hover:not(.active) {
opacity: 0.8;
}
.footer {
background-color: #f3f4f6;
padding: 0.75rem;
text-align: center;
font-size: 0.75rem;
color: #6b7280;
border-top: 1px solid #e5e7eb;
}
/* Kontextmenü Styling */
#context-menu {
position: absolute;
background-color: white;
border: 1px solid #e5e7eb;
border-radius: 0.25rem;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
z-index: 1000;
}
#context-menu .menu-item {
padding: 0.5rem 1rem;
cursor: pointer;
}
#context-menu .menu-item:hover {
background-color: #f3f4f6;
}
</style>
</head>
<body>
<div class="container">
<header class="header">
<h1>Interaktive Mindmap</h1>
<div class="search-container">
<input type="text" id="search-mindmap" class="search-input" placeholder="Suchen...">
</div>
</header>
<div class="toolbar">
<button id="addNode" class="btn">
<i data-feather="plus-circle"></i>
Knoten hinzufügen
</button>
<button id="addEdge" class="btn">
<i data-feather="git-branch"></i>
Verbindung erstellen
</button>
<button id="editNode" class="btn btn-secondary">
<i data-feather="edit-2"></i>
Knoten bearbeiten
</button>
<button id="deleteNode" class="btn btn-danger">
<i data-feather="trash-2"></i>
Knoten löschen
</button>
<button id="deleteEdge" class="btn btn-danger">
<i data-feather="scissors"></i>
Verbindung löschen
</button>
<button id="reLayout" class="btn btn-secondary">
<i data-feather="refresh-cw"></i>
Layout neu anordnen
</button>
<button id="exportMindmap" class="btn btn-secondary">
<i data-feather="download"></i>
Exportieren
</button>
</div>
<div id="category-filters" class="category-filters">
<!-- Wird dynamisch befüllt -->
</div>
<div id="cy"></div>
<footer class="footer">
Mindmap-Anwendung © 2023
</footer>
</div>
<!-- Unsere Mindmap JS -->
<script src="../js/mindmap.js"></script>
<!-- Icons initialisieren -->
<script>
document.addEventListener('DOMContentLoaded', () => {
if (typeof feather !== 'undefined') {
feather.replace();
}
});
</script>
</body>
</html>

659
static/js/mindmap.js Normal file
View File

@@ -0,0 +1,659 @@
/**
* Mindmap.js - Interaktive Mind-Map Implementierung
* - Cytoscape.js für Graph-Rendering
* - Fetch API für REST-Zugriffe
* - Socket.IO für Echtzeit-Synchronisation
*/
(async () => {
/* 1. Initialisierung und Grundkonfiguration */
const cy = cytoscape({
container: document.getElementById('cy'),
style: [
{
selector: 'node',
style: {
'label': 'data(name)',
'text-valign': 'center',
'color': '#fff',
'background-color': 'data(color)',
'width': 45,
'height': 45,
'font-size': 11,
'text-outline-width': 1,
'text-outline-color': '#000',
'text-outline-opacity': 0.5,
'text-wrap': 'wrap',
'text-max-width': 80
}
},
{
selector: 'node[icon]',
style: {
'background-image': function(ele) {
return `static/img/icons/${ele.data('icon')}.svg`;
},
'background-width': '60%',
'background-height': '60%',
'background-position-x': '50%',
'background-position-y': '40%',
'text-margin-y': 10
}
},
{
selector: 'edge',
style: {
'width': 2,
'line-color': '#888',
'target-arrow-shape': 'triangle',
'curve-style': 'bezier',
'target-arrow-color': '#888'
}
},
{
selector: ':selected',
style: {
'border-width': 3,
'border-color': '#f8f32b'
}
}
],
layout: {
name: 'breadthfirst',
directed: true,
padding: 30,
spacingFactor: 1.2
}
});
/* 2. Hilfs-Funktionen für API-Zugriffe */
const get = endpoint => fetch(endpoint).then(r => r.json());
const post = (endpoint, body) =>
fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
}).then(r => r.json());
const del = endpoint =>
fetch(endpoint, { method: 'DELETE' }).then(r => r.json());
/* 3. Kategorien laden für Style-Informationen */
let categories = await get('/api/categories');
/* 4. Daten laden und Rendering */
const loadMindmap = async () => {
try {
// Nodes und Beziehungen parallel laden
const [nodes, relationships] = await Promise.all([
get('/api/mind_map_nodes'),
get('/api/node_relationships')
]);
// Graph leeren (für Reload-Fälle)
cy.elements().remove();
// Knoten zum Graph hinzufügen
cy.add(
nodes.map(node => {
// Kategorie-Informationen für Styling abrufen
const category = categories.find(c => c.id === node.category_id) || {};
return {
data: {
id: node.id.toString(),
name: node.name,
description: node.description,
color: node.color_code || category.color_code || '#6b7280',
icon: node.icon || category.icon,
category_id: node.category_id
},
position: node.x && node.y ? { x: node.x, y: node.y } : undefined
};
})
);
// Kanten zum Graph hinzufügen
cy.add(
relationships.map(rel => ({
data: {
id: `${rel.parent_id}_${rel.child_id}`,
source: rel.parent_id.toString(),
target: rel.child_id.toString()
}
}))
);
// Layout anwenden wenn keine Positionsdaten vorhanden
const nodesWithoutPosition = cy.nodes().filter(node =>
!node.position() || (node.position().x === 0 && node.position().y === 0)
);
if (nodesWithoutPosition.length > 0) {
cy.layout({
name: 'breadthfirst',
directed: true,
padding: 30,
spacingFactor: 1.2
}).run();
}
// Tooltip-Funktionalität
cy.nodes().unbind('mouseover').bind('mouseover', (event) => {
const node = event.target;
const description = node.data('description');
if (description) {
const tooltip = document.getElementById('node-tooltip') ||
document.createElement('div');
if (!tooltip.id) {
tooltip.id = 'node-tooltip';
tooltip.style.position = 'absolute';
tooltip.style.backgroundColor = '#333';
tooltip.style.color = '#fff';
tooltip.style.padding = '8px';
tooltip.style.borderRadius = '4px';
tooltip.style.maxWidth = '250px';
tooltip.style.zIndex = 10;
tooltip.style.pointerEvents = 'none';
tooltip.style.transition = 'opacity 0.2s';
tooltip.style.boxShadow = '0 2px 10px rgba(0,0,0,0.3)';
document.body.appendChild(tooltip);
}
const renderedPosition = node.renderedPosition();
const containerRect = cy.container().getBoundingClientRect();
tooltip.innerHTML = description;
tooltip.style.left = (containerRect.left + renderedPosition.x + 25) + 'px';
tooltip.style.top = (containerRect.top + renderedPosition.y - 15) + 'px';
tooltip.style.opacity = '1';
}
});
cy.nodes().unbind('mouseout').bind('mouseout', () => {
const tooltip = document.getElementById('node-tooltip');
if (tooltip) {
tooltip.style.opacity = '0';
}
});
} catch (error) {
console.error('Fehler beim Laden der Mindmap:', error);
alert('Die Mindmap konnte nicht geladen werden. Bitte prüfen Sie die Konsole für Details.');
}
};
// Initial laden
await loadMindmap();
/* 5. Socket.IO für Echtzeit-Synchronisation */
const socket = io();
socket.on('node_added', async (node) => {
// Kategorie-Informationen für Styling abrufen
const category = categories.find(c => c.id === node.category_id) || {};
cy.add({
data: {
id: node.id.toString(),
name: node.name,
description: node.description,
color: node.color_code || category.color_code || '#6b7280',
icon: node.icon || category.icon,
category_id: node.category_id
}
});
// Layout neu anwenden, wenn nötig
if (!node.x || !node.y) {
cy.layout({ name: 'breadthfirst', directed: true, padding: 30 }).run();
}
});
socket.on('node_updated', (node) => {
const cyNode = cy.$id(node.id.toString());
if (cyNode.length > 0) {
// Kategorie-Informationen für Styling abrufen
const category = categories.find(c => c.id === node.category_id) || {};
cyNode.data({
name: node.name,
description: node.description,
color: node.color_code || category.color_code || '#6b7280',
icon: node.icon || category.icon,
category_id: node.category_id
});
if (node.x && node.y) {
cyNode.position({ x: node.x, y: node.y });
}
}
});
socket.on('node_deleted', (nodeId) => {
const cyNode = cy.$id(nodeId.toString());
if (cyNode.length > 0) {
cy.remove(cyNode);
}
});
socket.on('relationship_added', (rel) => {
cy.add({
data: {
id: `${rel.parent_id}_${rel.child_id}`,
source: rel.parent_id.toString(),
target: rel.child_id.toString()
}
});
});
socket.on('relationship_deleted', (rel) => {
const edgeId = `${rel.parent_id}_${rel.child_id}`;
const cyEdge = cy.$id(edgeId);
if (cyEdge.length > 0) {
cy.remove(cyEdge);
}
});
socket.on('category_updated', async () => {
// Kategorien neu laden
categories = await get('/api/categories');
// Nodes aktualisieren, die diese Kategorie verwenden
cy.nodes().forEach(node => {
const categoryId = node.data('category_id');
if (categoryId) {
const category = categories.find(c => c.id === categoryId);
if (category) {
node.data('color', node.data('color_code') || category.color_code);
node.data('icon', node.data('icon') || category.icon);
}
}
});
});
/* 6. UI-Interaktionen */
// Knoten hinzufügen
const btnAddNode = document.getElementById('addNode');
if (btnAddNode) {
btnAddNode.addEventListener('click', async () => {
const name = prompt('Knotenname eingeben:');
if (!name) return;
const description = prompt('Beschreibung (optional):');
// Kategorie auswählen
let categoryId = null;
if (categories.length > 0) {
const categoryOptions = categories.map((c, i) => `${i}: ${c.name}`).join('\n');
const categoryChoice = prompt(
`Kategorie auswählen (Nummer eingeben):\n${categoryOptions}`,
'0'
);
if (categoryChoice !== null) {
const index = parseInt(categoryChoice, 10);
if (!isNaN(index) && index >= 0 && index < categories.length) {
categoryId = categories[index].id;
}
}
}
// Knoten erstellen
await post('/api/mind_map_node', {
name,
description,
category_id: categoryId
});
// Darstellung wird durch Socket.IO Event übernommen
});
}
// Verbindung hinzufügen
const btnAddEdge = document.getElementById('addEdge');
if (btnAddEdge) {
btnAddEdge.addEventListener('click', async () => {
const sel = cy.$('node:selected');
if (sel.length !== 2) {
alert('Bitte genau zwei Knoten auswählen (Parent → Child)');
return;
}
const [parent, child] = sel.map(node => node.id());
await post('/api/node_relationship', {
parent_id: parent,
child_id: child
});
// Darstellung wird durch Socket.IO Event übernommen
});
}
// Knoten bearbeiten
const btnEditNode = document.getElementById('editNode');
if (btnEditNode) {
btnEditNode.addEventListener('click', async () => {
const sel = cy.$('node:selected');
if (sel.length !== 1) {
alert('Bitte genau einen Knoten auswählen');
return;
}
const node = sel[0];
const nodeData = node.data();
const name = prompt('Knotenname:', nodeData.name);
if (!name) return;
const description = prompt('Beschreibung:', nodeData.description || '');
// Kategorie auswählen
let categoryId = nodeData.category_id;
if (categories.length > 0) {
const categoryOptions = categories.map((c, i) => `${i}: ${c.name}`).join('\n');
const categoryChoice = prompt(
`Kategorie auswählen (Nummer eingeben):\n${categoryOptions}`,
categories.findIndex(c => c.id === categoryId).toString()
);
if (categoryChoice !== null) {
const index = parseInt(categoryChoice, 10);
if (!isNaN(index) && index >= 0 && index < categories.length) {
categoryId = categories[index].id;
}
}
}
// Knoten aktualisieren
await post(`/api/mind_map_node/${nodeData.id}`, {
name,
description,
category_id: categoryId
});
// Darstellung wird durch Socket.IO Event übernommen
});
}
// Knoten löschen
const btnDeleteNode = document.getElementById('deleteNode');
if (btnDeleteNode) {
btnDeleteNode.addEventListener('click', async () => {
const sel = cy.$('node:selected');
if (sel.length !== 1) {
alert('Bitte genau einen Knoten auswählen');
return;
}
if (confirm('Sind Sie sicher, dass Sie diesen Knoten löschen möchten?')) {
const nodeId = sel[0].id();
await del(`/api/mind_map_node/${nodeId}`);
// Darstellung wird durch Socket.IO Event übernommen
}
});
}
// Verbindung löschen
const btnDeleteEdge = document.getElementById('deleteEdge');
if (btnDeleteEdge) {
btnDeleteEdge.addEventListener('click', async () => {
const sel = cy.$('edge:selected');
if (sel.length !== 1) {
alert('Bitte genau eine Verbindung auswählen');
return;
}
if (confirm('Sind Sie sicher, dass Sie diese Verbindung löschen möchten?')) {
const edge = sel[0];
const parentId = edge.source().id();
const childId = edge.target().id();
await del(`/api/node_relationship/${parentId}/${childId}`);
// Darstellung wird durch Socket.IO Event übernommen
}
});
}
// Layout aktualisieren
const btnReLayout = document.getElementById('reLayout');
if (btnReLayout) {
btnReLayout.addEventListener('click', () => {
cy.layout({
name: 'breadthfirst',
directed: true,
padding: 30,
spacingFactor: 1.2
}).run();
});
}
/* 7. Position speichern bei Drag & Drop */
cy.on('dragfree', 'node', async (e) => {
const node = e.target;
const position = node.position();
await post(`/api/mind_map_node/${node.id()}/position`, {
x: Math.round(position.x),
y: Math.round(position.y)
});
// Andere Benutzer erhalten die Position über den node_updated Event
});
/* 8. Kontextmenü (optional) */
const setupContextMenu = () => {
cy.on('cxttap', 'node', function(e) {
const node = e.target;
const nodeData = node.data();
// Position des Kontextmenüs berechnen
const renderedPosition = node.renderedPosition();
const containerRect = cy.container().getBoundingClientRect();
const menuX = containerRect.left + renderedPosition.x;
const menuY = containerRect.top + renderedPosition.y;
// Kontextmenü erstellen oder aktualisieren
let contextMenu = document.getElementById('context-menu');
if (!contextMenu) {
contextMenu = document.createElement('div');
contextMenu.id = 'context-menu';
contextMenu.style.position = 'absolute';
contextMenu.style.backgroundColor = '#fff';
contextMenu.style.border = '1px solid #ccc';
contextMenu.style.borderRadius = '4px';
contextMenu.style.padding = '5px 0';
contextMenu.style.boxShadow = '0 2px 10px rgba(0,0,0,0.2)';
contextMenu.style.zIndex = 1000;
document.body.appendChild(contextMenu);
}
// Menüinhalte
contextMenu.innerHTML = `
<div class="menu-item" data-action="edit">Knoten bearbeiten</div>
<div class="menu-item" data-action="connect">Verbindung erstellen</div>
<div class="menu-item" data-action="delete">Knoten löschen</div>
`;
// Styling für Menüpunkte
const menuItems = contextMenu.querySelectorAll('.menu-item');
menuItems.forEach(item => {
item.style.padding = '8px 20px';
item.style.cursor = 'pointer';
item.style.fontSize = '14px';
item.addEventListener('mouseover', function() {
this.style.backgroundColor = '#f0f0f0';
});
item.addEventListener('mouseout', function() {
this.style.backgroundColor = 'transparent';
});
// Event-Handler
item.addEventListener('click', async function() {
const action = this.getAttribute('data-action');
switch(action) {
case 'edit':
// Knoten bearbeiten (gleiche Logik wie beim Edit-Button)
const name = prompt('Knotenname:', nodeData.name);
if (name) {
const description = prompt('Beschreibung:', nodeData.description || '');
await post(`/api/mind_map_node/${nodeData.id}`, { name, description });
}
break;
case 'connect':
// Modus zum Verbinden aktivieren
cy.nodes().unselect();
node.select();
alert('Wählen Sie nun einen zweiten Knoten aus, um eine Verbindung zu erstellen');
break;
case 'delete':
if (confirm('Sind Sie sicher, dass Sie diesen Knoten löschen möchten?')) {
await del(`/api/mind_map_node/${nodeData.id}`);
}
break;
}
// Menü schließen
contextMenu.style.display = 'none';
});
});
// Menü positionieren und anzeigen
contextMenu.style.left = menuX + 'px';
contextMenu.style.top = menuY + 'px';
contextMenu.style.display = 'block';
// Event-Listener zum Schließen des Menüs
const closeMenu = function() {
if (contextMenu) {
contextMenu.style.display = 'none';
}
document.removeEventListener('click', closeMenu);
};
// Verzögerung, um den aktuellen Click nicht zu erfassen
setTimeout(() => {
document.addEventListener('click', closeMenu);
}, 0);
e.preventDefault();
});
};
// Kontextmenü aktivieren (optional)
// setupContextMenu();
/* 9. Export-Funktion (optional) */
const btnExport = document.getElementById('exportMindmap');
if (btnExport) {
btnExport.addEventListener('click', () => {
const elements = cy.json().elements;
const exportData = {
nodes: elements.nodes.map(n => ({
id: n.data.id,
name: n.data.name,
description: n.data.description,
category_id: n.data.category_id,
x: Math.round(n.position?.x || 0),
y: Math.round(n.position?.y || 0)
})),
relationships: elements.edges.map(e => ({
parent_id: e.data.source,
child_id: e.data.target
}))
};
const blob = new Blob([JSON.stringify(exportData, null, 2)], {type: 'application/json'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'mindmap_export.json';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
});
}
/* 10. Filter-Funktion nach Kategorien (optional) */
const setupCategoryFilters = () => {
const filterContainer = document.getElementById('category-filters');
if (!filterContainer || !categories.length) return;
filterContainer.innerHTML = '';
// "Alle anzeigen" Option
const allBtn = document.createElement('button');
allBtn.innerText = 'Alle Kategorien';
allBtn.className = 'category-filter active';
allBtn.onclick = () => {
document.querySelectorAll('.category-filter').forEach(btn => btn.classList.remove('active'));
allBtn.classList.add('active');
cy.nodes().removeClass('filtered').show();
cy.edges().show();
};
filterContainer.appendChild(allBtn);
// Filter-Button pro Kategorie
categories.forEach(category => {
const btn = document.createElement('button');
btn.innerText = category.name;
btn.className = 'category-filter';
btn.style.backgroundColor = category.color_code;
btn.style.color = '#fff';
btn.onclick = () => {
document.querySelectorAll('.category-filter').forEach(btn => btn.classList.remove('active'));
btn.classList.add('active');
const matchingNodes = cy.nodes().filter(node => node.data('category_id') === category.id);
cy.nodes().addClass('filtered').hide();
matchingNodes.removeClass('filtered').show();
// Verbindungen zu/von diesen Knoten anzeigen
cy.edges().hide();
matchingNodes.connectedEdges().show();
};
filterContainer.appendChild(btn);
});
};
// Filter-Funktionalität aktivieren (optional)
// setupCategoryFilters();
/* 11. Suchfunktion (optional) */
const searchInput = document.getElementById('search-mindmap');
if (searchInput) {
searchInput.addEventListener('input', (e) => {
const searchTerm = e.target.value.toLowerCase();
if (!searchTerm) {
cy.nodes().removeClass('search-hidden').show();
cy.edges().show();
return;
}
cy.nodes().forEach(node => {
const name = node.data('name').toLowerCase();
const description = (node.data('description') || '').toLowerCase();
if (name.includes(searchTerm) || description.includes(searchTerm)) {
node.removeClass('search-hidden').show();
node.connectedEdges().show();
} else {
node.addClass('search-hidden').hide();
// Kanten nur verstecken, wenn beide verbundenen Knoten versteckt sind
node.connectedEdges().forEach(edge => {
const otherNode = edge.source().id() === node.id() ? edge.target() : edge.source();
if (otherNode.hasClass('search-hidden')) {
edge.hide();
}
});
}
});
});
}
console.log('Mindmap erfolgreich initialisiert');
})();

View File

@@ -69,21 +69,22 @@ class NeuralNetworkBackground {
// Konfigurationsobjekt für subtilere, sanftere Neuronen // Konfigurationsobjekt für subtilere, sanftere Neuronen
this.config = { this.config = {
nodeCount: 60, // Weniger Knoten für bessere Leistung und subtileres Aussehen nodeCount: 45, // Weniger Knoten für bessere Leistung und subtileres Aussehen
nodeSize: 2.8, // Kleinere Knoten für dezenteres Erscheinungsbild nodeSize: 3.5, // Größere Knoten für bessere Sichtbarkeit
nodeVariation: 0.6, // Weniger Varianz für gleichmäßigeres Erscheinungsbild nodeVariation: 0.5, // Weniger Varianz für gleichmäßigeres Erscheinungsbild
connectionDistance: 220, // Etwas geringere Verbindungsdistanz connectionDistance: 250, // Größere Verbindungsdistanz
connectionOpacity: 0.15, // Transparentere Verbindungen connectionOpacity: 0.22, // Deutlichere Verbindungen
animationSpeed: 0.02, // Langsamere Animation für sanftere Bewegung animationSpeed: 0.02, // Langsamere Animation für sanftere Bewegung
pulseSpeed: 0.002, // Langsameres Pulsieren für subtilere Animation pulseSpeed: 0.002, // Langsameres Pulsieren für subtilere Animation
flowSpeed: 0.6, // Langsamer für sanftere Animation flowSpeed: 0.6, // Langsamer für bessere Sichtbarkeit
flowDensity: 0.002, // Deutlich weniger Blitze für subtileres Erscheinungsbild flowDensity: 0.005, // Mehr Blitze gleichzeitig erzeugen
flowLength: 0.12, // Kürzere Blitze für dezentere Effekte flowLength: 0.12, // Kürzere Blitze für dezentere Effekte
maxConnections: 3, // Weniger Verbindungen für aufgeräumteres Erscheinungsbild maxConnections: 4, // Mehr Verbindungen pro Neuron
clusteringFactor: 0.4, // Moderate Clustering-Stärke clusteringFactor: 0.45, // Stärkeres Clustering
linesFadeDuration: 3500, // Längere Dauer für sanfteres Ein-/Ausblenden von Linien (ms) linesFadeDuration: 4000, // Längere Dauer für sanfteres Ein-/Ausblenden von Linien (ms)
linesWidth: 0.6, // Dünnere unterliegende Linien linesWidth: 0.9, // Dickere unterliegende Linien für bessere Sichtbarkeit
linesOpacity: 0.25 // Geringere Opazität für Linien linesOpacity: 0.35, // Höhere Opazität für Linien
maxFlowCount: 10 // Maximale Anzahl gleichzeitiger Flüsse
}; };
// Initialize // Initialize
@@ -373,11 +374,10 @@ class NeuralNetworkBackground {
const height = this.canvas.height / (window.devicePixelRatio || 1); const height = this.canvas.height / (window.devicePixelRatio || 1);
const now = Date.now(); 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++) { for (let i = 0; i < this.nodes.length; i++) {
const node = this.nodes[i];
// Update pulse phase for smoother animation // Update pulse phase for smoother animation
const node = this.nodes[i];
node.pulsePhase += this.config.pulseSpeed * (1 + (node.connections.length * 0.04)); node.pulsePhase += this.config.pulseSpeed * (1 + (node.connections.length * 0.04));
// Animate node position with gentler movement // Animate node position with gentler movement
@@ -394,59 +394,79 @@ class NeuralNetworkBackground {
node.y = Math.max(0, Math.min(height, node.y)); node.y = Math.max(0, Math.min(height, node.y));
} }
// Check if node should fire based on reduced firing rate // Setze alle Knoten standardmäßig auf inaktiv
if (now - node.lastFired > node.firingRate * 1.3) { // 30% langsamere Feuerrate 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.isActive = true;
node.lastFired = now; node.lastFired = now;
node.activationTime = now; // Track when activation started
// Activate connected nodes with probability based on connection strength // Wähle eine zufällige Verbindung dieses Knotens
for (const connIndex of node.connections) { const randomConnIdx = Math.floor(Math.random() * node.connections.length);
// Find the connection const connectedNodeIdx = node.connections[randomConnIdx];
// Finde die entsprechende Verbindung
const conn = this.connections.find(c => const conn = this.connections.find(c =>
(c.from === i && c.to === connIndex) || (c.from === connIndex && c.to === i) (c.from === randomNodeIdx && c.to === connectedNodeIdx) ||
(c.from === connectedNodeIdx && c.to === randomNodeIdx)
); );
if (conn) { if (conn) {
// Mark connection as recently activated // Markiere die Verbindung als kürzlich aktiviert
conn.lastActivated = now; conn.lastActivated = now;
// Wenn eine Verbindung aktiviert wird, verlängere ggf. ihre Sichtbarkeit // Stelle sicher, dass die Verbindung sichtbar bleibt
if (conn.fadeState === 'out') { if (conn.fadeState === 'out') {
conn.fadeState = 'visible'; conn.fadeState = 'visible';
conn.fadeStartTime = now; conn.fadeStartTime = now;
} }
// Verbindung soll schneller aufgebaut werden, wenn ein Neuron feuert // Verbindung soll schneller aufgebaut werden
if (conn.progress < 1) { if (conn.progress < 1) {
conn.buildSpeed = 0.015 + Math.random() * 0.01; // Schnellerer Aufbau während der Aktivierung conn.buildSpeed = 0.015 + Math.random() * 0.01;
} }
// Reduzierte Wahrscheinlichkeit für neue Flows // Erstelle einen neuen Flow, wenn nicht zu viele existieren
if (this.flows.length < 4 && Math.random() < conn.strength * 0.5) { // Reduzierte Wahrscheinlichkeit if (this.flows.length < this.config.maxFlowCount) {
// Bestimme die Richtung (vom aktivierten Knoten weg)
const direction = conn.from === randomNodeIdx;
this.flows.push({ this.flows.push({
connection: conn, connection: conn,
progress: 0, progress: 0,
direction: conn.from === i, // Flow from activated node direction: direction,
length: this.config.flowLength + Math.random() * 0.05, // Geringere Variation length: this.config.flowLength + Math.random() * 0.05,
intensity: 0.5 + Math.random() * 0.3, // Geringere Intensität für subtilere Darstellung
creationTime: now, creationTime: now,
totalDuration: 1000 + Math.random() * 600 // Längere Dauer für sanftere Animation totalDuration: 1000 + Math.random() * 600,
sourceNodeIdx: direction ? conn.from : conn.to,
targetNodeIdx: direction ? conn.to : conn.from
}); });
} }
// 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
} }
} }
} }
} else if (now - node.lastFired > 400) { // Deactivate after longer period
node.isActive = false;
}
}
// Aktualisiere die Ein-/Ausblendung von Verbindungen // Aktualisiere die Ein-/Ausblendung von Verbindungen
for (const connection of this.connections) { for (const connection of this.connections) {
@@ -466,68 +486,52 @@ class NeuralNetworkBackground {
connection.fadeState = 'out'; connection.fadeState = 'out';
connection.fadeStartTime = now; connection.fadeStartTime = now;
connection.fadeProgress = 1.0; 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') { } else if (connection.fadeState === 'out') {
// Ausblenden // Ausblenden, aber nie komplett verschwinden
connection.fadeProgress = Math.max(0.0, 1.0 - (elapsedTime / connection.fadeTotalDuration)); connection.fadeProgress = Math.max(0.1, 1.0 - (elapsedTime / connection.fadeTotalDuration));
if (connection.fadeProgress <= 0.0) {
// Setze Verbindung zurück, damit sie wieder eingeblendet werden kann // Verbindungen bleiben immer minimal sichtbar (nie komplett unsichtbar)
if (Math.random() < 0.4) { // 40% Chance, direkt wieder einzublenden if (connection.fadeProgress <= 0.1) {
// Statt Verbindung komplett zu verstecken, setzen wir sie zurück auf "in"
connection.fadeState = 'in'; connection.fadeState = 'in';
connection.fadeStartTime = now; connection.fadeStartTime = now;
connection.fadeProgress = 0.0; connection.fadeProgress = 0.1; // Minimal sichtbar bleiben
connection.visibleDuration = 10000 + Math.random() * 15000; // Neue Dauer generieren connection.visibleDuration = 15000 + Math.random() * 20000; // Längere Sichtbarkeit
// 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') { } else if (connection.fadeState === 'hidden') {
// Verbindung ist unsichtbar, warte auf Wiedereinblendung // Keine Verbindungen mehr verstecken, stattdessen immer wieder einblenden
if (elapsedTime > connection.hiddenDuration) {
connection.fadeState = 'in'; connection.fadeState = 'in';
connection.fadeStartTime = now; connection.fadeStartTime = now;
connection.fadeProgress = 0.0; connection.fadeProgress = 0.1;
// Verbindung wird komplett neu aufgebaut
connection.progress = 0;
}
} }
// Animierter Verbindungsaufbau: progress inkrementieren, aber nur wenn aktiv // Verbindungen immer vollständig aufbauen und nicht zurücksetzen
if (connection.progress < 1) { if (connection.progress < 1) {
// Verbindung wird nur aufgebaut, wenn sie gerade aktiv ist oder ein Blitz sie aufbaut // Konstante Aufbaugeschwindigkeit, unabhängig vom Status
const buildingSpeed = connection.buildSpeed || 0.002; // Langsamer Standard-Aufbau 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) { if (now - connection.lastActivated < 2000) {
connection.progress += buildingSpeed; buildSpeed = Math.max(buildSpeed, 0.006);
if (connection.progress > 1) connection.progress = 1;
} }
connection.progress += buildSpeed;
if (connection.progress > 1) {
connection.progress = 1;
// Zurücksetzen der Aufbaugeschwindigkeit // Zurücksetzen der Aufbaugeschwindigkeit
connection.buildSpeed = 0; connection.buildSpeed = 0;
} }
} }
}
// Update flows with proper fading // Update flows with proper fading
this.updateFlows(now); this.updateFlows(now);
// Seltener neue Flows erstellen // 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); this.createNewFlow(now);
} }
@@ -560,6 +564,22 @@ class NeuralNetworkBackground {
// Update flow progress // Update flow progress
flow.progress += this.config.flowSpeed / flow.connection.distance; 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 // Remove completed or expired flows
if (flow.progress > 1.0 || flowProgress >= 1.0) { if (flow.progress > 1.0 || flowProgress >= 1.0) {
this.flows.splice(i, 1); this.flows.splice(i, 1);
@@ -1111,11 +1131,11 @@ class NeuralNetworkBackground {
// Weniger Funken mit geringerer Vibration // Weniger Funken mit geringerer Vibration
const sparks = this.generateSparkPoints(zigzag, 4 + Math.floor(Math.random() * 2)); const sparks = this.generateSparkPoints(zigzag, 4 + Math.floor(Math.random() * 2));
// Dezenteres Funkenlicht mit Ein-/Ausblendeffekt // Intensiveres Funkenlicht mit dynamischem Ein-/Ausblendeffekt
const sparkBaseOpacity = this.isDarkMode ? 0.65 : 0.55; const sparkBaseOpacity = this.isDarkMode ? 0.75 : 0.65;
const sparkBaseColor = this.isDarkMode const sparkBaseColor = this.isDarkMode
? `rgba(220, 235, 245, ${sparkBaseOpacity * fadeFactor})` ? `rgba(230, 240, 250, ${sparkBaseOpacity * fadeFactor})`
: `rgba(180, 220, 245, ${sparkBaseOpacity * fadeFactor})`; : `rgba(190, 230, 250, ${sparkBaseOpacity * fadeFactor})`;
for (const spark of sparks) { for (const spark of sparks) {
this.ctx.beginPath(); this.ctx.beginPath();
@@ -1147,34 +1167,36 @@ class NeuralNetworkBackground {
this.ctx.fill(); this.ctx.fill();
} }
// Dezenterer Fortschrittseffekt an der Spitze des Blitzes // Deutlicherer und länger anhaltender Fortschrittseffekt an der Spitze des Blitzes
if (endProgress >= connProgress - 0.05 && connProgress < 0.95) { if (endProgress >= connProgress - 0.1 && connProgress < 0.98) {
const tipGlow = this.ctx.createRadialGradient( const tipGlow = this.ctx.createRadialGradient(
p2.x, p2.y, 0, 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)`); tipGlow.addColorStop(1, `rgba(${rgbFlowColor.r}, ${rgbFlowColor.g}, ${rgbFlowColor.b}, 0)`);
this.ctx.fillStyle = tipGlow; this.ctx.fillStyle = tipGlow;
this.ctx.beginPath(); 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(); this.ctx.fill();
} }
// Sanftere Start- und Endblitz-Fades // Verstärkter Start- und Endblitz-Fade mit längerer Sichtbarkeit
if (startProgress < 0.1) { if (startProgress < 0.15) {
const startFade = startProgress / 0.1; // 0 bis 1 const startFade = startProgress / 0.15; // 0 bis 1
const startGlow = this.ctx.createRadialGradient( const startGlow = this.ctx.createRadialGradient(
p1.x, p1.y, 0, 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)`); startGlow.addColorStop(1, `rgba(${rgbFlowColor.r}, ${rgbFlowColor.g}, ${rgbFlowColor.b}, 0)`);
this.ctx.fillStyle = startGlow; this.ctx.fillStyle = startGlow;
this.ctx.beginPath(); 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(); this.ctx.fill();
} }
@@ -1259,11 +1281,11 @@ class NeuralNetworkBackground {
return points; return points;
} }
// Hilfsfunktion: Erzeuge dezentere Funkenpunkte mit gemäßigter Verteilung // Hilfsfunktion: Erzeuge intensivere Funkenpunkte mit dynamischer Verteilung
generateSparkPoints(zigzag, sparkCount = 4) { generateSparkPoints(zigzag, sparkCount = 15) {
const sparks = []; const sparks = [];
// Weniger Funken // Mehr Funken für intensiveren Effekt
const actualSparkCount = Math.min(sparkCount, zigzag.length); const actualSparkCount = Math.min(sparkCount, zigzag.length * 2);
// Funken an zufälligen Stellen entlang des Blitzes // Funken an zufälligen Stellen entlang des Blitzes
for (let i = 0; i < actualSparkCount; i++) { for (let i = 0; i < actualSparkCount; i++) {
@@ -1280,15 +1302,31 @@ class NeuralNetworkBackground {
const x = zigzag[segIndex].x + dx * t; const x = zigzag[segIndex].x + dx * t;
const y = zigzag[segIndex].y + dy * t; const y = zigzag[segIndex].y + dy * t;
// Rechtwinkliger Versatz vom Segment (sanftere Verteilung) // Dynamischer Versatz für intensivere Funken
const offsetAngle = segmentAngle + Math.PI/2; const offsetAngle = segmentAngle + (Math.random() * Math.PI - Math.PI/2);
const offsetDistance = Math.random() * 4 - 2; // Geringerer Offset für dezentere Funken 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({ sparks.push({
x: x + Math.cos(offsetAngle) * offsetDistance, x: x + Math.cos(offsetAngle) * offsetDistance,
y: y + Math.sin(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; return sparks;

File diff suppressed because it is too large Load Diff

View File

@@ -626,7 +626,7 @@
<div class="text-center py-12"> <div class="text-center py-12">
<i class="fas fa-lightbulb text-5xl text-gray-400 mb-4"></i> <i class="fas fa-lightbulb text-5xl text-gray-400 mb-4"></i>
<p class="text-gray-500">Noch keine Gedanken erstellt</p> <p class="text-gray-500">Noch keine Gedanken erstellt</p>
<a href="{{ url_for('create_thought') }}" class="mt-4 inline-block px-4 py-2 bg-purple-600 text-white rounded-lg">Ersten Gedanken erstellen</a> <a href="{{ url_for('get_thought') }}" class="mt-4 inline-block px-4 py-2 bg-purple-600 text-white rounded-lg">Ersten Gedanken erstellen</a>
</div> </div>
{% endif %} {% endif %}
</div> </div>