Compare commits

..

69 Commits

Author SHA1 Message Date
a7bb9563b3 hintergrund Fixes 2025-04-29 20:49:43 +01:00
4e0c470663 Update compiled Python files in __pycache__ for utils module 2025-04-29 20:38:00 +01:00
b5300f74bd Update compiled Python files in __pycache__ for models and utils modules 2025-04-29 20:35:23 +01:00
6a53e621ca ''Über Uns'' 2025-04-29 20:31:02 +01:00
a59ce652af Impressum 2025-04-29 19:26:35 +01:00
27cfc95081 Merge branch 'tills-branch' of https://git.clickcandit.com/marwinm/website into tills-branch 2025-04-29 20:24:41 +02:00
c513666391 Refactor nodeCount to config object" Explanation: This commit message follows the Conventional C... 2025-04-29 20:11:43 +02:00
ae30dbce57 Flask Fix JAHAH 2025-04-29 19:08:49 +01:00
817ddd98e9 chore: Änderungen commited 2025-04-29 20:08:12 +02:00
bfce2fc7b7 chore: Änderungen commited 2025-04-29 20:06:29 +02:00
efbcd567ee chore: Änderungen commited 2025-04-29 20:04:32 +02:00
a873765d08 feat: Implement smooth animation with adjustable pulse and flow speeds 2025-04-29 20:02:51 +02:00
efbcadb95a chore: Änderungen commited 2025-04-29 20:00:58 +02:00
da3ccaffe9 Refactor nodeCount configuration in NeuralNetworkBackground.js 2025-04-29 19:59:09 +02:00
f4e04573bd chore: Änderungen commited 2025-04-29 19:57:44 +02:00
aa253f3871 chore: Änderungen commited 2025-04-29 19:27:29 +02:00
cfd6a25b21 📝 Commit Message: "Refactor app module binary files using Conventional Commits (feat)" Explanat... 2025-04-29 19:25:18 +02:00
d307763007 Aktualisiere die Konfiguration der neuronalen Netzwerk-Hintergrundanimation in neural-network-background.js, um die Anzahl der Knoten, Variationen, Geschwindigkeiten und Sichtbarkeiten anzupassen. Füge neue Parameter für aktive Flows und Flussweiterleitung hinzu, um die Animation dynamischer zu gestalten. Optimiere die Logik zur Verwaltung aktiver Flows und verbessere die Sichtbarkeit von Verbindungen für ein flüssigeres Benutzererlebnis. 2025-04-29 15:19:33 +02:00
d7e6912e08 Aktualisiere die Umgebungsvariablen in .env mit neuen Werten für SECRET_KEY und OPENAI_API_KEY. Entferne die alte Instanz von systades.db und aktualisiere die neuronale Netzwerk-Hintergrundanimation in neural-network-background.js mit verbesserten visuellen Effekten, einschließlich einer neuen Zickzack-Bewegung für Blitze und subtileren Glüheffekten. Implementiere eine sanftere Ausblendanimation für das Canvas-Element und verbessere die Funkenanimationen. Optimiere die Cluster-Generierung und -Verteilung für eine flüssigere Benutzererfahrung. 2025-04-29 14:58:54 +02:00
ffe96074f4 Verbessere die Funktionalität des neuronalen Netzwerk-Hintergrunds in neural-network-background.js durch die Einführung einer flexiblen Konfigurationsstruktur, die benutzerdefinierte Optionen unterstützt. Optimiere die Cluster-Generierung und -Verteilung, verbessere die Verbindungslogik zwischen Knoten und implementiere erweiterte Fehlerbehandlung in den API-Funktionen in mindmap.js. Füge Fallback-Knoten hinzu, um die Benutzererfahrung zu verbessern, und implementiere visuelle Rückmeldungen für die Initialisierung der Mindmap. 2025-04-29 12:32:18 +02:00
49ccf3908a Merge remote-tracking branch 'origin/tills-branch' into tills-branch 2025-04-29 10:28:36 +02:00
9514645904 keine ahnung ehrlich 2025-04-29 10:28:15 +02:00
63f45abb3e background, mindmap 2025-04-29 10:27:07 +02:00
7d74b5a7bf Implement Flask-Migrate for database migrations in app.py, disable CSRF protection, and update requirements.txt to include Flask-Migrate. Remove obsolete systades.db file and add migration configuration files for Alembic. 2025-04-28 21:41:38 +02:00
55f2553780 Update ROADMAP.md, remove CORS & flask-cors from app.py, and update requirements.txt: no longer use CORS for Flask-SocketIO. 2025-04-28 21:24:11 +02:00
0852ea070b 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. 2025-04-28 15:21:11 +02:00
7a0533ac09 Verbessere die Funktionalität des Chat-Assistenten in app.py: Aktualisiere die Systemnachricht mit spezifischen Informationen zur Systades-Wissensdatenbank und erweitere die API-Nachrichtenformatierung. Füge Unterstützung für ausgewählte Elemente aus der Datenbank hinzu und erhöhe die maximale Tokenanzahl für detailliertere Antworten. Implementiere eine neue JavaScript-Datei für eine neuronale Netzwerk-Hintergrundanimation und verbessere die CSS-Stile für den Light Mode. Optimiere die Benutzeroberfläche und die Lesbarkeit in beiden Modi. Aktualisiere die Grundstile für eine konsistente Darstellung. 2025-04-28 14:49:02 +02:00
65c44ab371 Refactor node relationship handling in app.py and introduce new routes for thoughts association with nodes. 2025-04-28 13:20:41 +02:00
5399169b11 jo 2025-04-27 21:22:28 +01:00
05f6f149ad Update ROADMAP.md: Mark completion of Phases 1, 2, and 3 with detailed task lists, introduce Phase 4 for user-defined mind maps, and outline future tasks including tagging and source management. Enhance visual design and UX elements, and update current improvements and future tasks for Q3 2024. 2025-04-27 18:19:43 +02:00
12ed413c04 Update README.md: Enhance error handling for robust data processing and node references, improve connection detection between nodes, and update current status to reflect 75% completion of Phase 3. Document critical bug fixes in node visualization and API data format handling. 2025-04-27 18:18:34 +02:00
ccf5a1d678 Update README.md: Add WebGL support for animated background effects, complete Phase 2 design tasks, and enhance MindMap features with improved clustering and visualization. Update current status and next steps to reflect recent progress. 2025-04-27 18:17:52 +02:00
eb23d638e6 Merge remote-tracking branch 'origin/tills-branch' into tills-branch 2025-04-27 18:16:30 +02:00
750dba2d6f Update requirements.txt: Remove specific version for openai package to allow for flexibility in dependency management. 2025-04-27 18:10:41 +01:00
6aa3780a96 Enhance MindMapVisualization functionality: Implement connection count updates for nodes upon data processing and fallback scenarios. Refactor connected node retrieval methods to improve safety and reliability by ensuring valid node IDs. Introduce a new method for checking links between nodes, enhancing overall robustness of the mind map visualization logic. 2025-04-27 18:16:09 +02:00
8890a62026 Merge remote-tracking branch 'origin/tills-branch' into tills-branch 2025-04-27 17:57:40 +02:00
6cf9b2a627 Update compiled Python files in __pycache__: Refresh app.cpython-313.pyc and models.cpython-313.pyc to reflect recent code changes and optimizations. 2025-04-27 17:14:32 +01:00
4f6aea8e20 Update neural network background animation: Enhance color visibility and node dynamics for improved visual effects. Introduce clustering for node positioning and optimize connection rendering with increased opacity and strength. Remove old database file and update systades.db for better data management. 2025-04-27 17:57:04 +02:00
e5f485d9d7 Update COMMON_ERRORS.md: Add a new entry for common error 'D' and maintain clarity in the documentation of frequent issues and their solutions. 2025-04-27 17:15:54 +02:00
cf3fc09a63 Merge remote-tracking branch 'origin/tills-branch' into tills-branch 2025-04-27 17:15:47 +02:00
10747a8336 Add new database file: Create systades.db to support application data storage and management. 2025-04-27 17:00:27 +01:00
7eb958f3c8 Update app.py and COMMON_ERRORS.md for improved clarity and functionality: Correct the comment in app.py from "Kontext-Prozessor" to "Context-Prozessor" for better understanding. Enhance COMMON_ERRORS.md by adding new common errors and solutions related to TypeScript usage, OAuth implementation, and neural network background animation issues. Update mindmap page scripts to ensure proper global availability of functions and improve error handling for user notifications. Adjust template references for Tailwind CSS and Alpine.js to support both CDN and local versions, ensuring better resource loading and compatibility. 2025-04-27 17:14:48 +02:00
4a3092a4d2 Update OpenAI API key and enhance app functionality: Replace the OpenAI API key in the .env file for improved access. Refactor app.py to include error handling for missing API keys and implement dark mode functionality with session management. Update README.md to reflect the use of Tailwind CSS via CDN and document the Content Security Policy (CSP) adjustments. Enhance mindmap data loading with a new API endpoint for refreshing data, ensuring better user experience during database connection issues. Update styles and templates for improved UI consistency and responsiveness. 2025-04-27 16:56:16 +02:00
2d8cdc052f Refactor chat interface in index.html and main.js: Improve user interaction by optimizing chat message rendering and enhancing initialization logic for the ChatGPT assistant. Update styles for better responsiveness and visual appeal, ensuring seamless integration with existing functionalities. 2025-04-27 15:12:52 +02:00
968515ce2b Overhaul website to modernize design, integrate SVG visualizations, and enhance KI functionality; update documentation for MindMapProjekt. 2025-04-27 15:09:29 +02:00
88f8e98df0 Enhance Neural Network Background Animation: Introduce subtle flowing network aesthetics with improved color schemes and animations. Update canvas handling to ensure proper layering and visibility. Implement new flow animations along connections for a more dynamic visual experience. Adjust node and connection properties for a cleaner, more organic look. Ensure compatibility with dark mode and optimize rendering methods for both WebGL and Canvas. Update styles to maintain transparency across themes. 2025-04-27 12:16:57 +01:00
e5409eef68 Remove unused background scripts and assets: Delete background.js, network-animation.js, network-background.js, and associated media files to streamline the project. Update base.html to reflect changes in script references and ensure proper functionality of the dark mode theme. 2025-04-27 11:51:35 +01:00
013bf76446 Merge branch 'tills-branch' of https://git.clickcandit.com/marwinm/website into tills-branch 2025-04-27 11:15:48 +01:00
808a3c7bbe Update compiled Python files and database: Refresh app and init_db bytecode files, and update mindmap database to reflect recent changes and improvements. 2025-04-27 10:14:44 +01:00
d117978005 Update GPT model to 'gpt-4o-mini' for chat functionality in app.py 2025-04-27 08:03:55 +02:00
48d8463481 Enhance chat interface styling in index.html: Add animations for chat messages and typing indicators, implement smooth scrolling for chat messages, and customize scrollbar appearance. Introduce hover effects for quick query buttons to improve user interaction and visual feedback. 2025-04-27 08:00:53 +02:00
08314ec703 Enhance embedded ChatGPT assistant functionality: Integrate a new chat interface within the index.html template, allowing users to interact with the assistant directly. Update main.js to ensure proper initialization and reference management. Improve user experience with quick query buttons and a responsive chat layout, while maintaining existing functionality in the application. 2025-04-27 08:00:16 +02:00
0bb7d8d0dc Add ChatGPT assistant initialization in main.js: Integrate a new ChatGPTAssistant instance during the MindMap application initialization, ensuring a global reference for enhanced user interaction. This update improves the functionality of the chat feature within the application. 2025-04-27 07:52:23 +02:00
4a28c2c453 Refactor chat_with_assistant function to support messages array input: Enhance the chatbot API by allowing an array of messages for context, extracting system messages, and updating the response format. Maintain backward compatibility with the previous prompt structure while improving error handling for empty inputs. 2025-04-27 07:49:40 +02:00
66d987857a Remove deprecated database management scripts and admin user creation functionality: Delete create_admin.py, fix_db.py, rebuild_db.py, and test_db.py to streamline the project structure and eliminate unused code. Update README.md with installation instructions and management tools for improved user guidance. 2025-04-27 07:46:48 +02:00
d58aba26c2 Refactor OpenAI integration and enhance mindmap UI: Update OpenAI client initialization to use a dedicated class, streamline API key management, and improve loading animations in the mindmap template. Add a modal for adding new thoughts with enhanced user feedback and error handling, while updating the loading overlay for better visual appeal and responsiveness. 2025-04-27 07:43:03 +02:00
8f0a6d4372 Update environment configuration and enhance app functionality: Add detailed comments to the .env file for better clarity, implement a route to reload environment variables dynamically, and ensure the .env file is loaded with force in the app. Remove obsolete build and development scripts to streamline the project structure. Update setup script to create a .env file if it doesn't exist, prompting users to configure necessary values. 2025-04-27 07:28:05 +02:00
5372fe220e Add flash message API and enhance mindmap visualization: Implement a new API endpoint for retrieving flash messages, integrate flash message display in the mindmap visualization, and improve user feedback with dynamic notifications. Update UI elements for better responsiveness and visual appeal, while removing obsolete background image references. 2025-04-27 07:18:32 +02:00
11ab15127c Refactor mindmap visualization and enhance user authentication UI: Implement API calls to load mindmap data dynamically, process hierarchical data into nodes and links, and improve error handling. Update login and registration templates for a modern design with enhanced validation and user experience. Remove obsolete network background images. 2025-04-27 07:08:38 +02:00
0705ecce59 Implement database path configuration and enhance category management: Update database URI to use an absolute path, ensure directory creation for the database, and implement default category creation on initialization. Add new routes for searching thoughts and user account management, while improving the UI with navigation updates for better accessibility. 2025-04-27 07:02:54 +02:00
1c59b0b616 node entfernt 2025-04-27 06:49:59 +02:00
d42c43db50 Enhance footer layout and mindmap functionality: Revamp footer structure with improved grid layout, add social media icons, and implement a newsletter subscription form. Update mindmap template to use SVG background, streamline script loading, and enhance visualization initialization with new event handlers for user interactions. 2025-04-27 06:33:01 +02:00
e46264b201 Fix: network background loading and fallback mechanism: Implement a retry logic for loading the network background image with a maximum of two attempts, first trying the SVG version and then falling back to a JPG if necessary. Add a fallback background drawing function to maintain visual continuity when image loading fails. Update placeholder comment in JPG file to reflect the use of an SVG alternative. 2025-04-27 06:26:10 +02:00
74307ba345 Add user account route and bookmark functionality: Implement '/my-account' route for user bookmarks and personal mindmap, enhance mindmap visualization with bookmark management, and update UI elements for better user experience. 2025-04-26 18:51:53 +01:00
14474c4eab Refactor UI and enhance functionality: Update welcome messages and input placeholders in the chat assistant, implement connection count updates in the mindmap visualization, and change branding from "MindMap" to "Systades" across templates for a cohesive user experience. 2025-04-26 18:40:27 +01:00
4797cc3b72 Optimize network background animation and enhance UI styles: Adjust animation speeds for smoother transitions, implement dark mode support, and improve card and navbar styles with glassmorphism effects. Update HTML structure for better responsiveness and visual appeal. 2025-04-25 23:39:41 +02:00
ab280b55af Update requirements and enhance mindmap UI: Comment out Pillow dependency, add network background script, and implement new styles and animations for improved visual effects in the mindmap template. 2025-04-25 21:30:35 +01:00
84b492d8d2 Update README and enhance application functionality: Add detailed installation instructions, integrate OpenAI GPT for the AI assistant, implement error handling for various HTTP errors, and improve the admin interface with user management features. Refactor mindmap visualization and enhance UI with modern design elements. 2025-04-25 00:30:04 +02:00
b0db3398f2 Erweitere die Anwendung um neue Funktionen: Implementiere eine dauerhafte Sitzung für den Dark Mode, füge Benutzer- und Gedankenbewertung hinzu, verbessere die Benutzeroberfläche und aktualisiere die Datenbankinitialisierung mit Beispielbenutzern und Gedanken. Optimiere die Templates für ein modernes Design und verbessere die Suchfunktionalität. 2025-04-24 18:08:04 +02:00
66 changed files with 7571 additions and 8606 deletions

17
.env
View File

@@ -1,15 +1,2 @@
# MindMap Umgebungsvariablen
# Kopiere diese Datei zu .env und passe die Werte an
# Flask
FLASK_APP=app.py
FLASK_DEBUG=1
SECRET_KEY=ETMyh4JfBfvpZSscqfuzjCOAvelm5lEju
# OpenAI API
OPENAI_API_KEY=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
# Datenbank
# Bei Bedarf kann hier eine andere Datenbank-URL angegeben werden
# Der Pfad wird relativ zum Projektverzeichnis angegeben
# SQLALCHEMY_DATABASE_URI=sqlite:////absoluter/pfad/zu/database/systades.db
SECRET_KEY=eed9298856dc9363cd32778265780d6904ba24e6a6b815a2cc382bcdd767ea7b
OPENAI_API_KEY=sk-dein-openai-api-schluessel-hier

View File

