Files
website/app.py

2279 lines
81 KiB
Python

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
from datetime import datetime, timedelta, timezone
from flask import Flask, render_template, request, redirect, url_for, flash, jsonify, session, g
from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user
from flask_sqlalchemy import SQLAlchemy
from werkzeug.security import generate_password_hash, check_password_hash
import json
from enum import Enum
from wtforms import StringField, PasswordField, BooleanField, TextAreaField, SelectField, HiddenField
from wtforms.validators import DataRequired, Email, Length, EqualTo, ValidationError
from functools import wraps
import secrets
from sqlalchemy.sql import func
import openai
from openai import OpenAI
from dotenv import load_dotenv
from flask_socketio import SocketIO, emit
from flask_migrate import Migrate
import sqlalchemy
import ssl
import certifi
import os
# Modelle importieren
from models import (
db, User, Thought, Comment, MindMapNode, ThoughtRelation, ThoughtRating,
RelationType, Category, UserMindmap, UserMindmapNode, MindmapNote,
node_thought_association, user_thought_bookmark, node_relationship
)
# Lade .env-Datei
load_dotenv() # force=True erzwingt die Synchronisierung
# Bestimme den absoluten Pfad zur Datenbank
basedir = os.path.abspath(os.path.dirname(__file__))
db_path = os.path.join(basedir, 'database', 'systades.db')
# Stellen Sie sicher, dass das Verzeichnis existiert
os.makedirs(os.path.dirname(db_path), exist_ok=True)
app = Flask(__name__)
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", "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")
# Variable zur Überprüfung, ob OpenAI verfügbar ist
openai_available = True
# Client mit SSL-Verify-Deaktivierung für problematische Umgebungen
try:
# Versuche, den OpenAI-Client zu initialisieren und zu testen
client = OpenAI(api_key=api_key)
# Deaktiviere SSL-Verifizierung, falls in Windows-Umgebungen Probleme auftreten
client._client.proxies = {}
client._client.verify = False
# Testanfrage, um zu prüfen, ob die API funktioniert
try:
# Einfache Testanfrage
resp = client.models.list()
openai_available = True
print("OpenAI API-Verbindung erfolgreich hergestellt.")
except Exception as e:
print(f"OpenAI API-Verbindungstest fehlgeschlagen: {e}")
openai_available = False
except Exception as e:
print(f"Fehler bei der Initialisierung des OpenAI-Clients: {e}")
openai_available = False
client = None
# Dark Mode Einstellung in Session speichern
@app.before_request
def handle_dark_mode():
if 'dark_mode' not in session:
session['dark_mode'] = False # Standardmäßig Light Mode
# Context processor für Dark Mode
@app.context_processor
def inject_dark_mode():
return {'dark_mode': session.get('dark_mode', False)}
# Route zum Umschalten des Dark Mode
@app.route('/toggle-dark-mode', methods=['POST'])
def toggle_dark_mode():
session['dark_mode'] = not session.get('dark_mode', False)
return jsonify({'success': True, 'dark_mode': session['dark_mode']})
# Context processor für globale Template-Variablen
@app.context_processor
def inject_globals():
"""Inject global variables into all templates."""
return {
'current_year': datetime.now().year
}
# Context-Prozessor für alle Templates
@app.context_processor
def inject_current_year():
return {'current_year': datetime.now().year}
# Initialisiere die Datenbank
db.init_app(app)
# Initialisiere den Login-Manager
login_manager = LoginManager(app)
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
# SocketIO initialisieren
socketio = SocketIO(app)
migrate = Migrate(app, db)
# Funktion zum Erstellen von Standardbenutzern
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"""
# Hauptkategorien
main_categories = [
{
"name": "Philosophie",
"description": "Philosophisches Denken und Konzepte",
"color_code": "#9F7AEA",
"icon": "fa-brain",
"subcategories": [
{"name": "Ethik", "description": "Moralische Grundsätze", "icon": "fa-balance-scale"},
{"name": "Logik", "description": "Gesetze des Denkens", "icon": "fa-project-diagram"},
{"name": "Erkenntnistheorie", "description": "Natur des Wissens", "icon": "fa-lightbulb"}
]
},
{
"name": "Wissenschaft",
"description": "Wissenschaftliche Disziplinen und Forschung",
"color_code": "#48BB78",
"icon": "fa-flask",
"subcategories": [
{"name": "Physik", "description": "Gesetze der Materie und Energie", "icon": "fa-atom"},
{"name": "Biologie", "description": "Wissenschaft des Lebens", "icon": "fa-dna"},
{"name": "Mathematik", "description": "Abstrakte Strukturen", "icon": "fa-calculator"},
{"name": "Informatik", "description": "Wissenschaft der Datenverarbeitung", "icon": "fa-laptop-code"}
]
},
{
"name": "Technologie",
"description": "Technologische Entwicklungen und Anwendungen",
"color_code": "#ED8936",
"icon": "fa-microchip",
"subcategories": [
{"name": "Künstliche Intelligenz", "description": "Intelligente Maschinen", "icon": "fa-robot"},
{"name": "Programmierung", "description": "Softwareentwicklung", "icon": "fa-code"},
{"name": "Elektronik", "description": "Elektronische Systeme", "icon": "fa-memory"}
]
},
{
"name": "Künste",
"description": "Kunstformen und kulturelle Ausdrucksweisen",
"color_code": "#ED64A6",
"icon": "fa-palette",
"subcategories": [
{"name": "Literatur", "description": "Schriftliche Werke", "icon": "fa-book"},
{"name": "Musik", "description": "Klangkunst", "icon": "fa-music"},
{"name": "Bildende Kunst", "description": "Visuelle Kunstformen", "icon": "fa-paint-brush"}
]
},
{
"name": "Psychologie",
"description": "Menschliches Verhalten und Geist",
"color_code": "#4299E1",
"icon": "fa-comments",
"subcategories": [
{"name": "Kognition", "description": "Denken und Wahrnehmen", "icon": "fa-brain"},
{"name": "Emotionen", "description": "Gefühlswelt", "icon": "fa-heart"},
{"name": "Persönlichkeit", "description": "Charaktereigenschaften", "icon": "fa-user"}
]
}
]
# Kategorien erstellen
for main_cat_data in main_categories:
# Prüfen, ob die Kategorie bereits existiert
existing_cat = Category.query.filter_by(name=main_cat_data["name"]).first()
if existing_cat:
continue
# Hauptkategorie erstellen
main_category = Category(
name=main_cat_data["name"],
description=main_cat_data["description"],
color_code=main_cat_data["color_code"],
icon=main_cat_data["icon"]
)
db.session.add(main_category)
db.session.flush() # Um die ID zu generieren
# Unterkategorien erstellen
for sub_cat_data in main_cat_data.get("subcategories", []):
sub_category = Category(
name=sub_cat_data["name"],
description=sub_cat_data["description"],
color_code=main_cat_data["color_code"],
icon=sub_cat_data.get("icon", main_cat_data["icon"]),
parent_id=main_category.id
)
db.session.add(sub_category)
db.session.commit()
print("Standard-Kategorien wurden erstellt!")
def initialize_database():
"""Initialisiert die Datenbank mit Grunddaten, falls diese leer ist"""
try:
print("Initialisiere die Datenbank...")
# Erstelle alle Tabellen
db.create_all()
# Prüfen, ob bereits Kategorien existieren
categories_count = Category.query.count()
users_count = User.query.count()
# Erstelle Standarddaten, wenn es keine Kategorien gibt
if categories_count == 0:
create_default_categories()
# Admin-Benutzer erstellen, wenn keine Benutzer vorhanden sind
if users_count == 0:
admin_user = User(
username="admin",
email="admin@example.com",
role="admin",
is_active=True
)
admin_user.set_password("admin123") # Sicheres Passwort in der Produktion verwenden!
db.session.add(admin_user)
db.session.commit()
print("Admin-Benutzer wurde erstellt!")
return True
except Exception as e:
print(f"Fehler bei Datenbank-Initialisierung: {e}")
return False
# Instead of before_first_request, which is deprecated in newer Flask versions
# Use a function to initialize the database that will be called during app creation
def init_app_database(app_instance):
"""Initialisiert die Datenbank für die Flask-App"""
with app_instance.app_context():
# Überprüfe und initialisiere die Datenbank bei Bedarf
if not initialize_db_if_needed(db, initialize_database):
print("WARNUNG: Datenbankinitialisierung fehlgeschlagen. Einige Funktionen könnten eingeschränkt sein.")
# Call the function to initialize the database
init_app_database(app)
# Benutzerdefinierter Decorator für Admin-Zugriff
def admin_required(f):
@wraps(f)
@login_required
def decorated_function(*args, **kwargs):
if not current_user.is_admin:
flash('Zugriff verweigert. Nur Administratoren dürfen diese Seite aufrufen.', 'error')
return redirect(url_for('index'))
return f(*args, **kwargs)
return decorated_function
@login_manager.user_loader
def load_user(id):
# Verwende session.get() anstelle von query.get() für SQLAlchemy 2.0 Kompatibilität
return db.session.get(User, int(id))
# Routes for authentication
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
user = User.query.filter_by(username=username).first()
if user and user.check_password(password):
login_user(user)
# Aktualisiere letzten Login-Zeitpunkt
user.last_login = datetime.now(timezone.utc)
db.session.commit()
next_page = request.args.get('next')
return redirect(next_page or url_for('index'))
flash('Ungültiger Benutzername oder Passwort')
return render_template('login.html')
@app.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
username = request.form.get('username')
email = request.form.get('email')
password = request.form.get('password')
if User.query.filter_by(username=username).first():
flash('Benutzername existiert bereits')
return redirect(url_for('register'))
if User.query.filter_by(email=email).first():
flash('E-Mail ist bereits registriert')
return redirect(url_for('register'))
user = User(username=username, email=email)
user.set_password(password)
db.session.add(user)
db.session.commit() # Commit, um eine ID für den Benutzer zu erhalten
# Erstelle eine Standard-Mindmap für den neuen Benutzer
try:
default_mindmap = UserMindmap(
name='Meine Mindmap',
description='Meine persönliche Wissenslandschaft',
user_id=user.id
)
db.session.add(default_mindmap)
db.session.commit()
except Exception as e:
print(f"Fehler beim Erstellen der Standard-Mindmap: {e}")
# Stelle sicher, dass wir trotzdem weitermachen können
db.session.rollback()
login_user(user)
flash('Dein Konto wurde erfolgreich erstellt!', 'success')
return redirect(url_for('index'))
return render_template('register.html')
@app.route('/logout')
@login_required
def logout():
logout_user()
return redirect(url_for('index'))
# Route for the homepage
@app.route('/')
def index():
return render_template('index.html')
# Route for the mindmap page
@app.route('/mindmap')
def mindmap():
"""Zeigt die Mindmap-Seite an."""
# Benutzer-Mindmaps, falls angemeldet
user_mindmaps = []
if current_user.is_authenticated:
user_mindmaps = UserMindmap.query.filter_by(user_id=current_user.id).all()
# Stelle sicher, dass der "Wissen"-Knoten existiert
wissen_node = MindMapNode.query.filter_by(name="Wissen").first()
if not wissen_node:
wissen_node = MindMapNode(
name="Wissen",
description="Zentrale Wissensbasis",
color_code="#4299E1",
is_public=True
)
db.session.add(wissen_node)
db.session.commit()
print("'Wissen'-Knoten wurde erstellt")
# Überprüfe, ob es Kategorien gibt, sonst erstelle sie
if Category.query.count() == 0:
create_default_categories()
print("Kategorien wurden erstellt")
# Stelle sicher, dass die Route für statische Dateien korrekt ist
mindmap_js_path = url_for('static', filename='js/mindmap-init.js')
return render_template('mindmap.html', user_mindmaps=user_mindmaps, mindmap_js_path=mindmap_js_path)
# Route for user profile
@app.route('/profile')
@login_required
def profile():
try:
# Versuche auf die neue Benutzermodellstruktur zuzugreifen
_ = current_user.bio # Dies wird fehlschlagen, wenn die Spalte nicht existiert
# Wenn keine Ausnahme, fahre mit normalem Profil fort
# Lade Benutzer-Mindmaps
user_mindmaps = UserMindmap.query.filter_by(user_id=current_user.id).all()
# Prüfe, ob der Benutzer eine Standard-Mindmap hat, sonst erstelle eine
if not user_mindmaps:
try:
default_mindmap = UserMindmap(
name='Meine Mindmap',
description='Meine persönliche Wissenslandschaft',
user_id=current_user.id
)
db.session.add(default_mindmap)
db.session.commit()
# Aktualisiere die Liste nach dem Erstellen
user_mindmaps = [default_mindmap]
except Exception as e:
print(f"Fehler beim Erstellen der Standard-Mindmap in Profil: {e}")
# Flash-Nachricht für den Benutzer
flash('Es gab ein Problem beim Laden deiner Mindmaps. Bitte versuche es später erneut.', 'warning')
# Lade Statistiken
thought_count = Thought.query.filter_by(user_id=current_user.id).count()
bookmark_count = db.session.query(user_thought_bookmark).filter(
user_thought_bookmark.c.user_id == current_user.id).count()
# Berechne tatsächliche Werte für Benutzerstatistiken
contributions_count = Comment.query.filter_by(user_id=current_user.id).count()
# Berechne Verbindungen (Anzahl der Gedankenverknüpfungen)
connections_count = ThoughtRelation.query.filter(
(ThoughtRelation.source_id.in_(
db.session.query(Thought.id).filter_by(user_id=current_user.id)
)) |
(ThoughtRelation.target_id.in_(
db.session.query(Thought.id).filter_by(user_id=current_user.id)
))
).count()
# Berechne durchschnittliche Bewertung der Gedanken des Benutzers
avg_rating = db.session.query(func.avg(ThoughtRating.relevance_score)).join(
Thought, Thought.id == ThoughtRating.thought_id
).filter(Thought.user_id == current_user.id).scalar() or 0
# Sammle alle Statistiken in einem Wörterbuch
stats = {
'thought_count': thought_count,
'bookmark_count': bookmark_count,
'connections_count': connections_count,
'contributions_count': contributions_count,
'followers_count': 0, # Platzhalter für zukünftige Funktionalität
'rating': round(avg_rating, 1)
}
# Hole die letzten Gedanken des Benutzers
thoughts = Thought.query.filter_by(user_id=current_user.id).order_by(Thought.created_at.desc()).limit(5).all()
# Hole den Standort des Benutzers aus der Datenbank, falls vorhanden
location = "Deutschland" # Standardwert
return render_template('profile.html',
user=current_user,
user_mindmaps=user_mindmaps,
stats=stats,
thoughts=thoughts,
location=location)
except (AttributeError, sqlalchemy.exc.OperationalError) as e:
# Die Spalte existiert nicht, verwende stattdessen das einfache Profil
print(f"Verwende einfaches Profil wegen Datenbankfehler: {e}")
flash('Dein Profil wird im einfachen Modus angezeigt, bis die Datenbank aktualisiert wird.', 'warning')
# Lade nur die grundlegenden Informationen
user_mindmaps = UserMindmap.query.filter_by(user_id=current_user.id).all()
thoughts = Thought.query.filter_by(user_id=current_user.id).order_by(Thought.created_at.desc()).limit(5).all()
return render_template('simple_profile.html',
user=current_user,
user_mindmaps=user_mindmaps,
thoughts=thoughts)
except Exception as e:
# Eine andere Ausnahme ist aufgetreten
print(f"Fehler beim Laden des Profils: {e}")
flash('Beim Laden Ihres Benutzerprofils ist ein Fehler aufgetreten. Wir zeigen Ihnen die Standard-Ansicht.', 'error')
# Erstelle grundlegende stats
stats = {
'thought_count': 0,
'bookmark_count': 0,
'connections_count': 0,
'contributions_count': 0,
'followers_count': 0,
'rating': 0.0
}
# Leere Listen für Mindmaps und Gedanken
user_mindmaps = []
thoughts = []
location = "Deutschland"
# Zeige das normale Profil mit minimalen Daten an
return render_template('profile.html',
user=current_user,
user_mindmaps=user_mindmaps,
stats=stats,
thoughts=thoughts,
location=location)
# Route für Benutzereinstellungen
@app.route('/settings', methods=['GET', 'POST'])
@login_required
def settings():
if request.method == 'POST':
action = request.form.get('action')
# Bestimme, ob es eine AJAX-Anfrage ist
is_ajax = request.headers.get('X-Requested-With') == 'XMLHttpRequest' or request.content_type and 'multipart/form-data' in request.content_type
if action == 'update_profile':
try:
current_user.bio = request.form.get('bio', '')
current_user.location = request.form.get('location', '')
current_user.website = request.form.get('website', '')
# Update avatar if provided
avatar_url = request.form.get('avatar_url')
if avatar_url:
current_user.avatar = avatar_url
db.session.commit()
if is_ajax:
return jsonify({
'success': True,
'message': 'Profil erfolgreich aktualisiert!'
})
else:
flash('Profil erfolgreich aktualisiert!', 'success')
except Exception as e:
db.session.rollback()
app.logger.error(f"Fehler beim Aktualisieren des Profils: {str(e)}")
if is_ajax:
return jsonify({
'success': False,
'message': 'Fehler beim Aktualisieren des Profils'
}), 500
else:
flash('Fehler beim Aktualisieren des Profils', 'error')
elif action == 'update_password':
try:
current_password = request.form.get('current_password')
new_password = request.form.get('new_password')
confirm_password = request.form.get('confirm_password')
if not current_user.check_password(current_password):
if is_ajax:
return jsonify({
'success': False,
'message': 'Aktuelles Passwort ist nicht korrekt'
}), 400
else:
flash('Aktuelles Passwort ist nicht korrekt', 'error')
elif new_password != confirm_password:
if is_ajax:
return jsonify({
'success': False,
'message': 'Neue Passwörter stimmen nicht überein'
}), 400
else:
flash('Neue Passwörter stimmen nicht überein', 'error')
else:
current_user.set_password(new_password)
db.session.commit()
if is_ajax:
return jsonify({
'success': True,
'message': 'Passwort erfolgreich aktualisiert!'
})
else:
flash('Passwort erfolgreich aktualisiert!', 'success')
except Exception as e:
db.session.rollback()
app.logger.error(f"Fehler beim Aktualisieren des Passworts: {str(e)}")
if is_ajax:
return jsonify({
'success': False,
'message': 'Fehler beim Aktualisieren des Passworts'
}), 500
else:
flash('Fehler beim Aktualisieren des Passworts', 'error')
if not is_ajax:
return redirect(url_for('settings'))
else:
# Standardantwort für AJAX, falls keine spezifische Antwort zurückgegeben wurde
return jsonify({
'success': True,
'message': 'Einstellungen aktualisiert'
})
return render_template('settings.html')
# API-Endpunkt für Flash-Nachrichten
@app.route('/api/get_flash_messages')
def get_flash_messages():
"""Liefert aktuelle Flash-Nachrichten für API/JS-Clients."""
# Hole alle gespeicherten Flash-Nachrichten
messages = []
flashed_messages = session.get('_flashes', [])
# Formatierung der Nachrichten für die API-Antwort
for category, message in flashed_messages:
messages.append({
'category': category,
'message': message
})
# Lösche die Nachrichten aus der Session, nachdem sie abgerufen wurden
session.pop('_flashes', None)
return jsonify(messages)
# Routes für rechtliche Seiten
@app.route('/impressum/')
def impressum():
return render_template('impressum.html')
@app.route('/datenschutz/')
def datenschutz():
return render_template('datenschutz.html')
@app.route('/agb/')
def agb():
return render_template('agb.html')
@app.route('/ueber-uns/')
def ueber_uns():
return render_template('ueber_uns.html')
# Benutzer-Mindmap-Funktionalität
@app.route('/my-mindmap/<int:mindmap_id>')
@login_required
def user_mindmap(mindmap_id):
mindmap = UserMindmap.query.get_or_404(mindmap_id)
# Sicherheitscheck: Nur eigene Mindmaps oder öffentliche Mindmaps
if mindmap.user_id != current_user.id and mindmap.is_private:
flash("Du hast keinen Zugriff auf diese Mindmap.", "error")
return redirect(url_for('profile'))
return render_template('user_mindmap.html', mindmap=mindmap)
@app.route('/mindmap/create', methods=['GET', 'POST'])
@login_required
def create_mindmap():
if request.method == 'POST':
name = request.form.get('name')
description = request.form.get('description')
is_private = request.form.get('is_private') == 'on'
new_mindmap = UserMindmap(
name=name,
description=description,
user_id=current_user.id,
is_private=is_private
)
db.session.add(new_mindmap)
db.session.commit()
flash('Neue Mindmap erfolgreich erstellt!', 'success')
return redirect(url_for('user_mindmap', mindmap_id=new_mindmap.id))
return render_template('create_mindmap.html')
@app.route('/mindmap/<int:mindmap_id>/edit', methods=['GET', 'POST'])
@login_required
def edit_mindmap(mindmap_id):
mindmap = UserMindmap.query.get_or_404(mindmap_id)
# Sicherheitscheck
if mindmap.user_id != current_user.id:
flash("Du kannst nur deine eigenen Mindmaps bearbeiten.", "error")
return redirect(url_for('profile'))
if request.method == 'POST':
mindmap.name = request.form.get('name')
mindmap.description = request.form.get('description')
mindmap.is_private = request.form.get('is_private') == 'on'
db.session.commit()
flash('Mindmap erfolgreich aktualisiert!', 'success')
return redirect(url_for('user_mindmap', mindmap_id=mindmap.id))
return render_template('edit_mindmap.html', mindmap=mindmap)
@app.route('/mindmap/<int:mindmap_id>/delete', methods=['POST'])
@login_required
def delete_mindmap(mindmap_id):
mindmap = UserMindmap.query.get_or_404(mindmap_id)
# Sicherheitscheck
if mindmap.user_id != current_user.id:
flash("Du kannst nur deine eigenen Mindmaps löschen.", "error")
return redirect(url_for('profile'))
db.session.delete(mindmap)
db.session.commit()
flash('Mindmap erfolgreich gelöscht!', 'success')
return redirect(url_for('profile'))
# API-Endpunkte für UserMindmap CRUD-Operationen
@app.route('/api/mindmaps', methods=['POST'])
@login_required
def api_create_user_mindmap():
data = request.get_json()
name = data.get('name')
description = data.get('description')
is_private = data.get('is_private', True)
if not name:
return jsonify({'error': 'Name ist erforderlich'}), 400
new_mindmap = UserMindmap(
name=name,
description=description,
user_id=current_user.id,
is_private=is_private
)
db.session.add(new_mindmap)
db.session.commit()
return jsonify({
'id': new_mindmap.id,
'name': new_mindmap.name,
'description': new_mindmap.description,
'is_private': new_mindmap.is_private,
'user_id': new_mindmap.user_id,
'created_at': new_mindmap.created_at.isoformat(),
'last_modified': new_mindmap.last_modified.isoformat()
}), 201
@app.route('/api/mindmaps', methods=['GET'])
@login_required
def api_get_user_mindmaps():
mindmaps = UserMindmap.query.filter_by(user_id=current_user.id).all()
return jsonify([{
'id': m.id,
'name': m.name,
'description': m.description,
'is_private': m.is_private,
'created_at': m.created_at.isoformat(),
'last_modified': m.last_modified.isoformat()
} for m in mindmaps])
@app.route('/api/mindmaps/<int:mindmap_id>', methods=['GET'])
@login_required
def api_get_user_mindmap_detail(mindmap_id):
mindmap = UserMindmap.query.filter_by(id=mindmap_id, user_id=current_user.id).first_or_404()
# Bestehende Logik von get_user_mindmap kann hier wiederverwendet oder angepasst werden
# Für eine einfache Detailansicht:
return jsonify({
'id': mindmap.id,
'name': mindmap.name,
'description': mindmap.description,
'is_private': mindmap.is_private,
'user_id': mindmap.user_id,
'created_at': mindmap.created_at.isoformat(),
'last_modified': mindmap.last_modified.isoformat(),
# Hier könnten auch Knoten und Notizen hinzugefügt werden, ähnlich wie in get_user_mindmap
})
@app.route('/api/mindmaps/<int:mindmap_id>', methods=['PUT'])
@login_required
def api_update_user_mindmap(mindmap_id):
mindmap = UserMindmap.query.filter_by(id=mindmap_id, user_id=current_user.id).first_or_404()
data = request.get_json()
mindmap.name = data.get('name', mindmap.name)
mindmap.description = data.get('description', mindmap.description)
mindmap.is_private = data.get('is_private', mindmap.is_private)
db.session.commit()
return jsonify({
'id': mindmap.id,
'name': mindmap.name,
'description': mindmap.description,
'is_private': mindmap.is_private,
'last_modified': mindmap.last_modified.isoformat()
})
@app.route('/api/mindmaps/<int:mindmap_id>', methods=['DELETE'])
@login_required
def api_delete_user_mindmap(mindmap_id):
mindmap = UserMindmap.query.filter_by(id=mindmap_id, user_id=current_user.id).first_or_404()
db.session.delete(mindmap)
db.session.commit()
return jsonify({'message': 'Mindmap erfolgreich gelöscht'}), 200
# API-Endpunkte für Mindmap-Daten (öffentlich und benutzerspezifisch)
@app.route('/api/mindmap/public')
def get_public_mindmap():
"""Liefert die Standard-Mindmap-Struktur basierend auf Kategorien."""
try:
# Hole alle Hauptkategorien
categories = Category.query.filter_by(parent_id=None).all()
# Transformiere zu einer Baumstruktur
category_tree = [build_category_tree(cat) for cat in categories]
return jsonify(category_tree)
except Exception as e:
print(f"Fehler beim Abrufen der Mindmap: {str(e)}")
return jsonify({
'success': False,
'error': 'Mindmap konnte nicht geladen werden'
}), 500
@app.route('/api/mindmap/public/add_node', methods=['POST'])
@login_required
def add_node_to_public_mindmap():
"""Fügt einen neuen Knoten zur öffentlichen Mindmap hinzu."""
try:
data = request.json
name = data.get('name')
description = data.get('description', '')
color_code = data.get('color_code', '#8b5cf6')
x_position = data.get('x_position', 0)
y_position = data.get('y_position', 0)
if not name:
return jsonify({
'success': False,
'error': 'Knotenname ist erforderlich'
}), 400
# Neuen Knoten erstellen
new_node = MindMapNode(
name=name,
description=description,
color_code=color_code
)
db.session.add(new_node)
db.session.flush() # ID generieren
# Als Beitrag des aktuellen Benutzers markieren
new_node.contributed_by = current_user.id
db.session.commit()
return jsonify({
'success': True,
'node_id': new_node.id,
'message': 'Knoten erfolgreich hinzugefügt'
})
except Exception as e:
db.session.rollback()
print(f"Fehler beim Hinzufügen des Knotens: {str(e)}")
return jsonify({
'success': False,
'error': f'Fehler beim Hinzufügen des Knotens: {str(e)}'
}), 500
@app.route('/api/mindmap/public/update_node/<int:node_id>', methods=['PUT'])
@login_required
def update_public_node(node_id):
"""Aktualisiert einen Knoten in der öffentlichen Mindmap."""
try:
node = MindMapNode.query.get_or_404(node_id)
data = request.json
# Aktualisiere Knotendaten
if 'name' in data:
node.name = data['name']
if 'description' in data:
node.description = data['description']
if 'color_code' in data:
node.color_code = data['color_code']
# Als bearbeitet markieren
node.last_modified = datetime.now(timezone.utc)
node.last_modified_by = current_user.id
db.session.commit()
return jsonify({
'success': True,
'message': 'Knoten erfolgreich aktualisiert'
})
except Exception as e:
db.session.rollback()
print(f"Fehler beim Aktualisieren des Knotens: {str(e)}")
return jsonify({
'success': False,
'error': f'Fehler beim Aktualisieren des Knotens: {str(e)}'
}), 500
@app.route('/api/mindmap/public/remove_node/<int:node_id>', methods=['DELETE'])
@login_required
def remove_node_from_public_mindmap(node_id):
"""Entfernt einen Knoten aus der öffentlichen Mindmap."""
try:
node = MindMapNode.query.get_or_404(node_id)
# Lösche den Knoten
db.session.delete(node)
db.session.commit()
return jsonify({
'success': True,
'message': 'Knoten erfolgreich entfernt'
})
except Exception as e:
db.session.rollback()
print(f"Fehler beim Entfernen des Knotens: {str(e)}")
return jsonify({
'success': False,
'error': f'Fehler beim Entfernen des Knotens: {str(e)}'
}), 500
@app.route('/api/mindmap/public/update_layout', methods=['POST'])
@login_required
def update_public_layout():
"""Aktualisiert die Positionen der Knoten in der öffentlichen Mindmap."""
try:
data = request.json
positions = data.get('positions', [])
for pos in positions:
node_id = pos.get('node_id')
node = MindMapNode.query.get(node_id)
if node:
# Position aktualisieren
node.x_position = pos.get('x_position', 0)
node.y_position = pos.get('y_position', 0)
db.session.commit()
return jsonify({
'success': True,
'message': 'Layout erfolgreich aktualisiert'
})
except Exception as e:
db.session.rollback()
print(f"Fehler beim Aktualisieren des Layouts: {str(e)}")
return jsonify({
'success': False,
'error': f'Fehler beim Aktualisieren des Layouts: {str(e)}'
}), 500
def build_category_tree(category):
"""
Erstellt eine Baumstruktur für eine Kategorie mit all ihren Unterkategorien
und dazugehörigen Knoten
Args:
category: Ein Category-Objekt
Returns:
dict: Eine JSON-serialisierbare Darstellung der Kategoriestruktur
"""
# Kategorie-Basisinformationen
category_dict = {
'id': category.id,
'name': category.name,
'description': category.description,
'color_code': category.color_code,
'icon': category.icon,
'nodes': [],
'children': []
}
# Knoten zur Kategorie hinzufügen
if category.nodes:
for node in category.nodes:
category_dict['nodes'].append({
'id': node.id,
'name': node.name,
'description': node.description or '',
'color_code': node.color_code or '#9F7AEA',
'thought_count': len(node.thoughts) if hasattr(node, 'thoughts') else 0
})
# Rekursiv Unterkategorien hinzufügen
if category.children:
for child in category.children:
category_dict['children'].append(build_category_tree(child))
return category_dict
@app.route('/api/mindmap/user/<int:mindmap_id>')
@login_required
def get_user_mindmap(mindmap_id):
"""Liefert die benutzerdefinierte Mindmap-Struktur."""
mindmap = UserMindmap.query.get_or_404(mindmap_id)
# Sicherheitscheck
if mindmap.user_id != current_user.id and mindmap.is_private:
return jsonify({'error': 'Nicht autorisiert'}), 403
# Hole alle verknüpften Knoten mit Position
nodes_data = db.session.query(
MindMapNode, UserMindmapNode
).join(
UserMindmapNode, UserMindmapNode.node_id == MindMapNode.id
).filter(
UserMindmapNode.user_mindmap_id == mindmap_id
).all()
# Knoten formatieren
nodes = []
for node, user_node in nodes_data:
nodes.append({
'id': node.id,
'name': node.name,
'description': node.description,
'color_code': node.color_code,
'x': user_node.x_position,
'y': user_node.y_position,
'scale': user_node.scale,
'thought_count': len(node.thoughts)
})
# Hole Notizen zu dieser Mindmap
notes = MindmapNote.query.filter_by(
mindmap_id=mindmap_id,
user_id=current_user.id
).all()
notes_data = [{
'id': note.id,
'content': note.content,
'node_id': note.node_id,
'thought_id': note.thought_id,
'color_code': note.color_code,
'created_at': note.created_at.isoformat()
} for note in notes]
return jsonify({
'id': mindmap.id,
'name': mindmap.name,
'description': mindmap.description,
'is_private': mindmap.is_private,
'nodes': nodes,
'notes': notes_data
})
@app.route('/api/mindmap/<int:mindmap_id>/add_node', methods=['POST'])
@login_required
def add_node_to_mindmap(mindmap_id):
"""Fügt einen öffentlichen Knoten zur Benutzer-Mindmap hinzu."""
data = request.json
mindmap = UserMindmap.query.get_or_404(mindmap_id)
# Sicherheitscheck
if mindmap.user_id != current_user.id:
return jsonify({'error': 'Nicht autorisiert'}), 403
node_id = data.get('node_id')
parent_id = data.get('parent_id') # Optional: Elternknoten für die Verknüpfung
x_pos = data.get('x', 0)
y_pos = data.get('y', 0)
# Knoten abrufen
node = MindMapNode.query.get_or_404(node_id)
# Prüfen, ob der Knoten bereits in der Mindmap existiert
existing = UserMindmapNode.query.filter_by(
user_mindmap_id=mindmap_id,
node_id=node_id
).first()
if existing:
# Update Position
existing.x_position = x_pos
existing.y_position = y_pos
else:
# Neuen Knoten hinzufügen
user_node = UserMindmapNode(
user_mindmap_id=mindmap_id,
node_id=node_id,
x_position=x_pos,
y_position=y_pos
)
db.session.add(user_node)
# Wenn ein Elternknoten angegeben wurde, Verbindung erstellen
if parent_id:
# Existenz des Elternknotens in der Mindmap prüfen
parent_node = MindMapNode.query.get(parent_id)
parent_user_node = UserMindmapNode.query.filter_by(
user_mindmap_id=mindmap_id,
node_id=parent_id
).first()
if parent_node and parent_user_node:
# Beziehung zwischen den Knoten erstellen
try:
parent_node.children.append(node)
db.session.flush()
except Exception as e:
print(f"Warnung: Fehler beim Erstellen der Beziehung: {e}")
db.session.commit()
return jsonify({
'success': True,
'node_id': node_id,
'x': x_pos,
'y': y_pos
})
@app.route('/api/mindmap/<int:mindmap_id>/remove_node/<int:node_id>', methods=['DELETE'])
@login_required
def remove_node_from_mindmap(mindmap_id, node_id):
"""Entfernt einen Knoten aus der Benutzer-Mindmap."""
mindmap = UserMindmap.query.get_or_404(mindmap_id)
# Sicherheitscheck
if mindmap.user_id != current_user.id:
return jsonify({'error': 'Nicht autorisiert'}), 403
user_node = UserMindmapNode.query.filter_by(
user_mindmap_id=mindmap_id,
node_id=node_id
).first_or_404()
db.session.delete(user_node)
db.session.commit()
return jsonify({'success': True})
@app.route('/api/mindmap/<int:mindmap_id>/update_node_position', methods=['POST'])
@login_required
def update_node_position(mindmap_id):
"""Aktualisiert die Position eines Knotens in der Benutzer-Mindmap."""
data = request.json
mindmap = UserMindmap.query.get_or_404(mindmap_id)
# Sicherheitscheck
if mindmap.user_id != current_user.id:
return jsonify({'error': 'Nicht autorisiert'}), 403
node_id = data.get('node_id')
x_pos = data.get('x')
y_pos = data.get('y')
scale = data.get('scale')
user_node = UserMindmapNode.query.filter_by(
user_mindmap_id=mindmap_id,
node_id=node_id
).first_or_404()
if x_pos is not None:
user_node.x_position = x_pos
if y_pos is not None:
user_node.y_position = y_pos
if scale is not None:
user_node.scale = scale
db.session.commit()
return jsonify({'success': True})
# Notizen-Funktionalität
@app.route('/api/mindmap/<int:mindmap_id>/notes', methods=['GET'])
@login_required
def get_mindmap_notes(mindmap_id):
"""Liefert alle Notizen zu einer Mindmap."""
mindmap = UserMindmap.query.get_or_404(mindmap_id)
# Sicherheitscheck
if mindmap.user_id != current_user.id:
return jsonify({'error': 'Nicht autorisiert'}), 403
notes = MindmapNote.query.filter_by(
mindmap_id=mindmap_id,
user_id=current_user.id
).all()
return jsonify([{
'id': note.id,
'content': note.content,
'node_id': note.node_id,
'thought_id': note.thought_id,
'color_code': note.color_code,
'created_at': note.created_at.isoformat(),
'last_modified': note.last_modified.isoformat()
} for note in notes])
@app.route('/api/mindmap/<int:mindmap_id>/notes', methods=['POST'])
@login_required
def add_mindmap_note(mindmap_id):
"""Fügt eine neue Notiz zur Mindmap hinzu."""
data = request.json
mindmap = UserMindmap.query.get_or_404(mindmap_id)
# Sicherheitscheck
if mindmap.user_id != current_user.id:
return jsonify({'error': 'Nicht autorisiert'}), 403
content = data.get('content')
node_id = data.get('node_id')
thought_id = data.get('thought_id')
color_code = data.get('color_code', "#FFF59D") # Gelber Standard
note = MindmapNote(
user_id=current_user.id,
mindmap_id=mindmap_id,
node_id=node_id,
thought_id=thought_id,
content=content,
color_code=color_code
)
db.session.add(note)
db.session.commit()
return jsonify({
'id': note.id,
'content': note.content,
'node_id': note.node_id,
'thought_id': note.thought_id,
'color_code': note.color_code,
'created_at': note.created_at.isoformat(),
'last_modified': note.last_modified.isoformat()
})
@app.route('/api/notes/<int:note_id>', methods=['PUT'])
@login_required
def update_note(note_id):
"""Aktualisiert eine bestehende Notiz."""
data = request.json
note = MindmapNote.query.get_or_404(note_id)
# Sicherheitscheck
if note.user_id != current_user.id:
return jsonify({'error': 'Nicht autorisiert'}), 403
content = data.get('content')
color_code = data.get('color_code')
if content:
note.content = content
if color_code:
note.color_code = color_code
note.last_modified = datetime.now(timezone.utc)
db.session.commit()
return jsonify({
'id': note.id,
'content': note.content,
'color_code': note.color_code,
'last_modified': note.last_modified.isoformat()
})
@app.route('/api/notes/<int:note_id>', methods=['DELETE'])
@login_required
def delete_note(note_id):
"""Löscht eine Notiz."""
note = MindmapNote.query.get_or_404(note_id)
# Sicherheitscheck
if note.user_id != current_user.id:
return jsonify({'error': 'Nicht autorisiert'}), 403
db.session.delete(note)
db.session.commit()
return jsonify({'success': True})
# API routes for mindmap and thoughts
@app.route('/api/mindmap')
def get_mindmap():
"""Gibt die Mindmap-Struktur zurück"""
try:
# Hauptknoten abrufen (Root-Knoten)
wissen_node = MindMapNode.query.filter_by(name="Wissen").first()
if not wissen_node:
# Wissen-Knoten erstellen, falls nicht vorhanden
wissen_node = MindMapNode(
name="Wissen",
description="Zentrale Wissensbasis",
color_code="#4299E1",
is_public=True
)
db.session.add(wissen_node)
db.session.commit()
# Alle anderen aktiven Knoten holen
nodes = MindMapNode.query.filter(MindMapNode.is_public == True).all()
# Ergebnisdaten vorbereiten
nodes_data = []
edges_data = []
# Knoten hinzufügen
for node in nodes:
nodes_data.append({
'id': node.id,
'name': node.name,
'description': node.description or '',
'color_code': node.color_code or '#9F7AEA'
})
# Wenn es nicht der Wissen-Knoten ist, Verbindung zum Wissen-Knoten hinzufügen
if node.id != wissen_node.id:
edges_data.append({
'source': wissen_node.id,
'target': node.id
})
# Beziehungen zwischen Knoten abfragen und hinzufügen
relationships = db.session.query(node_relationship).all()
for rel in relationships:
if rel.parent_id != wissen_node.id: # Doppelte Kanten vermeiden
edges_data.append({
'source': rel.parent_id,
'target': rel.child_id
})
return jsonify({
'nodes': nodes_data,
'edges': edges_data
})
except Exception as e:
print(f"Fehler beim Abrufen der Mindmap: {str(e)}")
return jsonify({
'success': False,
'error': 'Mindmap konnte nicht geladen werden'
}), 500
@app.route('/api/nodes/<int:node_id>/thoughts')
def get_node_thoughts(node_id):
"""Liefert alle Gedanken, die mit einem Knoten verknüpft sind."""
node = MindMapNode.query.get_or_404(node_id)
thoughts = []
for thought in node.thoughts:
author = thought.author
thoughts.append({
'id': thought.id,
'title': thought.title,
'abstract': thought.abstract,
'content': thought.content[:200] + '...' if len(thought.content) > 200 else thought.content,
'keywords': thought.keywords,
'author': {
'id': author.id,
'username': author.username
},
'created_at': thought.created_at.isoformat(),
'color_code': thought.color_code,
'avg_rating': thought.average_rating,
'bookmarked': current_user.is_authenticated and thought in current_user.bookmarked_thoughts
})
return jsonify(thoughts)
@app.route('/api/nodes/<int:node_id>/thoughts', methods=['POST'])
@login_required
def add_node_thought(node_id):
"""Fügt einen neuen Gedanken zu einem Knoten hinzu."""
data = request.json
node = MindMapNode.query.get_or_404(node_id)
title = data.get('title')
content = data.get('content')
abstract = data.get('abstract', '')
keywords = data.get('keywords', '')
color_code = data.get('color_code', '#B39DDB') # Standard-Lila
# Kategorie des Knotens bestimmen
category_name = node.category.name if node.category else "Allgemein"
thought = Thought(
title=title,
content=content,
abstract=abstract,
keywords=keywords,
color_code=color_code,
branch=category_name,
user_id=current_user.id,
source_type='User Input'
)
node.thoughts.append(thought)
db.session.add(thought)
db.session.commit()
return jsonify({
'id': thought.id,
'title': thought.title,
'success': True
})
@app.route('/api/thoughts/<int:thought_id>', methods=['GET'])
def get_thought(thought_id):
"""Liefert Details zu einem Gedanken."""
thought = Thought.query.get_or_404(thought_id)
author = thought.author
is_bookmarked = False
if current_user.is_authenticated:
is_bookmarked = thought in current_user.bookmarked_thoughts
# Verknüpfte Knoten abrufen
nodes = [{
'id': node.id,
'name': node.name,
'category': node.category.name if node.category else None
} for node in thought.nodes]
return jsonify({
'id': thought.id,
'title': thought.title,
'content': thought.content,
'abstract': thought.abstract,
'keywords': thought.keywords,
'branch': thought.branch,
'color_code': thought.color_code,
'created_at': thought.created_at.isoformat(),
'author': {
'id': author.id,
'username': author.username,
'avatar': author.avatar
},
'avg_rating': thought.average_rating,
'bookmarked': is_bookmarked,
'nodes': nodes,
'source_type': thought.source_type
})
@app.route('/api/thoughts', methods=['POST'])
@login_required
def add_thought():
"""Erstellt einen neuen Gedanken."""
data = request.json
title = data.get('title')
content = data.get('content')
abstract = data.get('abstract', '')
keywords = data.get('keywords', '')
branch = data.get('branch', 'Allgemein')
color_code = data.get('color_code', '#B39DDB')
thought = Thought(
title=title,
content=content,
abstract=abstract,
keywords=keywords,
branch=branch,
color_code=color_code,
user_id=current_user.id,
source_type='User Input'
)
# Knoten-IDs, falls vorhanden
node_ids = data.get('node_ids', [])
for node_id in node_ids:
node = MindMapNode.query.get(node_id)
if node:
thought.nodes.append(node)
db.session.add(thought)
db.session.commit()
return jsonify({
'id': thought.id,
'title': thought.title,
'success': True
})
@app.route('/api/thoughts/<int:thought_id>', methods=['PUT'])
@login_required
def update_thought(thought_id):
"""Aktualisiert einen bestehenden Gedanken."""
thought = Thought.query.get_or_404(thought_id)
# Sicherheitscheck
if thought.user_id != current_user.id and not current_user.is_admin:
return jsonify({'error': 'Nicht autorisiert'}), 403
data = request.json
# Aktualisiere Felder, die gesendet wurden
if 'title' in data:
thought.title = data['title']
if 'content' in data:
thought.content = data['content']
if 'abstract' in data:
thought.abstract = data['abstract']
if 'keywords' in data:
thought.keywords = data['keywords']
if 'color_code' in data:
thought.color_code = data['color_code']
thought.last_modified = datetime.now(timezone.utc)
db.session.commit()
return jsonify({
'id': thought.id,
'success': True
})
@app.route('/api/thoughts/<int:thought_id>', methods=['DELETE'])
@login_required
def delete_thought(thought_id):
"""Löscht einen Gedanken."""
thought = Thought.query.get_or_404(thought_id)
# Sicherheitscheck
if thought.user_id != current_user.id and not current_user.is_admin:
return jsonify({'error': 'Nicht autorisiert'}), 403
db.session.delete(thought)
db.session.commit()
return jsonify({'success': True})
@app.route('/api/thoughts/<int:thought_id>/bookmark', methods=['POST'])
@login_required
def bookmark_thought(thought_id):
"""Fügt einen Gedanken zu den Bookmarks des Benutzers hinzu oder entfernt ihn."""
thought = Thought.query.get_or_404(thought_id)
# Toggle Bookmark-Status
if thought in current_user.bookmarked_thoughts:
current_user.bookmarked_thoughts.remove(thought)
action = 'removed'
else:
current_user.bookmarked_thoughts.append(thought)
action = 'added'
db.session.commit()
return jsonify({
'success': True,
'action': action
})
@app.route('/api/categories')
def get_categories():
"""API-Endpunkt, der alle Kategorien als hierarchische Struktur zurückgibt"""
try:
# Hole alle Kategorien der obersten Ebene
categories = Category.query.filter_by(parent_id=None).all()
# Transformiere zu einer Baumstruktur
category_tree = [build_category_tree(cat) for cat in categories]
return jsonify(category_tree)
except Exception as e:
print(f"Fehler beim Abrufen der Kategorien: {str(e)}")
return jsonify({
'success': False,
'error': 'Kategorien konnten nicht geladen werden'
}), 500
@app.route('/api/set_dark_mode', methods=['POST'])
def set_dark_mode():
"""Speichert die Dark Mode-Einstellung in der Session."""
data = request.json
dark_mode = data.get('darkMode', False)
session['dark_mode'] = 'true' if dark_mode else 'false'
session.permanent = True
return jsonify({
'success': True,
'darkMode': dark_mode
})
@app.route('/api/get_dark_mode', methods=['GET'])
def get_dark_mode():
"""Liefert die aktuelle Dark Mode-Einstellung."""
dark_mode = session.get('dark_mode', 'true') # Standard: Dark Mode aktiviert
return jsonify({
'success': True,
'darkMode': dark_mode
})
# Fehlerhandler
@app.errorhandler(404)
def page_not_found(e):
return render_template('errors/404.html'), 404
@app.errorhandler(403)
def forbidden(e):
return render_template('errors/403.html'), 403
@app.errorhandler(500)
def internal_server_error(e):
return render_template('errors/500.html'), 500
@app.errorhandler(429)
def too_many_requests(e):
return render_template('errors/429.html'), 429
# OpenAI-Integration für KI-Assistenz
@app.route('/api/assistant', methods=['POST'])
def chat_with_assistant():
"""Chat mit dem KI-Assistenten"""
data = request.json
user_message = data.get('message', '')
# Überprüfe, ob die OpenAI-API verfügbar ist
if not openai_available or client is None:
# Fallback-Antwort, wenn OpenAI nicht verfügbar ist
fallback_message = {
"response": "Der KI-Assistent ist derzeit nicht verfügbar. Bitte versuchen Sie es später erneut oder kontaktieren Sie den Administrator.",
"thoughts": "Leider konnte keine Verbindung zur OpenAI-API hergestellt werden. Dies kann an SSL-Zertifikatsproblemen, Netzwerkproblemen oder API-Schlüsselproblemen liegen."
}
return jsonify(fallback_message)
# Versuche, eine Antwort von OpenAI zu erhalten
try:
# Check, ob es eine Datenbankanfrage ist
is_db_query, db_query_result = check_database_query(user_message)
if is_db_query:
return jsonify({
"response": db_query_result,
"thoughts": "Ihre Anfrage wurde als Datenbankanfrage erkannt und direkt beantwortet."
})
system_message = """Du bist SysTades, ein intelligenter Assistent in einer Wissensmanagement-Anwendung.
Deine Aufgabe ist es, tiefe, reflektierte Antworten auf Fragen der Benutzer zu geben.
Beziehe dich auf vorhandene Konzepte und betrachte Fragen stets aus mehreren Perspektiven.
Vermeide pauschale Antworten und beziehe immer verschiedene Denkansätze ein.
Antworte stets auf Deutsch und in einem nachdenklichen, philosophischen Ton.
"""
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": system_message},
{"role": "user", "content": user_message}
],
temperature=0.7,
timeout=60 # Erhöht auf 60 Sekunden für bessere Zuverlässigkeit
)
# Extrahiere die Antwort
assistant_response = response.choices[0].message.content
# Generiere zusätzliche "Gedanken" für verbesserte UX
thoughts_response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "Gib kurze Einblicke in deine Gedankenprozesse zu dieser Frage. Schreibe 1-3 Sätze zur Reflexion über die Frage und die wichtigsten Aspekte deiner Antwort. Bleibe dabei informell und persönlich. Schreibe auf Deutsch."},
{"role": "user", "content": user_message},
{"role": "assistant", "content": assistant_response}
],
temperature=0.7,
max_tokens=150
)
thoughts = thoughts_response.choices[0].message.content
return jsonify({
"response": assistant_response,
"thoughts": thoughts
})
except Exception as e:
print(f"Fehler bei der OpenAI-Anfrage: {e}")
import traceback
print(f"Stack Trace: {traceback.format_exc()}")
# Fallback-Antwort bei Fehler
fallback_message = {
"response": "Es tut mir leid, aber ich konnte Ihre Anfrage nicht verarbeiten. Bitte versuchen Sie es später erneut.",
"thoughts": "Ein technisches Problem ist aufgetreten. Dies könnte an Netzwerkproblemen, API-Grenzen oder Serverauslastung liegen."
}
return jsonify(fallback_message)
def check_database_query(user_message):
"""
Prüft, ob die Benutzeranfrage nach Datenbankinformationen sucht
und gibt relevante Informationen aus der Datenbank zurück.
Returns:
(bool, str): Tupel aus (ist_db_anfrage, db_antwort)
"""
user_message = user_message.lower()
# Schlüsselwörter, die auf eine Datenbankanfrage hindeuten
db_keywords = [
'wie viele', 'wieviele', 'anzahl', 'liste', 'zeige', 'zeig mir', 'datenbank',
'statistik', 'stats', 'benutzer', 'gedanken', 'kategorien', 'mindmap',
'neueste', 'kürzlich', 'gespeichert', 'zähle', 'suche nach', 'finde'
]
# Überprüfen, ob die Nachricht eine Datenbankanfrage sein könnte
is_db_query = any(keyword in user_message for keyword in db_keywords)
if not is_db_query:
return False, ""
# Datenbank-Antwort vorbereiten
response = "Hier sind Informationen aus der Datenbank:\n\n"
try:
# 1. Benutzer-Statistiken
if any(kw in user_message for kw in ['benutzer', 'user', 'nutzer']):
user_count = User.query.count()
active_users = User.query.filter_by(is_active=True).count()
admins = User.query.filter_by(role='admin').count()
response += f"**Benutzer-Statistiken:**\n"
response += f"- Gesamt: {user_count} Benutzer\n"
response += f"- Aktiv: {active_users} Benutzer\n"
response += f"- Administratoren: {admins} Benutzer\n\n"
# 2. Gedanken-Statistiken
if any(kw in user_message for kw in ['gedanken', 'thought', 'inhalt']):
thought_count = Thought.query.count()
if thought_count > 0:
avg_rating = db.session.query(func.avg(ThoughtRating.relevance_score)).scalar() or 0
avg_rating = round(avg_rating, 1)
recent_thoughts = Thought.query.order_by(Thought.created_at.desc()).limit(5).all()
response += f"**Gedanken-Statistiken:**\n"
response += f"- Gesamt: {thought_count} Gedanken\n"
response += f"- Durchschnittliche Bewertung: {avg_rating}/5\n\n"
if recent_thoughts:
response += "**Neueste Gedanken:**\n"
for thought in recent_thoughts:
response += f"- {thought.title} (von {thought.author.username})\n"
response += "\n"
else:
response += "Es sind noch keine Gedanken in der Datenbank gespeichert.\n\n"
# 3. Kategorien-Statistiken
if any(kw in user_message for kw in ['kategorie', 'category', 'thema']):
category_count = Category.query.filter_by(parent_id=None).count()
subcategory_count = Category.query.filter(Category.parent_id != None).count()
response += f"**Kategorien-Statistiken:**\n"
response += f"- Hauptkategorien: {category_count}\n"
response += f"- Unterkategorien: {subcategory_count}\n\n"
main_categories = Category.query.filter_by(parent_id=None).all()
if main_categories:
response += "**Hauptkategorien:**\n"
for category in main_categories:
subcats = Category.query.filter_by(parent_id=category.id).count()
response += f"- {category.name} ({subcats} Unterkategorien)\n"
response += "\n"
# 4. Mindmap-Statistiken
if any(kw in user_message for kw in ['mindmap', 'karte', 'map', 'knoten']):
public_nodes = MindMapNode.query.count()
user_mindmaps = UserMindmap.query.count()
response += f"**Mindmap-Statistiken:**\n"
response += f"- Öffentliche Knoten: {public_nodes}\n"
response += f"- Benutzerdefinierte Mindmaps: {user_mindmaps}\n\n"
if user_mindmaps > 0:
recent_mindmaps = UserMindmap.query.order_by(UserMindmap.created_at.desc()).limit(3).all()
if recent_mindmaps:
response += "**Neueste Mindmaps:**\n"
for mindmap in recent_mindmaps:
response += f"- {mindmap.name} (von {mindmap.user.username})\n"
response += "\n"
# Wenn keine spezifischen Daten angefordert wurden, allgemeine Übersicht geben
if not any(kw in user_message for kw in ['benutzer', 'user', 'gedanken', 'thought', 'kategorie', 'category', 'mindmap']):
users = User.query.count()
thoughts = Thought.query.count()
categories = Category.query.count()
nodes = MindMapNode.query.count()
user_maps = UserMindmap.query.count()
response += f"**Übersicht über die Datenbank:**\n"
response += f"- Benutzer: {users}\n"
response += f"- Gedanken: {thoughts}\n"
response += f"- Kategorien: {categories}\n"
response += f"- Mindmap-Knoten: {nodes}\n"
response += f"- Benutzerdefinierte Mindmaps: {user_maps}\n"
return True, response
except Exception as e:
import traceback
print(f"Fehler bei der Datenbankabfrage: {e}")
print(traceback.format_exc())
return True, "Es ist ein Fehler bei der Abfrage der Datenbank aufgetreten. Bitte versuchen Sie es später erneut."
@app.route('/search')
def search_thoughts_page():
"""Seite zur Gedankensuche anzeigen."""
return render_template('search.html')
@app.route('/my_account')
def my_account():
"""Zeigt die persönliche Merkliste an."""
if not current_user.is_authenticated:
flash('Bitte melde dich an, um auf deine Merkliste zuzugreifen.', 'warning')
return redirect(url_for('login'))
# Hole die Lesezeichen des Benutzers
bookmarked_thoughts = current_user.bookmarked_thoughts
return render_template('my_account.html', bookmarked_thoughts=bookmarked_thoughts)
# Dummy-Route, um 404-Fehler für fehlende Netzwerk-Hintergrundbilder zu vermeiden
@app.route('/static/network-bg.jpg')
@app.route('/static/network-bg.svg')
def dummy_network_bg():
"""Leere Antwort für die nicht mehr verwendeten Netzwerk-Hintergrundbilder."""
return '', 200
# Route zum expliziten Neu-Laden der Umgebungsvariablen
@app.route('/admin/reload-env', methods=['POST'])
@admin_required
def reload_env():
"""Lädt die Umgebungsvariablen aus der .env-Datei neu."""
try:
# Erzwinge das Neuladen der .env-Datei
load_dotenv(override=True, force=True)
# OpenAI API-Key ist bereits fest kodiert
# client wurde bereits mit festem API-Key initialisiert
# Weitere Umgebungsvariablen hier aktualisieren, falls nötig
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', app.config['SECRET_KEY'])
return jsonify({
'success': True,
'message': 'Umgebungsvariablen wurden erfolgreich neu geladen.'
})
except Exception as e:
return jsonify({
'success': False,
'message': f'Fehler beim Neuladen der Umgebungsvariablen: {str(e)}'
}), 500
# Flask starten
if __name__ == '__main__':
with app.app_context():
# Make sure tables exist
db.create_all()
socketio.run(app, debug=True, host='0.0.0.0')
@app.route('/api/refresh-mindmap')
def refresh_mindmap():
"""
API-Endpunkt zum Neuladen der Mindmap-Daten,
wenn die Datenbank-Verbindung vorübergehend fehlgeschlagen ist
"""
try:
# Stelle sicher, dass wir Kategorien haben
if Category.query.count() == 0:
create_default_categories()
# Überprüfe, ob wir bereits einen "Wissen"-Knoten haben
wissen_node = MindMapNode.query.filter_by(name="Wissen").first()
# Wenn kein "Wissen"-Knoten existiert, erstelle ihn
if not wissen_node:
wissen_node = MindMapNode(
name="Wissen",
description="Zentrale Wissensbasis",
color_code="#4299E1",
is_public=True
)
db.session.add(wissen_node)
db.session.commit()
# Hole alle Kategorien und Knoten
categories = Category.query.filter_by(parent_id=None).all()
category_tree = [build_category_tree(cat) for cat in categories]
# Hole alle Mindmap-Knoten außer dem "Wissen"-Knoten
nodes = MindMapNode.query.filter(MindMapNode.id != wissen_node.id).all()
# Vorbereiten der Node- und Edge-Arrays für die Antwort
node_data = []
edge_data = []
# Zuerst den "Wissen"-Knoten hinzufügen
node_data.append({
'id': wissen_node.id,
'name': wissen_node.name,
'description': wissen_node.description or '',
'color_code': wissen_node.color_code or '#4299E1',
'thought_count': len(wissen_node.thoughts),
'category_id': wissen_node.category_id
})
# Dann die anderen Knoten
for node in nodes:
node_obj = {
'id': node.id,
'name': node.name,
'description': node.description or '',
'color_code': node.color_code or '#9F7AEA',
'thought_count': len(node.thoughts),
'category_id': node.category_id
}
# Verbinde alle Top-Level-Knoten mit dem Wissen-Knoten
if not node.parents.all():
edge_data.append({
'source': wissen_node.id,
'target': node.id
})
# Verbindungen zwischen vorhandenen Knoten hinzufügen
node_children = node.children.all()
for child in node_children:
edge_data.append({
'source': node.id,
'target': child.id
})
node_data.append(node_obj)
return jsonify({
'success': True,
'categories': category_tree,
'nodes': node_data,
'edges': edge_data
})
except Exception as e:
print(f"Fehler beim Neuladen der Mindmap: {str(e)}")
return jsonify({
'success': False,
'error': 'Datenbankverbindung konnte nicht hergestellt werden'
}), 500
# Route zur Mindmap HTML-Seite
@app.route('/mindmap')
def mindmap_page():
return render_template('mindmap.html')
# Weiterleitung für Community/Forum-Routen
@app.route('/community')
@app.route('/Community')
@app.route('/forum')
@app.route('/Forum')
@app.route('/community_forum')
def redirect_community():
"""Leitet alle Community/Forum-URLs zur Startseite um"""
return redirect(url_for('index'))
@app.route('/static/js/mindmap-init.js')
def serve_mindmap_init_js():
"""Bedient die Mindmap-Initialisierungsdatei."""
return app.send_static_file('js/mindmap-init.js'), 200, {'Content-Type': 'application/javascript'}
# Datenbank-Update-Route (admin-geschützt)
@app.route('/admin/update-database', methods=['GET', 'POST'])
@admin_required
def admin_update_database():
"""Admin-Route zum Aktualisieren der Datenbank"""
message = None
success = None
if request.method == 'POST':
try:
import utils.update_db as update_db
update_success = update_db.update_user_table()
if update_success:
message = "Die Datenbank wurde erfolgreich aktualisiert."
success = True
else:
message = "Es gab ein Problem bei der Aktualisierung der Datenbank."
success = False
except Exception as e:
message = f"Fehler: {str(e)}"
success = False
return render_template('admin/update_database.html', message=message, success=success)
@app.route('/api/mindmap/<node_id>')
def get_mindmap_node(node_id):
"""Liefert die Mindmap-Daten für einen bestimmten Knoten und seine Subthemen."""
try:
if node_id == 'root':
# Hauptknoten (Wissen) abrufen
wissen_node = MindMapNode.query.filter_by(name="Wissen").first()
if not wissen_node:
wissen_node = MindMapNode(
name="Wissen",
description="Zentrale Wissensbasis",
color_code="#4299E1",
is_public=True
)
db.session.add(wissen_node)
db.session.commit()
# Alle direkten Kinder des Wissen-Knotens holen
nodes = wissen_node.children.all()
else:
# Bestimmten Knoten und seine Kinder abrufen
parent_node = MindMapNode.query.get_or_404(node_id)
nodes = parent_node.children.all()
wissen_node = parent_node
# Ergebnisdaten vorbereiten
nodes_data = []
edges_data = []
# Hauptknoten hinzufügen
nodes_data.append({
'id': wissen_node.id,
'name': wissen_node.name,
'description': wissen_node.description or '',
'color_code': wissen_node.color_code or '#4299E1',
'is_center': True,
'has_children': len(nodes) > 0
})
# Kinder hinzufügen
for node in nodes:
nodes_data.append({
'id': node.id,
'name': node.name,
'description': node.description or '',
'color_code': node.color_code or '#9F7AEA',
'is_center': False,
'has_children': len(node.children.all()) > 0
})
# Verbindung zum Elternknoten hinzufügen
edges_data.append({
'source_id': wissen_node.id,
'target_id': node.id,
'strength': 0.8
})
return jsonify({
'nodes': nodes_data,
'edges': edges_data
})
except Exception as e:
print(f"Fehler beim Abrufen der Mindmap-Daten: {str(e)}")
return jsonify({
'success': False,
'error': 'Mindmap-Daten konnten nicht geladen werden'
}), 500
# API-Endpunkte für Notizen und Layout-Speicherung
@app.route('/api/mindmap/node/<int:node_id>/notes', methods=['GET'])
@login_required
def get_node_notes(node_id):
"""Ruft die Notizen für einen Mindmap-Knoten ab"""
try:
# Prüfen, ob der Knoten existiert
node = MindMapNode.query.get_or_404(node_id)
# Prüfen, ob der Knoten in einer Mindmap des Benutzers ist
user_node = UserMindmapNode.query.filter_by(node_id=node_id).join(
UserMindmap, UserMindmapNode.user_mindmap_id == UserMindmap.id
).filter(UserMindmap.user_id == current_user.id).first()
if not user_node and not current_user.is_admin:
return jsonify({'success': False, 'message': 'Keine Berechtigung'}), 403
# Prüfen, ob eine Notiz für diesen Knoten existiert
note = MindmapNote.query.filter_by(
user_id=current_user.id,
node_id=node_id
).first()
if note:
return jsonify({
'success': True,
'notes': note.content,
'color_code': note.color_code
})
else:
return jsonify({
'success': True,
'notes': '',
'color_code': '#FFF59D' # Standard-Gelb
})
except Exception as e:
print(f"Fehler in get_node_notes: {str(e)}")
return jsonify({'success': False, 'message': str(e)}), 500
@app.route('/api/mindmap/node/<int:node_id>/notes', methods=['POST'])
@login_required
def update_node_notes(node_id):
"""Aktualisiert die Notizen für einen Mindmap-Knoten"""
try:
# Prüfen, ob der Knoten existiert
node = MindMapNode.query.get_or_404(node_id)
# Prüfen, ob der Knoten in einer Mindmap des Benutzers ist
user_node = UserMindmapNode.query.filter_by(node_id=node_id).join(
UserMindmap, UserMindmapNode.user_mindmap_id == UserMindmap.id
).filter(UserMindmap.user_id == current_user.id).first()
if not user_node and not current_user.is_admin:
return jsonify({'success': False, 'message': 'Keine Berechtigung'}), 403
# Daten aus dem Request abrufen
data = request.json
notes_content = data.get('notes', '')
color_code = data.get('color_code', '#FFF59D') # Standard-Gelb
# Prüfen, ob bereits eine Notiz für diesen Knoten existiert
note = MindmapNote.query.filter_by(
user_id=current_user.id,
node_id=node_id
).first()
if note:
# Vorhandene Notiz aktualisieren
note.content = notes_content
note.color_code = color_code
else:
# Neue Notiz erstellen - hier brauchen wir die Mindmap-ID
mindmap_id = user_node.user_mindmap_id
note = MindmapNote(
user_id=current_user.id,
mindmap_id=mindmap_id,
node_id=node_id,
content=notes_content,
color_code=color_code
)
db.session.add(note)
db.session.commit()
return jsonify({
'success': True,
'message': 'Notizen erfolgreich gespeichert'
})
except Exception as e:
db.session.rollback()
print(f"Fehler in update_node_notes: {str(e)}")
return jsonify({'success': False, 'message': str(e)}), 500
@app.route('/api/mindmap/<int:mindmap_id>/layout', methods=['POST'])
@login_required
def save_mindmap_layout(mindmap_id):
"""Speichert das Layout (Positionen der Knoten) einer Mindmap"""
try:
# Prüfe, ob die Mindmap dem Benutzer gehört
mindmap = UserMindmap.query.get_or_404(mindmap_id)
if mindmap.user_id != current_user.id and not current_user.is_admin:
return jsonify({'success': False, 'message': 'Keine Berechtigung'}), 403
# Daten aus dem Request abrufen
data = request.json
nodes_data = data.get('nodes', [])
# Positionen aller Knoten aktualisieren
for node_data in nodes_data:
node_id = node_data.get('id')
x_pos = node_data.get('x')
y_pos = node_data.get('y')
if node_id and x_pos is not None and y_pos is not None:
# UserMindmapNode-Eintrag aktualisieren
user_node = UserMindmapNode.query.filter_by(
user_mindmap_id=mindmap_id,
node_id=node_id
).first()
if user_node:
user_node.x_position = x_pos
user_node.y_position = y_pos
db.session.commit()
return jsonify({
'success': True,
'message': 'Layout erfolgreich gespeichert'
})
except Exception as e:
db.session.rollback()
print(f"Fehler in save_mindmap_layout: {str(e)}")
return jsonify({'success': False, 'message': str(e)}), 500
@app.route('/api/public-mindmap', methods=['GET'])
def get_public_mindmap():
"""Liefert die öffentliche Mindmap für die Knotenübernahme"""
try:
# Alle öffentlichen Knoten abrufen
public_nodes = MindMapNode.query.filter_by(is_public=True).all()
# Knoten formatieren
nodes = []
for node in public_nodes:
nodes.append({
'id': node.id,
'name': node.name,
'description': node.description or '',
'color_code': node.color_code or '#9F7AEA'
})
# Alle Beziehungen zwischen Knoten abrufen
edges = []
for node in public_nodes:
for child in node.children:
if child.is_public:
edges.append({
'source': node.id,
'target': child.id
})
return jsonify({
'success': True,
'nodes': nodes,
'edges': edges
})
except Exception as e:
print(f"Fehler in get_public_mindmap: {str(e)}")
return jsonify({'success': False, 'message': str(e)}), 500
# Automatische Datenbankinitialisierung - Aktualisiert für Flask 2.2+ Kompatibilität
def initialize_app():
"""Initialisierung der Anwendung"""
print("Initialisierung der Anwendung...")
with app.app_context():
# Prüfen, ob die Datenbank existiert und initialisiert ist
try:
# Prüfen, ob Tabellen existieren
db.create_all()
# Prüfen, ob Stammdaten vorhanden sind
if User.query.count() == 0:
print("Erstelle Standardbenutzer...")
create_default_users()
# Wir nutzen die initialize_database Funktion für Kategorien und Mindmap
# Diese Funktionalität ist bereits dort implementiert
initialize_database()
print("Datenbank wurde erfolgreich initialisiert.")
except Exception as e:
import traceback
print(f"Fehler bei der Datenbankinitialisierung: {e}")
print(traceback.format_exc())
# Moderne Methode für die Datenbankinitialisierung in Flask 2.2+
with app.app_context():
initialize_app()