@@ -1,33 +0,0 @@
# Dockerfile
FROM python:3.11-slim
# Arbeitsverzeichnis in Container
WORKDIR /app
# Systemabhängigkeiten installieren und Verzeichnisse anlegen
RUN apt-get update && \
apt-get install -y --no-install-recommends gcc && \
rm -rf /var/lib/apt/lists/* && \
mkdir -p /app/database
# pip auf den neuesten Stand bringen und Requirements installieren
COPY requirements.txt ./
RUN pip install --upgrade pip && \
pip install --no-cache-dir -U -r requirements.txt
# Anwendungscode kopieren
COPY . .
# Berechtigungen für database-Ordner
RUN chmod -R 777 /app/database
# Exponiere Port 5000 für Flask
EXPOSE 5000
# Setze Umgebungsvariablen
ENV FLASK_APP=app.py
ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1
# Startkommando mit spezifischen Flags für Produktion
CMD ["python", "app.py"]

Binary file not shown.

Binary file not shown.

833
app.py
View File

@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
import os
from datetime import datetime, timedelta, timezone
from datetime import datetime, timedelta
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
@@ -19,7 +19,6 @@ from openai import OpenAI
from dotenv import load_dotenv
from flask_socketio import SocketIO, emit
from flask_migrate import Migrate
import sqlalchemy
# Modelle importieren
from models import (
@@ -46,10 +45,10 @@ app.config['UPLOAD_FOLDER'] = os.getenv('UPLOAD_FOLDER', os.path.join(os.getcwd(
app.config['WTF_CSRF_ENABLED'] = False
# OpenAI API-Konfiguration
api_key = "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"
# api_key = os.environ.get("OPENAI_API_KEY")
# if not api_key:
# print("WARNUNG: Kein OPENAI_API_KEY in Umgebungsvariablen gefunden. KI-Funktionalität wird nicht verfügbar sein.")
api_key = os.environ.get("OPENAI_API_KEY")
if not api_key:
print("WARNUNG: Kein OPENAI_API_KEY in Umgebungsvariablen gefunden. KI-Funktionalität wird nicht verfügbar sein.")
api_key = "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"
client = OpenAI(api_key=api_key)
@@ -199,31 +198,97 @@ def initialize_database():
# 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(
# Prüfe, ob bereits Benutzer existieren
if User.query.count() == 0:
print("Erstelle Admin-Benutzer...")
admin = User(
username="admin",
email="admin@example.com",
role="admin",
is_active=True
is_admin=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!")
admin.set_password("admin123") # In echter Umgebung ein sicheres Passwort verwenden!
db.session.add(admin)
return True
# Prüfe, ob bereits Kategorien existieren
if Category.query.count() == 0:
print("Erstelle Standard-Kategorien...")
create_default_categories()
# Stelle sicher, dass die Standard-Knoten für die öffentliche Mindmap existieren
if MindMapNode.query.count() == 0:
print("Erstelle Standard-Knoten für die Mindmap...")
# Hauptknoten: Wissen
root_node = MindMapNode(
name="Wissen",
description="Zentrale Wissensbasis",
color_code="#4299E1",
is_public=True
)
db.session.add(root_node)
db.session.flush() # Um die ID zu generieren
# Verwandte Kategorien finden
philosophy = Category.query.filter_by(name="Philosophie").first()
science = Category.query.filter_by(name="Wissenschaft").first()
technology = Category.query.filter_by(name="Technologie").first()
arts = Category.query.filter_by(name="Künste").first()
# Erstelle Hauptthemenknoten
nodes = [
MindMapNode(
name="Philosophie",
description="Philosophisches Denken",
color_code="#9F7AEA",
category=philosophy,
is_public=True
),
MindMapNode(
name="Wissenschaft",
description="Wissenschaftliche Erkenntnisse",
color_code="#48BB78",
category=science,
is_public=True
),
MindMapNode(
name="Technologie",
description="Technologische Entwicklungen",
color_code="#ED8936",
category=technology,
is_public=True
),
MindMapNode(
name="Künste",
description="Künstlerische Ausdrucksformen",
color_code="#ED64A6",
category=arts,
is_public=True
)
]
# Füge Knoten zur Datenbank hinzu
for node in nodes:
db.session.add(node)
db.session.commit()
# Nachdem wir die IDs haben, füge die Verbindungen hinzu
all_nodes = MindMapNode.query.all()
root = MindMapNode.query.filter_by(name="Wissen").first()
if root:
for node in all_nodes:
if node.id != root.id:
root.children.append(node)
# Speichere die Änderungen
db.session.commit()
print("Datenbankinitialisierung abgeschlossen.")
except Exception as e:
print(f"Fehler bei Datenbank-Initialisierung: {e}")
return False
print(f"Fehler bei der Datenbankinitialisierung: {str(e)}")
db.session.rollback()
raise
# 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
@@ -250,8 +315,7 @@ def admin_required(f):
@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))
return User.query.get(int(id))
# Routes for authentication
@app.route('/login', methods=['GET', 'POST'])
@@ -261,16 +325,14 @@ def login():
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)
user.last_login = datetime.utcnow()
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')
@@ -292,21 +354,15 @@ def 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()
default_mindmap = UserMindmap(
name='Meine Mindmap',
description='Meine persönliche Wissenslandschaft',
user=user
)
db.session.add(default_mindmap)
db.session.commit()
login_user(user)
flash('Dein Konto wurde erfolgreich erstellt!', 'success')
@@ -327,152 +383,71 @@ def index():
# 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)
"""Zeigt die öffentliche Mindmap an."""
try:
# Sicherstellen, dass wir Kategorien haben
if Category.query.count() == 0:
create_default_categories()
# Hole alle Kategorien der obersten Ebene
categories = Category.query.filter_by(parent_id=None).all()
# Transformiere Kategorien in ein anzeigbares Format für die Vorlage
category_tree = [build_category_tree(cat) for cat in categories]
return render_template('mindmap.html', categories=category_tree)
except Exception as e:
# Bei Fehler leere Kategorienliste übergeben und Fehler protokollieren
print(f"Fehler beim Laden der Mindmap-Kategorien: {str(e)}")
return render_template('mindmap.html', categories=[], error=str(e))
# 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)
# Lade Benutzer-Mindmaps
user_mindmaps = UserMindmap.query.filter_by(user_id=current_user.id).all()
# 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
# Hole die Anzahl der Follower (falls implementiert)
# In diesem Beispiel nehmen wir an, dass es keine Follower-Funktionalität gibt
followers_count = 0
# 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,
thought_count=thought_count,
bookmark_count=bookmark_count,
connections_count=connections_count,
contributions_count=contributions_count,
followers_count=followers_count,
rating=round(avg_rating, 1),
location=location)
# Route für Benutzereinstellungen
@app.route('/settings', methods=['GET', 'POST'])
@@ -481,94 +456,32 @@ 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', '')
current_user.bio = request.form.get('bio')
# Update avatar if provided
avatar_url = request.form.get('avatar_url')
if avatar_url:
current_user.avatar = avatar_url
# 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')
db.session.commit()
flash('Profil erfolgreich aktualisiert!', 'success')
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')
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):
flash('Aktuelles Passwort ist nicht korrekt', 'error')
elif new_password != confirm_password:
flash('Neue Passwörter stimmen nicht überein', 'error')
else:
current_user.set_password(new_password)
db.session.commit()
flash('Passwort erfolgreich aktualisiert!', 'success')
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 redirect(url_for('settings'))
return render_template('settings.html')
@@ -685,156 +598,16 @@ def delete_mindmap(mindmap_id):
# API-Endpunkte für Mindmap-Daten
@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
"""Liefert die öffentliche Mindmap-Struktur."""
# Hole alle Kategorien der obersten Ebene
root_categories = Category.query.filter_by(parent_id=None).all()
# Baue Baumstruktur auf
result = []
for category in root_categories:
result.append(build_category_tree(category))
return jsonify(result)
def build_category_tree(category):
"""
@@ -1114,7 +887,7 @@ def update_note(note_id):
if color_code:
note.color_code = color_code
note.last_modified = datetime.now(timezone.utc)
note.last_modified = datetime.utcnow()
db.session.commit()
return jsonify({
@@ -1142,64 +915,32 @@ def delete_note(note_id):
# 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
"""API-Endpunkt zur Bereitstellung der Mindmap-Daten in hierarchischer Form."""
# Root-Knoten: Knoten ohne Eltern
root_nodes = MindMapNode.query.\
outerjoin(node_relationship, MindMapNode.id == node_relationship.c.child_id).\
filter(node_relationship.c.parent_id == None).all()
result = []
for node in root_nodes:
node_data = build_node_tree(node)
result.append(node_data)
return jsonify({"nodes": result})
def build_node_tree(node):
"""Erzeugt eine hierarchische Darstellung eines Knotens inkl. seiner Kindknoten."""
thought_count = len(node.thoughts)
node_data = {
"id": node.id,
"name": node.name,
"description": node.description or "",
"thought_count": thought_count,
"children": []
}
for child in node.children:
child_data = build_node_tree(child)
node_data["children"].append(child_data)
return node_data
@app.route('/api/nodes/<int:node_id>/thoughts')
def get_node_thoughts(node_id):
@@ -1365,7 +1106,7 @@ def update_thought(thought_id):
if 'color_code' in data:
thought.color_code = data['color_code']
thought.last_modified = datetime.now(timezone.utc)
thought.last_modified = datetime.utcnow()
db.session.commit()
return jsonify({
@@ -1570,11 +1311,8 @@ def chat_with_assistant():
})
try:
# OpenAI-Client mit dem API-Key initialisieren
client = OpenAI(api_key=api_key)
# Überprüfen ob OpenAI API-Key konfiguriert ist
if not api_key or api_key.startswith("sk-dummy"):
if not client.api_key or client.api_key.startswith("sk-dummy"):
print("Warnung: OpenAI API-Key ist nicht oder nur als Dummy konfiguriert!")
return jsonify({
'error': 'Der OpenAI API-Key ist nicht korrekt konfiguriert. Bitte konfigurieren Sie die Umgebungsvariable OPENAI_API_KEY.'
@@ -1584,13 +1322,12 @@ def chat_with_assistant():
import time
start_time = time.time()
# Erhöhtes Timeout für die API-Anfrage
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=api_messages,
max_tokens=1000,
max_tokens=1000, # Erhöht für ausführlichere Antworten und detaillierte Führungen
temperature=0.7,
timeout=60 # Erhöht auf 60 Sekunden für bessere Zuverlässigkeit
timeout=20 # 20 Sekunden Timeout
)
print(f"OpenAI API-Antwortzeit: {time.time() - start_time:.2f} Sekunden")
@@ -1604,30 +1341,12 @@ def chat_with_assistant():
except Exception as e:
import traceback
error_message = str(e)
stack_trace = traceback.format_exc()
print(f"Fehler bei der OpenAI-Anfrage: {str(e)}")
print(traceback.format_exc())
print(f"Fehler bei der OpenAI-Anfrage: {error_message}")
print(f"Stack Trace: {stack_trace}")
# Überprüfen auf spezifische Fehlertypen
if "timeout" in error_message.lower():
return jsonify({
'error': 'Die Anfrage hat zu lange gedauert. Bitte versuchen Sie es später erneut.'
}), 504
elif "rate limit" in error_message.lower():
return jsonify({
'error': 'API-Ratelimit erreicht. Bitte warten Sie einen Moment und versuchen Sie es erneut.'
}), 429
elif "internal server error" in error_message.lower() or "500" in error_message:
return jsonify({
'error': 'Es ist ein Serverfehler aufgetreten. Unser Team wurde benachrichtigt.'
}), 500
else:
# Allgemeine Fehlermeldung
return jsonify({
'error': 'Bei der Verarbeitung Ihrer Anfrage ist ein Fehler aufgetreten. Bitte versuchen Sie es später erneut.'
}), 500
return jsonify({
'error': f'Fehler bei der OpenAI-Anfrage: {str(e)}'
}), 500
def check_database_query(user_message):
"""
@@ -1760,42 +1479,14 @@ def refresh_mindmap():
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
# Hole alle Mindmap-Knoten
nodes = MindMapNode.query.all()
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,
@@ -1806,28 +1497,15 @@ def refresh_mindmap():
'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
})
# Verbindungen hinzufügen
node_obj['connections'] = [{'target': child.id} for child in node.children]
node_data.append(node_obj)
return jsonify({
'success': True,
'categories': category_tree,
'nodes': node_data,
'edges': edge_data
'nodes': node_data
})
except Exception as e:
@@ -1837,46 +1515,21 @@ def refresh_mindmap():
'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'))
# Fehlerbehandlung
@app.errorhandler(404)
def not_found(e):
return jsonify({'error': 'Nicht gefunden'}), 404
@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'}
@app.errorhandler(400)
def bad_request(e):
return jsonify({'error': 'Fehlerhafte Anfrage'}), 400
# 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 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.errorhandler(500)
def server_error(e):
return jsonify({'error': 'Serverfehler'}), 500

Binary file not shown.

Binary file not shown.

View File

@@ -1,103 +0,0 @@
import sqlite3
import os
import random
# Verbindung zur Datenbank herstellen
db_path = os.path.join(os.getcwd(), 'database', 'systades.db')
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
# Schema der mind_map_node Tabelle anzeigen
cursor.execute("PRAGMA table_info(mind_map_node)")
columns = cursor.fetchall()
print("Tabellenschema mind_map_node:")
for column in columns:
print(f"{column[1]} ({column[2]})")
# Existierende Knoten anzeigen
cursor.execute("SELECT id, name, description, color_code FROM mind_map_node LIMIT 5")
existing_nodes = cursor.fetchall()
print("\nBestehende Knoten:")
for node in existing_nodes:
print(f"ID: {node[0]}, Name: {node[1]}, Beschreibung: {node[2]}")
# Mögliche Kategorien abrufen (für die Verknüpfung)
cursor.execute("SELECT id, name FROM category")
categories = cursor.fetchall()
print("\nVerfügbare Kategorien:")
for category in categories:
print(f"ID: {category[0]}, Name: {category[1]}")
# Wissenschaftliche Themengebiete für neue Knoten
scientific_nodes = [
{
"name": "Quantenphysik",
"description": "Die Quantenphysik befasst sich mit dem Verhalten von Materie und Energie auf atomarer und subatomarer Ebene.",
"color_code": "#4B0082", # Indigo
"icon": "fa-atom"
},
{
"name": "Neurowissenschaften",
"description": "Interdisziplinäre Wissenschaft, die sich mit der Struktur und Funktion des Nervensystems und des Gehirns beschäftigt.",
"color_code": "#FF4500", # Orange-Rot
"icon": "fa-brain"
},
{
"name": "Künstliche Intelligenz",
"description": "Forschungsgebiet der Informatik, das sich mit der Automatisierung intelligenten Verhaltens befasst.",
"color_code": "#008080", # Teal
"icon": "fa-robot"
},
{
"name": "Klimaforschung",
"description": "Wissenschaftliche Untersuchung des Klimas, seiner Variationen und Veränderungen auf allen zeitlichen und räumlichen Skalen.",
"color_code": "#2E8B57", # Seegrün
"icon": "fa-cloud-sun"
},
{
"name": "Genetik",
"description": "Teilgebiet der Biologie, das sich mit Vererbung sowie der Funktion und Wirkung von Genen beschäftigt.",
"color_code": "#800080", # Lila
"icon": "fa-dna"
},
{
"name": "Astrophysik",
"description": "Zweig der Astronomie, der sich mit den physikalischen Eigenschaften des Universums befasst.",
"color_code": "#191970", # Mitternachtsblau
"icon": "fa-star"
}
]
# Neue Knoten hinzufügen
print("\nFüge neue wissenschaftliche Knoten hinzu...")
for node in scientific_nodes:
# Prüfen, ob der Knoten bereits existiert
cursor.execute("SELECT id FROM mind_map_node WHERE name = ?", (node["name"],))
existing = cursor.fetchone()
if existing:
print(f"Knoten '{node['name']}' existiert bereits mit ID {existing[0]}")
continue
# Zufällige Kategorie wählen, wenn vorhanden
category_id = None
if categories:
category_id = random.choice(categories)[0]
# Neuen Knoten einfügen
cursor.execute("""
INSERT INTO mind_map_node (name, description, color_code, icon, is_public, category_id)
VALUES (?, ?, ?, ?, ?, ?)
""", (
node["name"],
node["description"],
node["color_code"],
node["icon"],
True,
category_id
))
print(f"Knoten '{node['name']}' hinzugefügt")
# Änderungen übernehmen und Verbindung schließen
conn.commit()
print("\nDatenbank erfolgreich aktualisiert!")
conn.close()

View File

@@ -1,17 +0,0 @@
version: '3.9'
services:
web:
build: .
image: systades_app:latest
container_name: systades_app
restart: always
env_file:
- .env
ports:
- "5000:5000"
volumes:
- ./database:/app/database
volumes:
db_data:

View File

@@ -1 +0,0 @@

View File

@@ -2,12 +2,10 @@
# Kopiere diese Datei zu .env und passe die Werte an
# Flask
FLASK_APP=app.py
FLASK_ENV=development
SECRET_KEY=your-secret-key-replace-in-production
SECRET_KEY=dein-geheimer-schluessel-hier
# OpenAI API
OPENAI_API_KEY=sk-dein-openai-api-schluessel-hier
# Datenbank
# Bei Bedarf kann hier eine andere Datenbank-URL angegeben werden

View File

@@ -12,7 +12,7 @@ from datetime import datetime
# Erstelle eine temporäre Flask-App, um die Datenbank zu initialisieren
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///database/systades.db'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///systades.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db.init_app(app)

View File

@@ -1,40 +0,0 @@
"""Add missing user fields
Revision ID: 5a23f8c6db37
Revises: d4406f5b12f7
Create Date: 2025-05-02 10:45:00.000000
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '5a23f8c6db37'
down_revision = 'd4406f5b12f7'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('user', schema=None) as batch_op:
batch_op.add_column(sa.Column('bio', sa.Text(), nullable=True))
batch_op.add_column(sa.Column('location', sa.String(length=100), nullable=True))
batch_op.add_column(sa.Column('website', sa.String(length=200), nullable=True))
batch_op.add_column(sa.Column('avatar', sa.String(length=200), nullable=True))
batch_op.add_column(sa.Column('last_login', sa.DateTime(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('user', schema=None) as batch_op:
batch_op.drop_column('last_login')
batch_op.drop_column('avatar')
batch_op.drop_column('website')
batch_op.drop_column('location')
batch_op.drop_column('bio')
# ### end Alembic commands ###

View File

@@ -53,20 +53,11 @@ class User(db.Model, UserMixin):
created_at = db.Column(db.DateTime, default=datetime.utcnow)
is_active = db.Column(db.Boolean, default=True)
role = db.Column(db.String(20), default="user") # 'user', 'admin', 'moderator'
bio = db.Column(db.Text, nullable=True) # Profil-Bio
location = db.Column(db.String(100), nullable=True) # Standort
website = db.Column(db.String(200), nullable=True) # Website
avatar = db.Column(db.String(200), nullable=True) # Profilbild-URL
last_login = db.Column(db.DateTime, nullable=True) # Letzter Login
# Relationships
threads = db.relationship('Thread', backref='creator', lazy=True)
messages = db.relationship('Message', backref='author', lazy=True)
projects = db.relationship('Project', backref='owner', lazy=True)
mindmaps = db.relationship('UserMindmap', backref='user', lazy=True)
thoughts = db.relationship('Thought', backref='author', lazy=True)
bookmarked_thoughts = db.relationship('Thought', secondary=user_thought_bookmark,
lazy='dynamic', backref=db.backref('bookmarked_by', lazy='dynamic'))
def __repr__(self):
return f'<User {self.username}>'
@@ -76,14 +67,6 @@ class User(db.Model, UserMixin):
def check_password(self, password):
return check_password_hash(self.password, password)
@property
def is_admin(self):
return self.role == 'admin'
@is_admin.setter
def is_admin(self, value):
self.role = 'admin' if value else 'user'
class Category(db.Model):
"""Wissenschaftliche Kategorien für die Gliederung der öffentlichen Mindmap"""
@@ -322,41 +305,4 @@ class Document(db.Model):
file_size = db.Column(db.Integer, nullable=True)
def __repr__(self):
return f'<Document {self.title}>'
# Forum-Kategorie-Modell - entspricht den Hauptknotenpunkten der Mindmap
class ForumCategory(db.Model):
id = db.Column(db.Integer, primary_key=True)
node_id = db.Column(db.Integer, db.ForeignKey('mind_map_node.id'), nullable=False)
title = db.Column(db.String(200), nullable=False)
description = db.Column(db.Text)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
is_active = db.Column(db.Boolean, default=True)
# Beziehungen
node = db.relationship('MindMapNode', backref='forum_category')
posts = db.relationship('ForumPost', backref='category', lazy=True, cascade="all, delete-orphan")
def __repr__(self):
return f'<ForumCategory {self.title}>'
# Forum-Beitrag-Modell
class ForumPost(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(200), nullable=False)
content = db.Column(db.Text, nullable=False)
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('forum_category.id'), nullable=False)
parent_id = db.Column(db.Integer, db.ForeignKey('forum_post.id'), nullable=True)
is_pinned = db.Column(db.Boolean, default=False)
is_locked = db.Column(db.Boolean, default=False)
view_count = db.Column(db.Integer, default=0)
# Beziehungen
author = db.relationship('User', backref='forum_posts')
replies = db.relationship('ForumPost', backref=db.backref('parent', remote_side=[id]), lazy=True)
def __repr__(self):
return f'<ForumPost {self.title}>'
return f'<Document {self.title}>'

View File

@@ -1,53 +0,0 @@
#!/usr/bin/env powershell
# Windows PowerShell-Version des Start-Skripts
# Datum: 01.05.2025
# Docker-Status prüfen
Write-Host "Prüfe Docker-Status..." -ForegroundColor Cyan
try {
$status = docker ps -q
if ($LASTEXITCODE -ne 0) {
Write-Host "Docker ist nicht gestartet. Bitte starten Sie Docker Desktop." -ForegroundColor Red
exit 1
}
} catch {
Write-Host "Docker ist nicht verfügbar. Bitte installieren Sie Docker Desktop und starten Sie es." -ForegroundColor Red
Write-Host $_.Exception.Message
exit 1
}
# Alte Container stoppen und entfernen
$containerExists = docker ps -a --filter "name=systades_app" -q
if ($containerExists) {
Write-Host "Stoppe und entferne alten Container..." -ForegroundColor Yellow
docker rm -f systades_app
}
# Alte Images löschen
Write-Host "Entferne altes Image..." -ForegroundColor Yellow
docker rmi -f systades_app:latest
# Stelle sicher, dass das Datenbankverzeichnis existiert
if (-not (Test-Path "database")) {
New-Item -Path "database" -ItemType Directory -Force
}
# Docker-Compose Setup neu bauen
Write-Host "Baue Container neu..." -ForegroundColor Green
docker-compose build --no-cache
# Docker-Compose neu starten
Write-Host "Starte Container..." -ForegroundColor Green
docker-compose up -d --force-recreate
# Warte kurz und prüfe, ob der Container läuft
Write-Host "Prüfe Container-Status..." -ForegroundColor Cyan
Start-Sleep -Seconds 3
docker ps | Select-String "systades_app"
# Ausgabe
Write-Host "`nSystemstatus:" -ForegroundColor Cyan
Write-Host "----------------------------------------"
Write-Host "Systades-Anwendung ist jetzt unter http://localhost:5000 erreichbar." -ForegroundColor Green
Write-Host "Container-Logs können mit 'docker logs -f systades_app' angezeigt werden." -ForegroundColor Green
Write-Host "----------------------------------------"

View File

@@ -1,9 +1,6 @@
/* ChatGPT Assistent Styles - Verbesserte Version */
#chatgpt-assistant {
font-family: 'Inter', sans-serif;
bottom: 5.5rem;
z-index: 100;
max-height: 85vh;
}
#assistant-chat {
@@ -13,15 +10,6 @@
border-radius: 0.75rem;
overflow: hidden;
max-width: calc(100vw - 2rem);
max-height: 80vh !important;
}
#assistant-history {
max-height: calc(80vh - 150px);
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: rgba(156, 163, 175, 0.5) transparent;
padding-bottom: 2rem; /* Zusätzlicher Abstand unten */
}
#assistant-toggle {
@@ -34,6 +22,11 @@
transform: scale(1.1) rotate(10deg);
}
#assistant-history {
scrollbar-width: thin;
scrollbar-color: rgba(156, 163, 175, 0.5) transparent;
}
#assistant-history::-webkit-scrollbar {
width: 6px;
}
@@ -149,21 +142,14 @@
.typing-indicator span {
height: 8px;
width: 8px;
background-color: #888;
border-radius: 50%;
display: inline-block;
margin: 0 2px;
opacity: 0.6;
opacity: 0.4;
animation: bounce 1.4s infinite ease-in-out;
}
body.dark .typing-indicator span {
background-color: rgba(255, 255, 255, 0.7);
}
body:not(.dark) .typing-indicator span {
background-color: rgba(107, 114, 128, 0.8);
}
.typing-indicator span:nth-child(1) { animation-delay: 0s; }
.typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
.typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
@@ -187,12 +173,11 @@ body:not(.dark) .typing-indicator span {
@media (max-width: 640px) {
#assistant-chat {
width: calc(100vw - 2rem) !important;
max-height: 65vh !important;
}
#chatgpt-assistant {
right: 1rem;
bottom: 6rem;
bottom: 1rem;
}
}
@@ -215,38 +200,4 @@ main {
footer {
flex-shrink: 0;
}
/* Verbesserte Farbkontraste für Nachrichtenblasen */
.user-message {
background-color: rgba(124, 58, 237, 0.1) !important;
color: #4B5563 !important;
}
body.dark .user-message {
background-color: rgba(124, 58, 237, 0.2) !important;
color: #F9FAFB !important;
}
.assistant-message {
background-color: #F3F4F6 !important;
color: #1F2937 !important;
border-left: 3px solid #8B5CF6;
}
body.dark .assistant-message {
background-color: rgba(31, 41, 55, 0.5) !important;
color: #F9FAFB !important;
border-left: 3px solid #8B5CF6;
}
/* Chat-Assistent-Position im Footer-Bereich anpassen */
.chat-assistant {
max-height: 75vh;
bottom: 1.5rem;
}
.chat-assistant .chat-messages {
max-height: calc(75vh - 180px);
overflow-y: auto;
}

View File

@@ -40,8 +40,8 @@
--light-bg: #f9fafb;
--light-text: #1e293b;
--light-heading: #0f172a;
--light-primary: #7c3aed;
--light-primary-hover: #6d28d9;
--light-primary: #3b82f6;
--light-primary-hover: #4f46e5;
--light-secondary: #6b7280;
--light-border: #e5e7eb;
--light-card-bg: rgba(255, 255, 255, 0.92);
@@ -68,37 +68,18 @@ body {
line-height: 1.5;
}
/* Strikte Trennung: Dark Mode */
html.dark body,
body.dark {
/* Dark Mode */
html.dark body {
background-color: var(--bg-primary-dark);
color: var(--text-primary-dark);
}
/* Strikte Trennung: Light Mode */
html:not(.dark) body,
/* Light Mode */
body:not(.dark) {
background-color: var(--light-bg);
color: var(--light-text);
}
/* Verbesserte Trennung: Container und Karten */
body.dark .card,
body.dark .glass-card,
body.dark .panel {
background-color: var(--bg-secondary-dark);
border-color: var(--border-dark);
color: var(--text-primary-dark);
}
body:not(.dark) .card,
body:not(.dark) .glass-card,
body:not(.dark) .panel {
background-color: var(--light-card-bg);
border-color: var(--light-border);
color: var(--light-text);
}
/* Typography */
h1, h2, h3, h4, h5, h6 {
font-weight: 600;
@@ -407,7 +388,7 @@ html.dark ::-webkit-scrollbar-thumb:hover {
}
.section-heading {
font-size: 1.75rem;
font-size: 1.5rem;
}
}
@@ -474,60 +455,22 @@ body:not(.dark) a:hover {
}
/* Light Mode Buttons */
body:not(.dark) button:not(.toggle):not(.plain-btn) {
color: white !important;
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
}
body:not(.dark) .btn,
body:not(.dark) .btn-primary,
body:not(.dark) .btn-secondary,
body:not(.dark) .btn-success,
body:not(.dark) .btn-danger,
body:not(.dark) .btn-warning,
body:not(.dark) .btn-info {
color: white !important;
body:not(.dark) button:not(.toggle) {
background-color: var(--light-primary);
color: white;
border: none;
box-shadow: var(--light-shadow);
border-radius: 0.375rem;
padding: 0.5rem 1rem;
transition: all 0.3s ease;
}
body:not(.dark) .btn:hover,
body:not(.dark) button:not(.toggle):hover {
background: linear-gradient(135deg, #7c3aed, #6d28d9);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(109, 40, 217, 0.3);
}
/* Dark/Light Mode Switch Button */
.theme-toggle {
position: relative;
width: 48px;
height: 24px;
background: linear-gradient(to right, #7c3aed, #3b82f6);
border-radius: 24px;
padding: 2px;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1);
}
.theme-toggle::after {
content: '';
position: absolute;
width: 20px;
height: 20px;
background: white;
border-radius: 50%;
top: 2px;
left: 2px;
transition: all 0.3s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.theme-toggle.dark::after {
transform: translateX(24px);
}
.theme-toggle:hover::after {
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
background-color: var(--light-primary-hover);
transform: translateY(-2px);
box-shadow: 0 6px 10px rgba(0, 0, 0, 0.1);
}
/* Light Mode Cards und Panels */
@@ -580,489 +523,4 @@ body:not(.dark) .navbar {
background-color: var(--light-navbar-bg);
box-shadow: var(--light-shadow);
border-bottom: 1px solid var(--light-border);
}
/* Erweiterte Light-Mode-spezifische Stile */
body:not(.dark) .glass-effect {
background-color: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(209, 213, 219, 0.3);
}
body:not(.dark) .card {
background-color: rgba(255, 255, 255, 0.85);
border: 1px solid var(--light-border);
box-shadow: var(--light-shadow);
transition: all 0.3s ease;
}
body:not(.dark) .card:hover {
box-shadow: 0 8px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
transform: translateY(-2px);
}
/* Light Mode Buttons mit verbesserter Lesbarkeit */
body:not(.dark) .btn-primary {
background: linear-gradient(135deg, #6d28d9, #5b21b6);
color: white;
border: none;
transition: all 0.2s ease;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
font-weight: 600;
letter-spacing: 0.02em;
box-shadow: 0 2px 4px rgba(91, 33, 182, 0.3);
border-radius: 8px;
}
body:not(.dark) .btn-primary:hover {
background: linear-gradient(135deg, #7c3aed, #6d28d9);
box-shadow: 0 4px 12px rgba(109, 40, 217, 0.4), 0 2px 4px rgba(0, 0, 0, 0.1);
transform: translateY(-1px);
}
body:not(.dark) .btn-secondary {
background: linear-gradient(135deg, #ffffff, #f9fafb);
color: #1f2937;
border: 2px solid #e5e7eb;
font-weight: 600;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
border-radius: 8px;
}
body:not(.dark) .btn-secondary:hover {
background: linear-gradient(135deg, #f9fafb, #f3f4f6);
border-color: #d1d5db;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.12);
transform: translateY(-1px);
}
body:not(.dark) .btn-outline {
background-color: transparent;
color: var(--light-primary);
border: 1px solid var(--light-primary);
}
body:not(.dark) .btn-outline:hover {
background-color: rgba(124, 58, 237, 0.05);
}
/* Light Mode Formulare */
body:not(.dark) input,
body:not(.dark) select,
body:not(.dark) textarea {
background-color: white;
border: 1px solid #d1d5db;
color: #1f2937;
}
body:not(.dark) input:focus,
body:not(.dark) select:focus,
body:not(.dark) textarea:focus {
border-color: var(--light-primary);
box-shadow: 0 0 0 2px rgba(124, 58, 237, 0.2);
}
/* Light Mode Navigation */
body:not(.dark) .sidebar {
background-color: white;
border-right: 1px solid #e5e7eb;
}
body:not(.dark) .sidebar-link {
color: #4b5563;
}
body:not(.dark) .sidebar-link:hover {
background-color: #f3f4f6;
color: var(--light-primary);
}
body:not(.dark) .sidebar-link.active {
background-color: rgba(124, 58, 237, 0.08);
color: var(--light-primary);
font-weight: 500;
}
/* Light Mode Tabellen */
body:not(.dark) table {
border-color: #e5e7eb;
}
body:not(.dark) th {
background-color: #f9fafb;
color: #111827;
font-weight: 600;
}
body:not(.dark) tr:nth-child(even) {
background-color: #f9fafb;
}
body:not(.dark) tr:hover {
background-color: #f3f4f6;
}
/* Light Mode Icons */
body:not(.dark) .icon {
color: #6b7280;
}
body:not(.dark) .icon-primary {
color: var(--light-primary);
}
/* Light Mode Alerts/Benachrichtigungen */
body:not(.dark) .alert-info {
background-color: #eff6ff;
border-left: 4px solid #3b82f6;
color: #1e40af;
}
body:not(.dark) .alert-success {
background-color: #ecfdf5;
border-left: 4px solid #10b981;
color: #065f46;
}
body:not(.dark) .alert-warning {
background-color: #fffbeb;
border-left: 4px solid #f59e0b;
color: #92400e;
}
body:not(.dark) .alert-error {
background-color: #fef2f2;
border-left: 4px solid #ef4444;
color: #b91c1c;
}
/* Light Mode Badge */
body:not(.dark) .badge {
background-color: #e5e7eb;
color: #374151;
}
body:not(.dark) .badge-primary {
background-color: rgba(124, 58, 237, 0.1);
color: var(--light-primary);
}
/* Light Mode Mindmap spezifisch */
body:not(.dark) #cy {
background-color: rgba(255, 255, 255, 0.7);
border: 1px solid #e5e7eb;
}
body:not(.dark) .node {
border: 2px solid white;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
body:not(.dark) .node:hover,
body:not(.dark) .node.selected {
box-shadow: 0 0 0 2px rgba(124, 58, 237, 0.5), 0 4px 8px rgba(0, 0, 0, 0.1);
}
body:not(.dark) .edge {
opacity: 0.7;
}
body:not(.dark) .edge:hover,
body:not(.dark) .edge.selected {
opacity: 1;
}
/* Footer im Light Mode */
body:not(.dark) footer {
background-color: rgba(249, 250, 251, 0.7);
border-top: 1px solid #e5e7eb;
}
/* Alpine.js Transitions im Light Mode */
body:not(.dark) [x-cloak] {
display: none !important;
}
/* Suchfeldstyling im Light Mode */
body:not(.dark) .search-container input {
background-color: white;
border: 1px solid #d1d5db;
color: #1f2937;
}
body:not(.dark) .search-container input:focus {
border-color: var(--light-primary);
box-shadow: 0 0 0 2px rgba(124, 58, 237, 0.2);
}
body:not(.dark) .search-results {
background-color: white;
border: 1px solid #e5e7eb;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
body:not(.dark) .search-result-item:hover {
background-color: #f3f4f6;
}
/* Profile und Benutzermenü im Light Mode */
body:not(.dark) .avatar {
border: 2px solid white;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
body:not(.dark) .user-dropdown {
background-color: white;
border: 1px solid #e5e7eb;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
body:not(.dark) .user-dropdown-item:hover {
background-color: #f3f4f6;
}
/* Medienabfragen für Responsivität */
@media (max-width: 640px) {
/* Optimierungen für Smartphones */
body {
padding-left: 0;
padding-right: 0;
}
.container {
padding-left: 1rem;
padding-right: 1rem;
}
.hero-heading {
font-size: 2rem;
}
.section-heading {
font-size: 1.75rem;
}
.card, .panel, .glass-card {
padding: 1rem;
border-radius: 10px;
}
.navbar {
padding: 0.75rem 1rem;
}
/* Optimierte Touch-Ziele für mobile Geräte */
button, .btn, .nav-link, .menu-item {
min-height: 44px;
min-width: 44px;
display: flex;
align-items: center;
justify-content: center;
}
/* Verbesserte Lesbarkeit auf kleinen Bildschirmen */
p, li, input, textarea, button, .text-sm {
font-size: 1rem;
line-height: 1.5;
}
/* Anpassungen für Tabellen auf kleinen Bildschirmen */
table {
display: block;
overflow-x: auto;
white-space: nowrap;
}
/* Optimierte Formulare */
input, select, textarea {
font-size: 16px; /* Verhindert iOS-Zoom bei Fokus */
width: 100%;
}
/* Verbesserter Abstand für Touch-Targets */
nav a, nav button, .menu-item {
margin: 0.25rem 0;
padding: 0.75rem;
}
}
@media (min-width: 641px) and (max-width: 1024px) {
/* Optimierungen für Tablets */
.container {
padding-left: 1.5rem;
padding-right: 1.5rem;
}
/* Zweispaltige Layouts für mittlere Bildschirme */
.grid-cols-1 {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1.5rem;
}
/* Optimierte Navigationsleiste */
.navbar {
padding: 0.75rem 1.5rem;
}
}
@media (min-width: 1025px) {
/* Optimierungen für Desktop */
.container {
padding-left: 2rem;
padding-right: 2rem;
max-width: 1280px;
margin: 0 auto;
}
/* Mehrspaltige Layouts für große Bildschirme */
.grid-cols-1 {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 2rem;
}
/* Hover-Effekte nur auf Desktop-Geräten */
.card:hover, .panel:hover, .glass-card:hover {
transform: translateY(-5px);
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.15), 0 10px 10px -5px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
/* Desktop-spezifische Animationen */
.animate-hover {
transition: all 0.3s ease;
}
.animate-hover:hover {
transform: translateY(-3px);
box-shadow: var(--shadow-lg);
}
}
/* Responsive design improvements */
.responsive-grid {
display: grid;
gap: 1rem;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
}
.responsive-flex {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
.responsive-flex > * {
flex: 1 1 280px;
}
/* Accessibility improvements */
.focus-visible:focus-visible {
outline: 2px solid var(--accent-primary-light);
outline-offset: 2px;
}
body.dark .focus-visible:focus-visible {
outline: 2px solid var(--accent-primary-dark);
}
/* Print styles */
@media print {
body {
background: white !important;
color: black !important;
}
nav, footer, button, .no-print {
display: none !important;
}
main, article, .card, .panel, .container {
width: 100% !important;
border: none !important;
box-shadow: none !important;
color: black !important;
background: white !important;
}
a {
color: black !important;
text-decoration: underline !important;
}
a::after {
content: " (" attr(href) ")";
font-size: 0.8em;
font-weight: normal;
}
h1, h2, h3, h4, h5, h6 {
page-break-after: avoid;
page-break-inside: avoid;
}
img {
page-break-inside: avoid;
max-width: 100% !important;
}
table {
page-break-inside: avoid;
}
@page {
margin: 2cm;
}
}
/* Light Mode KI-Chatfenster */
body:not(.dark) .chat-container {
background-color: rgba(255, 255, 255, 0.95);
border: 1px solid rgba(0, 0, 0, 0.1);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
color: #1e293b;
}
body:not(.dark) .chat-message-ai {
background-color: rgba(124, 58, 237, 0.1);
border: 1px solid rgba(124, 58, 237, 0.2);
}
body:not(.dark) .chat-message-user {
background-color: rgba(59, 130, 246, 0.1);
border: 1px solid rgba(59, 130, 246, 0.2);
}
/* Anpassung der Chatfenster-Größe */
.chat-assistant {
max-height: 85vh; /* Vergrößert von 80vh */
bottom: 1rem; /* Etwas höher positionieren */
}
.chat-assistant .chat-messages {
max-height: calc(85vh - 180px); /* Angepasst für größeres Fenster */
overflow-y: auto;
padding-bottom: 2rem; /* Zusätzlicher Abstand um Abschneiden zu vermeiden */
}
/* Verbesserungen für das Mobilmenü */
@media (max-width: 768px) {
.mobile-menu-container {
max-height: 85vh;
overflow-y: auto;
}
#chatgpt-assistant {
bottom: 4.5rem !important;
}
.chat-assistant {
max-height: 70vh !important;
}
.chat-assistant .chat-messages {
max-height: calc(70vh - 160px) !important;
}
}

View File

@@ -1,253 +0,0 @@
/* Mindmap Container Styles */
.mindmap-container {
position: relative;
width: 100%;
height: 100%;
min-height: 600px;
background: var(--bg-primary);
border-radius: 12px;
overflow: hidden;
}
/* Toolbar Styles */
.mindmap-toolbar {
position: absolute;
top: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 8px;
padding: 8px;
background: var(--bg-secondary);
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
z-index: 1000;
transition: all 0.3s ease;
}
.dark .mindmap-toolbar {
background: rgba(30, 41, 59, 0.8);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
border: 1px solid rgba(255, 255, 255, 0.1);
}
/* Toolbar Buttons */
.mindmap-toolbar button {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border: none;
border-radius: 8px;
background: transparent;
color: var(--text-primary);
cursor: pointer;
transition: all 0.2s ease;
}
.mindmap-toolbar button:hover {
background: var(--accent-primary);
color: white;
transform: translateY(-1px);
}
.mindmap-toolbar button:active {
transform: translateY(0);
}
.mindmap-toolbar button i {
font-size: 16px;
}
/* Export Group Styles */
.export-group {
position: relative;
}
.export-options {
position: absolute;
top: 100%;
right: 0;
margin-top: 8px;
padding: 8px;
background: var(--bg-secondary);
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
display: none;
flex-direction: column;
gap: 4px;
min-width: 160px;
}
.dark .export-options {
background: rgba(30, 41, 59, 0.9);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.export-group:hover .export-options {
display: flex;
}
.export-options button {
width: 100%;
height: auto;
padding: 8px 12px;
justify-content: flex-start;
font-size: 14px;
border-radius: 6px;
}
/* Context Menu Styles */
.mindmap-context-menu {
position: fixed;
background: var(--bg-secondary);
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
padding: 8px;
z-index: 1000;
min-width: 180px;
}
.dark .mindmap-context-menu {
background: rgba(30, 41, 59, 0.9);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.mindmap-context-menu button {
display: flex;
align-items: center;
width: 100%;
padding: 8px 12px;
border: none;
background: transparent;
color: var(--text-primary);
cursor: pointer;
font-size: 14px;
border-radius: 6px;
transition: all 0.2s ease;
}
.mindmap-context-menu button:hover {
background: var(--accent-primary);
color: white;
}
.mindmap-context-menu button i {
margin-right: 8px;
width: 16px;
}
/* Node Styles */
.mindmap-node {
background-color: var(--bg-secondary);
border: 2px solid var(--accent-primary);
border-radius: 8px;
padding: 8px 12px;
transition: all 0.3s ease;
}
.mindmap-node:hover {
box-shadow: 0 0 0 2px var(--accent-primary);
transform: scale(1.05);
}
.mindmap-node.selected {
border-color: var(--accent-secondary);
box-shadow: 0 0 0 3px var(--accent-secondary);
}
/* Edge Styles */
.mindmap-edge {
width: 2px;
transition: all 0.3s ease;
}
.dark .mindmap-edge {
background-color: rgba(255, 255, 255, 0.2);
}
.mindmap-edge:hover {
width: 3px;
background-color: var(--accent-primary);
}
/* Animation Styles */
@keyframes nodeAppear {
from {
opacity: 0;
transform: scale(0.8);
}
to {
opacity: 1;
transform: scale(1);
}
}
.mindmap-node-new {
animation: nodeAppear 0.3s ease forwards;
}
/* Responsive Styles */
@media (max-width: 768px) {
.mindmap-toolbar {
flex-wrap: wrap;
width: calc(100% - 32px);
justify-content: center;
}
.export-options {
left: 0;
right: auto;
}
}
/* Loading State */
.mindmap-loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.mindmap-loading-spinner {
width: 40px;
height: 40px;
border: 4px solid var(--bg-secondary);
border-top-color: var(--accent-primary);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Tooltip Styles */
.mindmap-tooltip {
position: absolute;
background: var(--bg-secondary);
padding: 8px 12px;
border-radius: 6px;
font-size: 12px;
pointer-events: none;
z-index: 1000;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
max-width: 200px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.dark .mindmap-tooltip {
background: rgba(30, 41, 59, 0.9);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}

View File

@@ -1,11 +0,0 @@
<svg width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="100" cy="100" r="98" fill="url(#gradient)" stroke="#7C3AED" stroke-width="4"/>
<circle cx="100" cy="80" r="36" fill="white"/>
<path d="M100 140C77.9086 140 60 157.909 60 180H140C140 157.909 122.091 140 100 140Z" fill="white"/>
<defs>
<linearGradient id="gradient" x1="0" y1="0" x2="200" y2="200" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#8B5CF6"/>
<stop offset="1" stop-color="#3B82F6"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 583 B

View File

@@ -1,54 +1,25 @@
#!/usr/bin/env python3
"""
Generate favicon.ico from SVG using cairosvg and PIL
"""
# -*- coding: utf-8 -*-
import os
import io
from cairosvg import svg2png
from PIL import Image
import cairosvg
# Verzeichnis dieses Skripts
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
# Pfad zum SVG-Favicon
svg_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'favicon.svg')
# Ausgabepfad für das PNG
png_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'favicon.png')
# Ausgabepfad für das ICO
ico_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'favicon.ico')
def svg_to_ico(svg_path, ico_path, sizes=[16, 32, 48, 64, 128, 256]):
"""Convert SVG to multi-size ICO file"""
img_io = io.BytesIO()
# Höchste Auflösung für Zwischenspeicherung
max_size = max(sizes)
# SVG in PNG konvertieren
with open(svg_path, 'rb') as svg_file:
svg_data = svg_file.read()
svg2png(bytestring=svg_data, write_to=img_io, output_width=max_size, output_height=max_size)
# PNG in verschiedene Größen konvertieren
img = Image.open(img_io)
# Alle Größen für das ICO-Format vorbereiten
img_list = []
for size in sizes:
resized_img = img.resize((size, size), Image.LANCZOS)
img_list.append(resized_img)
# ICO-Datei speichern
img_list[0].save(
ico_path,
format='ICO',
sizes=[(img.width, img.height) for img in img_list],
append_images=img_list[1:]
)
print(f"Favicon {ico_path} wurde erstellt!")
# SVG zu PNG konvertieren
cairosvg.svg2png(url=svg_path, write_to=png_path, output_width=512, output_height=512)
# Ursprüngliches Favicon konvertieren
svg_to_ico(
os.path.join(CURRENT_DIR, 'favicon.svg'),
os.path.join(CURRENT_DIR, 'favicon.ico')
)
# PNG zu ICO konvertieren
img = Image.open(png_path)
img.save(ico_path, sizes=[(16, 16), (32, 32), (48, 48), (64, 64), (128, 128)])
# Neues Neuron-Favicon konvertieren
svg_to_ico(
os.path.join(CURRENT_DIR, 'neuron-favicon.svg'),
os.path.join(CURRENT_DIR, 'neuron-favicon.ico')
)
print(f"Favicon erfolgreich erstellt: {ico_path}")
# Optional: PNG-Datei löschen, wenn nur ICO benötigt wird
# os.remove(png_path)

View File

@@ -1,29 +0,0 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- Hintergrund -->
<rect width="32" height="32" rx="8" fill="#6d28d9" />
<!-- Mindmap-Punkte -->
<!-- Zentraler Punkt -->
<circle cx="16" cy="16" r="3.5" fill="#a78bfa" />
<!-- Umgebende Punkte -->
<circle cx="8" cy="10" r="2.5" fill="#8b5cf6" />
<circle cx="24" cy="10" r="2.5" fill="#8b5cf6" />
<circle cx="16" cy="26" r="2.5" fill="#8b5cf6" />
<!-- Verbindende Linien -->
<path d="M16 16 L8 10" stroke="white" stroke-width="1" stroke-linecap="round" />
<path d="M16 16 L24 10" stroke="white" stroke-width="1" stroke-linecap="round" />
<path d="M16 16 L16 26" stroke="white" stroke-width="1" stroke-linecap="round" />
<!-- Weitere Verbindungslinien für mehr Komplexität -->
<path d="M8 10 L16 26" stroke="#c4b5fd" stroke-width="0.8" stroke-linecap="round" stroke-dasharray="2 1" />
<path d="M24 10 L16 26" stroke="#c4b5fd" stroke-width="0.8" stroke-linecap="round" stroke-dasharray="2 1" />
<path d="M8 10 L24 10" stroke="#c4b5fd" stroke-width="0.8" stroke-linecap="round" stroke-dasharray="2 1" />
<!-- Kleine Dekoration-Punkte für Hintergrund-Ähnlichkeit -->
<circle cx="5" cy="20" r="0.8" fill="#ddd6fe" opacity="0.7" />
<circle cx="27" cy="20" r="0.8" fill="#ddd6fe" opacity="0.7" />
<circle cx="20" cy="5" r="0.8" fill="#ddd6fe" opacity="0.7" />
<circle cx="12" cy="5" r="0.8" fill="#ddd6fe" opacity="0.7" />
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1,59 +0,0 @@
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- Hintergrund mit Farbverlauf -->
<rect width="64" height="64" rx="16" fill="url(#paint0_linear)" />
<!-- Mindmap-Punkte -->
<!-- Zentraler Punkt -->
<circle cx="32" cy="32" r="8" fill="url(#glow_gradient)" filter="url(#glow)" />
<!-- Umgebende Punkte -->
<circle cx="16" cy="20" r="6" fill="#8b5cf6" />
<circle cx="48" cy="20" r="6" fill="#8b5cf6" />
<circle cx="32" cy="52" r="6" fill="#8b5cf6" />
<circle cx="16" cy="48" r="4" fill="#a78bfa" />
<circle cx="48" cy="48" r="4" fill="#a78bfa" />
<!-- Verbindende Linien (Hauptpfade) -->
<path d="M32 32 L16 20" stroke="white" stroke-width="2" stroke-linecap="round" />
<path d="M32 32 L48 20" stroke="white" stroke-width="2" stroke-linecap="round" />
<path d="M32 32 L32 52" stroke="white" stroke-width="2" stroke-linecap="round" />
<path d="M32 32 L16 48" stroke="white" stroke-width="2" stroke-linecap="round" />
<path d="M32 32 L48 48" stroke="white" stroke-width="2" stroke-linecap="round" />
<!-- Zusätzliche Verbindungslinien -->
<path d="M16 20 L16 48" stroke="#c4b5fd" stroke-width="1.5" stroke-linecap="round" stroke-dasharray="3 2" />
<path d="M48 20 L48 48" stroke="#c4b5fd" stroke-width="1.5" stroke-linecap="round" stroke-dasharray="3 2" />
<path d="M16 20 L48 20" stroke="#c4b5fd" stroke-width="1.5" stroke-linecap="round" stroke-dasharray="3 2" />
<path d="M16 48 L32 52" stroke="#c4b5fd" stroke-width="1.5" stroke-linecap="round" stroke-dasharray="3 2" />
<path d="M48 48 L32 52" stroke="#c4b5fd" stroke-width="1.5" stroke-linecap="round" stroke-dasharray="3 2" />
<!-- Kleine Dekoration-Punkte für Hintergrund-Ähnlichkeit -->
<circle cx="10" cy="36" r="1.5" fill="#ddd6fe" opacity="0.7" />
<circle cx="54" cy="36" r="1.5" fill="#ddd6fe" opacity="0.7" />
<circle cx="40" cy="10" r="1.5" fill="#ddd6fe" opacity="0.7" />
<circle cx="24" cy="10" r="1.5" fill="#ddd6fe" opacity="0.7" />
<circle cx="20" cy="36" r="1.2" fill="#ddd6fe" opacity="0.5" />
<circle cx="44" cy="36" r="1.2" fill="#ddd6fe" opacity="0.5" />
<circle cx="32" cy="16" r="1.2" fill="#ddd6fe" opacity="0.5" />
<!-- Definitionen für Farbverläufe und Effekte -->
<defs>
<!-- Haupthintergrund-Farbverlauf -->
<linearGradient id="paint0_linear" x1="0" y1="0" x2="64" y2="64" gradientUnits="userSpaceOnUse">
<stop stop-color="#6d28d9" />
<stop offset="1" stop-color="#4c1d95" />
</linearGradient>
<!-- Glüheffekt für den zentralen Punkt -->
<filter id="glow" x="20" y="20" width="24" height="24" filterUnits="userSpaceOnUse">
<feGaussianBlur stdDeviation="2" result="blur" />
<feComposite in="SourceGraphic" in2="blur" operator="over" />
</filter>
<!-- Farbverlauf für den zentralen Punkt -->
<linearGradient id="glow_gradient" x1="24" y1="24" x2="40" y2="40" gradientUnits="userSpaceOnUse">
<stop stop-color="#a78bfa" />
<stop offset="1" stop-color="#8b5cf6" />
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -1,214 +0,0 @@
/**
* Mindmap Initialisierung und Event-Handling
*/
// Warte auf die Cytoscape-Instanz
document.addEventListener('mindmap-loaded', function() {
const cy = window.cy;
if (!cy) return;
// Event-Listener für Knoten-Klicks
cy.on('tap', 'node', function(evt) {
const node = evt.target;
// Alle vorherigen Hervorhebungen zurücksetzen
cy.nodes().forEach(n => {
n.removeStyle();
n.connectedEdges().removeStyle();
});
// Speichere ausgewählten Knoten
window.mindmapInstance.selectedNode = node;
// Aktiviere leuchtenden Effekt statt Umkreisung
node.style({
'background-opacity': 1,
'background-color': node.data('color'),
'shadow-color': node.data('color'),
'shadow-opacity': 1,
'shadow-blur': 15,
'shadow-offset-x': 0,
'shadow-offset-y': 0
});
// Verbundene Kanten und Knoten hervorheben
const connectedEdges = node.connectedEdges();
const connectedNodes = node.neighborhood('node');
connectedEdges.style({
'line-color': '#a78bfa',
'target-arrow-color': '#a78bfa',
'source-arrow-color': '#a78bfa',
'line-opacity': 0.8,
'width': 2
});
connectedNodes.style({
'shadow-opacity': 0.7,
'shadow-blur': 10,
'shadow-color': '#a78bfa'
});
// Info-Panel aktualisieren
updateInfoPanel(node);
// Seitenleiste aktualisieren
updateSidebar(node);
});
// Klick auf Hintergrund - Auswahl zurücksetzen
cy.on('tap', function(evt) {
if (evt.target === cy) {
resetSelection(cy);
}
});
// Zoom-Controls
document.getElementById('zoomIn')?.addEventListener('click', () => {
cy.zoom({
level: cy.zoom() * 1.2,
renderedPosition: { x: cy.width() / 2, y: cy.height() / 2 }
});
});
document.getElementById('zoomOut')?.addEventListener('click', () => {
cy.zoom({
level: cy.zoom() / 1.2,
renderedPosition: { x: cy.width() / 2, y: cy.height() / 2 }
});
});
document.getElementById('resetView')?.addEventListener('click', () => {
cy.fit();
resetSelection(cy);
});
// Legend-Toggle
document.getElementById('toggleLegend')?.addEventListener('click', () => {
const legend = document.getElementById('categoryLegend');
if (legend) {
isLegendVisible = !isLegendVisible;
legend.style.display = isLegendVisible ? 'block' : 'none';
}
});
// Keyboard-Controls
document.addEventListener('keydown', (e) => {
if (e.key === '+' || e.key === '=') {
cy.zoom({
level: cy.zoom() * 1.2,
renderedPosition: { x: cy.width() / 2, y: cy.height() / 2 }
});
} else if (e.key === '-' || e.key === '_') {
cy.zoom({
level: cy.zoom() / 1.2,
renderedPosition: { x: cy.width() / 2, y: cy.height() / 2 }
});
} else if (e.key === 'Escape') {
resetSelection(cy);
}
});
});
/**
* Aktualisiert das Info-Panel mit Knoteninformationen
* @param {Object} node - Der ausgewählte Knoten
*/
function updateInfoPanel(node) {
const infoPanel = document.getElementById('infoPanel');
if (!infoPanel) return;
const data = node.data();
const connectedNodes = node.neighborhood('node');
let html = `
<h3>${data.label || data.name}</h3>
<p class="category">${data.category || 'Keine Kategorie'}</p>
${data.description ? `<p class="description">${data.description}</p>` : ''}
<div class="connections">
<h4>Verbindungen (${connectedNodes.length})</h4>
<ul>
`;
connectedNodes.forEach(connectedNode => {
const connectedData = connectedNode.data();
html += `
<li style="color: ${connectedData.color || '#60a5fa'}">
${connectedData.label || connectedData.name}
</li>
`;
});
html += `
</ul>
</div>
`;
infoPanel.innerHTML = html;
infoPanel.style.display = 'block';
}
/**
* Aktualisiert die Seitenleiste mit Knoteninformationen
* @param {Object} node - Der ausgewählte Knoten
*/
function updateSidebar(node) {
const sidebar = document.getElementById('sidebar');
if (!sidebar) return;
const data = node.data();
const connectedNodes = node.neighborhood('node');
let html = `
<div class="node-details">
<h3>${data.label || data.name}</h3>
<p class="category">${data.category || 'Keine Kategorie'}</p>
${data.description ? `<p class="description">${data.description}</p>` : ''}
<div class="connections">
<h4>Verbindungen (${connectedNodes.length})</h4>
<ul>
`;
connectedNodes.forEach(connectedNode => {
const connectedData = connectedNode.data();
html += `
<li style="color: ${connectedData.color || '#60a5fa'}">
${connectedData.label || connectedData.name}
</li>
`;
});
html += `
</ul>
</div>
</div>
`;
sidebar.innerHTML = html;
}
/**
* Setzt die Auswahl zurück
* @param {Object} cy - Cytoscape-Instanz
*/
function resetSelection(cy) {
window.mindmapInstance.selectedNode = null;
// Alle Hervorhebungen zurücksetzen
cy.nodes().forEach(node => {
node.removeStyle();
node.connectedEdges().removeStyle();
});
// Info-Panel ausblenden
const infoPanel = document.getElementById('infoPanel');
if (infoPanel) {
infoPanel.style.display = 'none';
}
// Seitenleiste leeren
const sidebar = document.getElementById('sidebar');
if (sidebar) {
sidebar.innerHTML = '';
}
}

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>

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

@@ -0,0 +1,749 @@
/**
* 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 = async endpoint => {
try {
const response = await fetch(endpoint);
if (!response.ok) {
console.error(`API-Fehler: ${endpoint} antwortet mit Status ${response.status}`);
return []; // Leeres Array zurückgeben bei Fehlern
}
return await response.json();
} catch (error) {
console.error(`Fehler beim Abrufen von ${endpoint}:`, error);
return []; // Leeres Array zurückgeben bei Netzwerkfehlern
}
};
const post = async (endpoint, body) => {
try {
const response = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (!response.ok) {
console.error(`API-Fehler: ${endpoint} antwortet mit Status ${response.status}`);
return {}; // Leeres Objekt zurückgeben bei Fehlern
}
return await response.json();
} catch (error) {
console.error(`Fehler beim POST zu ${endpoint}:`, error);
return {}; // Leeres Objekt zurückgeben bei Netzwerkfehlern
}
};
const del = async endpoint => {
try {
const response = await fetch(endpoint, { method: 'DELETE' });
if (!response.ok) {
console.error(`API-Fehler: ${endpoint} antwortet mit Status ${response.status}`);
return {}; // Leeres Objekt zurückgeben bei Fehlern
}
return await response.json();
} catch (error) {
console.error(`Fehler beim DELETE zu ${endpoint}:`, error);
return {}; // Leeres Objekt zurückgeben bei Netzwerkfehlern
}
};
/* 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();
// Überprüfen, ob nodes ein Array ist, wenn nicht, setze es auf ein leeres Array
const nodesArray = Array.isArray(nodes) ? nodes : [];
// Knoten zum Graph hinzufügen
cy.add(
nodesArray.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
};
})
);
// Überprüfen, ob relationships ein Array ist, wenn nicht, setze es auf ein leeres Array
const relationshipsArray = Array.isArray(relationships) ? relationships : [];
// Kanten zum Graph hinzufügen
cy.add(
relationshipsArray.map(rel => ({
data: {
id: `${rel.parent_id}_${rel.child_id}`,
source: rel.parent_id.toString(),
target: rel.child_id.toString()
}
}))
);
// Wenn keine Knoten geladen wurden, Fallback-Knoten erstellen
if (nodesArray.length === 0) {
// Mindestens einen Standardknoten hinzufügen
cy.add({
data: {
id: 'fallback-1',
name: 'Mindmap',
description: 'Erstellen Sie hier Ihre eigene Mindmap',
color: '#3b82f6',
icon: 'help-circle'
},
position: { x: 300, y: 200 }
});
// Erfolgsmeldung anzeigen
console.log('Mindmap erfolgreich initialisiert mit Fallback-Knoten');
// Info-Meldung für Benutzer anzeigen
const infoBox = document.createElement('div');
infoBox.classList.add('info-message');
infoBox.style.position = 'absolute';
infoBox.style.top = '50%';
infoBox.style.left = '50%';
infoBox.style.transform = 'translate(-50%, -50%)';
infoBox.style.padding = '15px 20px';
infoBox.style.backgroundColor = 'rgba(59, 130, 246, 0.9)';
infoBox.style.color = 'white';
infoBox.style.borderRadius = '8px';
infoBox.style.zIndex = '5';
infoBox.style.maxWidth = '80%';
infoBox.style.textAlign = 'center';
infoBox.style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)';
infoBox.innerHTML = 'Mindmap erfolgreich initialisiert.<br>Verwenden Sie die Werkzeugleiste, um Knoten hinzuzufügen.';
document.getElementById('cy').appendChild(infoBox);
// Meldung nach 5 Sekunden ausblenden
setTimeout(() => {
infoBox.style.opacity = '0';
infoBox.style.transition = 'opacity 0.5s ease';
setTimeout(() => {
if (infoBox.parentNode) {
infoBox.parentNode.removeChild(infoBox);
}
}, 500);
}, 5000);
}
// 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

@@ -247,63 +247,130 @@ class ChatGPTAssistant {
const bubble = document.createElement('div');
bubble.className = sender === 'user'
? 'user-message rounded-lg py-2 px-3 max-w-[85%]'
: 'assistant-message rounded-lg py-2 px-3 max-w-[85%]';
// Nachrichtentext einfügen, falls Markdown-Parser verfügbar, nutzen
if (this.markdownParser) {
bubble.innerHTML = this.markdownParser.parse(text);
? 'bg-primary-100 dark:bg-primary-900 text-gray-800 dark:text-white rounded-lg py-2 px-3 max-w-[85%]'
: 'bg-gray-100 dark:bg-dark-700 text-gray-800 dark:text-white rounded-lg py-2 px-3 max-w-[85%]';
// Formatierung des Texts (mit Markdown für Assistent-Nachrichten)
let formattedText = '';
if (sender === 'assistant' && this.markdownParser) {
// Für Assistentnachrichten Markdown verwenden
try {
formattedText = this.markdownParser.parse(text);
// CSS für Markdown-Formatierung hinzufügen
const markdownStyles = `
.markdown-bubble h1, .markdown-bubble h2, .markdown-bubble h3,
.markdown-bubble h4, .markdown-bubble h5, .markdown-bubble h6 {
font-weight: bold;
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
.markdown-bubble h1 { font-size: 1.4rem; }
.markdown-bubble h2 { font-size: 1.3rem; }
.markdown-bubble h3 { font-size: 1.2rem; }
.markdown-bubble h4 { font-size: 1.1rem; }
.markdown-bubble ul, .markdown-bubble ol {
padding-left: 1.5rem;
margin: 0.5rem 0;
}
.markdown-bubble ul { list-style-type: disc; }
.markdown-bubble ol { list-style-type: decimal; }
.markdown-bubble p { margin: 0.5rem 0; }
.markdown-bubble code {
font-family: monospace;
background-color: rgba(0, 0, 0, 0.1);
padding: 1px 4px;
border-radius: 3px;
}
.markdown-bubble pre {
background-color: rgba(0, 0, 0, 0.1);
padding: 0.5rem;
border-radius: 4px;
overflow-x: auto;
margin: 0.5rem 0;
}
.markdown-bubble pre code {
background-color: transparent;
padding: 0;
}
.markdown-bubble blockquote {
border-left: 3px solid rgba(0, 0, 0, 0.2);
padding-left: 0.8rem;
margin: 0.5rem 0;
font-style: italic;
}
.dark .markdown-bubble code {
background-color: rgba(255, 255, 255, 0.1);
}
.dark .markdown-bubble pre {
background-color: rgba(255, 255, 255, 0.1);
}
.dark .markdown-bubble blockquote {
border-left-color: rgba(255, 255, 255, 0.2);
}
`;
// Füge die Styles hinzu, wenn sie noch nicht vorhanden sind
if (!document.querySelector('#markdown-chat-styles')) {
const style = document.createElement('style');
style.id = 'markdown-chat-styles';
style.textContent = markdownStyles;
document.head.appendChild(style);
}
// Klasse für Markdown-Formatierung hinzufügen
bubble.classList.add('markdown-bubble');
} catch (error) {
console.error('Fehler bei der Markdown-Formatierung:', error);
// Fallback zur einfachen Formatierung
formattedText = text.split('\n').map(line => {
if (line.trim() === '') return '<br>';
return `<p>${line}</p>`;
}).join('');
}
} else {
bubble.textContent = text;
// Für Benutzernachrichten einfache Formatierung
formattedText = text.split('\n').map(line => {
if (line.trim() === '') return '<br>';
return `<p>${line}</p>`;
}).join('');
}
// Links in der Nachricht klickbar machen
const links = bubble.querySelectorAll('a');
links.forEach(link => {
link.target = '_blank';
link.rel = 'noopener noreferrer';
link.className = 'text-primary-600 dark:text-primary-400 underline';
});
// Code-Blöcke stylen
const codeBlocks = bubble.querySelectorAll('pre');
codeBlocks.forEach(block => {
block.className = 'bg-gray-100 dark:bg-dark-900 p-2 rounded my-2 overflow-x-auto';
});
const inlineCode = bubble.querySelectorAll('code:not(pre code)');
inlineCode.forEach(code => {
code.className = 'bg-gray-100 dark:bg-dark-900 px-1 rounded font-mono text-sm';
});
bubble.innerHTML = formattedText;
messageEl.appendChild(bubble);
this.chatHistory.appendChild(messageEl);
// Scrolle zum Ende des Chat-Verlaufs
this.chatHistory.scrollTop = this.chatHistory.scrollHeight;
if (this.chatHistory) {
this.chatHistory.appendChild(messageEl);
// Scroll zum Ende des Verlaufs
this.chatHistory.scrollTop = this.chatHistory.scrollHeight;
}
}
/**
* Zeigt Vorschläge für mögliche Fragen an
* @param {Array} suggestions - Array von Vorschlägen
* Zeigt Vorschläge als klickbare Pills an
* @param {string[]} suggestions - Liste von Vorschlägen
*/
showSuggestions(suggestions) {
if (!this.suggestionArea || !suggestions || !suggestions.length) return;
if (!this.suggestionArea) return;
// Vorherige Vorschläge entfernen
this.suggestionArea.innerHTML = '';
// Neue Vorschläge hinzufügen
suggestions.forEach((text, index) => {
const pill = document.createElement('button');
pill.className = 'suggestion-pill text-sm px-3 py-1.5 rounded-full bg-primary-100 dark:bg-primary-900 text-primary-800 dark:text-primary-200 hover:bg-primary-200 dark:hover:bg-primary-800 transition-all duration-200';
pill.style.animationDelay = `${index * 0.1}s`;
pill.textContent = text;
this.suggestionArea.appendChild(pill);
});
// Vorschlagsbereich anzeigen
this.suggestionArea.classList.remove('hidden');
if (suggestions && suggestions.length > 0) {
suggestions.forEach(suggestion => {
const pill = document.createElement('button');
pill.className = 'suggestion-pill text-sm bg-gray-200 dark:bg-dark-600 hover:bg-gray-300 dark:hover:bg-dark-500 text-gray-800 dark:text-gray-200 rounded-full px-3 py-1 mb-2 transition-colors';
pill.textContent = suggestion;
this.suggestionArea.appendChild(pill);
});
this.suggestionArea.classList.remove('hidden');
} else {
this.suggestionArea.classList.add('hidden');
}
}
/**
@@ -342,27 +409,14 @@ class ChatGPTAssistant {
messages: this.messages
}),
cache: 'no-cache', // Kein Cache verwenden
credentials: 'same-origin', // Cookies senden
timeout: 60000 // 60 Sekunden Timeout
credentials: 'same-origin' // Cookies senden
});
// Ladeindikator entfernen
this.removeLoadingIndicator();
if (!response.ok) {
const errorText = await response.text();
let errorMessage;
try {
// Versuche, die Fehlermeldung zu parsen
const errorData = JSON.parse(errorText);
errorMessage = errorData.error || `Serverfehler: ${response.status} ${response.statusText}`;
} catch {
// Bei Parsing-Fehler verwende Standardfehlermeldung
errorMessage = `Serverfehler: ${response.status} ${response.statusText}`;
}
throw new Error(errorMessage);
throw new Error(`Serverfehler: ${response.status} ${response.statusText}`);
}
const data = await response.json();
@@ -388,45 +442,24 @@ class ChatGPTAssistant {
// Ladeindikator entfernen, falls noch vorhanden
this.removeLoadingIndicator();
// Spezielle Fehlermeldungen für bestimmte Fehlertypen
const errorMessage = error.message || '';
let userFriendlyMessage = 'Es gab ein Problem mit der Anfrage.';
if (errorMessage.includes('timeout') || errorMessage.includes('Zeitüberschreitung')) {
userFriendlyMessage = 'Die Antwort hat zu lange gedauert. Der Server ist möglicherweise überlastet.';
} else if (errorMessage.includes('500') || errorMessage.includes('Internal Server Error')) {
userFriendlyMessage = 'Ein Serverfehler ist aufgetreten. Wir arbeiten an einer Lösung.';
} else if (errorMessage.includes('429') || errorMessage.includes('rate limit')) {
userFriendlyMessage = 'Die API-Anfragelimits wurden erreicht. Bitte warte einen Moment.';
}
// Fehlermeldung anzeigen oder Wiederholungsversuch starten
if (this.retryCount < this.maxRetries) {
this.retryCount++;
this.addMessage('assistant', `${userFriendlyMessage} Ich versuche es erneut... (Versuch ${this.retryCount}/${this.maxRetries})`);
this.addMessage('assistant', 'Es gab ein Problem mit der Anfrage. Ich versuche es erneut...');
// Letzte Benutzernachricht speichern für den Wiederholungsversuch
const lastUserMessageIndex = this.messages.findLastIndex(msg => msg.role === 'user');
if (lastUserMessageIndex >= 0) {
const lastUserMessage = this.messages[lastUserMessageIndex].content;
// Kurze Verzögerung vor dem erneuten Versuch
setTimeout(() => {
// Letzte Benutzernachricht aus dem Messages-Array entfernen
const lastUserMessage = this.messages[this.messages.length - 2].content;
this.messages = this.messages.slice(0, -2); // Entferne Benutzernachricht und Fehlermeldung
// Kurze Verzögerung vor dem erneuten Versuch mit exponentieller Backoff-Strategie
const retryDelay = 1500 * Math.pow(2, this.retryCount - 1); // 1.5s, 3s, 6s, ...
setTimeout(() => {
// Entferne Fehlermeldung aus dem Messages-Array, behalte aber die Benutzernachricht
this.messages = this.messages.filter(msg =>
!(msg.role === 'assistant' && msg.content.includes('versuche es erneut'))
);
// Erneuter Versand mit gleicher Nachricht
this.inputField.value = lastUserMessage;
this.sendMessage();
}, retryDelay);
}
// Erneuter Versand mit gleicher Nachricht
this.inputField.value = lastUserMessage;
this.sendMessage();
}, 1500);
} else {
// Maximale Anzahl an Wiederholungsversuchen erreicht
this.addMessage('assistant', 'Es tut mir leid, aber es gab ein Problem bei der Verarbeitung deiner Anfrage. Bitte versuche es später noch einmal oder kontaktiere den Support, falls das Problem weiterhin besteht.');
this.addMessage('assistant', 'Es tut mir leid, aber es gab ein Problem bei der Verarbeitung deiner Anfrage. Bitte versuche es später noch einmal.');
this.retryCount = 0; // Zurücksetzen für die nächste Anfrage
}
} finally {
@@ -479,33 +512,26 @@ class ChatGPTAssistant {
}
/**
* Zeigt eine Ladeanimation an
* Zeigt einen Ladeindikator im Chat an
*/
showLoadingIndicator() {
if (!this.chatHistory) return;
// Prüfen, ob bereits ein Ladeindikator angezeigt wird
if (document.getElementById('assistant-loading-indicator')) return;
// Entferne vorhandenen Ladeindikator (falls vorhanden)
this.removeLoadingIndicator();
const loadingEl = document.createElement('div');
loadingEl.id = 'assistant-loading';
loadingEl.className = 'flex justify-start';
loadingEl.id = 'assistant-loading-indicator';
const bubble = document.createElement('div');
bubble.className = 'assistant-message rounded-lg py-3 px-4 max-w-[85%] flex items-center';
bubble.className = 'bg-gray-100 dark:bg-dark-700 text-gray-800 dark:text-white rounded-lg py-2 px-3';
bubble.innerHTML = '<div class="typing-indicator"><span></span><span></span><span></span></div>';
const typingIndicator = document.createElement('div');
typingIndicator.className = 'typing-indicator';
typingIndicator.innerHTML = `
<span></span>
<span></span>
<span></span>
`;
bubble.appendChild(typingIndicator);
loadingEl.appendChild(bubble);
this.chatHistory.appendChild(loadingEl);
// Scroll zum Ende des Verlaufs
this.chatHistory.scrollTop = this.chatHistory.scrollHeight;
}
@@ -513,7 +539,7 @@ class ChatGPTAssistant {
* Entfernt den Ladeindikator aus dem Chat
*/
removeLoadingIndicator() {
const loadingIndicator = document.getElementById('assistant-loading-indicator');
const loadingIndicator = document.getElementById('assistant-loading');
if (loadingIndicator) {
loadingIndicator.remove();
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,844 @@
/**
* MindMap D3.js Modul
* Visualisiert die Mindmap mit D3.js
*/
class MindMapVisualization {
constructor(containerSelector, options = {}) {
this.containerSelector = containerSelector;
this.container = d3.select(containerSelector);
this.width = options.width || this.container.node().clientWidth || 800;
this.height = options.height || 600;
this.nodeRadius = options.nodeRadius || 14;
this.selectedNodeRadius = options.selectedNodeRadius || 20;
this.linkDistance = options.linkDistance || 150;
this.chargeStrength = options.chargeStrength || -900;
this.centerForce = options.centerForce || 0.15;
this.onNodeClick = options.onNodeClick || ((node) => console.log('Node clicked:', node));
this.nodes = [];
this.links = [];
this.simulation = null;
this.svg = null;
this.linkElements = null;
this.nodeElements = null;
this.textElements = null;
this.tooltipEnabled = options.tooltipEnabled !== undefined ? options.tooltipEnabled : true;
this.mouseoverNode = null;
this.selectedNode = null;
this.zoomFactor = 1;
this.tooltipDiv = null;
this.isLoading = true;
// Lade die gemerkten Knoten
this.bookmarkedNodes = this.loadBookmarkedNodes();
// Sicherstellen, dass der Container bereit ist
if (this.container.node()) {
this.init();
this.setupDefaultNodes();
// Sofortige Datenladung
window.setTimeout(() => {
this.loadData();
}, 100);
} else {
console.error('Mindmap-Container nicht gefunden:', containerSelector);
}
}
// Standardknoten als Fallback einrichten, falls die API nicht reagiert
setupDefaultNodes() {
// Basis-Mindmap mit Hauptthemen
const defaultNodes = [
{ id: "root", name: "Wissen", description: "Zentrale Wissensbasis", thought_count: 0 },
{ id: "philosophy", name: "Philosophie", description: "Philosophisches Denken", thought_count: 0 },
{ id: "science", name: "Wissenschaft", description: "Wissenschaftliche Erkenntnisse", thought_count: 0 },
{ id: "technology", name: "Technologie", description: "Technologische Entwicklungen", thought_count: 0 },
{ id: "arts", name: "Künste", description: "Künstlerische Ausdrucksformen", thought_count: 0 }
];
const defaultLinks = [
{ source: "root", target: "philosophy" },
{ source: "root", target: "science" },
{ source: "root", target: "technology" },
{ source: "root", target: "arts" }
];
// Als Fallback verwenden, falls die API fehlschlägt
this.defaultNodes = defaultNodes;
this.defaultLinks = defaultLinks;
}
init() {
// SVG erstellen, wenn noch nicht vorhanden
if (!this.svg) {
// Container zuerst leeren
this.container.html('');
this.svg = this.container
.append('svg')
.attr('width', '100%')
.attr('height', this.height)
.attr('viewBox', `0 0 ${this.width} ${this.height}`)
.attr('class', 'mindmap-svg')
.call(
d3.zoom()
.scaleExtent([0.1, 5])
.on('zoom', (event) => {
this.handleZoom(event.transform);
})
);
// Hauptgruppe für alles, was zoom-transformierbar ist
this.g = this.svg.append('g');
// Tooltip initialisieren
if (!d3.select('body').select('.node-tooltip').size()) {
this.tooltipDiv = d3.select('body')
.append('div')
.attr('class', 'node-tooltip')
.style('opacity', 0)
.style('position', 'absolute')
.style('pointer-events', 'none')
.style('background', 'rgba(20, 20, 40, 0.9)')
.style('color', '#ffffff')
.style('border', '1px solid rgba(160, 80, 255, 0.2)')
.style('border-radius', '6px')
.style('padding', '8px 12px')
.style('font-size', '14px')
.style('max-width', '250px')
.style('box-shadow', '0 10px 25px rgba(0, 0, 0, 0.5), 0 0 10px rgba(160, 80, 255, 0.2)');
} else {
this.tooltipDiv = d3.select('body').select('.node-tooltip');
}
}
// Force-Simulation initialisieren
this.simulation = d3.forceSimulation()
.force('link', d3.forceLink().id(d => d.id).distance(this.linkDistance))
.force('charge', d3.forceManyBody().strength(this.chargeStrength))
.force('center', d3.forceCenter(this.width / 2, this.height / 2).strength(this.centerForce))
.force('collision', d3.forceCollide().radius(this.nodeRadius * 2));
// Globale Mindmap-Instanz für externe Zugriffe setzen
window.mindmapInstance = this;
}
handleZoom(transform) {
this.g.attr('transform', transform);
this.zoomFactor = transform.k;
// Knotengröße anpassen, um bei Zoom lesbar zu bleiben
if (this.nodeElements) {
this.nodeElements
.attr('r', d => (d === this.selectedNode ? this.selectedNodeRadius : this.nodeRadius) / Math.sqrt(transform.k));
}
// Textgröße anpassen
if (this.textElements) {
this.textElements
.style('font-size', `${12 / Math.sqrt(transform.k)}px`);
}
}
async loadData() {
try {
// Ladeindikator anzeigen
this.showLoading();
// Verwende sofort die Standarddaten für eine schnelle erste Anzeige
this.nodes = [...this.defaultNodes];
this.links = [...this.defaultLinks];
// Visualisierung sofort aktualisieren
this.isLoading = false;
this.updateVisualization();
// Status auf bereit setzen - don't wait for API
this.container.attr('data-status', 'ready');
// API-Aufruf mit kürzerem Timeout im Hintergrund durchführen
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 Sekunden Timeout
const response = await fetch('/api/mindmap', {
signal: controller.signal,
headers: {
'Cache-Control': 'no-cache',
'Pragma': 'no-cache'
}
});
clearTimeout(timeoutId);
if (!response.ok) {
console.warn(`HTTP Fehler: ${response.status}, versuche erneute Verbindung`);
// Bei Verbindungsfehler versuchen, die Verbindung neu herzustellen
const retryResponse = await fetch('/api/refresh-mindmap', {
headers: {
'Cache-Control': 'no-cache',
'Pragma': 'no-cache'
}
});
if (!retryResponse.ok) {
throw new Error(`Retry failed with status: ${retryResponse.status}`);
}
const retryData = await retryResponse.json();
if (!retryData.success || !retryData.nodes || retryData.nodes.length === 0) {
console.warn('Keine Mindmap-Daten nach Neuversuch, verwende weiterhin Standard-Daten.');
return; // Keep using default data
}
// Flache Liste von Knoten und Verbindungen erstellen
this.nodes = [];
this.links = [];
// Knoten direkt übernehmen
retryData.nodes.forEach(node => {
this.nodes.push({
id: node.id,
name: node.name,
description: node.description || '',
thought_count: node.thought_count || 0,
color: this.generateColorFromString(node.name),
});
// Verbindungen hinzufügen
if (node.connections && node.connections.length > 0) {
node.connections.forEach(conn => {
this.links.push({
source: node.id,
target: conn.target
});
});
}
});
// Visualisierung aktualisieren mit den tatsächlichen Daten
this.updateVisualization();
return;
}
const data = await response.json();
if (!data || !data.nodes || data.nodes.length === 0) {
console.warn('Keine Mindmap-Daten vorhanden, verwende weiterhin Standard-Daten.');
return; // Keep using default data
}
// Flache Liste von Knoten und Verbindungen erstellen
this.nodes = [];
this.links = [];
this.processHierarchicalData(data.nodes);
// Visualisierung aktualisieren mit den tatsächlichen Daten
this.updateVisualization();
} catch (error) {
console.warn('Fehler beim Laden der Mindmap-Daten, verwende Standarddaten:', error);
// Already using default data, no action needed
}
} catch (error) {
console.error('Kritischer Fehler bei der Mindmap-Darstellung:', error);
this.showError('Fehler beim Laden der Mindmap-Daten. Bitte laden Sie die Seite neu.');
this.container.attr('data-status', 'error');
}
}
showLoading() {
// Element nur leeren, wenn es noch kein SVG enthält
if (!this.container.select('svg').size()) {
this.container.html(`
<div class="flex justify-center items-center h-full">
<div class="text-center">
<div class="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-primary-400 mx-auto mb-4"></div>
<p class="text-lg text-white">Mindmap wird geladen...</p>
</div>
</div>
`);
}
}
processHierarchicalData(hierarchicalNodes, parentId = null) {
hierarchicalNodes.forEach(node => {
// Knoten hinzufügen, wenn noch nicht vorhanden
if (!this.nodes.find(n => n.id === node.id)) {
this.nodes.push({
id: node.id,
name: node.name,
description: node.description || '',
thought_count: node.thought_count || 0,
color: this.generateColorFromString(node.name),
});
}
// Verbindung zum Elternknoten hinzufügen
if (parentId !== null) {
this.links.push({
source: parentId,
target: node.id
});
}
// Rekursiv für Kindknoten aufrufen
if (node.children && node.children.length > 0) {
this.processHierarchicalData(node.children, node.id);
}
});
}
generateColorFromString(str) {
// Erzeugt eine deterministische Farbe basierend auf dem String
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash);
}
// Verwende deterministische Farbe aus unserem Farbschema
const colors = [
'#4080ff', // primary-400
'#a040ff', // secondary-400
'#205cf5', // primary-500
'#8020f5', // secondary-500
'#1040e0', // primary-600
'#6010e0', // secondary-600
];
return colors[Math.abs(hash) % colors.length];
}
updateVisualization() {
// Starte die Visualisierung nur, wenn nicht mehr im Ladezustand
if (this.isLoading) return;
// Container leeren, wenn Diagramm neu erstellt wird
if (!this.svg) {
this.container.html('');
this.init();
}
// Performance-Optimierung: Deaktiviere Transition während des Datenladens
const useTransitions = false;
// Links (Edges) erstellen
this.linkElements = this.g.selectAll('.link')
.data(this.links)
.join(
enter => enter.append('line')
.attr('class', 'link')
.attr('stroke', '#ffffff30')
.attr('stroke-width', 2)
.attr('stroke-dasharray', d => d.relation_type === 'contradicts' ? '5,5' : null)
.attr('marker-end', d => d.relation_type === 'builds_upon' ? 'url(#arrowhead)' : null),
update => update
.attr('stroke', '#ffffff30')
.attr('stroke-dasharray', d => d.relation_type === 'contradicts' ? '5,5' : null)
.attr('marker-end', d => d.relation_type === 'builds_upon' ? 'url(#arrowhead)' : null),
exit => exit.remove()
);
// Pfeilspitze für gerichtete Beziehungen hinzufügen (falls noch nicht vorhanden)
if (!this.svg.select('defs').node()) {
const defs = this.svg.append('defs');
defs.append('marker')
.attr('id', 'arrowhead')
.attr('viewBox', '0 -5 10 10')
.attr('refX', 20)
.attr('refY', 0)
.attr('orient', 'auto')
.attr('markerWidth', 6)
.attr('markerHeight', 6)
.append('path')
.attr('d', 'M0,-5L10,0L0,5')
.attr('fill', '#ffffff50');
}
// Simplified Effekte definieren, falls noch nicht vorhanden
if (!this.svg.select('#glow').node()) {
const defs = this.svg.select('defs').size() ? this.svg.select('defs') : this.svg.append('defs');
// Glow-Effekt für Knoten
const filter = defs.append('filter')
.attr('id', 'glow')
.attr('x', '-50%')
.attr('y', '-50%')
.attr('width', '200%')
.attr('height', '200%');
filter.append('feGaussianBlur')
.attr('stdDeviation', '1')
.attr('result', 'blur');
filter.append('feComposite')
.attr('in', 'SourceGraphic')
.attr('in2', 'blur')
.attr('operator', 'over');
// Blur-Effekt für Schatten
const blurFilter = defs.append('filter')
.attr('id', 'blur')
.attr('x', '-50%')
.attr('y', '-50%')
.attr('width', '200%')
.attr('height', '200%');
blurFilter.append('feGaussianBlur')
.attr('stdDeviation', '1');
}
// Knoten-Gruppe erstellen/aktualisieren
const nodeGroups = this.g.selectAll('.node-group')
.data(this.nodes)
.join(
enter => {
const group = enter.append('g')
.attr('class', 'node-group')
.call(d3.drag()
.on('start', (event, d) => this.dragStarted(event, d))
.on('drag', (event, d) => this.dragged(event, d))
.on('end', (event, d) => this.dragEnded(event, d)));
// Hintergrundschatten für besseren Kontrast
group.append('circle')
.attr('class', 'node-shadow')
.attr('r', d => this.nodeRadius * 1.2)
.attr('fill', 'rgba(0, 0, 0, 0.3)')
.attr('filter', 'url(#blur)');
// Kreis für jeden Knoten
group.append('circle')
.attr('class', d => `node ${this.isNodeBookmarked(d.id) ? 'bookmarked' : ''}`)
.attr('r', this.nodeRadius)
.attr('fill', d => d.color || this.generateColorFromString(d.name))
.attr('stroke', d => this.isNodeBookmarked(d.id) ? '#FFD700' : '#ffffff50')
.attr('stroke-width', d => this.isNodeBookmarked(d.id) ? 3 : 2)
.attr('filter', 'url(#glow)');
// Text-Label mit besserem Kontrast
group.append('text')
.attr('class', 'node-label')
.attr('dy', '0.35em')
.attr('text-anchor', 'middle')
.attr('fill', '#ffffff')
.attr('stroke', 'rgba(0, 0, 0, 0.4)')
.attr('stroke-width', '0.7px')
.attr('paint-order', 'stroke')
.style('font-size', '12px')
.style('font-weight', '500')
.style('pointer-events', 'none')
.text(d => d.name.length > 12 ? d.name.slice(0, 10) + '...' : d.name);
// Interaktivität hinzufügen
group
.on('mouseover', (event, d) => this.nodeMouseover(event, d))
.on('mouseout', (event, d) => this.nodeMouseout(event, d))
.on('click', (event, d) => this.nodeClicked(event, d));
return group;
},
update => {
// Knoten aktualisieren
update.select('.node')
.attr('class', d => `node ${this.isNodeBookmarked(d.id) ? 'bookmarked' : ''}`)
.attr('fill', d => d.color || this.generateColorFromString(d.name))
.attr('stroke', d => this.isNodeBookmarked(d.id) ? '#FFD700' : '#ffffff50')
.attr('stroke-width', d => this.isNodeBookmarked(d.id) ? 3 : 2);
// Text aktualisieren
update.select('.node-label')
.text(d => d.name.length > 12 ? d.name.slice(0, 10) + '...' : d.name);
return update;
},
exit => exit.remove()
);
// Einzelne Elemente für direkten Zugriff speichern
this.nodeElements = this.g.selectAll('.node');
this.textElements = this.g.selectAll('.node-label');
// Performance-Optimierung: Weniger Simulationsschritte für schnellere Stabilisierung
this.simulation
.nodes(this.nodes)
.on('tick', () => this.ticked())
.alpha(0.3) // Reduzierter Wert für schnellere Stabilisierung
.alphaDecay(0.05); // Erhöhter Wert für schnellere Stabilisierung
this.simulation.force('link')
.links(this.links);
// Simulation neu starten
this.simulation.restart();
// Update connection counts
this.updateConnectionCounts();
}
ticked() {
// Linienpositionen aktualisieren
this.linkElements
.attr('x1', d => d.source.x)
.attr('y1', d => d.source.y)
.attr('x2', d => d.target.x)
.attr('y2', d => d.target.y);
// Knotenpositionen aktualisieren
this.g.selectAll('.node-group')
.attr('transform', d => `translate(${d.x}, ${d.y})`);
}
dragStarted(event, d) {
if (!event.active) this.simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
}
dragEnded(event, d) {
if (!event.active) this.simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
nodeMouseover(event, d) {
this.mouseoverNode = d;
// Tooltip anzeigen
if (this.tooltipEnabled) {
const isBookmarked = this.isNodeBookmarked(d.id);
const tooltipContent = `
<div class="p-2">
<strong>${d.name}</strong>
${d.description ? `<p class="text-sm text-gray-200 mt-1">${d.description}</p>` : ''}
<div class="text-xs text-gray-300 mt-1">
Gedanken: ${d.thought_count}
</div>
<div class="mt-2">
<button id="bookmark-button" class="px-2 py-1 text-xs rounded bg-gray-700 hover:bg-gray-600 text-white"
data-nodeid="${d.id}">
${isBookmarked ? '<i class="fas fa-bookmark mr-1"></i> Gemerkt' : '<i class="far fa-bookmark mr-1"></i> Merken'}
</button>
</div>
</div>
`;
this.tooltipDiv
.html(tooltipContent)
.style('left', (event.pageX + 10) + 'px')
.style('top', (event.pageY - 10) + 'px')
.transition()
.duration(200)
.style('opacity', 1);
// Event-Listener für den Bookmark-Button hinzufügen
document.getElementById('bookmark-button').addEventListener('click', (e) => {
e.stopPropagation();
const nodeId = e.currentTarget.getAttribute('data-nodeid');
const isNowBookmarked = this.toggleBookmark(nodeId);
// Button-Text aktualisieren
if (isNowBookmarked) {
e.currentTarget.innerHTML = '<i class="fas fa-bookmark mr-1"></i> Gemerkt';
} else {
e.currentTarget.innerHTML = '<i class="far fa-bookmark mr-1"></i> Merken';
}
});
}
// Knoten visuell hervorheben
d3.select(event.currentTarget).select('circle')
.transition()
.duration(200)
.attr('r', this.nodeRadius * 1.2)
.attr('stroke', this.isNodeBookmarked(d.id) ? '#FFD700' : '#ffffff');
}
nodeMouseout(event, d) {
this.mouseoverNode = null;
// Tooltip ausblenden
if (this.tooltipEnabled) {
this.tooltipDiv
.transition()
.duration(200)
.style('opacity', 0);
}
// Knoten-Stil zurücksetzen, wenn nicht ausgewählt
const nodeElement = d3.select(event.currentTarget).select('circle');
if (d !== this.selectedNode) {
const isBookmarked = this.isNodeBookmarked(d.id);
nodeElement
.transition()
.duration(200)
.attr('r', this.nodeRadius)
.attr('stroke', isBookmarked ? '#FFD700' : '#ffffff50')
.attr('stroke-width', isBookmarked ? 3 : 2);
}
}
nodeClicked(event, d) {
// Frühere Auswahl zurücksetzen
if (this.selectedNode && this.selectedNode !== d) {
this.g.selectAll('.node')
.filter(n => n === this.selectedNode)
.transition()
.duration(200)
.attr('r', this.nodeRadius)
.attr('stroke', '#ffffff50');
}
// Neue Auswahl hervorheben
if (this.selectedNode !== d) {
this.selectedNode = d;
d3.select(event.currentTarget).select('circle')
.transition()
.duration(200)
.attr('r', this.selectedNodeRadius)
.attr('stroke', '#ffffff');
}
// Callback mit Node-Daten aufrufen
this.onNodeClick(d);
}
showError(message) {
this.container.html(`
<div class="w-full text-center p-6">
<div class="mb-4 text-red-500">
<i class="fas fa-exclamation-triangle text-4xl"></i>
</div>
<p class="text-lg text-gray-200">${message}</p>
</div>
`);
}
// Fokussiert die Ansicht auf einen bestimmten Knoten
focusNode(nodeId) {
const node = this.nodes.find(n => n.id === nodeId);
if (!node) return;
// Simuliere einen Klick auf den Knoten
const nodeElement = this.g.selectAll('.node-group')
.filter(d => d.id === nodeId);
nodeElement.dispatch('click');
// Zentriere den Knoten in der Ansicht
const transform = d3.zoomIdentity
.translate(this.width / 2, this.height / 2)
.scale(1.2)
.translate(-node.x, -node.y);
this.svg.transition()
.duration(750)
.call(
d3.zoom().transform,
transform
);
}
// Filtert die Mindmap basierend auf einem Suchbegriff
filterBySearchTerm(searchTerm) {
if (!searchTerm || searchTerm.trim() === '') {
// Alle Knoten anzeigen
this.g.selectAll('.node-group')
.style('opacity', 1)
.style('pointer-events', 'all');
this.g.selectAll('.link')
.style('opacity', 1);
return;
}
const searchLower = searchTerm.toLowerCase();
const matchingNodes = this.nodes.filter(node =>
node.name.toLowerCase().includes(searchLower) ||
(node.description && node.description.toLowerCase().includes(searchLower))
);
const matchingNodeIds = new Set(matchingNodes.map(n => n.id));
// Passende Knoten hervorheben, andere ausblenden
this.g.selectAll('.node-group')
.style('opacity', d => matchingNodeIds.has(d.id) ? 1 : 0.2)
.style('pointer-events', d => matchingNodeIds.has(d.id) ? 'all' : 'none');
// Verbindungen zwischen passenden Knoten hervorheben
this.g.selectAll('.link')
.style('opacity', d =>
matchingNodeIds.has(d.source.id) && matchingNodeIds.has(d.target.id) ? 1 : 0.1
);
// Auf den ersten passenden Knoten fokussieren, wenn vorhanden
if (matchingNodes.length > 0) {
this.focusNode(matchingNodes[0].id);
}
}
/**
* Updates the thought_count property for each node based on existing connections
*/
updateConnectionCounts() {
// Reset all counts first
this.nodes.forEach(node => {
// Initialize thought_count if it doesn't exist
if (typeof node.thought_count !== 'number') {
node.thought_count = 0;
}
// Count connections for this node
const connectedNodes = this.getConnectedNodes(node);
node.thought_count = connectedNodes.length;
});
// Update UI to show counts
this.updateNodeLabels();
}
/**
* Updates the visual representation of node labels to include connection counts
*/
updateNodeLabels() {
if (!this.textElements) return;
this.textElements.text(d => {
if (d.thought_count > 0) {
return `${d.name} (${d.thought_count})`;
}
return d.name;
});
}
/**
* Adds a new connection between nodes and updates the counts
*/
addConnection(sourceNode, targetNode) {
if (!sourceNode || !targetNode) return false;
// Check if connection already exists
if (this.isConnected(sourceNode, targetNode)) return false;
// Add new connection
this.links.push({
source: sourceNode.id,
target: targetNode.id
});
// Update counts
this.updateConnectionCounts();
// Update visualization
this.updateVisualization();
return true;
}
// Lädt gemerkete Knoten aus dem LocalStorage
loadBookmarkedNodes() {
try {
const bookmarked = localStorage.getItem('bookmarkedNodes');
return bookmarked ? JSON.parse(bookmarked) : [];
} catch (error) {
console.error('Fehler beim Laden der gemerkten Knoten:', error);
return [];
}
}
// Speichert gemerkete Knoten im LocalStorage
saveBookmarkedNodes() {
try {
localStorage.setItem('bookmarkedNodes', JSON.stringify(this.bookmarkedNodes));
} catch (error) {
console.error('Fehler beim Speichern der gemerkten Knoten:', error);
}
}
// Prüft, ob ein Knoten gemerkt ist
isNodeBookmarked(nodeId) {
return this.bookmarkedNodes.includes(nodeId);
}
// Merkt einen Knoten oder hebt die Markierung auf
toggleBookmark(nodeId) {
const index = this.bookmarkedNodes.indexOf(nodeId);
if (index === -1) {
// Node hinzufügen
this.bookmarkedNodes.push(nodeId);
this.updateNodeAppearance(nodeId, true);
} else {
// Node entfernen
this.bookmarkedNodes.splice(index, 1);
this.updateNodeAppearance(nodeId, false);
}
// Änderungen speichern
this.saveBookmarkedNodes();
// Event auslösen für andere Komponenten
const event = new CustomEvent('nodeBookmarkToggled', {
detail: {
nodeId: nodeId,
isBookmarked: index === -1
}
});
document.dispatchEvent(event);
return index === -1; // true wenn jetzt gemerkt, false wenn Markierung aufgehoben
}
// Aktualisiert das Aussehen eines Knotens basierend auf Bookmark-Status
updateNodeAppearance(nodeId, isBookmarked) {
this.g.selectAll('.node-group')
.filter(d => d.id === nodeId)
.select('.node')
.classed('bookmarked', isBookmarked)
.attr('stroke', isBookmarked ? '#FFD700' : '#ffffff50')
.attr('stroke-width', isBookmarked ? 3 : 2);
}
// Aktualisiert das Aussehen aller gemerkten Knoten
updateAllBookmarkedNodes() {
this.g.selectAll('.node-group')
.each((d) => {
const isBookmarked = this.isNodeBookmarked(d.id);
this.updateNodeAppearance(d.id, isBookmarked);
});
}
/**
* Gibt alle direkt verbundenen Knoten eines Knotens zurück
* @param {Object} node - Der Knoten, für den die Verbindungen gesucht werden
* @returns {Array} Array der verbundenen Knotenobjekte
*/
getConnectedNodes(node) {
if (!node || !this.links || !this.nodes) return [];
const nodeId = node.id;
const connectedIds = new Set();
this.links.forEach(link => {
if (link.source === nodeId || (link.source && link.source.id === nodeId)) {
connectedIds.add(link.target.id ? link.target.id : link.target);
}
if (link.target === nodeId || (link.target && link.target.id === nodeId)) {
connectedIds.add(link.source.id ? link.source.id : link.source);
}
});
return this.nodes.filter(n => connectedIds.has(n.id));
}
}
// Exportiere die Klasse für die Verwendung in anderen Modulen
window.MindMapVisualization = MindMapVisualization;

File diff suppressed because it is too large Load Diff

1904
static/mindmap.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -18,11 +18,11 @@ class NeuralNetworkBackground {
// Standardkonfiguration mit subtileren Werten
this.config = {
nodeCount: 10, // Weniger Knoten
nodeCount: 50, // Weniger Knoten
nodeSize: 1.2, // Kleinere Knoten
connectionDistance: 150, // Reduzierte Verbindungsdistanz
connectionOpacity: 0.3, // Sanftere Verbindungslinien
clusterCount: 7, // Weniger Cluster
clusterCount: 2, // Weniger Cluster
clusterRadius: 380, // Größerer Cluster-Radius für mehr Verteilung
animationSpeed: 0.25, // Langsamere Animation
flowDensity: 0.05, // Deutlich weniger Flussanimationen
@@ -1106,66 +1106,4 @@ window.addEventListener('beforeunload', () => {
if (window.neuralNetworkBackground) {
window.neuralNetworkBackground.destroy();
}
});
function applyNeuralNetworkStyle(cy) {
cy.style()
.selector('node')
.style({
'label': 'data(label)',
'text-valign': 'center',
'text-halign': 'center',
'color': 'data(fontColor)',
'text-outline-width': 2,
'text-outline-color': 'rgba(0,0,0,0.8)',
'text-outline-opacity': 0.9,
'font-size': 'data(fontSize)',
'font-weight': '500',
'text-margin-y': 8,
'width': function(ele) {
if (ele.data('isCenter')) return 120;
return ele.data('neuronSize') ? ele.data('neuronSize') * 10 : 80;
},
'height': function(ele) {
if (ele.data('isCenter')) return 120;
return ele.data('neuronSize') ? ele.data('neuronSize') * 10 : 80;
},
'background-color': 'data(color)',
'background-opacity': 0.9,
'border-width': 2,
'border-color': '#ffffff',
'border-opacity': 0.8,
'shape': 'ellipse',
'transition-property': 'background-color, background-opacity, border-width',
'transition-duration': '0.3s',
'transition-timing-function': 'ease-in-out'
})
.selector('edge')
.style({
'width': function(ele) {
return ele.data('strength') ? ele.data('strength') * 3 : 1;
},
'curve-style': 'bezier',
'line-color': function(ele) {
const sourceColor = ele.source().data('color');
return sourceColor || '#8a8aaa';
},
'line-opacity': function(ele) {
return ele.data('strength') ? ele.data('strength') * 0.8 : 0.4;
},
'line-style': function(ele) {
const strength = ele.data('strength');
if (!strength) return 'solid';
if (strength <= 0.4) return 'dotted';
if (strength <= 0.6) return 'dashed';
return 'solid';
},
'target-arrow-shape': 'none',
'source-endpoint': '0% 50%',
'target-endpoint': '100% 50%',
'transition-property': 'line-opacity, width',
'transition-duration': '0.3s',
'transition-timing-function': 'ease-in-out'
})
.update();
}
});

File diff suppressed because it is too large Load Diff

View File

@@ -1,46 +0,0 @@
{% extends "base.html" %}
{% block title %}Datenbank aktualisieren{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-10">
<div class="bg-gray-800 bg-opacity-70 rounded-lg p-6 mb-6">
<h1 class="text-2xl font-bold text-purple-400 mb-4">Datenbank aktualisieren</h1>
{% if message %}
<div class="mb-6 p-4 rounded-lg {{ 'bg-green-800 bg-opacity-50' if success else 'bg-red-800 bg-opacity-50' }}">
<p class="text-white">{{ message }}</p>
</div>
{% endif %}
<div class="mb-6">
<p class="text-gray-300 mb-4">
Diese Funktion aktualisiert die Datenbankstruktur, um mit dem aktuellen Datenmodell kompatibel zu sein.
Dabei werden folgende Änderungen vorgenommen:
</p>
<ul class="list-disc pl-6 text-gray-300 mb-6">
<li>Hinzufügen von <code>bio</code>, <code>location</code>, <code>website</code>, <code>avatar</code> und <code>last_login</code> zur Benutzer-Tabelle</li>
</ul>
<div class="bg-yellow-800 bg-opacity-30 p-4 rounded-lg mb-6">
<p class="text-yellow-200">
<i class="fas fa-exclamation-triangle mr-2"></i>
<strong>Warnung:</strong> Bitte stelle sicher, dass du ein Backup der Datenbank erstellt hast, bevor du fortfährst.
</p>
</div>
</div>
<form method="POST" action="{{ url_for('admin_update_database') }}">
<div class="flex justify-between">
<a href="{{ url_for('index') }}" class="px-4 py-2 bg-gray-700 text-white rounded-lg hover:bg-gray-600">
Zurück zur Startseite
</a>
<button type="submit" class="px-4 py-2 bg-purple-700 text-white rounded-lg hover:bg-purple-600">
Datenbank aktualisieren
</button>
</div>
</form>
</div>
</div>
{% endblock %}

View File

@@ -6,7 +6,8 @@
<title>Systades - {% block title %}{% endblock %}</title>
<!-- Favicon -->
<link rel="icon" href="{{ url_for('static', filename='img/neuron-favicon.svg') }}" type="image/svg+xml">
<link rel="icon" href="{{ url_for('static', filename='img/favicon.svg') }}" type="image/svg+xml">
<link rel="icon" href="{{ url_for('static', filename='img/favicon.ico') }}" sizes="any">
<!-- Meta Tags -->
<meta name="description" content="Eine interaktive Plattform zum Visualisieren, Erforschen und Teilen von Wissen">
@@ -58,20 +59,6 @@
800: '#0e1220',
900: '#0a0e19'
}
},
keyframes: {
float: {
'0%, 100%': { transform: 'translateY(0)' },
'50%': { transform: 'translateY(-5px)' }
},
'bounce-slow': {
'0%, 100%': { transform: 'translateY(0)' },
'50%': { transform: 'translateY(-8px)' }
}
},
animation: {
float: 'float 3s ease-in-out infinite',
'bounce-slow': 'bounce-slow 2s ease-in-out infinite'
}
}
}
@@ -82,8 +69,8 @@
<link href="{{ url_for('static', filename='fonts/inter.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='fonts/jetbrains-mono.css') }}" rel="stylesheet">
<!-- Font Awesome vom CDN -->
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" rel="stylesheet">
<!-- Icons - Self-hosted Font Awesome -->
<link href="{{ url_for('static', filename='css/all.min.css') }}" rel="stylesheet">
<!-- Assistent CSS -->
<link href="{{ url_for('static', filename='css/assistant.css') }}" rel="stylesheet">
@@ -100,9 +87,6 @@
<!-- Neural Network Background CSS -->
<link href="{{ url_for('static', filename='css/neural-network-background.css') }}" rel="stylesheet">
<!-- Mindmap CSS -->
<link href="{{ url_for('static', filename='css/mindmap.css') }}" rel="stylesheet">
<!-- D3.js für Visualisierungen -->
<script src="https://d3js.org/d3.v7.min.js"></script>
@@ -111,6 +95,12 @@
<!-- ChatGPT Assistant -->
<script src="{{ url_for('static', filename='js/modules/chatgpt-assistant.js') }}"></script>
<!-- MindMap Visualization Module -->
<script src="{{ url_for('static', filename='js/modules/mindmap.js') }}"></script>
<!-- MindMap Page Module -->
<script src="{{ url_for('static', filename='js/modules/mindmap-page.js') }}"></script>
<!-- Neural Network Background Script -->
<script src="{{ url_for('static', filename='neural-network-background.js') }}"></script>
@@ -121,20 +111,18 @@
<!-- Seitenspezifische Styles -->
{% block extra_css %}{% endblock %}
<!-- Custom dark/light mode styles -->
<!-- Custom dark mode styles -->
<!-- ► ► FarbToken strikt getrennt ◄ ◄ -->
<style>
/* LightMode */
:root {
--bg-primary:#f8fafc;
--bg-secondary:#f1f5f9;
--bg-primary:#f4f6fa;
--bg-secondary:#e9ecf3;
--text-primary:#232837;
--text-secondary:#475569;
--accent-primary:#7c3aed;
--accent-secondary:#8b5cf6;
--glow-effect:0 0 8px rgba(139,92,246,.08);
background-image: linear-gradient(to bottom right, rgba(248, 250, 252, 0.8), rgba(241, 245, 249, 0.8));
background-attachment: fixed;
}
/* DarkMode */
.dark {
@@ -148,8 +136,7 @@
}
body {
@apply min-h-screen bg-[color:var(--bg-primary)] text-[color:var(--text-primary)];
transition: background-color 0.5s ease-in-out, color 0.3s ease-in-out, background-image 0.5s ease-in-out;
@apply min-h-screen bg-[color:var(--bg-primary)] text-[color:var(--text-primary)] transition-colors duration-300;
}
/* Utilities */
@@ -162,151 +149,6 @@
.glass-navbar { @apply glass-morphism border backdrop-blur-xl; }
.light .glass-navbar { background-color:rgba(255,255,255,.8); border-color:rgba(0,0,0,.05); }
.dark .glass-navbar { background-color:rgba(10,14,25,.8); border-color:rgba(255,255,255,.05); }
/* Light-Mode spezifische Stile */
body:not(.dark) {
background-color: var(--bg-primary);
color: var(--text-primary);
}
.nav-link-light {
color: var(--text-secondary);
transition: all 0.3s ease;
}
.nav-link-light:hover {
color: var(--text-primary);
background-color: rgba(126, 34, 206, 0.1);
}
.nav-link-light-active {
color: var(--accent-primary);
background-color: rgba(126, 34, 206, 0.15);
font-weight: 500;
}
/* Kartendesign im Light-Mode */
body:not(.dark) .card {
background-color: white;
border: 1px solid #e5e7eb;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
body:not(.dark) .card:hover {
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
a:hover {
color: var(--light-primary-hover);
}
/* Light Mode Buttons */
body:not(.dark) .btn,
body:not(.dark) button:not(.toggle) {
background: linear-gradient(135deg, #7c3aed, #6d28d9);
color: white !important;
border: none;
box-shadow: 0 2px 4px rgba(124, 58, 237, 0.25);
border-radius: 8px;
padding: 0.625rem 1.25rem;
transition: all 0.2s ease;
font-weight: 600;
letter-spacing: 0.02em;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
body:not(.dark) .btn:hover,
body:not(.dark) button:not(.toggle):hover {
background: linear-gradient(135deg, #8b5cf6, #7c3aed);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(124, 58, 237, 0.3);
color: white !important;
}
/* KI-Chat Button im Light-Mode */
body:not(.dark) [onclick*="MindMap.assistant.toggleAssistant"] {
background: linear-gradient(135deg, #7c3aed, #4f46e5);
color: white !important;
font-weight: 500;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
body:not(.dark) [onclick*="MindMap.assistant.toggleAssistant"]:hover {
background: linear-gradient(135deg, #8b5cf6, #6366f1);
box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3);
}
/* Style improvements for the theme toggle button */
.theme-toggle {
position: relative;
width: 48px;
height: 24px;
border-radius: 24px;
padding: 2px;
cursor: pointer;
transition: all 0.3s ease;
overflow: hidden;
}
body.dark .theme-toggle {
background: linear-gradient(to right, #7c3aed, #3b82f6);
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.3), 0 0 10px rgba(124, 58, 237, 0.3);
border: 1px solid rgba(255, 255, 255, 0.1);
}
body:not(.dark) .theme-toggle {
background: linear-gradient(to right, #8b5cf6, #60a5fa);
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1), 0 0 10px rgba(124, 58, 237, 0.15);
border: 1px solid rgba(0, 0, 0, 0.05);
}
.theme-toggle::after {
content: '';
position: absolute;
width: 20px;
height: 20px;
border-radius: 50%;
top: 2px;
transition: all 0.3s ease;
z-index: 2;
}
body.dark .theme-toggle::after {
background: #f1f5f9 url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%237c3aed' width='14' height='14'%3E%3Cpath d='M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z'%3E%3C/path%3E%3C/svg%3E") no-repeat center center;
transform: translateX(24px);
box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
}
body:not(.dark) .theme-toggle::after {
background: #fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23f59e0b' width='14' height='14'%3E%3Cpath d='M12 2.25a.75.75 0 01.75.75v2.25a.75.75 0 01-1.5 0V3a.75.75 0 01.75-.75zM7.5 12a4.5 4.5 0 119 0 4.5 4.5 0 01-9 0zM18.894 6.166a.75.75 0 00-1.06-1.06l-1.591 1.59a.75.75 0 101.06 1.061l1.591-1.59zM21.75 12a.75.75 0 01-.75.75h-2.25a.75.75 0 010-1.5H21a.75.75 0 01.75.75zM17.834 18.894a.75.75 0 001.06-1.06l-1.59-1.591a.75.75 0 10-1.061 1.06l1.59 1.591zM12 18a.75.75 0 01.75.75V21a.75.75 0 01-1.5 0v-2.25A.75.75 0 0112 18zM7.758 17.303a.75.75 0 00-1.061-1.06l-1.591 1.59a.75.75 0 001.06 1.061l1.591-1.59zM6 12a.75.75 0 01-.75.75H3a.75.75 0 010-1.5h2.25A.75.75 0 016 12zM6.697 7.757a.75.75 0 001.06-1.06l-1.59-1.591a.75.75 0 00-1.061 1.06l1.59 1.591z'%3E%3C/path%3E%3C/svg%3E") no-repeat center center;
transform: translateX(2px);
box-shadow: 0 0 8px rgba(124, 58, 237, 0.2);
}
.theme-toggle:hover::after {
box-shadow: 0 0 12px rgba(124, 58, 237, 0.4);
}
/* Fixes for light mode button text colors */
body:not(.dark) .btn-primary {
color: white !important;
}
/* Fix for KI-Chat container */
#chatgpt-assistant {
position: fixed;
bottom: 1.5rem;
right: 1.5rem;
z-index: 100;
}
.chat-assistant {
max-height: 80vh !important;
}
.chat-assistant .chat-messages {
max-height: calc(80vh - 160px) !important;
}
</style>
</head>
<body data-page="{{ request.endpoint }}" class="relative overflow-x-hidden dark bg-gray-900 text-white" x-data="{
@@ -316,17 +158,6 @@
showSettingsModal: false,
init() {
this.initDarkMode();
},
initDarkMode() {
// Lade zuerst den Wert aus dem localStorage (client-seitig)
const storedMode = localStorage.getItem('colorMode');
if (storedMode) {
this.darkMode = storedMode === 'dark';
}
// Dann hole die Server-Einstellung, die Vorrang hat
this.fetchDarkModeFromSession();
},
@@ -336,7 +167,7 @@
.then(data => {
if (data.success) {
this.darkMode = data.darkMode === 'true';
this.applyDarkMode();
document.querySelector('html').classList.toggle('dark', this.darkMode);
}
})
.catch(error => {
@@ -344,17 +175,10 @@
});
},
applyDarkMode() {
document.querySelector('html').classList.toggle('dark', this.darkMode);
document.querySelector('body').classList.toggle('dark', this.darkMode);
localStorage.setItem('colorMode', this.darkMode ? 'dark' : 'light');
},
toggleDarkMode() {
this.darkMode = !this.darkMode;
this.applyDarkMode();
document.querySelector('html').classList.toggle('dark', this.darkMode);
// Server über Änderung informieren
fetch('/api/set_dark_mode', {
method: 'POST',
headers: {
@@ -365,10 +189,12 @@
.then(response => response.json())
.then(data => {
if (data.success) {
// Event auslösen für andere Komponenten
localStorage.setItem('darkMode', this.darkMode ? 'dark' : 'light');
document.dispatchEvent(new CustomEvent('darkModeToggled', {
detail: { isDark: this.darkMode }
}));
} else {
console.error('Fehler beim Speichern der Dark Mode-Einstellung:', data.error);
}
})
.catch(error => {
@@ -384,7 +210,6 @@
<div class="container mx-auto flex justify-between items-center">
<!-- Logo -->
<a href="{{ url_for('index') }}" class="flex items-center group">
<img src="{{ url_for('static', filename='img/neuron-logo.svg') }}" alt="Systades Logo" class="w-8 h-8 mr-2 transform transition-transform group-hover:scale-110">
<span class="text-2xl font-bold gradient-text transform transition-transform group-hover:scale-105">Systades</span>
</a>
@@ -416,7 +241,7 @@
class="nav-link flex items-center"
x-bind:class="darkMode
? 'bg-gradient-to-r from-purple-900/90 to-indigo-800/90 text-white font-medium px-4 py-2 rounded-xl hover:shadow-lg hover:shadow-purple-800/30 transition-all duration-300'
: 'bg-gradient-to-r from-purple-600 to-indigo-500 text-white font-medium px-4 py-2 rounded-xl hover:shadow-md transition-all duration-300'">
: 'bg-gradient-to-r from-purple-600/30 to-indigo-500/30 text-gray-800 font-medium px-4 py-2 rounded-xl hover:shadow-md transition-all duration-300'">
<i class="fa-solid fa-robot mr-2"></i>KI-Chat
</button>
{% if current_user.is_authenticated %}
@@ -432,14 +257,25 @@
<!-- Rechte Seite -->
<div class="flex items-center space-x-4">
<!-- Dark/Light Mode Schalter -->
<button
@click="toggleDarkMode()"
class="theme-toggle relative w-12 h-6 rounded-full transition-all duration-300 flex items-center overflow-hidden"
aria-label="Dark Mode umschalten"
>
<span class="sr-only" x-text="darkMode ? 'Zum Light Mode wechseln' : 'Zum Dark Mode wechseln'"></span>
</button>
<!-- Dark Mode Toggle Switch -->
<div class="flex items-center cursor-pointer" @click="toggleDarkMode">
<div class="relative w-12 h-6">
<input type="checkbox" id="darkModeToggle" class="sr-only" x-model="darkMode">
<div class="block w-12 h-6 rounded-full transition-colors duration-300"
x-bind:class="darkMode ? 'bg-purple-800/50' : 'bg-gray-400/50'"></div>
<div class="dot absolute left-1 top-1 w-4 h-4 rounded-full transition-transform duration-300 shadow-md"
x-bind:class="darkMode ? 'bg-purple-600 transform translate-x-6' : 'bg-white'"></div>
</div>
<div class="ml-3 hidden sm:block"
x-bind:class="darkMode ? 'text-white/90' : 'text-gray-700'">
<span x-text="darkMode ? 'Dunkel' : 'Hell'"></span>
</div>
<div class="ml-2 sm:hidden"
x-bind:class="darkMode ? 'text-white/90' : 'text-gray-700'">
<i class="fa-solid" :class="darkMode ? 'fa-sun' : 'fa-moon'"></i>
</div>
</div>
<!-- Profil-Link oder Login -->
{% if current_user.is_authenticated %}
<div class="relative" x-data="{ open: false }">
@@ -453,21 +289,12 @@
{% if current_user.avatar %}
<img src="{{ current_user.avatar }}" alt="{{ current_user.username }}" class="w-full h-full object-cover">
{% else %}
<svg width="100%" height="100%" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="100" cy="100" r="98" fill="url(#user-gradient)" stroke="#7C3AED" stroke-width="4"/>
<circle cx="100" cy="80" r="36" fill="white"/>
<path d="M100 140C77.9086 140 60 157.909 60 180H140C140 157.909 122.091 140 100 140Z" fill="white"/>
<defs>
<linearGradient id="user-gradient" x1="0" y1="0" x2="200" y2="200" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#8B5CF6"/>
<stop offset="1" stop-color="#3B82F6"/>
</linearGradient>
</defs>
</svg>
{{ current_user.username[0].upper() }}
{% endif %}
</div>
<span class="hidden md:block">{{ current_user.username }}</span>
<i class="fas fa-chevron-down text-xs opacity-60 ml-1.5"></i>
<span class="text-sm hidden lg:block">{{ current_user.username }}</span>
<i class="fa-solid fa-chevron-down text-xs hidden lg:block transition-transform duration-200"
x-bind:class="open ? 'transform rotate-180' : ''"></i>
</button>
<!-- Dropdown-Menü -->
@@ -585,7 +412,7 @@
class="block w-full text-left py-3.5 px-4 rounded-xl transition-all duration-200 flex items-center"
x-bind:class="darkMode
? 'bg-gradient-to-r from-purple-600/30 to-blue-500/30 text-white hover:from-purple-600/40 hover:to-blue-500/40'
: 'bg-gradient-to-r from-purple-600 to-blue-500 text-white hover:from-purple-600/90 hover:to-blue-500/90'">
: 'bg-gradient-to-r from-purple-500/10 to-blue-400/10 text-gray-900 hover:from-purple-500/20 hover:to-blue-400/20'">
<i class="fa-solid fa-robot w-5 mr-3"></i>KI-Chat
</button>
{% if current_user.is_authenticated %}
@@ -651,10 +478,6 @@
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
Mindmap
</a>
<a href="{{ url_for('search_thoughts_page') }}" class="text-sm transition-all duration-200"
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
Suche
</a>
{% if current_user.is_authenticated %}
<a href="{{ url_for('profile') }}" class="text-sm transition-all duration-200"
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
@@ -734,286 +557,58 @@
<!-- Hilfsscripts -->
{% block scripts %}{% endblock %}
{% block extra_js %}{% endblock %}
<!-- ChatGPT Initialisierung -->
<!-- KI-Chat Initialisierung -->
<script>
// Prüfe, ob ChatGPTAssistant bereits existiert
if (typeof ChatGPTAssistant === 'undefined') {
class ChatGPTAssistant {
constructor() {
this.chatContainer = null;
this.messages = [];
this.isOpen = false;
}
// Initialisiere den ChatGPT-Assistenten direkt, um sicherzustellen,
// dass er auf jeder Seite verfügbar ist, selbst wenn MindMap nicht geladen ist
document.addEventListener('DOMContentLoaded', function() {
// Prüfen, ob der Assistent bereits durch MindMap initialisiert wurde
if (!window.MindMap || !window.MindMap.assistant) {
console.log('KI-Assistent wird direkt initialisiert...');
const assistant = new ChatGPTAssistant();
assistant.init();
init() {
// Chat-Container erstellen, falls noch nicht vorhanden
if (!document.getElementById('chat-assistant-container')) {
this.createChatInterface();
}
// Event-Listener für Chat-Button
const chatButton = document.getElementById('chat-assistant-button');
if (chatButton) {
chatButton.addEventListener('click', () => this.toggleChat());
}
// Event-Listener für Senden-Button
const sendButton = document.getElementById('chat-send-button');
if (sendButton) {
sendButton.addEventListener('click', () => this.sendMessage());
}
// Event-Listener für Eingabefeld (Enter-Taste)
const inputField = document.getElementById('chat-input');
if (inputField) {
inputField.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
this.sendMessage();
}
});
}
console.log('KI-Assistent erfolgreich initialisiert');
}
createChatInterface() {
// Chat-Button erstellen
const chatButton = document.createElement('button');
chatButton.id = 'chat-assistant-button';
chatButton.className = 'fixed bottom-6 right-6 bg-primary-600 text-white rounded-full p-4 shadow-lg z-50 hover:bg-primary-700 transition-all';
chatButton.innerHTML = '<i class="fas fa-robot text-xl"></i>';
document.body.appendChild(chatButton);
// Chat-Container erstellen
const chatContainer = document.createElement('div');
chatContainer.id = 'chat-assistant-container';
chatContainer.className = 'fixed bottom-24 right-6 w-80 md:w-96 bg-white dark:bg-gray-800 rounded-xl shadow-xl z-50 flex flex-col transition-all duration-300 transform scale-0 origin-bottom-right';
chatContainer.style.height = '500px';
chatContainer.style.maxHeight = '70vh';
// Chat-Header
chatContainer.innerHTML = `
<div class="p-4 border-b dark:border-gray-700 flex justify-between items-center">
<h3 class="font-bold text-gray-800 dark:text-white">Systades Assistent</h3>
<button id="chat-close-button" class="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
<i class="fas fa-times"></i>
</button>
</div>
<div id="chat-messages" class="flex-1 overflow-y-auto p-4 space-y-4"></div>
<div class="p-4 border-t dark:border-gray-700">
<div class="flex space-x-2">
<input id="chat-input" type="text" placeholder="Frage stellen..." class="flex-1 px-4 py-2 rounded-lg border dark:border-gray-700 dark:bg-gray-700 dark:text-white focus:outline-none focus:ring-2 focus:ring-primary-500">
<button id="chat-send-button" class="bg-primary-600 text-white px-4 py-2 rounded-lg hover:bg-primary-700 transition-all">
<i class="fas fa-paper-plane"></i>
</button>
</div>
</div>
`;
document.body.appendChild(chatContainer);
this.chatContainer = chatContainer;
// Event-Listener für Schließen-Button
const closeButton = document.getElementById('chat-close-button');
if (closeButton) {
closeButton.addEventListener('click', () => this.toggleChat());
}
}
toggleChat() {
this.isOpen = !this.isOpen;
if (this.isOpen) {
this.chatContainer.classList.remove('scale-0');
this.chatContainer.classList.add('scale-100');
} else {
this.chatContainer.classList.remove('scale-100');
this.chatContainer.classList.add('scale-0');
}
}
async sendMessage() {
const inputField = document.getElementById('chat-input');
const messageText = inputField.value.trim();
if (!messageText) return;
// Benutzer-Nachricht anzeigen
this.addMessage('user', messageText);
inputField.value = '';
// Lade-Indikator anzeigen
this.addMessage('assistant', '...', 'loading-message');
try {
// API-Anfrage senden
const response = await fetch('/api/assistant', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
messages: this.messages.map(msg => ({
role: msg.role,
content: msg.content
}))
})
});
const data = await response.json();
// Lade-Nachricht entfernen
const loadingMessage = document.getElementById('loading-message');
if (loadingMessage) {
loadingMessage.remove();
}
if (data.error) {
this.addMessage('assistant', 'Entschuldigung, es ist ein Fehler aufgetreten: ' + data.error);
} else {
this.addMessage('assistant', data.response);
}
} catch (error) {
console.error('Fehler bei der API-Anfrage:', error);
// Lade-Nachricht entfernen
const loadingMessage = document.getElementById('loading-message');
if (loadingMessage) {
loadingMessage.remove();
}
this.addMessage('assistant', 'Entschuldigung, es ist ein Fehler aufgetreten. Bitte versuche es später erneut.');
}
}
addMessage(role, content, id = null) {
const messagesContainer = document.getElementById('chat-messages');
// Nachricht zum Array hinzufügen (außer Lade-Nachrichten)
if (id !== 'loading-message') {
this.messages.push({ role, content });
}
// Nachricht zum DOM hinzufügen
const messageElement = document.createElement('div');
messageElement.className = `p-3 rounded-lg ${role === 'user' ? 'bg-primary-100 dark:bg-primary-900/30 ml-6' : 'bg-gray-100 dark:bg-gray-700 mr-6'}`;
if (id) {
messageElement.id = id;
}
messageElement.innerHTML = `
<div class="flex items-start">
<div class="w-8 h-8 rounded-full flex items-center justify-center ${role === 'user' ? 'bg-primary-600' : 'bg-gray-600'} text-white mr-2">
<i class="fas ${role === 'user' ? 'fa-user' : 'fa-robot'} text-xs"></i>
</div>
<div class="flex-1 text-sm ${role === 'user' ? 'text-gray-800 dark:text-gray-200' : 'text-gray-700 dark:text-gray-300'}">
${content}
</div>
</div>
`;
messagesContainer.appendChild(messageElement);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
// Speichere in window.MindMap, falls es existiert, oder erstelle es
if (!window.MindMap) {
window.MindMap = {};
}
window.MindMap.assistant = assistant;
}
// Initialisiere den ChatGPT-Assistenten direkt
document.addEventListener('DOMContentLoaded', function() {
// Prüfen, ob der Assistent bereits durch MindMap initialisiert wurde
if (!window.MindMap || !window.MindMap.assistant) {
console.log('KI-Assistent wird direkt initialisiert...');
const assistant = new ChatGPTAssistant();
assistant.init();
// Speichere in window.MindMap, falls es existiert, oder erstelle es
if (!window.MindMap) {
window.MindMap = {};
}
window.MindMap.assistant = assistant;
}
});
}
});
</script>
<!-- Dark/Light-Mode vereinheitlicht -->
<!-- Dark/Light-Mode persistent und robust -->
<script>
// Globaler Zugriff für externe Skripte
window.MindMap = window.MindMap || {};
// Funktion zum Anwenden des Dark Mode, strikt getrennt
function applyDarkModeClasses(isDarkMode) {
if (isDarkMode) {
document.documentElement.classList.add('dark');
document.body.classList.add('dark');
localStorage.setItem('colorMode', 'dark');
} else {
document.documentElement.classList.remove('dark');
document.body.classList.remove('dark');
localStorage.setItem('colorMode', 'light');
}
// Alpine.js darkMode-Variable aktualisieren, falls zutreffend
const appEl = document.querySelector('body');
if (appEl && appEl.__x) {
appEl.__x.$data.darkMode = isDarkMode;
}
// Event für andere Komponenten auslösen
document.dispatchEvent(new CustomEvent('darkModeToggled', {
detail: { isDark: isDarkMode }
}));
}
window.MindMap.toggleDarkMode = function() {
const isDark = document.documentElement.classList.contains('dark');
const newIsDark = !isDark;
// DOM aktualisieren
applyDarkModeClasses(newIsDark);
// Server aktualisieren
fetch('/api/set_dark_mode', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ darkMode: newIsDark })
})
.catch(console.error);
};
// Initialisierung beim Laden
document.addEventListener('DOMContentLoaded', function() {
// Reihenfolge der Prüfungen: Serverseitige Einstellung > Lokale Einstellung > Browser-Präferenz
// 1. Zuerst lokale Einstellung prüfen
const storedMode = localStorage.getItem('colorMode');
if (storedMode) {
applyDarkModeClasses(storedMode === 'dark');
} else {
// 2. Fallback auf Browser-Präferenz
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
applyDarkModeClasses(prefersDark);
}
// 3. Serverseitige Einstellung abrufen und anwenden
fetch('/api/get_dark_mode')
.then(response => response.json())
.then(data => {
if (data.success) {
const serverDarkMode = data.darkMode === true || data.darkMode === 'true';
applyDarkModeClasses(serverDarkMode);
}
})
.catch(error => console.error('Fehler beim Abrufen des Dark Mode Status:', error));
// Listener für Änderungen der Browser-Präferenz
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
if (localStorage.getItem('colorMode') === null) {
applyDarkModeClasses(e.matches);
(function() {
function applyMode(mode) {
if (mode === 'dark') {
document.documentElement.classList.add('dark');
localStorage.setItem('colorMode', 'dark');
} else {
document.documentElement.classList.remove('dark');
localStorage.setItem('colorMode', 'light');
}
}
// Beim Laden: Präferenz aus localStorage oder System übernehmen
const stored = localStorage.getItem('colorMode');
if (stored === 'dark' || stored === 'light') {
applyMode(stored);
} else {
// Systempräferenz als Fallback
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
applyMode(prefersDark ? 'dark' : 'light');
}
// Umschalter für alle Mode-Toggles
window.toggleColorMode = function() {
const isDark = document.documentElement.classList.contains('dark');
applyMode(isDark ? 'light' : 'dark');
};
// Optional: globales Event für andere Skripte
window.addEventListener('storage', function(e) {
if (e.key === 'colorMode') applyMode(e.newValue);
});
});
})();
</script>
</body>
</html>

View File

@@ -1,192 +0,0 @@
{% extends 'base.html' %}
{% block title %}{{ category.title }} - Forum{% endblock %}
{% block extra_css %}
<style>
.thread-item {
transition: all 0.2s ease;
}
.thread-item:hover {
transform: translateX(2px);
}
.thread-pinned {
border-left-width: 4px;
}
</style>
{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-8">
<!-- Breadcrumb Navigation -->
<div class="mb-6 flex items-center text-sm">
<a href="{{ url_for('community') }}" class="opacity-75 hover:opacity-100 transition-opacity">
<i class="fas fa-home mr-1"></i> Forum
</a>
<span class="mx-2 opacity-50">/</span>
<span class="font-medium">{{ category.title }}</span>
</div>
<!-- Kategorie-Header -->
<div class="mb-8 flex flex-wrap items-center justify-between gap-4">
<div class="flex items-center">
<!-- Kategorie-Icon -->
<div class="w-12 h-12 rounded-xl mr-4 flex items-center justify-center text-white"
style="background-color: {{ node.color_code or '#6d28d9' }}">
<i class="fas {{ node.icon or 'fa-folder' }} text-2xl"></i>
</div>
<!-- Kategorie-Info -->
<div>
<h1 class="text-2xl font-bold">{{ category.title }}</h1>
<p class="opacity-75">{{ category.description }}</p>
</div>
</div>
<!-- Neues Thema erstellen -->
<a href="{{ url_for('new_post', category_id=category.id) }}"
class="px-5 py-2.5 rounded-lg transition-all duration-300 flex items-center"
x-bind:class="darkMode
? 'bg-indigo-700 hover:bg-indigo-600 text-white'
: 'bg-indigo-500 hover:bg-indigo-600 text-white'">
<i class="fas fa-plus-circle mr-2"></i>
Neues Thema
</a>
</div>
<!-- Threads anzeigen -->
<div class="mb-8 rounded-xl overflow-hidden"
x-bind:class="darkMode ? 'bg-gray-800/60 border border-white/10' : 'bg-white border border-gray-200'">
<!-- Header -->
<div class="p-4 border-b" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
<div class="grid grid-cols-12 gap-4">
<div class="col-span-7 font-medium">Thema</div>
<div class="col-span-1 text-center font-medium hidden md:block">Antworten</div>
<div class="col-span-2 text-center font-medium hidden md:block">Autor</div>
<div class="col-span-2 text-center font-medium hidden md:block">Letzte Antwort</div>
</div>
</div>
<!-- Thread-Liste -->
{% if threads_data %}
{% for thread_data in threads_data %}
{% set thread = thread_data.thread %}
<div class="thread-item p-4 border-b last:border-b-0 {{ 'thread-pinned' if thread.is_pinned }}"
x-bind:class="darkMode
? 'border-white/10 hover:bg-gray-700/50 {{ 'border-l-yellow-500' if thread.is_pinned }}'
: 'border-gray-200 hover:bg-gray-50 {{ 'border-l-yellow-500' if thread.is_pinned }}'">
<a href="{{ url_for('forum_post', post_id=thread.id) }}" class="block">
<div class="grid grid-cols-12 gap-4">
<!-- Thema -->
<div class="col-span-12 md:col-span-7">
<div class="flex items-start">
<!-- Status-Icons -->
<div class="flex flex-col items-center mr-3 pt-1">
{% if thread.is_pinned %}
<i class="fas fa-thumbtack text-yellow-500" title="Angepinnt"></i>
{% endif %}
{% if thread.is_locked %}
<i class="fas fa-lock text-red-500 mt-1" title="Gesperrt"></i>
{% endif %}
</div>
<!-- Themen-Info -->
<div>
<h3 class="font-medium leading-snug mb-1 {% if thread.is_locked %}opacity-70{% endif %}">
{{ thread.title }}
</h3>
<div class="flex items-center text-xs opacity-70 mt-1">
<span><i class="fas fa-eye mr-1"></i> {{ thread.view_count }}</span>
<span class="mx-2 block md:hidden"></span>
<span class="block md:hidden"><i class="fas fa-reply mr-1"></i> {{ thread_data.reply_count }}</span>
<span class="mx-2"></span>
<span><i class="fas fa-clock mr-1"></i> {{ thread.created_at.strftime('%d.%m.%Y, %H:%M') }}</span>
</div>
</div>
</div>
</div>
<!-- Antworten -->
<div class="col-span-1 text-center hidden md:flex items-center justify-center">
<span class="px-2.5 py-1 rounded-full text-sm font-medium"
x-bind:class="darkMode
? 'bg-indigo-900/40 text-indigo-300'
: 'bg-indigo-100 text-indigo-800'">
{{ thread_data.reply_count }}
</span>
</div>
<!-- Autor -->
<div class="col-span-2 text-center hidden md:flex items-center justify-center">
<div class="flex items-center">
<div class="w-7 h-7 rounded-full flex items-center justify-center text-white text-xs font-medium overflow-hidden mr-2"
style="background: linear-gradient(135deg, #8b5cf6, #6366f1);">
{% if thread.author.avatar %}
<img src="{{ thread.author.avatar }}" alt="{{ thread.author.username }}" class="w-full h-full object-cover">
{% else %}
{{ thread.author.username[0].upper() }}
{% endif %}
</div>
<span class="text-sm truncate max-w-[80px]">{{ thread.author.username }}</span>
</div>
</div>
<!-- Letzte Antwort -->
<div class="col-span-2 text-center hidden md:block text-sm">
{% if thread_data.latest_reply %}
<div>{{ thread_data.latest_reply.created_at.strftime('%d.%m.%Y') }}</div>
<div class="opacity-75 text-xs">{{ thread_data.latest_reply.created_at.strftime('%H:%M') }} Uhr</div>
{% else %}
<span class="opacity-60">Keine Antworten</span>
{% endif %}
</div>
</div>
</a>
</div>
{% endfor %}
{% else %}
<div class="p-8 text-center">
<div class="text-3xl mb-3 opacity-30"><i class="fas fa-comments"></i></div>
<h3 class="text-xl font-semibold mb-2">Keine Themen vorhanden</h3>
<p class="opacity-75 mb-4">In dieser Kategorie wurden noch keine Themen erstellt.</p>
<a href="{{ url_for('new_post', category_id=category.id) }}"
class="inline-block px-5 py-2.5 rounded-lg transition-all duration-300"
x-bind:class="darkMode
? 'bg-indigo-700 hover:bg-indigo-600 text-white'
: 'bg-indigo-500 hover:bg-indigo-600 text-white'">
<i class="fas fa-plus-circle mr-2"></i>
Erstes Thema erstellen
</a>
</div>
{% endif %}
</div>
<!-- Link zur Mindmap -->
<div class="rounded-xl p-5 mb-4 flex items-center"
x-bind:class="darkMode ? 'bg-purple-900/20 border border-purple-800/30' : 'bg-purple-50 border border-purple-100'">
<div class="text-3xl mr-4 opacity-80">
<i class="fas fa-diagram-project" style="color: {{ node.color_code }}"></i>
</div>
<div>
<h3 class="font-medium mb-1">Mindmap-Knotenpunkt: {{ node.name }}</h3>
<p class="text-sm opacity-75">In der Mindmap findest du weitere Informationen zu diesem Themenbereich.</p>
</div>
<div class="ml-auto">
<a href="{{ url_for('mindmap') }}"
class="px-4 py-2 rounded-lg inline-block text-sm transition-all"
x-bind:class="darkMode
? 'bg-purple-800/60 hover:bg-purple-700/60 text-white'
: 'bg-white hover:bg-purple-100 text-purple-800 border border-purple-200'">
Zur Mindmap
</a>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// Hier können bei Bedarf kategoriespezifische Scripts eingefügt werden
</script>
{% endblock %}

View File

@@ -1,344 +0,0 @@
{% extends 'base.html' %}
{% block title %}Beitrag bearbeiten{% endblock %}
{% block extra_css %}
<style>
.markdown-preview {
min-height: 200px;
max-height: 400px;
overflow-y: auto;
line-height: 1.6;
}
.markdown-preview p {
margin-bottom: 1rem;
}
.markdown-preview h1, .markdown-preview h2, .markdown-preview h3,
.markdown-preview h4, .markdown-preview h5, .markdown-preview h6 {
margin-top: 1.5rem;
margin-bottom: 0.75rem;
font-weight: 600;
}
.markdown-preview h1 { font-size: 1.8rem; }
.markdown-preview h2 { font-size: 1.5rem; }
.markdown-preview h3 { font-size: 1.3rem; }
.markdown-preview h4 { font-size: 1.1rem; }
.markdown-preview ul, .markdown-preview ol {
margin-left: 1.5rem;
margin-bottom: 1rem;
}
.markdown-preview ul { list-style-type: disc; }
.markdown-preview ol { list-style-type: decimal; }
.markdown-preview pre {
background-color: rgba(0,0,0,0.05);
padding: 1rem;
border-radius: 0.5rem;
overflow-x: auto;
margin: 1rem 0;
}
.markdown-preview code {
font-family: 'JetBrains Mono', monospace;
font-size: 0.9em;
padding: 0.1em 0.3em;
border-radius: 0.3em;
background-color: rgba(0,0,0,0.05);
}
.markdown-preview pre code {
padding: 0;
background-color: transparent;
}
.markdown-preview blockquote {
border-left: 4px solid;
padding-left: 1rem;
margin-left: 0;
margin-right: 0;
margin-bottom: 1rem;
opacity: 0.8;
}
.dark .markdown-preview code {
background-color: rgba(255,255,255,0.1);
}
.dark .markdown-preview blockquote {
border-color: rgba(255,255,255,0.2);
}
.node-mention {
display: inline-flex;
align-items: center;
background-color: rgba(109, 40, 217, 0.1);
color: #6d28d9;
border-radius: 4px;
padding: 1px 6px;
font-size: 0.9em;
margin: 0 2px;
font-weight: 500;
}
.dark .node-mention {
background-color: rgba(167, 139, 250, 0.2);
color: #a78bfa;
}
</style>
{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-8">
<!-- Breadcrumb Navigation -->
<div class="mb-6 flex items-center text-sm">
<a href="{{ url_for('community') }}" class="opacity-75 hover:opacity-100 transition-opacity">
<i class="fas fa-home mr-1"></i> Forum
</a>
<span class="mx-2 opacity-50">/</span>
<a href="{{ url_for('forum_category', category_id=post.category_id) }}" class="opacity-75 hover:opacity-100 transition-opacity">
{{ post.category.title }}
</a>
<span class="mx-2 opacity-50">/</span>
{% if post.parent_id %}
<a href="{{ url_for('forum_post', post_id=post.parent_id) }}" class="opacity-75 hover:opacity-100 transition-opacity truncate max-w-[200px]">
{{ post.parent.title }}
</a>
<span class="mx-2 opacity-50">/</span>
{% endif %}
<span class="font-medium">Beitrag bearbeiten</span>
</div>
<!-- Formular-Header -->
<div class="mb-6">
<h1 class="text-2xl font-bold mb-2">Beitrag bearbeiten</h1>
<p class="opacity-75">
{% if post.parent_id %}
Antwort auf <span class="font-medium">{{ post.parent.title }}</span>
{% else %}
in der Kategorie <span class="font-medium">{{ post.category.title }}</span>
{% endif %}
</p>
</div>
<!-- Formular -->
<div class="mb-8 rounded-xl overflow-hidden"
x-bind:class="darkMode ? 'bg-gray-800/60 border border-white/10' : 'bg-white border border-gray-200'">
<div class="p-4 border-b font-medium" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
<i class="fas fa-edit mr-2"></i>
Beitrag bearbeiten
</div>
<div class="p-6">
<form action="{{ url_for('edit_post', post_id=post.id) }}" method="POST" x-data="{
title: '{{ post.title|safe }}',
content: '{{ post.content|replace('\n', '\\n')|replace('\'', '\\\'')|safe }}',
showPreview: false,
previewHtml: '',
updatePreview() {
// Verarbeite den Inhalt
if (this.content.trim() === '') {
this.previewHtml = '<div class=\'opacity-50 italic\'>Die Vorschau wird hier angezeigt...</div>';
return;
}
// Verarbeite Markdown
let html = marked.parse(this.content);
// Ersetze @Knotenname mit entsprechenden Links
html = html.replace(/@([a-zA-Z0-9äöüÄÖÜß_-]+)/g, '<span class=\'node-mention\'><i class=\'fas fa-diagram-project fa-xs mr-1\'></i>$1</span>');
this.previewHtml = html;
}
}">
<div class="mb-6">
<label for="title" class="block mb-2 font-medium">Titel</label>
<div class="rounded-lg overflow-hidden"
x-bind:class="darkMode ? 'border border-white/20' : 'border border-gray-300'">
<input type="text" id="title" name="title"
class="w-full px-4 py-3"
x-bind:class="darkMode
? 'bg-gray-700/50 text-white placeholder-gray-400 focus:border-indigo-500'
: 'bg-white text-gray-700 placeholder-gray-400 focus:border-indigo-500'"
x-model="title"
required>
</div>
</div>
<div class="mb-6">
<div class="flex justify-between items-center mb-2">
<label for="content" class="font-medium">Inhalt</label>
<div class="flex space-x-2">
<button type="button"
class="px-3 py-1 rounded text-sm flex items-center"
x-bind:class="darkMode
? 'bg-gray-700 hover:bg-gray-600 text-white'
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'"
@click="showPreview = false"
x-bind:disabled="!showPreview"
x-bind:class="{'opacity-50': !showPreview}">
<i class="fas fa-edit mr-1"></i> Bearbeiten
</button>
<button type="button"
class="px-3 py-1 rounded text-sm flex items-center"
x-bind:class="darkMode
? 'bg-gray-700 hover:bg-gray-600 text-white'
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'"
@click="updatePreview(); showPreview = true"
x-bind:disabled="showPreview"
x-bind:class="{'opacity-50': showPreview}">
<i class="fas fa-eye mr-1"></i> Vorschau
</button>
</div>
</div>
<!-- Editor -->
<div class="rounded-lg overflow-hidden mb-2"
x-bind:class="darkMode ? 'border border-white/20' : 'border border-gray-300'"
x-show="!showPreview">
<textarea id="content" name="content" rows="12"
class="w-full p-3 resize-y"
x-bind:class="darkMode
? 'bg-gray-700/50 text-white placeholder-gray-400 focus:border-indigo-500'
: 'bg-white text-gray-700 placeholder-gray-400 focus:border-indigo-500'"
x-model="content"
required></textarea>
</div>
<!-- Preview -->
<div class="rounded-lg overflow-hidden mb-2 p-4 markdown-preview"
x-bind:class="darkMode
? 'border border-white/20 bg-gray-700/30'
: 'border border-gray-300 bg-gray-50'"
x-show="showPreview"
x-html="previewHtml">
</div>
<!-- Markdown-Hilfsmittel -->
<div class="mb-4" x-show="!showPreview">
<div class="text-xs opacity-70">
<p>Du kannst Knotenpunkte der Mindmap durch <code>@Knotenname</code> verlinken.</p>
<p>Dieser Editor unterstützt Markdown-Formatierung:</p>
<div class="flex flex-wrap gap-2 mt-1">
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="**" data-before="" data-after="" title="Fett">
<i class="fas fa-bold"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="*" data-before="" data-after="" title="Kursiv">
<i class="fas fa-italic"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="`" data-before="" data-after="" title="Code">
<i class="fas fa-code"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="[Link-Text](URL)" data-before="" data-after="" title="Link">
<i class="fas fa-link"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="\n```\nCode-Block\n```" data-before="" data-after="" title="Code-Block">
<i class="fas fa-file-code"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format=">" data-before="" data-after="" title="Zitat">
<i class="fas fa-quote-right"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="- " data-before="" data-after="" title="Liste">
<i class="fas fa-list-ul"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="1. " data-before="" data-after="" title="Nummerierte Liste">
<i class="fas fa-list-ol"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="# " data-before="" data-after="" title="Überschrift">
<i class="fas fa-heading"></i>
</button>
</div>
</div>
</div>
</div>
<div class="flex justify-between items-center">
<a href="{{ url_for('forum_post', post_id=post.parent_id or post.id) }}"
class="px-5 py-2.5 rounded-lg transition-all duration-300 flex items-center"
x-bind:class="darkMode
? 'bg-gray-700 hover:bg-gray-600 text-white'
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'">
Abbrechen
</a>
<button type="submit"
class="px-5 py-2.5 rounded-lg transition-all duration-300 flex items-center"
x-bind:class="darkMode
? 'bg-indigo-700 hover:bg-indigo-600 text-white'
: 'bg-indigo-500 hover:bg-indigo-600 text-white'">
<i class="fas fa-save mr-2"></i>
Änderungen speichern
</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Markdown-Buttons für den Beitragseditor
document.querySelectorAll('.markdown-button').forEach(button => {
button.addEventListener('click', function() {
const textarea = document.getElementById('content');
const format = this.dataset.format;
const before = this.dataset.before || '';
const after = this.dataset.after || '';
// Hole die aktuelle Auswahl
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const selection = textarea.value.substring(start, end);
// Wende die Formatierung an
let formattedText;
if (format.includes('\n')) {
// Für Formate mit Zeilenumbrüchen (z.B. Code-Blöcke)
formattedText = format.replace('Code-Block', selection || 'Code-Block');
} else if (format.includes('[Link-Text](URL)')) {
formattedText = format.replace('Link-Text', selection || 'Link-Text');
} else if (format === '- ' || format === '1. ' || format === '# ' || format === '> ') {
// Für Listen und Überschriften: am Anfang der Zeile einfügen
const beforeSelection = textarea.value.substring(0, start);
const afterSelection = textarea.value.substring(end);
// Finde den Anfang der aktuellen Zeile
const lastNewline = beforeSelection.lastIndexOf('\n');
const lineStart = lastNewline === -1 ? 0 : lastNewline + 1;
// Füge das Format am Zeilenanfang ein
formattedText = beforeSelection.substring(0, lineStart) +
format +
beforeSelection.substring(lineStart) +
selection +
afterSelection;
// Setze die neue Cursor-Position
const newCursorPos = end + format.length;
textarea.value = formattedText;
textarea.setSelectionRange(newCursorPos, newCursorPos);
// Alpine.js Model aktualisieren
textarea.dispatchEvent(new Event('input'));
return; // Früher zurückkehren, da wir die Formatierung bereits angewendet haben
} else {
// Für einfache Formatierungen wie fett, kursiv, Code
formattedText = before + format + selection + format + after;
}
// Ersetze den Text
textarea.value = textarea.value.substring(0, start) + formattedText + textarea.value.substring(end);
// Setze den Fokus zurück auf das Textarea
textarea.focus();
// Alpine.js Model aktualisieren
textarea.dispatchEvent(new Event('input'));
// Setze die Auswahl neu, wenn es eine Auswahl gab
if (selection) {
const newStart = start + before.length + format.length;
const newEnd = newStart + selection.length;
textarea.setSelectionRange(newStart, newEnd);
} else {
// Setze den Cursor in die Mitte von **|** oder `|`
const newCursorPos = start + before.length + format.length;
textarea.setSelectionRange(newCursorPos, newCursorPos);
}
});
});
});
</script>
{% endblock %}

View File

@@ -1,125 +0,0 @@
{% extends 'base.html' %}
{% block title %}Community Forum{% endblock %}
{% block extra_css %}
<style>
.forum-category {
transition: all 0.3s ease;
}
.forum-category:hover {
transform: translateY(-2px);
}
.category-icon {
font-size: 1.5rem;
width: 2.5rem;
height: 2.5rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 0.75rem;
}
</style>
{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-8">
<!-- Seitenüberschrift -->
<div class="mb-8 text-center">
<h1 class="text-3xl font-bold mb-2 gradient-text">Community Forum</h1>
<p class="text-lg opacity-75">Diskutiere mit anderen Nutzern über die Hauptthemenbereiche der Mindmap</p>
</div>
<!-- Forumskategorien -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
{% if categories_data %}
{% for cat_data in categories_data %}
<a href="{{ url_for('forum_category', category_id=cat_data.category.id) }}" class="forum-category block">
<div class="rounded-xl p-5 h-full"
x-bind:class="darkMode ? 'bg-gray-800/60 hover:bg-gray-800/80 border border-white/10' : 'bg-white hover:bg-gray-50 border border-gray-200 shadow-md'">
<div class="flex items-start">
<!-- Kategorie-Icon -->
<div class="category-icon mr-4 text-white"
style="background-color: {{ cat_data.category.node.color_code or '#6d28d9' }}">
<i class="fas {{ cat_data.category.node.icon or 'fa-folder' }}"></i>
</div>
<!-- Kategorie-Info -->
<div class="flex-grow">
<h3 class="text-xl font-semibold mb-2">{{ cat_data.category.title }}</h3>
<p class="opacity-75 text-sm mb-3">{{ cat_data.category.description }}</p>
<!-- Statistik -->
<div class="flex flex-wrap gap-4 text-sm opacity-80">
<div class="flex items-center">
<i class="fas fa-comment-alt mr-2"></i>
<span>{{ cat_data.total_posts }} Themen</span>
</div>
<div class="flex items-center">
<i class="fas fa-reply mr-2"></i>
<span>{{ cat_data.total_replies }} Antworten</span>
</div>
{% if cat_data.latest_post %}
<div class="flex items-center">
<i class="fas fa-clock mr-2"></i>
<span>Neuster Beitrag: {{ cat_data.latest_post.created_at.strftime('%d.%m.%Y, %H:%M') }}</span>
</div>
{% endif %}
</div>
</div>
<!-- Pfeil-Icon -->
<div class="ml-2">
<i class="fas fa-chevron-right opacity-50"></i>
</div>
</div>
</div>
</a>
{% endfor %}
{% else %}
<div class="col-span-2 text-center py-8">
<div class="text-3xl mb-3 opacity-30"><i class="fas fa-exclamation-circle"></i></div>
<h3 class="text-xl font-semibold mb-2">Keine Forum-Kategorien gefunden</h3>
<p class="opacity-75">Es sind derzeit keine Kategorien für Diskussionen verfügbar.</p>
</div>
{% endif %}
</div>
<!-- Hinweis zur Nutzung -->
<div class="rounded-xl p-6 text-center mb-8"
x-bind:class="darkMode ? 'bg-indigo-900/30 border border-indigo-700/30' : 'bg-indigo-50 border border-indigo-100'">
<h3 class="text-xl font-semibold mb-3">
<i class="fas fa-lightbulb mr-2 text-yellow-500"></i>
So funktioniert das Forum
</h3>
<p class="mb-4">Das Community-Forum ist nach den Hauptknotenpunkten der Systades-Mindmap strukturiert.
In deinen Beiträgen kannst du Knotenpunkte mit <code>@Knotenname</code> verlinken.</p>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mt-6">
<div class="p-4 rounded-lg"
x-bind:class="darkMode ? 'bg-indigo-800/40' : 'bg-white border border-indigo-100'">
<div class="text-2xl mb-2"><i class="fas fa-users text-indigo-400"></i></div>
<h4 class="font-medium mb-1">Fachliche Diskussionen</h4>
<p class="text-sm opacity-75">Tausche dich mit anderen zu spezifischen Themen aus</p>
</div>
<div class="p-4 rounded-lg"
x-bind:class="darkMode ? 'bg-indigo-800/40' : 'bg-white border border-indigo-100'">
<div class="text-2xl mb-2"><i class="fas fa-link text-indigo-400"></i></div>
<h4 class="font-medium mb-1">Wissensvernetzung</h4>
<p class="text-sm opacity-75">Verknüpfe Inhalte durch Knotenreferenzen</p>
</div>
<div class="p-4 rounded-lg"
x-bind:class="darkMode ? 'bg-indigo-800/40' : 'bg-white border border-indigo-100'">
<div class="text-2xl mb-2"><i class="fas fa-markdown text-indigo-400"></i></div>
<h4 class="font-medium mb-1">Markdown Support</h4>
<p class="text-sm opacity-75">Formatiere deine Beiträge mit Markdown</p>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// Hier können bei Bedarf forumspezifische Scripts eingefügt werden
</script>
{% endblock %}

View File

@@ -1,355 +0,0 @@
{% extends 'base.html' %}
{% block title %}Neues Thema - {{ category.title }}{% endblock %}
{% block extra_css %}
<style>
.markdown-preview {
min-height: 200px;
max-height: 400px;
overflow-y: auto;
line-height: 1.6;
}
.markdown-preview p {
margin-bottom: 1rem;
}
.markdown-preview h1, .markdown-preview h2, .markdown-preview h3,
.markdown-preview h4, .markdown-preview h5, .markdown-preview h6 {
margin-top: 1.5rem;
margin-bottom: 0.75rem;
font-weight: 600;
}
.markdown-preview h1 { font-size: 1.8rem; }
.markdown-preview h2 { font-size: 1.5rem; }
.markdown-preview h3 { font-size: 1.3rem; }
.markdown-preview h4 { font-size: 1.1rem; }
.markdown-preview ul, .markdown-preview ol {
margin-left: 1.5rem;
margin-bottom: 1rem;
}
.markdown-preview ul { list-style-type: disc; }
.markdown-preview ol { list-style-type: decimal; }
.markdown-preview pre {
background-color: rgba(0,0,0,0.05);
padding: 1rem;
border-radius: 0.5rem;
overflow-x: auto;
margin: 1rem 0;
}
.markdown-preview code {
font-family: 'JetBrains Mono', monospace;
font-size: 0.9em;
padding: 0.1em 0.3em;
border-radius: 0.3em;
background-color: rgba(0,0,0,0.05);
}
.markdown-preview pre code {
padding: 0;
background-color: transparent;
}
.markdown-preview blockquote {
border-left: 4px solid;
padding-left: 1rem;
margin-left: 0;
margin-right: 0;
margin-bottom: 1rem;
opacity: 0.8;
}
.dark .markdown-preview code {
background-color: rgba(255,255,255,0.1);
}
.dark .markdown-preview blockquote {
border-color: rgba(255,255,255,0.2);
}
.node-mention {
display: inline-flex;
align-items: center;
background-color: rgba(109, 40, 217, 0.1);
color: #6d28d9;
border-radius: 4px;
padding: 1px 6px;
font-size: 0.9em;
margin: 0 2px;
font-weight: 500;
}
.dark .node-mention {
background-color: rgba(167, 139, 250, 0.2);
color: #a78bfa;
}
</style>
{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-8">
<!-- Breadcrumb Navigation -->
<div class="mb-6 flex items-center text-sm">
<a href="{{ url_for('community') }}" class="opacity-75 hover:opacity-100 transition-opacity">
<i class="fas fa-home mr-1"></i> Forum
</a>
<span class="mx-2 opacity-50">/</span>
<a href="{{ url_for('forum_category', category_id=category.id) }}" class="opacity-75 hover:opacity-100 transition-opacity">
{{ category.title }}
</a>
<span class="mx-2 opacity-50">/</span>
<span class="font-medium">Neues Thema</span>
</div>
<!-- Formular-Header -->
<div class="mb-6">
<h1 class="text-2xl font-bold mb-2">Neues Thema erstellen</h1>
<p class="opacity-75">in der Kategorie <span class="font-medium">{{ category.title }}</span></p>
</div>
<!-- Formular -->
<div class="mb-8 rounded-xl overflow-hidden"
x-bind:class="darkMode ? 'bg-gray-800/60 border border-white/10' : 'bg-white border border-gray-200'">
<div class="p-4 border-b font-medium" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
<i class="fas fa-plus-circle mr-2"></i>
Neues Thema
</div>
<div class="p-6">
<form action="{{ url_for('new_post', category_id=category.id) }}" method="POST" x-data="{
title: '',
content: '',
showPreview: false,
previewHtml: '',
updatePreview() {
// Verarbeite den Inhalt
if (this.content.trim() === '') {
this.previewHtml = '<div class=\'opacity-50 italic\'>Die Vorschau wird hier angezeigt...</div>';
return;
}
// Verarbeite Markdown
let html = marked.parse(this.content);
// Ersetze @Knotenname mit entsprechenden Links
html = html.replace(/@([a-zA-Z0-9äöüÄÖÜß_-]+)/g, '<span class=\'node-mention\'><i class=\'fas fa-diagram-project fa-xs mr-1\'></i>$1</span>');
this.previewHtml = html;
}
}">
<div class="mb-6">
<label for="title" class="block mb-2 font-medium">Titel des Themas</label>
<div class="rounded-lg overflow-hidden"
x-bind:class="darkMode ? 'border border-white/20' : 'border border-gray-300'">
<input type="text" id="title" name="title"
class="w-full px-4 py-3"
x-bind:class="darkMode
? 'bg-gray-700/50 text-white placeholder-gray-400 focus:border-indigo-500'
: 'bg-white text-gray-700 placeholder-gray-400 focus:border-indigo-500'"
placeholder="Ein prägnanter Titel für dein Thema"
x-model="title"
required>
</div>
</div>
<div class="mb-6">
<div class="flex justify-between items-center mb-2">
<label for="content" class="font-medium">Inhalt</label>
<div class="flex space-x-2">
<button type="button"
class="px-3 py-1 rounded text-sm flex items-center"
x-bind:class="darkMode
? 'bg-gray-700 hover:bg-gray-600 text-white'
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'"
@click="showPreview = false"
x-bind:disabled="!showPreview"
x-bind:class="{'opacity-50': !showPreview}">
<i class="fas fa-edit mr-1"></i> Bearbeiten
</button>
<button type="button"
class="px-3 py-1 rounded text-sm flex items-center"
x-bind:class="darkMode
? 'bg-gray-700 hover:bg-gray-600 text-white'
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'"
@click="updatePreview(); showPreview = true"
x-bind:disabled="showPreview"
x-bind:class="{'opacity-50': showPreview}">
<i class="fas fa-eye mr-1"></i> Vorschau
</button>
</div>
</div>
<!-- Editor -->
<div class="rounded-lg overflow-hidden mb-2"
x-bind:class="darkMode ? 'border border-white/20' : 'border border-gray-300'"
x-show="!showPreview">
<textarea id="content" name="content" rows="12"
class="w-full p-3 resize-y"
x-bind:class="darkMode
? 'bg-gray-700/50 text-white placeholder-gray-400 focus:border-indigo-500'
: 'bg-white text-gray-700 placeholder-gray-400 focus:border-indigo-500'"
placeholder="Schreibe deinen Beitrag hier (unterstützt Markdown und @Knotenname-Erwähnungen)..."
x-model="content"
required></textarea>
</div>
<!-- Preview -->
<div class="rounded-lg overflow-hidden mb-2 p-4 markdown-preview"
x-bind:class="darkMode
? 'border border-white/20 bg-gray-700/30'
: 'border border-gray-300 bg-gray-50'"
x-show="showPreview"
x-html="previewHtml">
</div>
<!-- Markdown-Hilfsmittel -->
<div class="mb-4" x-show="!showPreview">
<div class="text-xs opacity-70">
<p>Du kannst Knotenpunkte der Mindmap durch <code>@Knotenname</code> verlinken.</p>
<p>Dieser Editor unterstützt Markdown-Formatierung:</p>
<div class="flex flex-wrap gap-2 mt-1">
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="**" data-before="" data-after="" title="Fett">
<i class="fas fa-bold"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="*" data-before="" data-after="" title="Kursiv">
<i class="fas fa-italic"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="`" data-before="" data-after="" title="Code">
<i class="fas fa-code"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="[Link-Text](URL)" data-before="" data-after="" title="Link">
<i class="fas fa-link"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="\n```\nCode-Block\n```" data-before="" data-after="" title="Code-Block">
<i class="fas fa-file-code"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format=">" data-before="" data-after="" title="Zitat">
<i class="fas fa-quote-right"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="- " data-before="" data-after="" title="Liste">
<i class="fas fa-list-ul"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="1. " data-before="" data-after="" title="Nummerierte Liste">
<i class="fas fa-list-ol"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="# " data-before="" data-after="" title="Überschrift">
<i class="fas fa-heading"></i>
</button>
</div>
</div>
</div>
</div>
<div class="flex justify-between items-center">
<a href="{{ url_for('forum_category', category_id=category.id) }}"
class="px-5 py-2.5 rounded-lg transition-all duration-300 flex items-center"
x-bind:class="darkMode
? 'bg-gray-700 hover:bg-gray-600 text-white'
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'">
Abbrechen
</a>
<button type="submit"
class="px-5 py-2.5 rounded-lg transition-all duration-300 flex items-center"
x-bind:class="darkMode
? 'bg-indigo-700 hover:bg-indigo-600 text-white'
: 'bg-indigo-500 hover:bg-indigo-600 text-white'">
<i class="fas fa-paper-plane mr-2"></i>
Thema erstellen
</button>
</div>
</form>
</div>
</div>
<!-- Link zur Mindmap -->
<div class="rounded-xl p-5 mb-4 flex items-center"
x-bind:class="darkMode ? 'bg-purple-900/20 border border-purple-800/30' : 'bg-purple-50 border border-purple-100'">
<div class="text-3xl mr-4 opacity-80">
<i class="fas fa-diagram-project" style="color: {{ category.node.color_code }}"></i>
</div>
<div>
<h3 class="font-medium mb-1">Mindmap-Knotenpunkt: {{ category.node.name }}</h3>
<p class="text-sm opacity-75">Dieser Diskussionsbereich ist mit dem Mindmap-Knotenpunkt "{{ category.node.name }}" verknüpft.</p>
</div>
<div class="ml-auto">
<a href="{{ url_for('mindmap') }}"
class="px-4 py-2 rounded-lg inline-block text-sm transition-all"
x-bind:class="darkMode
? 'bg-purple-800/60 hover:bg-purple-700/60 text-white'
: 'bg-white hover:bg-purple-100 text-purple-800 border border-purple-200'">
Zur Mindmap
</a>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Markdown-Buttons für den Beitragseditor
document.querySelectorAll('.markdown-button').forEach(button => {
button.addEventListener('click', function() {
const textarea = document.getElementById('content');
const format = this.dataset.format;
const before = this.dataset.before || '';
const after = this.dataset.after || '';
// Hole die aktuelle Auswahl
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const selection = textarea.value.substring(start, end);
// Wende die Formatierung an
let formattedText;
if (format.includes('\n')) {
// Für Formate mit Zeilenumbrüchen (z.B. Code-Blöcke)
formattedText = format.replace('Code-Block', selection || 'Code-Block');
} else if (format.includes('[Link-Text](URL)')) {
formattedText = format.replace('Link-Text', selection || 'Link-Text');
} else if (format === '- ' || format === '1. ' || format === '# ' || format === '> ') {
// Für Listen und Überschriften: am Anfang der Zeile einfügen
const beforeSelection = textarea.value.substring(0, start);
const afterSelection = textarea.value.substring(end);
// Finde den Anfang der aktuellen Zeile
const lastNewline = beforeSelection.lastIndexOf('\n');
const lineStart = lastNewline === -1 ? 0 : lastNewline + 1;
// Füge das Format am Zeilenanfang ein
formattedText = beforeSelection.substring(0, lineStart) +
format +
beforeSelection.substring(lineStart) +
selection +
afterSelection;
// Setze die neue Cursor-Position
const newCursorPos = end + format.length;
textarea.value = formattedText;
textarea.setSelectionRange(newCursorPos, newCursorPos);
// Alpine.js Model aktualisieren
textarea.dispatchEvent(new Event('input'));
return; // Früher zurückkehren, da wir die Formatierung bereits angewendet haben
} else {
// Für einfache Formatierungen wie fett, kursiv, Code
formattedText = before + format + selection + format + after;
}
// Ersetze den Text
textarea.value = textarea.value.substring(0, start) + formattedText + textarea.value.substring(end);
// Setze den Fokus zurück auf das Textarea
textarea.focus();
// Alpine.js Model aktualisieren
textarea.dispatchEvent(new Event('input'));
// Setze die Auswahl neu, wenn es eine Auswahl gab
if (selection) {
const newStart = start + before.length + format.length;
const newEnd = newStart + selection.length;
textarea.setSelectionRange(newStart, newEnd);
} else {
// Setze den Cursor in die Mitte von **|** oder `|`
const newCursorPos = start + before.length + format.length;
textarea.setSelectionRange(newCursorPos, newCursorPos);
}
});
});
});
</script>
{% endblock %}

View File

@@ -1,511 +0,0 @@
{% extends 'base.html' %}
{% block title %}{{ post.title }} - Forum{% endblock %}
{% block extra_css %}
<style>
.post-content {
line-height: 1.7;
}
.post-content p {
margin-bottom: 1rem;
}
.post-content h1, .post-content h2, .post-content h3,
.post-content h4, .post-content h5, .post-content h6 {
margin-top: 1.5rem;
margin-bottom: 0.75rem;
font-weight: 600;
}
.post-content h1 { font-size: 1.8rem; }
.post-content h2 { font-size: 1.5rem; }
.post-content h3 { font-size: 1.3rem; }
.post-content h4 { font-size: 1.1rem; }
.post-content ul, .post-content ol {
margin-left: 1.5rem;
margin-bottom: 1rem;
}
.post-content ul { list-style-type: disc; }
.post-content ol { list-style-type: decimal; }
.post-content pre {
background-color: rgba(0,0,0,0.05);
padding: 1rem;
border-radius: 0.5rem;
overflow-x: auto;
margin: 1rem 0;
}
.post-content code {
font-family: 'JetBrains Mono', monospace;
font-size: 0.9em;
padding: 0.1em 0.3em;
border-radius: 0.3em;
background-color: rgba(0,0,0,0.05);
}
.post-content pre code {
padding: 0;
background-color: transparent;
}
.post-content blockquote {
border-left: 4px solid;
padding-left: 1rem;
margin-left: 0;
margin-right: 0;
margin-bottom: 1rem;
opacity: 0.8;
}
.post-content img {
max-width: 100%;
height: auto;
border-radius: 0.5rem;
margin: 1rem 0;
}
.post-content table {
width: 100%;
border-collapse: collapse;
margin: 1rem 0;
}
.post-content th, .post-content td {
padding: 0.5rem;
border: 1px solid;
border-color: rgba(0,0,0,0.1);
}
.post-content th {
background-color: rgba(0,0,0,0.05);
}
.post-content a {
color: #6d28d9;
text-decoration: none;
}
.post-content a:hover {
text-decoration: underline;
}
.dark .post-content code {
background-color: rgba(255,255,255,0.1);
}
.dark .post-content th, .dark .post-content td {
border-color: rgba(255,255,255,0.1);
}
.dark .post-content th {
background-color: rgba(255,255,255,0.05);
}
.dark .post-content a {
color: #a78bfa;
}
.node-mention {
display: inline-flex;
align-items: center;
background-color: rgba(109, 40, 217, 0.1);
color: #6d28d9;
border-radius: 4px;
padding: 1px 6px;
font-size: 0.9em;
margin: 0 2px;
font-weight: 500;
}
.dark .node-mention {
background-color: rgba(167, 139, 250, 0.2);
color: #a78bfa;
}
</style>
{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-8">
<!-- Breadcrumb Navigation -->
<div class="mb-6 flex items-center text-sm">
<a href="{{ url_for('community') }}" class="opacity-75 hover:opacity-100 transition-opacity">
<i class="fas fa-home mr-1"></i> Forum
</a>
<span class="mx-2 opacity-50">/</span>
<a href="{{ url_for('forum_category', category_id=category.id) }}" class="opacity-75 hover:opacity-100 transition-opacity">
{{ category.title }}
</a>
<span class="mx-2 opacity-50">/</span>
<span class="font-medium truncate max-w-[300px]">{{ post.title }}</span>
</div>
<!-- Beitrags-Header -->
<div class="mb-6">
<h1 class="text-2xl font-bold mb-2">{{ post.title }}</h1>
<div class="flex flex-wrap items-center gap-3 text-sm opacity-75">
<span><i class="fas fa-calendar-alt mr-1"></i> {{ post.created_at.strftime('%d.%m.%Y, %H:%M') }}</span>
<span><i class="fas fa-eye mr-1"></i> {{ post.view_count }} Aufrufe</span>
<span><i class="fas fa-reply mr-1"></i> {{ replies|length }} Antworten</span>
{% if post.is_pinned or post.is_locked %}
<div class="flex gap-2 ml-2">
{% if post.is_pinned %}
<span class="px-2 py-0.5 text-xs rounded-full"
x-bind:class="darkMode ? 'bg-yellow-700/50 text-yellow-300' : 'bg-yellow-100 text-yellow-800'">
<i class="fas fa-thumbtack mr-1"></i> Angepinnt
</span>
{% endif %}
{% if post.is_locked %}
<span class="px-2 py-0.5 text-xs rounded-full"
x-bind:class="darkMode ? 'bg-red-700/50 text-red-300' : 'bg-red-100 text-red-800'">
<i class="fas fa-lock mr-1"></i> Gesperrt
</span>
{% endif %}
</div>
{% endif %}
</div>
</div>
<!-- Hauptbeitrag -->
<div class="mb-8 rounded-xl overflow-hidden"
x-bind:class="darkMode ? 'bg-gray-800/60 border border-white/10' : 'bg-white border border-gray-200 shadow-sm'">
<!-- Beitrags-Header -->
<div class="p-4 border-b" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
<div class="flex items-center justify-between">
<div class="flex items-center">
<!-- Autor-Avatar -->
<div class="w-10 h-10 rounded-full flex items-center justify-center text-white font-medium text-sm overflow-hidden mr-3"
style="background: linear-gradient(135deg, #8b5cf6, #6366f1);">
{% if post.author.avatar %}
<img src="{{ post.author.avatar }}" alt="{{ post.author.username }}" class="w-full h-full object-cover">
{% else %}
<svg width="100%" height="100%" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="100" cy="100" r="98" fill="url(#post-avatar-gradient)" stroke="#7C3AED" stroke-width="4"/>
<circle cx="100" cy="80" r="36" fill="white"/>
<path d="M100 140C77.9086 140 60 157.909 60 180H140C140 157.909 122.091 140 100 140Z" fill="white"/>
<defs>
<linearGradient id="post-avatar-gradient" x1="0" y1="0" x2="200" y2="200" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#8B5CF6"/>
<stop offset="1" stop-color="#3B82F6"/>
</linearGradient>
</defs>
</svg>
{% endif %}
</div>
<!-- Autor-Info -->
<div>
<div class="font-medium">{{ post.author.username }}</div>
<div class="text-xs opacity-70">Erstellt am {{ post.created_at.strftime('%d.%m.%Y, %H:%M') }}</div>
</div>
</div>
<!-- Aktionen -->
<div class="flex items-center space-x-2">
{% if current_user.id == post.user_id or current_user.role == 'admin' %}
<a href="{{ url_for('edit_post', post_id=post.id) }}"
class="p-2 rounded transition-colors"
x-bind:class="darkMode
? 'hover:bg-gray-700/50 text-gray-300'
: 'hover:bg-gray-100 text-gray-600'">
<i class="fas fa-edit"></i>
</a>
<form action="{{ url_for('delete_post', post_id=post.id) }}" method="POST" class="inline" onsubmit="return confirm('Möchtest du diesen Beitrag wirklich löschen?');">
<button type="submit"
class="p-2 rounded transition-colors"
x-bind:class="darkMode
? 'hover:bg-red-800/50 text-red-300'
: 'hover:bg-red-100 text-red-600'">
<i class="fas fa-trash-alt"></i>
</button>
</form>
{% endif %}
<!-- Moderation-Optionen -->
{% if current_user.role in ['admin', 'moderator'] %}
<div class="ml-2 border-l" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'"></div>
<form action="{{ url_for('toggle_pin_post', post_id=post.id) }}" method="POST" class="inline">
<button type="submit"
class="p-2 rounded transition-colors"
x-bind:class="darkMode
? 'hover:bg-yellow-800/50 text-yellow-300'
: 'hover:bg-yellow-100 text-yellow-600'"
title="{% if post.is_pinned %}Nicht mehr anpinnen{% else %}Anpinnen{% endif %}">
<i class="fas fa-thumbtack"></i>
</button>
</form>
<form action="{{ url_for('toggle_lock_post', post_id=post.id) }}" method="POST" class="inline">
<button type="submit"
class="p-2 rounded transition-colors"
x-bind:class="darkMode
? 'hover:bg-blue-800/50 text-blue-300'
: 'hover:bg-blue-100 text-blue-600'"
title="{% if post.is_locked %}Entsperren{% else %}Sperren{% endif %}">
<i class="fas {% if post.is_locked %}fa-unlock{% else %}fa-lock{% endif %}"></i>
</button>
</form>
{% endif %}
</div>
</div>
</div>
<!-- Beitrags-Inhalt -->
<div class="p-6">
<div class="post-content markdown-content" id="main-post-content">
{{ post.content|safe }}
</div>
{% if post.updated_at and post.updated_at != post.created_at %}
<div class="mt-6 pt-4 text-xs opacity-60 border-t" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
<i class="fas fa-edit mr-1"></i> Zuletzt bearbeitet: {{ post.updated_at.strftime('%d.%m.%Y, %H:%M') }}
</div>
{% endif %}
</div>
</div>
<!-- Antworten-Bereich -->
<div class="mb-8">
<h2 class="text-xl font-semibold mb-4">
<i class="fas fa-reply mr-2 opacity-60"></i>
{{ replies|length }} Antworten
</h2>
<!-- Antworten-Liste -->
{% if replies %}
{% for reply in replies %}
<div class="mb-5 rounded-xl overflow-hidden"
x-bind:class="darkMode ? 'bg-gray-800/40 border border-white/10' : 'bg-white border border-gray-200'">
<!-- Antwort-Header -->
<div class="p-3 border-b" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
<div class="flex items-center justify-between">
<div class="flex items-center">
<!-- Autor-Avatar -->
<div class="w-8 h-8 rounded-full flex items-center justify-center text-white font-medium text-xs overflow-hidden mr-3"
style="background: linear-gradient(135deg, #8b5cf6, #6366f1);">
{% if reply.author.avatar %}
<img src="{{ reply.author.avatar }}" alt="{{ reply.author.username }}" class="w-full h-full object-cover">
{% else %}
<svg width="100%" height="100%" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="100" cy="100" r="98" fill="url(#reply-avatar-gradient)" stroke="#7C3AED" stroke-width="4"/>
<circle cx="100" cy="80" r="36" fill="white"/>
<path d="M100 140C77.9086 140 60 157.909 60 180H140C140 157.909 122.091 140 100 140Z" fill="white"/>
<defs>
<linearGradient id="reply-avatar-gradient" x1="0" y1="0" x2="200" y2="200" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#8B5CF6"/>
<stop offset="1" stop-color="#3B82F6"/>
</linearGradient>
</defs>
</svg>
{% endif %}
</div>
<!-- Autor-Info -->
<div>
<div class="font-medium text-sm">{{ reply.author.username }}</div>
<div class="text-xs opacity-70">{{ reply.created_at.strftime('%d.%m.%Y, %H:%M') }}</div>
</div>
</div>
<!-- Aktionen -->
<div class="flex items-center space-x-1">
{% if current_user.id == reply.user_id or current_user.role == 'admin' %}
<a href="{{ url_for('edit_post', post_id=reply.id) }}"
class="p-1.5 rounded text-sm transition-colors"
x-bind:class="darkMode
? 'hover:bg-gray-700/50 text-gray-300'
: 'hover:bg-gray-100 text-gray-600'">
<i class="fas fa-edit"></i>
</a>
<form action="{{ url_for('delete_post', post_id=reply.id) }}" method="POST" class="inline" onsubmit="return confirm('Möchtest du diese Antwort wirklich löschen?');">
<button type="submit"
class="p-1.5 rounded text-sm transition-colors"
x-bind:class="darkMode
? 'hover:bg-red-800/50 text-red-300'
: 'hover:bg-red-100 text-red-600'">
<i class="fas fa-trash-alt"></i>
</button>
</form>
{% endif %}
</div>
</div>
</div>
<!-- Antwort-Inhalt -->
<div class="p-5">
<div class="post-content markdown-content reply-content" id="reply-content-{{ reply.id }}">
{{ reply.content|safe }}
</div>
{% if reply.updated_at and reply.updated_at != reply.created_at %}
<div class="mt-4 pt-3 text-xs opacity-60 border-t" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
<i class="fas fa-edit mr-1"></i> Zuletzt bearbeitet: {{ reply.updated_at.strftime('%d.%m.%Y, %H:%M') }}
</div>
{% endif %}
</div>
</div>
{% endfor %}
{% else %}
<div class="rounded-xl p-6 text-center"
x-bind:class="darkMode ? 'bg-gray-800/40 border border-white/10' : 'bg-white border border-gray-200'">
<div class="text-3xl mb-3 opacity-30"><i class="fas fa-comments"></i></div>
<h3 class="text-lg font-semibold mb-2">Noch keine Antworten</h3>
<p class="opacity-75">Sei der Erste, der auf diesen Beitrag antwortet!</p>
</div>
{% endif %}
</div>
<!-- Antwort-Formular -->
{% if not post.is_locked %}
<div class="mb-8 rounded-xl overflow-hidden"
x-bind:class="darkMode ? 'bg-gray-800/60 border border-white/10' : 'bg-white border border-gray-200'">
<div class="p-4 border-b font-medium" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
<i class="fas fa-reply mr-2"></i>
Antworten
</div>
<div class="p-6">
<form action="{{ url_for('reply_to_post', post_id=post.id) }}" method="POST">
<div class="mb-4">
<label for="content" class="block mb-2 font-medium">Deine Antwort</label>
<div class="mb-2 rounded-lg overflow-hidden"
x-bind:class="darkMode ? 'border border-white/20' : 'border border-gray-300'">
<textarea id="content" name="content" rows="6"
class="w-full p-3 resize-y"
x-bind:class="darkMode
? 'bg-gray-700/50 text-white placeholder-gray-400 focus:border-indigo-500'
: 'bg-white text-gray-700 placeholder-gray-400 focus:border-indigo-500'"
placeholder="Schreibe deine Antwort hier (unterstützt Markdown und @Knotenname-Erwähnungen)..."
required></textarea>
</div>
<div class="text-xs opacity-70">
<p>Du kannst Knotenpunkte der Mindmap durch <code>@Knotenname</code> verlinken.</p>
<p>Dieser Editor unterstützt Markdown-Formatierung:</p>
<div class="flex flex-wrap gap-2 mt-1">
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="**" data-before="" data-after="" title="Fett">
<i class="fas fa-bold"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="*" data-before="" data-after="" title="Kursiv">
<i class="fas fa-italic"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="`" data-before="" data-after="" title="Code">
<i class="fas fa-code"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="[Link-Text](URL)" data-before="" data-after="" title="Link">
<i class="fas fa-link"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="\n```\nCode-Block\n```" data-before="" data-after="" title="Code-Block">
<i class="fas fa-file-code"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format=">" data-before="" data-after="" title="Zitat">
<i class="fas fa-quote-right"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="- " data-before="" data-after="" title="Liste">
<i class="fas fa-list-ul"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="1. " data-before="" data-after="" title="Nummerierte Liste">
<i class="fas fa-list-ol"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="# " data-before="" data-after="" title="Überschrift">
<i class="fas fa-heading"></i>
</button>
</div>
</div>
</div>
<div>
<button type="submit"
class="px-5 py-2.5 rounded-lg transition-all duration-300 flex items-center"
x-bind:class="darkMode
? 'bg-indigo-700 hover:bg-indigo-600 text-white'
: 'bg-indigo-500 hover:bg-indigo-600 text-white'">
<i class="fas fa-paper-plane mr-2"></i>
Antwort senden
</button>
</div>
</form>
</div>
</div>
{% else %}
<div class="rounded-xl p-5 text-center mb-6"
x-bind:class="darkMode ? 'bg-red-900/20 border border-red-800/30' : 'bg-red-50 border border-red-100'">
<i class="fas fa-lock mr-2 text-red-500"></i>
<span>Dieser Beitrag ist geschlossen. Es können keine neuen Antworten mehr verfasst werden.</span>
</div>
{% endif %}
</div>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Markdown und Knotenerwähnungen verarbeiten
const processContent = (content) => {
// Verarbeite Markdown mit marked.js
let html = marked.parse(content);
// Ersetze @Knotenname mit entsprechenden Links
html = html.replace(/@([a-zA-Z0-9äöüÄÖÜß_-]+)/g, '<span class="node-mention"><i class="fas fa-diagram-project fa-xs mr-1"></i>$1</span>');
return html;
};
// Markdown-Inhalt für Hauptbeitrag rendern
const mainPostContent = document.getElementById('main-post-content');
if (mainPostContent) {
mainPostContent.innerHTML = processContent(mainPostContent.textContent.trim());
}
// Markdown-Inhalt für Antworten rendern
document.querySelectorAll('.reply-content').forEach(reply => {
reply.innerHTML = processContent(reply.textContent.trim());
});
// Markdown-Buttons für das Antwortformular
document.querySelectorAll('.markdown-button').forEach(button => {
button.addEventListener('click', function() {
const textarea = document.getElementById('content');
const format = this.dataset.format;
const before = this.dataset.before || '';
const after = this.dataset.after || '';
// Hole die aktuelle Auswahl
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const selection = textarea.value.substring(start, end);
// Wende die Formatierung an
let formattedText;
if (format.includes('\n')) {
// Für Formate mit Zeilenumbrüchen (z.B. Code-Blöcke)
formattedText = format.replace('Code-Block', selection || 'Code-Block');
} else if (format.includes('[Link-Text](URL)')) {
formattedText = format.replace('Link-Text', selection || 'Link-Text');
} else if (format === '- ' || format === '1. ' || format === '# ' || format === '> ') {
// Für Listen und Überschriften: am Anfang der Zeile einfügen
const beforeSelection = textarea.value.substring(0, start);
const afterSelection = textarea.value.substring(end);
// Finde den Anfang der aktuellen Zeile
const lastNewline = beforeSelection.lastIndexOf('\n');
const lineStart = lastNewline === -1 ? 0 : lastNewline + 1;
// Füge das Format am Zeilenanfang ein
formattedText = beforeSelection.substring(0, lineStart) +
format +
beforeSelection.substring(lineStart) +
selection +
afterSelection;
// Setze die neue Cursor-Position
const newCursorPos = end + format.length;
textarea.value = formattedText;
textarea.setSelectionRange(newCursorPos, newCursorPos);
return; // Früher zurückkehren, da wir die Formatierung bereits angewendet haben
} else {
// Für einfache Formatierungen wie fett, kursiv, Code
formattedText = before + format + selection + format + after;
}
// Ersetze den Text
textarea.value = textarea.value.substring(0, start) + formattedText + textarea.value.substring(end);
// Setze den Fokus zurück auf das Textarea
textarea.focus();
// Setze die Auswahl neu, wenn es eine Auswahl gab
if (selection) {
const newStart = start + before.length + format.length;
const newEnd = newStart + selection.length;
textarea.setSelectionRange(newStart, newEnd);
} else {
// Setze den Cursor in die Mitte von **|** oder `|`
const newCursorPos = start + before.length + format.length;
textarea.setSelectionRange(newCursorPos, newCursorPos);
}
});
});
});
</script>
{% endblock %}

View File

@@ -1,137 +0,0 @@
{% extends 'base.html' %}
{% block title %}Community Forum Vorschau{% endblock %}
{% block extra_css %}
<style>
.forum-category {
transition: all 0.3s ease;
}
.forum-category:hover {
transform: translateY(-2px);
}
.category-icon {
font-size: 1.5rem;
width: 2.5rem;
height: 2.5rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 0.75rem;
}
</style>
{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-8">
<!-- Seitenüberschrift -->
<div class="mb-8 text-center">
<h1 class="text-3xl font-bold mb-2 gradient-text">Community Forum</h1>
<p class="text-lg opacity-75">Diskutiere mit anderen Nutzern über die Hauptthemenbereiche der Mindmap</p>
</div>
<!-- Login-Aufforderung -->
<div class="rounded-xl p-6 text-center mb-8 bg-indigo-50 dark:bg-indigo-900/30 border border-indigo-100 dark:border-indigo-700/30">
<h3 class="text-xl font-semibold mb-3">
<i class="fas fa-lock mr-2 text-indigo-500"></i>
Anmeldung erforderlich
</h3>
<p class="mb-4">Um am Community-Forum teilzunehmen und alle Funktionen nutzen zu können, musst du dich anmelden oder registrieren.</p>
<div class="flex justify-center gap-4 mt-4">
<a href="{{ url_for('login', next=url_for('forum')) }}" class="px-4 py-2 bg-indigo-500 hover:bg-indigo-600 text-white rounded-lg transition-colors">
<i class="fas fa-sign-in-alt mr-2"></i>Anmelden
</a>
<a href="{{ url_for('register') }}" class="px-4 py-2 bg-purple-500 hover:bg-purple-600 text-white rounded-lg transition-colors">
<i class="fas fa-user-plus mr-2"></i>Registrieren
</a>
</div>
</div>
<!-- Forumskategorien Vorschau -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
{% if categories_data %}
{% for cat_data in categories_data %}
<div class="forum-category block">
<div class="rounded-xl p-5 h-full"
x-bind:class="darkMode ? 'bg-gray-800/60 border border-white/10' : 'bg-white border border-gray-200 shadow-md'">
<div class="flex items-start">
<!-- Kategorie-Icon -->
<div class="category-icon mr-4 text-white"
style="background-color: {{ cat_data.category.node.color_code or '#6d28d9' }}">
<i class="fas {{ cat_data.category.node.icon or 'fa-folder' }}"></i>
</div>
<!-- Kategorie-Info -->
<div class="flex-grow">
<h3 class="text-xl font-semibold mb-2">{{ cat_data.category.title }}</h3>
<p class="opacity-75 text-sm mb-3">{{ cat_data.category.description }}</p>
<!-- Statistik -->
<div class="flex flex-wrap gap-4 text-sm opacity-80">
<div class="flex items-center">
<i class="fas fa-comment-alt mr-2"></i>
<span>{{ cat_data.total_posts }} Themen</span>
</div>
<div class="flex items-center">
<i class="fas fa-reply mr-2"></i>
<span>{{ cat_data.total_replies }} Antworten</span>
</div>
</div>
</div>
<!-- Pfeil-Icon -->
<div class="ml-2">
<i class="fas fa-lock opacity-50"></i>
</div>
</div>
</div>
</div>
{% endfor %}
{% else %}
<div class="col-span-2 text-center py-8">
<div class="text-3xl mb-3 opacity-30"><i class="fas fa-exclamation-circle"></i></div>
<h3 class="text-xl font-semibold mb-2">Keine Forum-Kategorien gefunden</h3>
<p class="opacity-75">Es sind derzeit keine Kategorien für Diskussionen verfügbar.</p>
</div>
{% endif %}
</div>
<!-- Hinweis zur Nutzung -->
<div class="rounded-xl p-6 text-center mb-8"
x-bind:class="darkMode ? 'bg-indigo-900/30 border border-indigo-700/30' : 'bg-indigo-50 border border-indigo-100'">
<h3 class="text-xl font-semibold mb-3">
<i class="fas fa-lightbulb mr-2 text-yellow-500"></i>
So funktioniert das Forum
</h3>
<p class="mb-4">Das Community-Forum ist nach den Hauptknotenpunkten der Systades-Mindmap strukturiert.
In deinen Beiträgen kannst du Knotenpunkte mit <code>@Knotenname</code> verlinken.</p>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mt-6">
<div class="p-4 rounded-lg"
x-bind:class="darkMode ? 'bg-indigo-800/40' : 'bg-white border border-indigo-100'">
<div class="text-2xl mb-2"><i class="fas fa-users text-indigo-400"></i></div>
<h4 class="font-medium mb-1">Fachliche Diskussionen</h4>
<p class="text-sm opacity-75">Tausche dich mit anderen zu spezifischen Themen aus</p>
</div>
<div class="p-4 rounded-lg"
x-bind:class="darkMode ? 'bg-indigo-800/40' : 'bg-white border border-indigo-100'">
<div class="text-2xl mb-2"><i class="fas fa-link text-indigo-400"></i></div>
<h4 class="font-medium mb-1">Wissensvernetzung</h4>
<p class="text-sm opacity-75">Verknüpfe Inhalte durch Knotenreferenzen</p>
</div>
<div class="p-4 rounded-lg"
x-bind:class="darkMode ? 'bg-indigo-800/40' : 'bg-white border border-indigo-100'">
<div class="text-2xl mb-2"><i class="fas fa-markdown text-indigo-400"></i></div>
<h4 class="font-medium mb-1">Markdown Support</h4>
<p class="text-sm opacity-75">Formatiere deine Beiträge mit Markdown</p>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// Hier können bei Bedarf forumspezifische Scripts eingefügt werden
</script>
{% endblock %}

View File

@@ -1,365 +0,0 @@
{% extends "base.html" %}
{% block title %}Mindmap erstellen{% endblock %}
{% block extra_css %}
<style>
/* Spezifische Stile für die Mindmap-Erstellungsseite */
.form-container {
background-color: var(--bg-secondary);
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
body.dark .form-container {
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.2);
border: 1px solid rgba(255, 255, 255, 0.05);
}
body:not(.dark) .form-container {
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(0, 0, 0, 0.05);
}
.form-header {
padding: 1.5rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
body:not(.dark) .form-header {
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
}
.form-body {
padding: 1.5rem;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
}
.form-input,
.form-textarea {
width: 100%;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
transition: all 0.3s ease;
}
body.dark .form-input,
body.dark .form-textarea {
background-color: rgba(15, 23, 42, 0.6);
border: 1px solid rgba(255, 255, 255, 0.1);
color: #f1f5f9;
}
body:not(.dark) .form-input,
body:not(.dark) .form-textarea {
background-color: white;
border: 1px solid #e2e8f0;
color: #334155;
}
body.dark .form-input:focus,
body.dark .form-textarea:focus {
border-color: #7c3aed;
box-shadow: 0 0 0 2px rgba(124, 58, 237, 0.2);
outline: none;
}
body:not(.dark) .form-input:focus,
body:not(.dark) .form-textarea:focus {
border-color: #7c3aed;
box-shadow: 0 0 0 2px rgba(124, 58, 237, 0.2);
outline: none;
}
.form-textarea {
min-height: 120px;
resize: vertical;
}
.form-switch {
display: flex;
align-items: center;
}
.form-switch input[type="checkbox"] {
height: 0;
width: 0;
visibility: hidden;
position: absolute;
}
.form-switch label {
cursor: pointer;
width: 50px;
height: 25px;
background: rgba(100, 116, 139, 0.3);
display: block;
border-radius: 25px;
position: relative;
margin-right: 10px;
transition: all 0.3s ease;
}
.form-switch label:after {
content: '';
position: absolute;
top: 3px;
left: 3px;
width: 19px;
height: 19px;
background: #fff;
border-radius: 19px;
transition: 0.3s;
}
.form-switch input:checked + label {
background: #7c3aed;
}
.form-switch input:checked + label:after {
left: calc(100% - 3px);
transform: translateX(-100%);
}
.btn-submit {
background-color: #7c3aed;
color: white;
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
font-weight: 500;
transition: all 0.3s ease;
border: none;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.btn-submit:hover {
background-color: #6d28d9;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(109, 40, 217, 0.2);
}
.btn-cancel {
background-color: transparent;
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
font-weight: 500;
transition: all 0.3s ease;
border: 1px solid;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
body.dark .btn-cancel {
color: #e2e8f0;
border-color: rgba(255, 255, 255, 0.1);
}
body:not(.dark) .btn-cancel {
color: #475569;
border-color: #e2e8f0;
}
.btn-cancel:hover {
transform: translateY(-2px);
}
body.dark .btn-cancel:hover {
background-color: rgba(255, 255, 255, 0.05);
}
body:not(.dark) .btn-cancel:hover {
background-color: rgba(0, 0, 0, 0.05);
}
/* Animation für den Seiteneintritt */
@keyframes slideInUp {
from {
transform: translateY(20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.form-container {
animation: slideInUp 0.5s ease forwards;
}
/* Animation für Hover-Effekte */
.input-animation {
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.input-animation:focus {
transform: scale(1.01);
}
</style>
{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-8 animate-fadeIn">
<div class="max-w-3xl mx-auto">
<!-- Titel mit Animation -->
<div class="text-center mb-8 animate-pulse">
<h1 class="text-3xl font-bold mb-2 mystical-glow gradient-text">
Neue Mindmap erstellen
</h1>
<p class="opacity-80">Erstelle deine eigene Wissenslandkarte und organisiere deine Gedanken</p>
</div>
<div class="form-container">
<div class="form-header">
<h2 class="text-xl font-semibold">Mindmap-Details</h2>
</div>
<div class="form-body">
<form action="{{ url_for('create_mindmap') }}" method="POST">
<div class="form-group">
<label for="name" class="form-label">Name der Mindmap</label>
<input type="text" id="name" name="name" class="form-input input-animation" required placeholder="z.B. Meine Philosophie-Mindmap">
</div>
<div class="form-group">
<label for="description" class="form-label">Beschreibung</label>
<textarea id="description" name="description" class="form-textarea input-animation" placeholder="Worum geht es in dieser Mindmap?"></textarea>
</div>
<div class="form-group">
<div class="form-switch">
<input type="checkbox" id="is_private" name="is_private" checked>
<label for="is_private"></label>
<span>Private Mindmap (nur für dich sichtbar)</span>
</div>
</div>
<div class="flex justify-between mt-6">
<a href="{{ url_for('profile') }}" class="btn-cancel">
<i class="fas fa-arrow-left"></i>
Zurück
</a>
<button type="submit" class="btn-submit">
<i class="fas fa-save"></i>
Mindmap erstellen
</button>
</div>
</form>
<!-- Mindmap-Vorschau -->
<div class="mt-8">
<h3 class="text-xl font-semibold mb-4">Vorschau</h3>
<div class="mindmap-container">
<div id="cy" class="w-full h-[400px] rounded-xl border"
x-bind:class="darkMode ? 'border-gray-700' : 'border-gray-200'">
</div>
</div>
</div>
</div>
</div>
<!-- Tipps-Sektion -->
<div class="mt-8 p-5 rounded-lg border animate-fadeIn"
x-bind:class="darkMode ? 'bg-slate-800/40 border-slate-700/50' : 'bg-white border-slate-200'">
<h3 class="text-xl font-semibold mb-3"
x-bind:class="darkMode ? 'text-white' : 'text-gray-800'">
<i class="fa-solid fa-lightbulb text-yellow-400 mr-2"></i>Tipps zum Erstellen einer Mindmap
</h3>
<div x-bind:class="darkMode ? 'text-gray-300' : 'text-gray-600'">
<ul class="list-disc pl-5 space-y-2">
<li>Wähle einen prägnanten, aber aussagekräftigen Namen für deine Mindmap</li>
<li>Beginne mit einem zentralen Konzept und arbeite dich nach außen vor</li>
<li>Verwende verschiedene Farben für unterschiedliche Kategorien oder Themenbereiche</li>
<li>Füge Notizen zu Knoten hinzu, um komplexere Ideen zu erklären</li>
<li>Verknüpfe verwandte Konzepte, um Beziehungen zu visualisieren</li>
</ul>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.26.0/cytoscape.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape-cose-bilkent/4.1.0/cytoscape-cose-bilkent.min.js"></script>
<script nonce="{{ csp_nonce }}">
document.addEventListener('DOMContentLoaded', function() {
// Einfache Animationen für die Eingabefelder
const inputs = document.querySelectorAll('.input-animation');
inputs.forEach(input => {
// Subtile Skalierung bei Fokus
input.addEventListener('focus', function() {
this.style.transform = 'scale(1.01)';
this.style.boxShadow = '0 4px 12px rgba(124, 58, 237, 0.15)';
});
input.addEventListener('blur', function() {
this.style.transform = 'scale(1)';
this.style.boxShadow = 'none';
});
});
// Formular-Absenden-Animation
const form = document.querySelector('form');
form.addEventListener('submit', function(e) {
const submitBtn = this.querySelector('.btn-submit');
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Wird erstellt...';
submitBtn.disabled = true;
});
// Mindmap-Vorschau initialisieren
const mindmap = new MindMap.Visualization('cy', {
enableEditing: true,
onNodeClick: function(nodeData) {
console.log("Knoten ausgewählt:", nodeData);
}
});
// Formularfelder mit Mindmap verbinden
const nameInput = document.getElementById('name');
const descriptionInput = document.getElementById('description');
// Aktualisiere Mindmap wenn sich die Eingaben ändern
nameInput.addEventListener('input', function() {
if (mindmap.cy) {
const rootNode = mindmap.cy.$('#root');
if (rootNode.length > 0) {
rootNode.data('name', this.value || 'Neue Mindmap');
}
}
});
// Initialisiere die Mindmap
mindmap.initialize().then(() => {
console.log("Mindmap-Vorschau initialisiert");
// Setze initiale Werte
if (nameInput.value) {
const rootNode = mindmap.cy.$('#root');
if (rootNode.length > 0) {
rootNode.data('name', nameInput.value);
}
}
}).catch(error => {
console.error("Fehler bei der Initialisierung der Mindmap:", error);
});
});
</script>
{% endblock %}

View File

@@ -1,423 +0,0 @@
{% extends "base.html" %}
{% block title %}Mindmap bearbeiten{% endblock %}
{% block extra_css %}
<style>
/* Spezifische Stile für die Mindmap-Bearbeitungsseite */
.form-container {
background-color: var(--bg-secondary);
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
body.dark .form-container {
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.2);
border: 1px solid rgba(255, 255, 255, 0.05);
}
body:not(.dark) .form-container {
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(0, 0, 0, 0.05);
}
.form-header {
padding: 1.5rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
body:not(.dark) .form-header {
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
}
.form-body {
padding: 1.5rem;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
}
.form-input,
.form-textarea {
width: 100%;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
transition: all 0.3s ease;
}
body.dark .form-input,
body.dark .form-textarea {
background-color: rgba(15, 23, 42, 0.6);
border: 1px solid rgba(255, 255, 255, 0.1);
color: #f1f5f9;
}
body:not(.dark) .form-input,
body:not(.dark) .form-textarea {
background-color: white;
border: 1px solid #e2e8f0;
color: #334155;
}
body.dark .form-input:focus,
body.dark .form-textarea:focus {
border-color: #7c3aed;
box-shadow: 0 0 0 2px rgba(124, 58, 237, 0.2);
outline: none;
}
body:not(.dark) .form-input:focus,
body:not(.dark) .form-textarea:focus {
border-color: #7c3aed;
box-shadow: 0 0 0 2px rgba(124, 58, 237, 0.2);
outline: none;
}
.form-textarea {
min-height: 120px;
resize: vertical;
}
.form-switch {
display: flex;
align-items: center;
}
.form-switch input[type="checkbox"] {
height: 0;
width: 0;
visibility: hidden;
position: absolute;
}
.form-switch label {
cursor: pointer;
width: 50px;
height: 25px;
background: rgba(100, 116, 139, 0.3);
display: block;
border-radius: 25px;
position: relative;
margin-right: 10px;
transition: all 0.3s ease;
}
.form-switch label:after {
content: '';
position: absolute;
top: 3px;
left: 3px;
width: 19px;
height: 19px;
background: #fff;
border-radius: 19px;
transition: 0.3s;
}
.form-switch input:checked + label {
background: #7c3aed;
}
.form-switch input:checked + label:after {
left: calc(100% - 3px);
transform: translateX(-100%);
}
.btn-submit {
background-color: #7c3aed;
color: white;
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
font-weight: 500;
transition: all 0.3s ease;
border: none;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.btn-submit:hover {
background-color: #6d28d9;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(109, 40, 217, 0.2);
}
.btn-cancel {
background-color: transparent;
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
font-weight: 500;
transition: all 0.3s ease;
border: 1px solid;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
body.dark .btn-cancel {
color: #e2e8f0;
border-color: rgba(255, 255, 255, 0.1);
}
body:not(.dark) .btn-cancel {
color: #475569;
border-color: #e2e8f0;
}
.btn-cancel:hover {
transform: translateY(-2px);
}
body.dark .btn-cancel:hover {
background-color: rgba(255, 255, 255, 0.05);
}
body:not(.dark) .btn-cancel:hover {
background-color: rgba(0, 0, 0, 0.05);
}
/* Animation für den Seiteneintritt */
@keyframes slideInUp {
from {
transform: translateY(20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.form-container {
animation: slideInUp 0.5s ease forwards;
}
/* Animation für Hover-Effekte */
.input-animation {
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.input-animation:focus {
transform: scale(1.01);
}
</style>
{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-8 animate-fadeIn">
<div class="max-w-3xl mx-auto">
<!-- Titel mit Animation -->
<div class="text-center mb-8 animate-pulse">
<h1 class="text-3xl font-bold mb-2 mystical-glow gradient-text">
Mindmap bearbeiten
</h1>
<p class="opacity-80">Aktualisiere die Details deiner Mindmap</p>
</div>
<div class="form-container">
<div class="form-header">
<h2 class="text-xl font-semibold">Mindmap-Details</h2>
</div>
<div class="form-body">
<form action="{{ url_for('edit_mindmap', mindmap_id=mindmap.id) }}" method="POST">
<div class="form-group">
<label for="name" class="form-label">Name der Mindmap</label>
<input type="text" id="name" name="name" class="form-input input-animation" required
placeholder="z.B. Meine Philosophie-Mindmap" value="{{ mindmap.name }}">
</div>
<div class="form-group">
<label for="description" class="form-label">Beschreibung</label>
<textarea id="description" name="description" class="form-textarea input-animation"
placeholder="Worum geht es in dieser Mindmap?">{{ mindmap.description }}</textarea>
</div>
<div class="form-group">
<div class="form-switch">
<input type="checkbox" id="is_private" name="is_private" {% if mindmap.is_private %}checked{% endif %}>
<label for="is_private"></label>
<span>Private Mindmap (nur für dich sichtbar)</span>
</div>
</div>
<div class="flex justify-between mt-6">
<a href="{{ url_for('mindmap', mindmap_id=mindmap.id) }}" class="btn-cancel">
<i class="fas fa-arrow-left"></i>
Zurück
</a>
<button type="submit" class="btn-submit">
<i class="fas fa-save"></i>
Änderungen speichern
</button>
</div>
</form>
<!-- Mindmap-Editor -->
<div class="mt-8">
<h3 class="text-xl font-semibold mb-4">Mindmap bearbeiten</h3>
<div class="mindmap-container">
<div id="cy" class="w-full h-[600px] rounded-xl border"
x-bind:class="darkMode ? 'border-gray-700' : 'border-gray-200'">
</div>
</div>
<!-- Bearbeitungshinweise -->
<div class="mt-4 text-sm opacity-80">
<p><i class="fas fa-info-circle mr-2"></i>Klicke auf Knoten zum Bearbeiten, ziehe sie zum Neuanordnen oder nutze die Toolbar für weitere Funktionen.</p>
</div>
</div>
</div>
</div>
<!-- Tipps-Sektion -->
<div class="mt-8 p-5 rounded-lg border animate-fadeIn"
x-bind:class="darkMode ? 'bg-slate-800/40 border-slate-700/50' : 'bg-white border-slate-200'">
<h3 class="text-xl font-semibold mb-3"
x-bind:class="darkMode ? 'text-white' : 'text-gray-800'">
<i class="fa-solid fa-lightbulb text-yellow-400 mr-2"></i>Tipps zum Bearbeiten einer Mindmap
</h3>
<div x-bind:class="darkMode ? 'text-gray-300' : 'text-gray-600'">
<ul class="list-disc pl-5 space-y-2">
<li>Überprüfe, ob der Name noch zum aktuellen Inhalt passt</li>
<li>Aktualisiere die Beschreibung, um neue Aspekte zu berücksichtigen</li>
<li>Entscheide, ob die Sichtbarkeitseinstellungen noch passend sind</li>
<li>Nutze aussagekräftige Namen für bessere Auffindbarkeit</li>
<li>Behalte die Konsistenz mit verknüpften Konzepten im Auge</li>
</ul>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.26.0/cytoscape.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape-cose-bilkent/4.1.0/cytoscape-cose-bilkent.min.js"></script>
<script nonce="{{ csp_nonce }}">
document.addEventListener('DOMContentLoaded', function() {
// Einfache Animationen für die Eingabefelder
const inputs = document.querySelectorAll('.input-animation');
inputs.forEach(input => {
// Subtile Skalierung bei Fokus
input.addEventListener('focus', function() {
this.style.transform = 'scale(1.01)';
this.style.boxShadow = '0 4px 12px rgba(124, 58, 237, 0.15)';
});
input.addEventListener('blur', function() {
this.style.transform = 'scale(1)';
this.style.boxShadow = 'none';
});
});
// Formular-Absenden-Animation
const form = document.querySelector('form');
form.addEventListener('submit', function(e) {
const submitBtn = this.querySelector('.btn-submit');
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Wird gespeichert...';
submitBtn.disabled = true;
});
// Mindmap initialisieren
const mindmap = new MindMap.Visualization('cy', {
enableEditing: true,
apiEndpoint: '/api/mindmap/{{ mindmap.id }}',
onNodeClick: function(nodeData) {
console.log("Knoten ausgewählt:", nodeData);
},
onChange: function(data) {
// Automatisches Speichern bei Änderungen
fetch('/api/mindmap/{{ mindmap.id }}/update', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token() }}'
},
body: JSON.stringify(data)
}).then(response => {
if (!response.ok) {
throw new Error('Netzwerkfehler beim Speichern');
}
console.log('Änderungen gespeichert');
}).catch(error => {
console.error('Fehler beim Speichern:', error);
alert('Fehler beim Speichern der Änderungen');
});
}
});
// Formularfelder mit Mindmap verbinden
const nameInput = document.getElementById('name');
const descriptionInput = document.getElementById('description');
// Aktualisiere Mindmap wenn sich die Eingaben ändern
nameInput.addEventListener('input', function() {
if (mindmap.cy) {
const rootNode = mindmap.cy.$('#root');
if (rootNode.length > 0) {
rootNode.data('name', this.value || 'Mindmap');
mindmap.saveToServer();
}
}
});
// Initialisiere die Mindmap mit existierenden Daten
mindmap.initialize().then(() => {
console.log("Mindmap-Editor initialisiert");
// Lade existierende Daten
fetch('/api/mindmap/{{ mindmap.id }}/data')
.then(response => response.json())
.then(data => {
mindmap.loadData(data);
console.log("Mindmap-Daten geladen");
})
.catch(error => {
console.error("Fehler beim Laden der Mindmap-Daten:", error);
alert("Fehler beim Laden der Mindmap");
});
}).catch(error => {
console.error("Fehler bei der Initialisierung des Editors:", error);
});
// Autosave-Status Anzeige
const statusIndicator = document.createElement('div');
statusIndicator.className = 'fixed bottom-4 right-4 px-4 py-2 rounded-full text-sm transition-all duration-300';
document.body.appendChild(statusIndicator);
// Zeige Speicherstatus
function showStatus(message, isError = false) {
statusIndicator.textContent = message;
statusIndicator.className = `fixed bottom-4 right-4 px-4 py-2 rounded-full text-sm transition-all duration-300 ${
isError
? 'bg-red-500 text-white'
: 'bg-green-500 text-white'
}`;
setTimeout(() => {
statusIndicator.className = 'fixed bottom-4 right-4 px-4 py-2 rounded-full text-sm transition-all duration-300 opacity-0';
}, 2000);
}
// Event-Listener für Speicherstatus
document.addEventListener('mindmapSaved', () => {
showStatus('Änderungen gespeichert');
});
document.addEventListener('mindmapError', (event) => {
showStatus(event.detail.message, true);
});
});
</script>
{% endblock %}

View File

@@ -39,35 +39,6 @@
animation: textReveal 1s cubic-bezier(0.77, 0, 0.18, 1) forwards;
}
/* Marker-Animation für den Text */
@keyframes markerAnimation {
0% { width: 0; opacity: 0; }
20% { width: 100%; opacity: 0.7; }
80% { width: 100%; opacity: 0.7; }
100% { width: 0; opacity: 0; }
}
.marker-animation {
position: relative;
}
.marker-animation::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
height: 6px;
width: 0;
background: linear-gradient(to right, rgba(109, 40, 217, 0.3), rgba(139, 92, 246, 0.6), rgba(109, 40, 217, 0.3));
border-radius: 3px;
opacity: 0;
animation: markerAnimation 2.5s ease-in-out forwards;
}
.marker-animation-delay::after {
animation-delay: 1.5s;
}
.delay-1 { animation-delay: 0.2s; }
.delay-2 { animation-delay: 0.4s; }
.delay-3 { animation-delay: 0.6s; }
@@ -100,20 +71,16 @@
/* Chat section styles */
.embedded-chat {
height: 500px;
height: 350px;
border-radius: 1rem;
overflow: hidden;
transition: all 0.3s ease;
border: 1px solid;
display: flex;
flex-direction: column;
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 5px 10px -5px rgba(0, 0, 0, 0.05);
}
.dark .embedded-chat {
background-color: rgba(17, 24, 39, 0.7);
border-color: rgba(109, 40, 217, 0.2);
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.3), 0 5px 10px -5px rgba(0, 0, 0, 0.2);
}
.embedded-chat {
@@ -122,118 +89,9 @@
}
#embedded-chat-messages {
flex: 1;
height: 250px;
overflow-y: auto;
padding: 1.25rem;
min-height: 320px;
}
.chat-input-container {
padding: 1.25rem;
border-top: 1px solid;
background-color: rgba(255, 255, 255, 0.3);
}
.dark .chat-input-container {
background-color: rgba(17, 24, 39, 0.6);
border-color: rgba(75, 85, 99, 0.4);
}
.mystical-input {
background-color: rgba(255, 255, 255, 0.8);
border: 1px solid rgba(209, 213, 219, 0.5);
color: #4B5563;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
outline: none;
transition: all 0.3s ease;
width: 100%;
font-size: 1rem;
}
.dark .mystical-input {
background-color: rgba(31, 41, 55, 0.7);
border-color: rgba(75, 85, 99, 0.4);
color: #E5E7EB;
}
.mystical-input:focus {
border-color: rgba(139, 92, 246, 0.5);
box-shadow: 0 0 0 2px rgba(139, 92, 246, 0.2);
}
.dark .mystical-input:focus {
border-color: rgba(139, 92, 246, 0.5);
box-shadow: 0 0 0 2px rgba(139, 92, 246, 0.3);
}
/* Verbesserte Lesbarkeit für Chat-Nachrichten */
.chat-message {
margin-bottom: 1.25rem;
}
.chat-bubble {
padding: 1rem;
border-radius: 0.75rem;
max-width: 85%;
font-size: 1rem;
line-height: 1.5;
}
.assistant-bubble {
background-color: rgba(243, 244, 246, 0.95);
color: #374151;
}
.dark .assistant-bubble {
background-color: rgba(31, 41, 55, 0.95);
color: #E5E7EB;
}
.user-bubble {
background-color: rgba(139, 92, 246, 0.15);
color: #4B5563;
}
.dark .user-bubble {
background-color: rgba(124, 58, 237, 0.3);
color: #E5E7EB;
}
/* Beispiel-Buttons verbessert */
.quick-query-container {
margin-top: 0.75rem;
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
align-items: center;
}
.quick-query-btn {
font-size: 0.8rem;
padding: 0.4rem 0.75rem;
border-radius: 2rem;
background-color: rgba(243, 244, 246, 0.8);
color: #4B5563;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
border: 1px solid rgba(209, 213, 219, 0.5);
}
.dark .quick-query-btn {
background-color: rgba(55, 65, 81, 0.8);
color: #E5E7EB;
border-color: rgba(75, 85, 99, 0.4);
}
.quick-query-btn:hover {
background-color: rgba(229, 231, 235, 0.9);
transform: translateY(-1px);
}
.dark .quick-query-btn:hover {
background-color: rgba(75, 85, 99, 0.9);
}
/* Chat typing indicator */
@@ -273,9 +131,16 @@
<div class="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
<div class="text-center mb-16">
<h1 class="hero-heading mb-8 text-gray-900 dark:text-white">
<div class="overflow-hidden flex justify-center gap-6">
<span class="relative inline-block text-reveal marker-animation">Wissen.</span>
<span class="relative inline-block text-reveal delay-1 marker-animation marker-animation-delay">Vernetzen.</span>
<div class="overflow-hidden">
<span class="gradient-text inline-block text-reveal">Wissen</span>
</div>
<div class="overflow-hidden mt-2">
<span class="inline-block text-reveal delay-1">neu</span>
</div>
<div class="mt-2 relative overflow-hidden">
<span class="relative inline-block text-reveal delay-2">vernetzen
<div class="absolute -bottom-2 left-0 right-0 h-1 bg-gradient-to-r from-purple-500/0 via-purple-500/70 to-purple-500/0 rounded-full"></div>
</span>
</div>
</h1>
<div class="overflow-hidden">
@@ -314,12 +179,6 @@
<div class="text-center">
<div class="text-3xl font-bold gradient-text mb-2 animate-float">Systades</div>
<div class="text-lg text-gray-700 dark:text-gray-300">WISSEN VERNETZEN</div>
<!-- Animierte Pfeilspitze -->
<div class="mt-6 flex justify-center">
<svg width="20" height="12" viewBox="0 0 20 12" fill="none" xmlns="http://www.w3.org/2000/svg" class="text-white animate-bounce-slow">
<path d="M10 12L0 2L2 0L10 8L18 0L20 2L10 12Z" fill="currentColor" fill-opacity="0.7"/>
</svg>
</div>
</div>
</div>
</div>
@@ -412,14 +271,14 @@
</div>
<!-- Chat Messages -->
<div id="embedded-chat-messages" class="border-b-0">
<div id="embedded-chat-messages" class="border-b border-gray-200 dark:border-gray-700">
<!-- Assistant Message -->
<div class="chat-message flex">
<div class="w-9 h-9 rounded-full bg-gradient-to-r from-purple-600 to-indigo-600 flex items-center justify-center text-white mr-3 flex-shrink-0">
<div class="mb-4 flex">
<div class="w-8 h-8 rounded-full bg-gradient-to-r from-purple-600 to-indigo-600 flex items-center justify-center text-white mr-2 flex-shrink-0">
<i class="fa-solid fa-robot text-sm"></i>
</div>
<div class="chat-bubble assistant-bubble">
<div class="markdown-content">
<div class="bg-gray-100 dark:bg-gray-800 rounded-lg p-3 max-w-[80%]">
<div class="text-gray-700 dark:text-gray-300 markdown-content">
<p>Hallo! Ich bin dein Systades-Assistent. Wie kann ich dir heute helfen?</p>
<p>Du kannst mir Fragen zu:</p>
<ul>
@@ -433,24 +292,24 @@
</div>
<!-- User Message -->
<div class="chat-message flex justify-end">
<div class="chat-bubble user-bubble">
<p>
<div class="mb-4 flex justify-end">
<div class="bg-purple-100 dark:bg-purple-900/30 rounded-lg p-3 max-w-[80%]">
<p class="text-gray-800 dark:text-gray-200">
Kann ich mit deiner Hilfe eine Mindmap zum Thema Künstliche Intelligenz erstellen?
</p>
</div>
<div class="w-9 h-9 rounded-full bg-gray-300 dark:bg-gray-600 flex items-center justify-center text-gray-700 dark:text-gray-300 ml-3 flex-shrink-0">
<div class="w-8 h-8 rounded-full bg-gray-300 dark:bg-gray-600 flex items-center justify-center text-gray-700 dark:text-gray-300 ml-2 flex-shrink-0">
<i class="fa-solid fa-user text-sm"></i>
</div>
</div>
<!-- Assistant Response -->
<div class="chat-message flex" id="demo-ai-response">
<div class="w-9 h-9 rounded-full bg-gradient-to-r from-purple-600 to-indigo-600 flex items-center justify-center text-white mr-3 flex-shrink-0">
<div class="mb-4 flex" id="demo-ai-response">
<div class="w-8 h-8 rounded-full bg-gradient-to-r from-purple-600 to-indigo-600 flex items-center justify-center text-white mr-2 flex-shrink-0">
<i class="fa-solid fa-robot text-sm"></i>
</div>
<div class="chat-bubble assistant-bubble">
<div class="markdown-content">
<div class="bg-gray-100 dark:bg-gray-800 rounded-lg p-3 max-w-[80%]">
<div class="text-gray-700 dark:text-gray-300 markdown-content">
<p>Ja, natürlich! Ich kann dir dabei helfen, eine Mindmap zum Thema <strong>Künstliche Intelligenz</strong> zu erstellen.</p>
<p>Du kannst wie folgt vorgehen:</p>
<ol>
@@ -466,19 +325,19 @@
</div>
<!-- Chat Input -->
<div class="chat-input-container">
<div class="p-4">
<div class="flex">
<input type="text" placeholder="Stelle eine Frage..." class="mystical-input flex-grow" disabled>
<button class="ml-3 bg-gradient-to-r from-purple-600 to-indigo-600 text-white px-3 py-2 rounded-lg disabled:opacity-50 flex-shrink-0 hover:shadow-md transition-all duration-200" disabled>
<button class="ml-2 bg-gradient-to-r from-purple-600 to-indigo-600 text-white p-2 rounded-lg disabled:opacity-50" disabled>
<i class="fa-solid fa-paper-plane"></i>
</button>
</div>
<!-- Quick Queries -->
<div class="quick-query-container">
<span class="text-sm text-gray-500 dark:text-gray-400 mr-1">Beispiele:</span>
<button data-question="Was sind die wichtigsten Grundlagen der Künstlichen Intelligenz?" class="quick-query-btn hover:shadow-sm">KI-Grundlagen</button>
<button data-question="Wie kann ich eine Mindmap zum Thema Neuronale Netzwerke erstellen?" class="quick-query-btn hover:shadow-sm">Mindmap erstellen</button>
<button data-question="Zeige mir alle verfügbaren Kategorien in der Datenbank" class="quick-query-btn hover:shadow-sm">Datenbank durchsuchen</button>
<div class="mt-3 flex flex-wrap gap-2">
<span class="text-xs text-gray-500 dark:text-gray-400 mr-1">Beispiele:</span>
<button data-question="Was sind die wichtigsten Grundlagen der Künstlichen Intelligenz?" class="quick-query-btn text-xs px-2 py-1 rounded-full bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600 cursor-pointer transition-colors">KI-Grundlagen</button>
<button data-question="Wie kann ich eine Mindmap zum Thema Neuronale Netzwerke erstellen?" class="quick-query-btn text-xs px-2 py-1 rounded-full bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600 cursor-pointer transition-colors">Mindmap erstellen</button>
<button data-question="Zeige mir alle verfügbaren Kategorien in der Datenbank" class="quick-query-btn text-xs px-2 py-1 rounded-full bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600 cursor-pointer transition-colors">Datenbank durchsuchen</button>
</div>
</div>
</div>

View File

@@ -1,232 +1,234 @@
{% extends "base.html" %}
{% block title %}Interaktive Mindmap{% endblock %}
{% block extra_css %}
<style>
/* Grundlegendes Layout */
.mindmap-container {
position: relative;
width: 100%;
height: calc(100vh - 64px);
background: linear-gradient(135deg, #1a1f2e 0%, #0f172a 100%);
overflow: hidden;
}
/* Hauptcontainer für die Mindmap */
#cy {
width: 100%;
height: 100%;
background: transparent;
}
/* Header-Bereich */
.mindmap-header {
position: absolute;
top: 0;
left: 0;
right: 0;
padding: 1.5rem;
background: rgba(15, 23, 42, 0.8);
backdrop-filter: blur(10px);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
z-index: 10;
}
.mindmap-title {
font-size: 2rem;
font-weight: 700;
color: #fff;
margin: 0;
background: linear-gradient(90deg, #60a5fa, #8b5cf6);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
/* Kontrollpanel */
.control-panel {
position: absolute;
right: 2rem;
top: 50%;
transform: translateY(-50%);
background: rgba(15, 23, 42, 0.9);
border-radius: 1rem;
padding: 1.5rem;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
z-index: 10;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.control-button {
display: flex;
align-items: center;
padding: 0.75rem 1rem;
margin: 0.5rem 0;
background: rgba(255, 255, 255, 0.1);
border: none;
border-radius: 0.5rem;
color: #fff;
cursor: pointer;
transition: all 0.3s ease;
}
.control-button:hover {
background: rgba(255, 255, 255, 0.2);
transform: translateX(-5px);
}
.control-button i {
margin-right: 0.75rem;
}
/* Info-Panel */
.info-panel {
position: absolute;
left: 2rem;
top: 50%;
transform: translateY(-50%);
background: rgba(15, 23, 42, 0.9);
border-radius: 1rem;
padding: 1.5rem;
width: 300px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
z-index: 10;
border: 1px solid rgba(255, 255, 255, 0.1);
opacity: 0;
transform: translateX(-20px);
transition: all 0.3s ease;
}
.info-panel.visible {
opacity: 1;
transform: translateY(-50%) translateX(0);
}
.info-title {
font-size: 1.25rem;
font-weight: 600;
color: #fff;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.info-content {
color: rgba(255, 255, 255, 0.8);
font-size: 0.95rem;
line-height: 1.6;
}
/* Kategorie-Legende */
.category-legend {
position: absolute;
bottom: 2rem;
left: 50%;
transform: translateX(-50%);
background: rgba(15, 23, 42, 0.9);
border-radius: 1rem;
padding: 1rem 2rem;
display: flex;
gap: 1.5rem;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
z-index: 10;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.category-item {
display: flex;
align-items: center;
color: rgba(255, 255, 255, 0.8);
font-size: 0.9rem;
}
.category-color {
width: 12px;
height: 12px;
border-radius: 50%;
margin-right: 0.5rem;
}
/* Animationen */
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}
.pulse {
animation: pulse 2s infinite;
}
</style>
{% endblock %}
{% block content %}
<div class="mindmap-container">
<!-- Header -->
<div class="mindmap-header">
<h1 class="mindmap-title">Interaktive Wissenslandkarte</h1>
<!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>
<!-- Hauptvisualisierung -->
<div id="cy"></div>
<!-- Kontrollpanel -->
<div class="control-panel">
<button id="zoomIn" class="control-button">
<i class="fas fa-search-plus"></i>
<span>Vergrößern</span>
</button>
<button id="zoomOut" class="control-button">
<i class="fas fa-search-minus"></i>
<span>Verkleinern</span>
</button>
<button id="resetView" class="control-button">
<i class="fas fa-sync"></i>
<span>Zurücksetzen</span>
</button>
<button id="toggleLegend" class="control-button">
<i class="fas fa-layer-group"></i>
<span>Legende</span>
</button>
</div>
<!-- Info-Panel -->
<div id="infoPanel" class="info-panel">
<h3 class="info-title">Knotendetails</h3>
<div class="info-content"></div>
</div>
<!-- Kategorie-Legende -->
<div id="categoryLegend" class="category-legend">
<div class="category-item">
<div class="category-color" style="background-color: #60a5fa;"></div>
<span>Philosophie</span>
</div>
<div class="category-item">
<div class="category-color" style="background-color: #8b5cf6;"></div>
<span>Wissenschaft</span>
</div>
<div class="category-item">
<div class="category-color" style="background-color: #10b981;"></div>
<span>Technologie</span>
</div>
<div class="category-item">
<div class="category-color" style="background-color: #f59e0b;"></div>
<span>Künste</span>
</div>
<div class="category-item">
<div class="category-color" style="background-color: #ef4444;"></div>
<span>Psychologie</span>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<!-- Cytoscape und Erweiterungen -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.26.0/cytoscape.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape-cose-bilkent/4.1.0/cytoscape-cose-bilkent.min.js"></script>
<!-- Unsere JavaScript-Dateien -->
<script src="{{ url_for('static', filename='js/update_mindmap.js') }}"></script>
{% endblock %}
<!-- Unsere Mindmap JS -->
<script src="{{ url_for('static', filename='js/mindmap.js') }}"></script>
<!-- Icons initialisieren -->
<script>
document.addEventListener('DOMContentLoaded', () => {
if (typeof feather !== 'undefined') {
feather.replace();
}
});
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -1,75 +0,0 @@
{% extends "base.html" %}
{% block title %}Einfaches Profil{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-10">
<div class="bg-gray-800 bg-opacity-70 rounded-lg p-6 mb-6">
<h1 class="text-3xl font-bold text-purple-400 mb-4">Hallo, {{ user.username }}</h1>
<div class="text-gray-300 mb-4">
<p>E-Mail: {{ user.email }}</p>
<p>Mitglied seit: {{ user.created_at.strftime('%d.%m.%Y') }}</p>
</div>
<h2 class="text-xl font-semibold text-purple-300 mt-6 mb-3">Deine Mindmaps</h2>
{% if user_mindmaps %}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{% for mindmap in user_mindmaps %}
<div class="bg-gray-700 bg-opacity-50 p-4 rounded-lg">
<h3 class="text-lg font-medium text-purple-400 mb-2">{{ mindmap.name }}</h3>
<p class="text-gray-300 text-sm mb-3">{{ mindmap.description }}</p>
<div class="flex justify-between text-xs text-gray-400">
<span>Erstellt: {{ mindmap.created_at.strftime('%d.%m.%Y') }}</span>
</div>
<div class="mt-4 flex justify-between">
<a href="{{ url_for('mindmap') }}?id={{ mindmap.id }}" class="text-purple-400 hover:text-purple-300">
<i class="fas fa-eye mr-1"></i> Anzeigen
</a>
<a href="{{ url_for('edit_mindmap', mindmap_id=mindmap.id) }}" class="text-blue-400 hover:text-blue-300">
<i class="fas fa-edit mr-1"></i> Bearbeiten
</a>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="text-center py-6">
<p class="text-gray-400">Du hast noch keine Mindmaps erstellt</p>
<a href="{{ url_for('create_mindmap') }}" class="mt-3 inline-block px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700">
Erste Mindmap erstellen
</a>
</div>
{% endif %}
<h2 class="text-xl font-semibold text-purple-300 mt-8 mb-3">Deine Gedanken</h2>
{% if thoughts %}
<div class="space-y-4">
{% for thought in thoughts %}
<div class="bg-gray-700 bg-opacity-50 p-4 rounded-lg">
<h3 class="text-lg font-medium text-purple-400 mb-2">{{ thought.title }}</h3>
<p class="text-gray-300 text-sm mb-2">
{{ thought.abstract[:150] ~ '...' if thought.abstract and thought.abstract|length > 150 else thought.abstract }}
</p>
<div class="flex justify-between text-xs text-gray-400">
<span>Erstellt: {{ thought.created_at.strftime('%d.%m.%Y') }}</span>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="text-center py-6">
<p class="text-gray-400">Du hast noch keine Gedanken erstellt</p>
</div>
{% endif %}
<div class="mt-8 flex justify-between">
<a href="{{ url_for('index') }}" class="px-4 py-2 bg-gray-700 text-white rounded-lg hover:bg-gray-600">
Zurück zur Startseite
</a>
<a href="{{ url_for('logout') }}" class="px-4 py-2 bg-red-700 text-white rounded-lg hover:bg-red-600">
Abmelden
</a>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,391 +0,0 @@
{% extends "base.html" %}
{% block title %}{{ mindmap.name }} - Mindmap{% endblock %}
{% block extra_css %}
<style>
/* Container für die Mindmap mit verbesserten Glaseffekten */
#cy {
width: 100%;
height: calc(100vh - 14rem);
background-color: rgba(15, 23, 42, 0.3);
border-radius: 1rem;
overflow: hidden;
}
body:not(.dark) #cy {
background-color: rgba(255, 255, 255, 0.3);
}
/* Info-Panel */
.node-info-panel {
position: absolute;
top: 1rem;
right: 1rem;
width: 300px;
max-height: calc(100vh - 16rem);
overflow-y: auto;
border-radius: 1rem;
opacity: 0;
transform: translateX(20px);
pointer-events: none;
transition: all 0.3s ease;
}
.node-info-panel.visible {
opacity: 1;
transform: translateX(0);
pointer-events: all;
}
body.dark .node-info-panel {
background-color: rgba(15, 23, 42, 0.8);
border: 1px solid rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
}
body:not(.dark) .node-info-panel {
background-color: rgba(255, 255, 255, 0.85);
border: 1px solid rgba(0, 0, 0, 0.1);
backdrop-filter: blur(10px);
color: #1e293b;
}
/* Toolbar */
.mindmap-toolbar {
display: flex;
align-items: center;
justify-content: center;
padding: 0.75rem;
border-radius: 0.75rem;
margin-bottom: 1rem;
flex-wrap: wrap;
gap: 0.5rem;
}
body.dark .mindmap-toolbar {
background-color: rgba(15, 23, 42, 0.6);
border: 1px solid rgba(255, 255, 255, 0.1);
}
body:not(.dark) .mindmap-toolbar {
background-color: rgba(255, 255, 255, 0.8);
border: 1px solid rgba(0, 0, 0, 0.1);
}
.mindmap-action-btn {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
font-weight: 500;
transition: all 0.3s ease;
cursor: pointer;
}
body.dark .mindmap-action-btn {
background-color: rgba(30, 41, 59, 0.7);
color: rgba(255, 255, 255, 0.9);
border: 1px solid rgba(255, 255, 255, 0.1);
}
body:not(.dark) .mindmap-action-btn {
background-color: rgba(255, 255, 255, 0.8);
color: #1e293b;
border: 1px solid rgba(0, 0, 0, 0.1);
}
body.dark .mindmap-action-btn:hover {
background-color: rgba(51, 65, 85, 0.8);
transform: translateY(-2px);
}
body:not(.dark) .mindmap-action-btn:hover {
background-color: rgba(243, 244, 246, 0.9);
transform: translateY(-2px);
}
/* Node-Link Styles */
.node-link {
display: inline-block;
padding: 0.35rem 0.75rem;
margin: 0.25rem;
border-radius: 1rem;
font-size: 0.8rem;
cursor: pointer;
transition: all 0.3s ease;
white-space: nowrap;
color: white;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}
.node-link:hover {
transform: translateY(-2px);
filter: brightness(1.1);
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.15);
}
/* Notes */
.note-container {
position: absolute;
width: 250px;
cursor: move;
opacity: 0;
transform: scale(0.95);
transition: all 0.3s ease;
z-index: 10;
}
.note-container.visible {
opacity: 1;
transform: scale(1);
}
/* Note-Werkzeuge */
.note-tools {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem;
}
.note-tool {
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
font-size: 0.7rem;
opacity: 0.7;
}
.note-tool:hover {
opacity: 1;
transform: translateY(-1px);
}
/* Color Palette */
.color-palette {
display: flex;
gap: 0.25rem;
flex-wrap: wrap;
padding: 0.25rem;
}
.color-swatch {
width: 1rem;
height: 1rem;
border-radius: 50%;
cursor: pointer;
transition: all 0.2s ease;
}
.color-swatch:hover {
transform: scale(1.2);
}
/* Notizen-Editor */
.note-content {
min-height: 100px;
padding: 0.75rem;
outline: none;
width: 100%;
font-size: 0.9rem;
resize: both;
overflow: auto;
}
/* Kontext-Menü */
#context-menu {
min-width: 180px;
border-radius: 0.5rem;
overflow: hidden;
z-index: 1000;
}
body.dark #context-menu {
background-color: rgba(15, 23, 42, 0.95);
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.3);
}
body:not(.dark) #context-menu {
background-color: rgba(255, 255, 255, 0.95);
border: 1px solid rgba(0, 0, 0, 0.1);
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.1);
color: #1e293b;
}
.menu-item {
padding: 0.75rem 1rem;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 0.5rem;
}
body.dark .menu-item:hover {
background-color: rgba(255, 255, 255, 0.1);
}
body:not(.dark) .menu-item:hover {
background-color: rgba(0, 0, 0, 0.05);
}
/* Dialogfenster */
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
}
.dialog-content {
width: 90%;
max-width: 500px;
border-radius: 1rem;
padding: 1.5rem;
max-height: 80vh;
overflow-y: auto;
}
body.dark .dialog-content {
background-color: rgba(15, 23, 42, 0.95);
border: 1px solid rgba(255, 255, 255, 0.1);
}
body:not(.dark) .dialog-content {
background-color: rgba(255, 255, 255, 0.95);
border: 1px solid rgba(0, 0, 0, 0.1);
color: #1e293b;
}
</style>
{% endblock %}
{% block content %}
<div class="container mx-auto max-w-7xl px-4 py-6">
<!-- Header -->
<div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-6 gap-4">
<div>
<h1 class="text-3xl font-bold gradient-text">{{ mindmap.name }}</h1>
<p class="opacity-80 mt-1">{{ mindmap.description }}</p>
</div>
<div class="flex gap-3 flex-wrap">
{% if mindmap.user_id == current_user.id %}
<a href="{{ url_for('edit_mindmap', mindmap_id=mindmap.id) }}" class="mindmap-action-btn">
<i class="fas fa-edit"></i>
<span>Bearbeiten</span>
</a>
{% endif %}
<a href="{{ url_for('profile') }}" class="mindmap-action-btn">
<i class="fas fa-arrow-left"></i>
<span>Zurück</span>
</a>
</div>
</div>
<!-- Toolbar -->
<div class="mindmap-toolbar mb-4">
<button id="fit-btn" class="mindmap-action-btn">
<i class="fas fa-expand"></i>
<span>Alles anzeigen</span>
</button>
<button id="reset-btn" class="mindmap-action-btn">
<i class="fas fa-redo"></i>
<span>Layout zurücksetzen</span>
</button>
<button id="toggle-labels-btn" class="mindmap-action-btn">
<i class="fas fa-tags"></i>
<span>Labels ein/aus</span>
</button>
<button id="add-note-btn" class="mindmap-action-btn">
<i class="fas fa-sticky-note"></i>
<span>Notiz hinzufügen</span>
</button>
<button id="save-layout-btn" class="mindmap-action-btn">
<i class="fas fa-save"></i>
<span>Layout speichern</span>
</button>
</div>
<!-- Mindmap Container mit Positionsindikator -->
<div class="relative rounded-xl overflow-hidden border transition-all duration-300"
x-bind:class="darkMode ? 'border-gray-700/50' : 'border-gray-300/50'">
<div id="cy"></div>
<!-- Informationsanzeige für ausgewählten Knoten -->
<div id="node-info-panel" class="node-info-panel p-4">
<h3 class="text-xl font-bold gradient-text mb-2">Knotendetails</h3>
<p id="node-description" class="mb-4 opacity-80"></p>
<h4 class="font-semibold mb-2 opacity-90">Verbundene Knoten</h4>
<div id="connected-nodes" class="flex flex-wrap mb-4"></div>
<div class="flex flex-wrap gap-2 mt-4">
<button id="add-thought-btn" class="mindmap-action-btn">
<i class="fas fa-lightbulb"></i>
<span>Gedanke hinzufügen</span>
</button>
<button id="view-thoughts-btn" class="mindmap-action-btn">
<i class="fas fa-list"></i>
<span>Gedanken anzeigen</span>
</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="{{ url_for('static', filename='js/cytoscape.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/mindmap-init.js') }}"></script>
<script nonce="{{ csp_nonce }}">
document.addEventListener('DOMContentLoaded', function() {
// Benutzer-Mindmap-ID für die API-Anfragen
const mindmapId = {{ mindmap.id }};
// Erstellt eine neue MindMap-Instanz für die Benutzer-Mindmap
window.userMindmap = new MindMap('#cy', {
editable: true,
isUserLoggedIn: true,
isPublicMap: false,
userMindmapId: mindmapId,
fitViewOnInit: true,
callbacks: {
onLoad: function() {
console.log('Benutzerdefinierte Mindmap wurde geladen');
}
}
});
// Event-Listener für Notiz-Button
document.getElementById('add-note-btn').addEventListener('click', function() {
// Erstellt eine neue Notiz in der Mitte des Viewports
const position = window.userMindmap.cy.pan();
window.userMindmap.showAddNoteDialog({
x: position.x,
y: position.y
});
});
// Event-Listener für Layout-Speichern-Button
document.getElementById('save-layout-btn').addEventListener('click', function() {
window.userMindmap.saveLayout();
});
});
</script>
{% endblock %}

View File

@@ -1,65 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import sqlite3
import sys
# Bestimme den absoluten Pfad zur Datenbank
basedir = os.path.abspath(os.path.dirname(__file__))
db_path = os.path.join(basedir, 'database', 'systades.db')
def update_user_table():
"""Aktualisiert die User-Tabelle mit den fehlenden Spalten"""
# Überprüfe, ob die Datenbankdatei existiert
if not os.path.exists(db_path):
print(f"Datenbank nicht gefunden unter: {db_path}")
return False
# Verbindung zur Datenbank herstellen
try:
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
# Überprüfe, ob die neuen Spalten bereits existieren
cursor.execute("PRAGMA table_info(user)")
columns = [info[1] for info in cursor.fetchall()]
# Neue Spalten, die hinzugefügt werden müssen
new_columns = {
'bio': 'TEXT',
'location': 'VARCHAR(100)',
'website': 'VARCHAR(200)',
'avatar': 'VARCHAR(200)',
'last_login': 'DATETIME'
}
# Spalten hinzufügen, die noch nicht existieren
for col_name, col_type in new_columns.items():
if col_name not in columns:
print(f"Füge Spalte '{col_name}' zur User-Tabelle hinzu...")
cursor.execute(f"ALTER TABLE user ADD COLUMN {col_name} {col_type}")
# Änderungen speichern
conn.commit()
print("User-Tabelle erfolgreich aktualisiert!")
return True
except sqlite3.Error as e:
print(f"Fehler bei der Datenbankaktualisierung: {e}")
return False
finally:
if conn:
conn.close()
if __name__ == "__main__":
# Führe die Aktualisierung durch
success = update_user_table()
if success:
print("Die Datenbank wurde erfolgreich aktualisiert.")
sys.exit(0)
else:
print("Es gab ein Problem bei der Datenbankaktualisierung.")
sys.exit(1)

View File

@@ -55,7 +55,7 @@ def create_user(username, email, password, is_admin=False):
user = User(
username=username,
email=email,
role='admin' if is_admin else 'user',
is_admin=is_admin,
created_at=datetime.utcnow()
)
user.set_password(password)

View File

@@ -1,5 +1,5 @@
home = C:\Program Files\Python313
include-system-site-packages = false
version = 3.13.3
executable = C:\Users\firem\Desktop\111\Systades\website\.venv\Scripts\python.exe
command = C:\Users\firem\Desktop\111\Systades\website\.venv\Scripts\python.exe -m venv C:\Users\firem\Desktop\111\Systades\website\venv
executable = C:\Program Files\Python313\python.exe
command = C:\Program Files\Python313\python.exe -m venv C:\Users\TTOMCZA.EMEA\Dev\website\venv

58
windows_setup.bat Normal file
View File

@@ -0,0 +1,58 @@
@echo off
echo Mindmap Projekt - Windows Setup
echo ==============================
echo.
REM Prüfen, ob Python installiert ist
python --version >nul 2>&1
if %errorlevel% neq 0 (
echo Python ist nicht installiert oder nicht im PATH.
echo Bitte installiere Python 3.11 von https://www.python.org/downloads/
echo und stelle sicher, dass "Add Python to PATH" während der Installation aktiviert ist.
pause
exit /b 1
)
echo Erstelle virtuelle Umgebung...
python -m venv venv
if %errorlevel% neq 0 (
echo Fehler beim Erstellen der virtuellen Umgebung.
pause
exit /b 1
)
echo Aktiviere virtuelle Umgebung...
call venv\Scripts\activate.bat
if %errorlevel% neq 0 (
echo Fehler beim Aktivieren der virtuellen Umgebung.
pause
exit /b 1
)
echo Aktualisiere pip...
python -m pip install --upgrade pip
if %errorlevel% neq 0 (
echo Warnung: Pip konnte nicht aktualisiert werden. Fahre trotzdem fort.
)
echo Installiere Abhängigkeiten...
pip install -r requirements.txt
if %errorlevel% neq 0 (
echo Fehler beim Installieren der Abhängigkeiten.
pause
exit /b 1
)
echo.
echo Setup abgeschlossen!
echo.
echo Zum Starten des Servers:
echo 1. Führe "venv\Scripts\activate.bat" aus
echo 2. Führe "python TOOLS.py db:rebuild" aus (Nur beim ersten Mal oder zum Zurücksetzen der Datenbank)
echo 3. Führe "python TOOLS.py user:admin" aus (Erstellt einen Admin-Benutzer: admin/admin)
echo 4. Führe "python TOOLS.py server:run" aus
echo.
echo Die Anwendung ist dann unter http://localhost:5000 erreichbar.
echo.
pause