Compare commits

16 Commits

Author SHA1 Message Date
b8ad3aea13 Implement cybernetwork background features: Add routes for serving cybernetwork CSS and JavaScript files, update base.html to include the new CSS and initialization script, and enhance style.css with smooth scrolling and background imports. This update improves the visual experience and functionality of the cybernetwork background in the application. 2025-04-27 15:01:38 +02:00
edf3049e42 Remove deprecated files and templates: Delete unused files including deployment scripts, environment configurations, and various HTML templates to streamline the project structure. This cleanup enhances maintainability and reduces clutter in the codebase. 2025-04-27 14:50:20 +02: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
6167 changed files with 5816 additions and 749383 deletions

13
.env Normal file
View File

@@ -0,0 +1,13 @@
# MindMap Umgebungsvariablen
# Kopiere diese Datei zu .env und passe die Werte an
# Flask
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
# Der Pfad wird relativ zum Projektverzeichnis angegeben
# SQLALCHEMY_DATABASE_URI=sqlite:////absoluter/pfad/zu/database/systades.db OPENAI_API_KEY=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

View File

@@ -1,10 +0,0 @@
FROM python:3.9-slim-buster
WORKDIR /app
COPY requirements.txt requirements.txt
RUN pip install -r requirements.txt
COPY website .
CMD ["python", "app.py"]

165
README.md
View File

@@ -1,67 +1,134 @@
# MindMap Wissensnetzwerk # MindMapProjekt - Roadmap
Eine interaktive Plattform zum Visualisieren, Erforschen und Teilen von Wissen mit integriertem ChatGPT-Assistenten. ## Projektübersicht
Das MindMapProjekt ist eine interaktive Plattform zum Visualisieren, Erforschen und Teilen von Wissen. Das Projekt wird umfassend überarbeitet, um ein modernes, benutzerfreundliches Design und erweiterte Funktionalitäten zu bieten.
## Features ## Technischer Stack
- **Backend**: Python/Flask
- **Frontend**:
- Tailwind CSS für moderne UI
- SVG-Bibliotheken für Visualisierungen (D3.js)
- JavaScript/Alpine.js für interaktive Komponenten
- **Datenbank**: SQLite mit SQLAlchemy
- **KI-Integration**: OpenAI API für intelligente Assistenz
- Interaktive Mindmap zur Visualisierung von Wissensverbindungen ## Installation und Verwendung
- Gedanken mit verschiedenen Beziehungstypen verknüpfen
- Suchfunktion für Gedanken und Verbindungen
- Bewertungssystem für Gedanken
- Dark/Light Mode
- **Integrierter KI-Assistent** mit OpenAI GPT-Integration
## Installation ### Installation
1. Repository klonen
2. Virtuelle Umgebung erstellen: `python -m venv venv`
3. Virtuelle Umgebung aktivieren:
- Windows: `venv\Scripts\activate`
- Unix/MacOS: `source venv/bin/activate`
4. Abhängigkeiten installieren: `pip install -r requirements.txt`
5. Datenbank initialisieren: `python TOOLS.py db:rebuild`
6. Admin-Benutzer erstellen: `python TOOLS.py user:admin`
7. Server starten: `python TOOLS.py server:run`
1. Repository klonen: ### Standardbenutzer
``` - **Admin-Benutzer**: Username: `admin` / Passwort: `admin`
git clone <repository-url> - **Testbenutzer**: Username: `user` / Passwort: `user`
cd website
```
2. Python-Abhängigkeiten installieren: ### Verwaltungswerkzeuge mit TOOLS.py
``` Das Projekt enthält ein zentrales Verwaltungsskript `TOOLS.py`, das verschiedene Hilfsfunktionen bietet:
pip install -r requirements.txt
```
3. Environment-Variablen konfigurieren: #### Datenbankverwaltung
``` - `python TOOLS.py db:fix` - Reparieren der Datenbankstruktur
cp example.env .env - `python TOOLS.py db:rebuild` - Datenbank neu aufbauen (löscht alle Daten!)
``` - `python TOOLS.py db:test` - Datenbankverbindung und Modelle testen
Bearbeite die `.env`-Datei und füge deinen OpenAI API-Schlüssel ein. - `python TOOLS.py db:stats` - Datenbankstatistiken anzeigen
4. Datenbank initialisieren: #### Benutzerverwaltung
``` - `python TOOLS.py user:list` - Alle Benutzer anzeigen
python init_db.py - `python TOOLS.py user:create -u USERNAME -e EMAIL -p PASSWORD [-a]` - Neuen Benutzer erstellen
``` - `python TOOLS.py user:admin` - Admin-Benutzer erstellen (admin/admin)
- `python TOOLS.py user:reset-pw -u USERNAME -p NEWPASSWORD` - Benutzerpasswort zurücksetzen
- `python TOOLS.py user:delete -u USERNAME` - Benutzer löschen
5. Anwendung starten: #### Serververwaltung
``` - `python TOOLS.py server:run [--host HOST] [--port PORT] [--no-debug]` - Entwicklungsserver starten
python run.py
```
## Verwendung des KI-Assistenten Für detaillierte Hilfe: `python TOOLS.py -h`
Der KI-Assistent ist über folgende Wege zugänglich: ## Roadmap der Überarbeitung
1. **Schwebende Schaltfläche**: In der unteren rechten Ecke der Webseite ist eine Roboter-Schaltfläche, die den Assistenten öffnet. ### Phase 1: Grundlegende Infrastruktur ✅
2. **Navigation**: In der Hauptnavigation gibt es ebenfalls eine Schaltfläche mit Roboter-Symbol. - [x] Bestandsaufnahme des aktuellen Projekts
3. **Startseite**: Im "KI-Assistent"-Abschnitt auf der Startseite gibt es einen "KI-Chat starten"-Button. - [x] Erstellung der Roadmap
- [x] Aktualisierung der Abhängigkeiten
- [x] Integration von Tailwind CSS
- [x] Einrichtung der SVG-Bibliotheken (D3.js)
- [x] Favicon erstellen
- [x] Setup-Skript für einfache Installation
Der Assistent kann bei folgenden Aufgaben helfen: ### Phase 2: Design-Überarbeitung 🔄
- [x] Implementierung des Dark Mode
- [x] Erstellung eines modernen, minimalistischen UI mit Tech-Ästhetik
- [x] Responsive Design für alle Geräte
- [ ] Gestaltung der Landing Page mit großer Typografie
- Erklärung von Themen und Konzepten ### Phase 3: Mindmap-Funktionalitäten 🔄
- Suche nach Verbindungen zwischen Gedanken - [x] Verbesserte Visualisierung mit SVG und D3.js
- Beantwortung von Fragen zur Plattform - [x] Implementierung der Mouseover-Funktion
- Vorschläge für neue Gedankenverbindungen - [x] Entwicklung der Suchfunktion für Knoten
- [ ] Tagging-System für Inhalte
- [ ] Quellenmanagement und -verlinkung
- [ ] Upload-Funktionalität an Knotenpunkten
## Technologie-Stack ### Phase 4: Kernseitenentwicklung
- [ ] Überarbeitung der Startseite mit neuen Features
- [ ] Entwicklung der "Wer sind wir?"-Seite
- [ ] Implementierung von Impressum und Datenschutzerklärung
- [ ] Erstellung der Kontaktseite mit FAQs
- [ ] Überarbeitung des Benutzerprofilbereichs
- **Backend**: Flask, SQLAlchemy ### Phase 5: Community-Features
- **Frontend**: HTML, CSS, JavaScript, Tailwind CSS, Alpine.js - [ ] Entwicklung des Autorenbereichs
- **KI**: OpenAI GPT API - [ ] Implementierung von Community-Bereichen für Themenbereiche
- **Datenbank**: SQLite (Standard), kann auf andere Datenbanken umgestellt werden - [ ] Verbesserter Kommentarbereich
- [ ] Benutzerrechtemanagement
## Konfiguration ### Phase 6: KI-Integration
- [ ] Implementierung des Frage-Antwort-Systems
- [ ] KI-generierte Themeneinleitungen
- [ ] Intelligente Suchunterstützung
- [ ] Geführte Pfade durch Themenbereiche
- [ ] Vorgeschlagene Chat-Möglichkeiten
Die Anwendung kann über Umgebungsvariablen konfiguriert werden. Siehe `example.env` für verfügbare Optionen. ### Phase 7: Benutzerprofilfunktionen
- [ ] Speichern von Thematiken
- [ ] Persönliche Mindmap/Pinboard
- [ ] Beitragsmanagement
- [ ] Benutzerstatistiken und -aktivitäten
### Phase 8: Testing und Optimierung
- [ ] Umfassende Tests aller Funktionen
- [ ] Performance-Optimierung
- [ ] SEO-Implementierung
- [ ] Barrierefreiheit prüfen und verbessern
### Phase 9: Dokumentation und Einführung
- [ ] Erstellung von Benutzeranleitungen
- [ ] Entwicklerdokumentation
- [ ] Administratorenhandbuch
- [ ] Guided Tour für neue Benutzer
## Aktueller Status
- **Phase 1**: ✅ Abgeschlossen
- **Phase 2**: 🔄 In Bearbeitung (75% abgeschlossen)
- **Phase 3**: 🔄 In Bearbeitung (50% abgeschlossen)
## Aktuelle Fortschritte
- Grundlegende UI modernisiert mit Tailwind CSS und Dark Mode
- Neues Favicon für bessere visuelle Identität erstellt
- Setup-Prozess vereinfacht mit einem Shell-Skript
- Mindmap-Visualisierung komplett überarbeitet mit D3.js für eine interaktivere Erfahrung
- Responsive Design für optimale Darstellung auf allen Geräten
## Nächste Schritte
- Fertigstellung der Landing Page
- Erstellung der "Wer sind wir?"-Seite
- Implementierung des Tagging-Systems für Gedanken
- Verbesserung der Gedankenansicht im Mindmap-Bereich
*Zuletzt aktualisiert: 01.06.2024*

102
ROADMAP.md Normal file
View File

@@ -0,0 +1,102 @@
# Systades Mindmap - Entwicklungs-Roadmap
Diese Roadmap beschreibt die geplante Entwicklung der dynamischen, benutzerorientierten Mindmap-Funktionalität für das Systades-Projekt.
## Phase 1: Grundlegendes Datenmodell und Backend (Abgeschlossen)
- [x] Entwurf des Datenbankschemas für benutzerorientierte Mindmaps
- [x] Implementierung der Modelle in models.py
- [x] Erstellung der API-Endpunkte für CRUD-Operationen
- [x] Integration mit der bestehenden Benutzerauthentifizierung
- [x] Seed-Daten für die Entwicklung und Tests
## Phase 2: Dynamische Mindmap-Visualisierung (Aktuell)
- [ ] Anpassung des Frontend-Codes zur Verwendung der DB-Daten anstelle des SVG
- [ ] Implementierung von AJAX-Anfragen zum Laden der Mindmap-Daten
- [ ] Dynamisches Rendering der Knoten, Verbindungen und Labels
- [ ] Drag-and-Drop-Funktionalität für die Bewegung von Knoten
- [ ] Zoom- und Pan-Funktionalität mit Persistenz der Ansicht
## Phase 3: Benutzerdefinierte Mindmaps
- [ ] UI für das Erstellen, Bearbeiten und Löschen eigener Mindmaps
- [ ] Funktion zum Hinzufügen/Entfernen von Knoten aus der öffentlichen Mindmap
- [ ] Speichern der Knotenpositionen und Ansichtseinstellungen
- [ ] Benutzerspezifische Visualisierungseinstellungen
- [ ] Dashboard mit Übersicht aller Mindmaps des Benutzers
## Phase 4: Notizen und Annotationen
- [ ] UI für das Hinzufügen privater Notizen zu Knoten
- [ ] Visuelle Anzeige von Notizen in der Mindmap
- [ ] Texteditor mit Markdown-Unterstützung für Notizen
- [ ] Kategorisierung und Farbkodierung von Notizen
- [ ] Suchfunktion für Notizen
## Phase 5: Integrationen und Erweiterungen
- [ ] Import/Export-Funktionalität für Mindmaps (JSON, PNG)
- [ ] Teilen von Mindmaps (öffentlich/privat/mit bestimmten Benutzern)
- [ ] Kollaborative Bearbeitung von Mindmaps
- [ ] Verknüpfung mit externen Ressourcen (Links, Dateien)
- [ ] Versionierung von Mindmaps
## Phase 6: KI-Integration und Analyse
- [ ] KI-gestützte Vorschläge für Verbindungen zwischen Knoten
- [ ] Automatische Kategorisierung von Inhalten
- [ ] Visualisierung von Beziehungsstärken und -typen
- [ ] Mindmap-Statistiken und Analysen
- [ ] KI-basierte Zusammenfassung von Teilbereichen der Mindmap
## Phase 7: Optimierung und Skalierung
- [ ] Performance-Optimierung für große Mindmaps
- [ ] Verbesserung der Benutzerfreundlichkeit basierend auf Feedback
- [ ] Erweiterte Such- und Filterfunktionen
- [ ] Mobile Optimierung
- [ ] Offline-Funktionalität mit Synchronisierung
## Technische Schulden und Refactoring
- [ ] Trennung der Datenbank-Logik vom Flask-App-Code
- [ ] Einführung von Unit-Tests und Integration-Tests
- [ ] Überarbeitung der API-Dokumentation
- [ ] Caching-Strategien für bessere Performance
- [ ] Verbesserte Fehlerbehandlung und Logging
---
## Implementierungsdetails
### Datenbankschema
Das Datenbankschema umfasst folgende Hauptentitäten:
1. **Category** - Wissenschaftliche Kategorien für die öffentliche Mindmap
2. **MindMapNode** - Öffentliche Mindmap-Knoten mit Metadaten
3. **UserMindmap** - Benutzerdefinierte Mindmaps
4. **UserMindmapNode** - Verknüpfung zwischen Benutzermindmaps und öffentlichen Knoten
5. **MindmapNote** - Benutzerspezifische Notizen
6. **Thought** - Gedanken und Inhalte, die Knoten zugeordnet sind
7. **ThoughtRelation** - Beziehungen zwischen Gedanken
### Frontend-Technologien
- D3.js für die Visualisierung der Mindmap
- AJAX für dynamisches Laden von Daten
- Interaktive Bedienelemente mit JavaScript
- Responsive Design mit Tailwind CSS
### Backend-APIs
Die implementierten API-Endpunkte umfassen:
- `/api/mindmap/public` - Abrufen der öffentlichen Mindmap-Struktur
- `/api/mindmap/user/<id>` - Abrufen benutzerdefinierter Mindmaps
- `/api/mindmap/<id>/add_node` - Hinzufügen eines Knotens zur Benutzer-Mindmap
- `/api/mindmap/<id>/remove_node/<node_id>` - Entfernen eines Knotens
- `/api/mindmap/<id>/update_node_position` - Aktualisierung von Knotenpositionen
- `/api/mindmap/<id>/notes` - Verwaltung von Notizen
- `/api/nodes/<id>/thoughts` - Abrufen und Hinzufügen von Gedanken zu Knoten

125
TOOLS.py Executable file
View File

@@ -0,0 +1,125 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
TOOLS.py - Main utility script for the website application.
This script provides a command-line interface to all utilities
for database management, user management, and server administration.
Usage:
python3 TOOLS.py [command] [options]
Available commands:
- db:fix Fix database schema
- db:rebuild Completely rebuild the database
- db:test Test database connection and models
- db:stats Show database statistics
- user:list List all users
- user:create Create a new user
- user:admin Create admin user (username: admin, password: admin)
- user:reset-pw Reset user password
- user:delete Delete a user
- server:run Run the development server
Examples:
python3 TOOLS.py db:rebuild
python3 TOOLS.py user:admin
python3 TOOLS.py server:run --port 8080
"""
import os
import sys
import argparse
from utils import (
fix_database_schema, rebuild_database, run_all_tests, print_database_stats,
list_users, create_user, reset_password, delete_user, create_admin_user,
run_development_server
)
def parse_args():
parser = argparse.ArgumentParser(
description='Website Administration Tools',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__
)
# Main command argument
parser.add_argument('command', help='Command to execute')
# Additional arguments
parser.add_argument('--username', '-u', help='Username for user commands')
parser.add_argument('--email', '-e', help='Email for user creation')
parser.add_argument('--password', '-p', help='Password for user creation/reset')
parser.add_argument('--admin', '-a', action='store_true', help='Make user an admin')
parser.add_argument('--host', help='Host for server (default: 127.0.0.1)')
parser.add_argument('--port', type=int, help='Port for server (default: 5000)')
parser.add_argument('--no-debug', action='store_true', help='Disable debug mode for server')
return parser.parse_args()
def main():
args = parse_args()
# Database commands
if args.command == 'db:fix':
fix_database_schema()
elif args.command == 'db:rebuild':
print("WARNING: This will delete all data in the database!")
confirm = input("Are you sure you want to continue? (y/n): ").lower()
if confirm == 'y':
rebuild_database()
else:
print("Aborted.")
elif args.command == 'db:test':
run_all_tests()
elif args.command == 'db:stats':
print_database_stats()
# User commands
elif args.command == 'user:list':
list_users()
elif args.command == 'user:create':
if not args.username or not args.email or not args.password:
print("Error: Username, email, and password are required.")
print("Example: python3 TOOLS.py user:create -u username -e email -p password [-a]")
sys.exit(1)
create_user(args.username, args.email, args.password, args.admin)
elif args.command == 'user:admin':
create_admin_user()
elif args.command == 'user:reset-pw':
if not args.username or not args.password:
print("Error: Username and password are required.")
print("Example: python3 TOOLS.py user:reset-pw -u username -p new_password")
sys.exit(1)
reset_password(args.username, args.password)
elif args.command == 'user:delete':
if not args.username:
print("Error: Username is required.")
print("Example: python3 TOOLS.py user:delete -u username")
sys.exit(1)
delete_user(args.username)
# Server commands
elif args.command == 'server:run':
host = args.host or '127.0.0.1'
port = args.port or 5000
debug = not args.no_debug
run_development_server(host=host, port=port, debug=debug)
else:
print(f"Unknown command: {args.command}")
print("Run 'python3 TOOLS.py -h' for usage information")
sys.exit(1)
if __name__ == '__main__':
main()

Binary file not shown.

Binary file not shown.

1195
app.py Executable file

File diff suppressed because it is too large Load Diff

4
cookies.txt Normal file
View File

@@ -0,0 +1,4 @@
# Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.

View File

@@ -1,33 +0,0 @@
@echo off
echo Copying network image to website/static/network-bg.jpg...
if not exist "website\static" (
echo Error: website/static directory does not exist.
echo Make sure you are running this script from the main project directory.
pause
exit /b 1
)
if "%~1"=="" (
echo Usage: copy-network-image.bat [path_to_image]
echo Example: copy-network-image.bat d2efd014-1325-471f-b9a7-90d025eb81d6.png
pause
exit /b 1
)
if not exist "%~1" (
echo Error: The specified image file "%~1" does not exist.
pause
exit /b 1
)
copy /Y "%~1" "website\static\network-bg.jpg" > nul
if %errorlevel% equ 0 (
echo Success! The image has been copied to website/static/network-bg.jpg
echo Please restart the Flask server to see the changes.
) else (
echo Error: Failed to copy the image.
)
pause

BIN
database/systades.db Normal file

Binary file not shown.

BIN
database/systades.db.backup Normal file

Binary file not shown.

View File

@@ -1,7 +0,0 @@
version: "3.9"
services:
web:
build: .
ports:
- "5000:5000"
restart: always

View File

@@ -9,4 +9,5 @@ OPENAI_API_KEY=sk-dein-openai-api-schluessel-hier
# Datenbank # Datenbank
# Bei Bedarf kann hier eine andere Datenbank-URL angegeben werden # Bei Bedarf kann hier eine andere Datenbank-URL angegeben werden
# SQLALCHEMY_DATABASE_URI=sqlite:///mindmap.db # Der Pfad wird relativ zum Projektverzeichnis angegeben
# SQLALCHEMY_DATABASE_URI=sqlite:////absoluter/pfad/zu/database/systades.db

258
init_db.py Executable file
View File

@@ -0,0 +1,258 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from app import app, initialize_database, db_path
from models import db, User, Thought, Comment, MindMapNode, ThoughtRelation, ThoughtRating, RelationType
from models import Category, UserMindmap, UserMindmapNode, MindmapNote
import os
def init_database():
"""Initialisiert die Datenbank mit Beispieldaten."""
with app.app_context():
# Datenbank löschen und neu erstellen
if os.path.exists(db_path):
os.remove(db_path)
# Stellen Sie sicher, dass das Verzeichnis existiert
os.makedirs(os.path.dirname(db_path), exist_ok=True)
db.create_all()
# Admin-Benutzer erstellen
admin = User(username='admin', email='admin@example.com', is_admin=True)
admin.set_password('admin')
db.session.add(admin)
# Beispiel-Benutzer erstellen
user = User(username='user', email='user@example.com')
user.set_password('user')
db.session.add(user)
# Commit, um IDs zu generieren
db.session.commit()
# Wissenschaftliche Kategorien erstellen
science = Category(name='Wissenschaft', description='Wissenschaftliche Erkenntnisse',
color_code='#4CAF50', icon='flask')
db.session.add(science)
philosophy = Category(name='Philosophie', description='Philosophische Theorien und Gedanken',
color_code='#9C27B0', icon='lightbulb')
db.session.add(philosophy)
technology = Category(name='Technologie', description='Technologische Entwicklungen',
color_code='#FF9800', icon='microchip')
db.session.add(technology)
db.session.commit()
# Wissenschaftliche Unterkategorien
physics = Category(name='Physik', description='Studium der Materie und Energie',
color_code='#81C784', icon='atom', parent_id=science.id)
biology = Category(name='Biologie', description='Studium lebender Organismen',
color_code='#66BB6A', icon='leaf', parent_id=science.id)
chemistry = Category(name='Chemie', description='Studium der Stoffe und ihrer Reaktionen',
color_code='#A5D6A7', icon='vial', parent_id=science.id)
db.session.add_all([physics, biology, chemistry])
# Technologie-Unterkategorien
informatics = Category(name='Informatik', description='Studium der Informationsverarbeitung',
color_code='#FFB74D', icon='laptop-code', parent_id=technology.id)
ai = Category(name='Künstliche Intelligenz', description='Entwicklung intelligenter Systeme',
color_code='#FFA726', icon='robot', parent_id=technology.id)
db.session.add_all([informatics, ai])
# Philosophie-Unterkategorien
ethics = Category(name='Ethik', description='Moralphilosophie und Wertesysteme',
color_code='#BA68C8', icon='balance-scale', parent_id=philosophy.id)
logic = Category(name='Logik', description='Studie der gültigen Schlussfolgerungen',
color_code='#AB47BC', icon='project-diagram', parent_id=philosophy.id)
db.session.add_all([ethics, logic])
db.session.commit()
# Knoten für die öffentliche Mindmap erstellen
nodes = {
'quantenmechanik': MindMapNode(
name='Quantenmechanik',
description='Physikalische Theorie zur Beschreibung der Materie auf atomarer Ebene',
color_code='#81C784',
category_id=physics.id,
created_by_id=admin.id
),
'relativitaetstheorie': MindMapNode(
name='Relativitätstheorie',
description='Einsteins Theorien zur Raumzeit und Gravitation',
color_code='#81C784',
category_id=physics.id,
created_by_id=admin.id
),
'genetik': MindMapNode(
name='Genetik',
description='Wissenschaft der Gene und Vererbung',
color_code='#66BB6A',
category_id=biology.id,
created_by_id=admin.id
),
'machine_learning': MindMapNode(
name='Machine Learning',
description='Algorithmen, die aus Daten lernen können',
color_code='#FFA726',
category_id=ai.id,
created_by_id=admin.id
),
'ki_ethik': MindMapNode(
name='KI-Ethik',
description='Moralische Implikationen künstlicher Intelligenz',
color_code='#BA68C8',
category_id=ethics.id,
created_by_id=user.id
)
}
for node in nodes.values():
db.session.add(node)
db.session.commit()
# Verknüpfungen zwischen Knoten herstellen (Hierarchie)
nodes['machine_learning'].parents.append(nodes['ki_ethik'])
db.session.commit()
# Gedanken erstellen
thoughts = [
{
'title': 'Künstliche Intelligenz und Bewusstsein',
'content': 'Die Frage nach maschinellem Bewusstsein ist fundamental für die KI-Ethik. Aktuelle KI-Systeme haben kein Bewusstsein, aber fortschrittliche KI könnte in Zukunft Eigenschaften entwickeln, die diesem nahekommen.',
'abstract': 'Eine Untersuchung der philosophischen Implikationen von KI-Bewusstsein.',
'keywords': 'KI, Bewusstsein, Ethik, Philosophie',
'branch': 'Philosophie',
'color_code': '#BA68C8',
'source_type': 'Markdown',
'user_id': user.id,
'node': nodes['ki_ethik']
},
{
'title': 'Quantenmechanik und Realität',
'content': 'Die Kopenhagener Deutung und ihre Auswirkungen auf unser Verständnis der Realität. Quantenmechanik stellt grundlegende Annahmen über Determinismus und Lokalität in Frage.',
'abstract': 'Eine Analyse verschiedener Interpretationen der Quantenmechanik.',
'keywords': 'Quantenmechanik, Physik, Realität',
'branch': 'Physik',
'color_code': '#81C784',
'source_type': 'PDF',
'user_id': admin.id,
'node': nodes['quantenmechanik']
},
{
'title': 'Deep Learning Fortschritte',
'content': 'Die neuesten Fortschritte im Deep Learning haben zu beeindruckenden Ergebnissen in Bereichen wie Computer Vision, Natural Language Processing und Reinforcement Learning geführt.',
'abstract': 'Überblick über aktuelle Deep Learning-Techniken und ihre Anwendungen.',
'keywords': 'Deep Learning, Neural Networks, AI',
'branch': 'Technologie',
'color_code': '#FFA726',
'source_type': 'Webpage',
'user_id': admin.id,
'node': nodes['machine_learning']
}
]
thought_objects = []
for t_data in thoughts:
node = t_data.pop('node')
thought = Thought(**t_data)
node.thoughts.append(thought)
thought_objects.append(thought)
db.session.add(thought)
db.session.commit()
# Beziehungen zwischen Gedanken
relation = ThoughtRelation(
source_id=thought_objects[0].id,
target_id=thought_objects[2].id,
relation_type=RelationType.INSPIRES,
created_by_id=user.id
)
db.session.add(relation)
# Bewertungen erstellen
rating1 = ThoughtRating(
thought_id=thought_objects[0].id,
user_id=admin.id,
relevance_score=5
)
rating2 = ThoughtRating(
thought_id=thought_objects[2].id,
user_id=user.id,
relevance_score=4
)
db.session.add_all([rating1, rating2])
# Kommentare erstellen
for thought in thought_objects:
comment = Comment(
content=f'Interessante Perspektive zu {thought.title}!',
thought_id=thought.id,
user_id=admin.id if thought.user_id != admin.id else user.id
)
db.session.add(comment)
# Benutzer-Mindmaps erstellen
user_mindmap = UserMindmap(
name='Meine KI-Forschung',
description='Meine persönliche Sammlung zu KI und Ethik',
user_id=user.id
)
db.session.add(user_mindmap)
db.session.commit()
# Knoten zur Benutzer-Mindmap hinzufügen
user_mindmap_nodes = [
UserMindmapNode(
user_mindmap_id=user_mindmap.id,
node_id=nodes['machine_learning'].id,
x_position=200,
y_position=300
),
UserMindmapNode(
user_mindmap_id=user_mindmap.id,
node_id=nodes['ki_ethik'].id,
x_position=500,
y_position=200
)
]
db.session.add_all(user_mindmap_nodes)
# Private Notizen
note = MindmapNote(
user_id=user.id,
mindmap_id=user_mindmap.id,
node_id=nodes['ki_ethik'].id,
content="Recherchiere mehr über aktuelle ethische Richtlinien für KI-Entwicklung!",
color_code="#FFF59D"
)
db.session.add(note)
# Gedanken zu Bookmarks hinzufügen
user.bookmarked_thoughts.append(thought_objects[0])
admin.bookmarked_thoughts.append(thought_objects[1])
# Finaler Commit
db.session.commit()
print("Datenbank wurde erfolgreich initialisiert!")
def init_db():
"""Alias für Kompatibilität mit älteren Scripts."""
init_database()
if __name__ == '__main__':
init_database()
print("Datenbank wurde erfolgreich initialisiert!")
print("Sie können die Anwendung jetzt mit 'python app.py' starten")
print("Anmelden mit:")
print(" Admin: username=admin, password=admin")
print(" User: username=user, password=user")

230
models.py Executable file
View File

@@ -0,0 +1,230 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from flask_sqlalchemy import SQLAlchemy
from flask_login import UserMixin
from datetime import datetime
from werkzeug.security import generate_password_hash, check_password_hash
from enum import Enum
db = SQLAlchemy()
# Beziehungstypen für Gedankenverknüpfungen
class RelationType(Enum):
SUPPORTS = "stützt"
CONTRADICTS = "widerspricht"
BUILDS_UPON = "baut auf auf"
GENERALIZES = "verallgemeinert"
SPECIFIES = "spezifiziert"
INSPIRES = "inspiriert"
# Beziehungstabelle für viele-zu-viele Beziehung zwischen MindMapNodes
node_relationship = db.Table('node_relationship',
db.Column('parent_id', db.Integer, db.ForeignKey('mind_map_node.id'), primary_key=True),
db.Column('child_id', db.Integer, db.ForeignKey('mind_map_node.id'), primary_key=True)
)
# Beziehungstabelle für öffentliche Knoten und Gedanken
node_thought_association = db.Table('node_thought_association',
db.Column('node_id', db.Integer, db.ForeignKey('mind_map_node.id'), primary_key=True),
db.Column('thought_id', db.Integer, db.ForeignKey('thought.id'), primary_key=True)
)
# Beziehungstabelle für Benutzer-spezifische Mindmap-Knoten und Gedanken
user_mindmap_thought_association = db.Table('user_mindmap_thought_association',
db.Column('user_mindmap_id', db.Integer, db.ForeignKey('user_mindmap.id'), primary_key=True),
db.Column('thought_id', db.Integer, db.ForeignKey('thought.id'), primary_key=True)
)
# Beziehungstabelle für Benutzer-Bookmarks von Gedanken
user_thought_bookmark = db.Table('user_thought_bookmark',
db.Column('user_id', db.Integer, db.ForeignKey('user.id'), primary_key=True),
db.Column('thought_id', db.Integer, db.ForeignKey('thought.id'), primary_key=True),
db.Column('created_at', db.DateTime, default=datetime.utcnow)
)
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
password_hash = db.Column(db.String(128))
is_admin = db.Column(db.Boolean, default=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
last_login = db.Column(db.DateTime)
avatar = db.Column(db.String(200))
bio = db.Column(db.Text)
# Beziehungen
thoughts = db.relationship('Thought', backref='author', lazy=True)
comments = db.relationship('Comment', backref='author', lazy=True)
user_mindmaps = db.relationship('UserMindmap', backref='user', lazy=True)
mindmap_notes = db.relationship('MindmapNote', backref='user', lazy=True)
bookmarked_thoughts = db.relationship('Thought', secondary=user_thought_bookmark,
backref=db.backref('bookmarked_by', lazy='dynamic'))
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
class Category(db.Model):
"""Wissenschaftliche Kategorien für die Gliederung der öffentlichen Mindmap"""
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), unique=True, nullable=False)
description = db.Column(db.Text)
color_code = db.Column(db.String(7)) # Hex color
icon = db.Column(db.String(50))
parent_id = db.Column(db.Integer, db.ForeignKey('category.id'), nullable=True)
# Beziehungen
children = db.relationship('Category', backref=db.backref('parent', remote_side=[id]))
nodes = db.relationship('MindMapNode', backref='category', lazy=True)
class MindMapNode(db.Model):
"""Öffentliche Mindmap-Knoten, die für alle Benutzer sichtbar sind"""
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False)
description = db.Column(db.Text)
color_code = db.Column(db.String(7))
icon = db.Column(db.String(50))
is_public = db.Column(db.Boolean, default=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
created_by_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True)
category_id = db.Column(db.Integer, db.ForeignKey('category.id'), nullable=True)
# Beziehungen für Baumstruktur (mehrere Eltern möglich)
parents = db.relationship(
'MindMapNode',
secondary=node_relationship,
primaryjoin=(node_relationship.c.child_id == id),
secondaryjoin=(node_relationship.c.parent_id == id),
backref=db.backref('children', lazy='dynamic'),
lazy='dynamic'
)
# Beziehungen zu Gedanken
thoughts = db.relationship('Thought',
secondary=node_thought_association,
backref=db.backref('nodes', lazy='dynamic'))
# Beziehung zum Ersteller
created_by = db.relationship('User', backref='created_nodes')
class UserMindmap(db.Model):
"""Benutzerspezifische Mindmap, die vom Benutzer personalisierbar ist"""
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False)
description = db.Column(db.Text)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
last_modified = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
is_private = db.Column(db.Boolean, default=True)
# Beziehungen zu öffentlichen Knoten
public_nodes = db.relationship('MindMapNode',
secondary='user_mindmap_node',
backref=db.backref('in_user_mindmaps', lazy='dynamic'))
# Beziehungen zu Gedanken
thoughts = db.relationship('Thought',
secondary=user_mindmap_thought_association,
backref=db.backref('in_user_mindmaps', lazy='dynamic'))
# Notizen zu dieser Mindmap
notes = db.relationship('MindmapNote', backref='mindmap', lazy=True)
# Beziehungstabelle für benutzerorientierte Mindmaps und öffentliche Knoten
class UserMindmapNode(db.Model):
"""Speichert die Beziehung zwischen Benutzer-Mindmaps und öffentlichen Knoten inkl. Position"""
user_mindmap_id = db.Column(db.Integer, db.ForeignKey('user_mindmap.id'), primary_key=True)
node_id = db.Column(db.Integer, db.ForeignKey('mind_map_node.id'), primary_key=True)
x_position = db.Column(db.Float, default=0) # Position X auf der Mindmap
y_position = db.Column(db.Float, default=0) # Position Y auf der Mindmap
scale = db.Column(db.Float, default=1.0) # Größe des Knotens
added_at = db.Column(db.DateTime, default=datetime.utcnow)
class MindmapNote(db.Model):
"""Private Notizen der Benutzer zu ihrer Mindmap"""
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
mindmap_id = db.Column(db.Integer, db.ForeignKey('user_mindmap.id'), nullable=False)
node_id = db.Column(db.Integer, db.ForeignKey('mind_map_node.id'), nullable=True)
thought_id = db.Column(db.Integer, db.ForeignKey('thought.id'), nullable=True)
content = db.Column(db.Text, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
last_modified = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
color_code = db.Column(db.String(7), default="#FFF59D") # Farbe der Notiz
class Thought(db.Model):
"""Gedanken und Inhalte, die in der Mindmap verknüpft werden können"""
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(200), nullable=False)
content = db.Column(db.Text, nullable=False)
abstract = db.Column(db.Text)
keywords = db.Column(db.String(500))
color_code = db.Column(db.String(7)) # Hex color code
source_type = db.Column(db.String(50)) # PDF, Markdown, Text etc.
branch = db.Column(db.String(100), nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
last_modified = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Beziehungen
comments = db.relationship('Comment', backref='thought', lazy=True, cascade="all, delete-orphan")
ratings = db.relationship('ThoughtRating', backref='thought', lazy=True)
outgoing_relations = db.relationship(
'ThoughtRelation',
foreign_keys='ThoughtRelation.source_id',
backref='source_thought',
lazy=True
)
incoming_relations = db.relationship(
'ThoughtRelation',
foreign_keys='ThoughtRelation.target_id',
backref='target_thought',
lazy=True
)
@property
def average_rating(self):
if not self.ratings:
return 0
return sum(r.relevance_score for r in self.ratings) / len(self.ratings)
class ThoughtRelation(db.Model):
"""Beziehungen zwischen Gedanken"""
id = db.Column(db.Integer, primary_key=True)
source_id = db.Column(db.Integer, db.ForeignKey('thought.id'), nullable=False)
target_id = db.Column(db.Integer, db.ForeignKey('thought.id'), nullable=False)
relation_type = db.Column(db.Enum(RelationType), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
created_by_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
# Beziehung zum Ersteller
created_by = db.relationship('User', backref='created_relations')
class ThoughtRating(db.Model):
"""Bewertungen von Gedanken durch Benutzer"""
id = db.Column(db.Integer, primary_key=True)
thought_id = db.Column(db.Integer, db.ForeignKey('thought.id'), nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
relevance_score = db.Column(db.Integer, nullable=False) # 1-5
created_at = db.Column(db.DateTime, default=datetime.utcnow)
__table_args__ = (
db.UniqueConstraint('thought_id', 'user_id', name='unique_thought_rating'),
)
# Beziehung zum Benutzer
user = db.relationship('User', backref='ratings')
class Comment(db.Model):
"""Kommentare zu Gedanken"""
id = db.Column(db.Integer, primary_key=True)
content = db.Column(db.Text, nullable=False)
thought_id = db.Column(db.Integer, db.ForeignKey('thought.id'), nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
last_modified = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)

View File

@@ -1,6 +1,14 @@
flask flask==2.2.5
flask-login flask-login==0.6.2
flask-wtf flask-wtf
email-validator email-validator
python-dotenv python-dotenv
flask-sqlalchemy werkzeug==2.2.3
flask-sqlalchemy==3.0.5
openai==1.3.0
requests==2.31.0
flask-cors==4.0.0
gunicorn==21.2.0
#pillow==10.0.1
pytest==7.4.0
pytest-flask==1.2.0

426
static/css/base-styles.css Normal file
View File

@@ -0,0 +1,426 @@
/* Globale Variablen */
:root {
--dark-bg: #0e1220;
--dark-card-bg: rgba(24, 28, 45, 0.8);
--dark-element-bg: rgba(24, 28, 45, 0.8);
--light-bg: #f0f4f8;
--light-card-bg: rgba(255, 255, 255, 0.85);
--accent-color: #b38fff;
--accent-gradient: linear-gradient(135deg, #b38fff, #58a9ff);
--accent-gradient-hover: linear-gradient(135deg, #c7a8ff, #70b5ff);
--blur-amount: 20px;
--border-radius: 28px;
--card-border-radius: 24px;
--button-radius: 18px;
--nav-item-radius: 14px;
}
/* Dark Mode Einstellungen */
html.dark {
color-scheme: dark;
}
/* Base Styles */
html, body {
background-color: var(--dark-bg) !important;
min-height: 100vh;
width: 100%;
color: #ffffff;
margin: 0;
padding: 0;
font-family: 'Inter', system-ui, -apple-system, sans-serif;
overflow-x: hidden;
transition: background-color 0.5s ease, color 0.5s ease;
}
/* Sicherstellen, dass der dunkle Hintergrund die gesamte Seite abdeckt */
#app-container, .container, main, .mx-auto, .py-12, #content-wrapper {
background-color: transparent !important;
width: 100%;
}
/* Light Mode Einstellungen */
html.light, html.light body {
background-color: var(--light-bg) !important;
color: #1a202c;
}
/* Große Headings mit verbesserten Stilen */
h1.hero-heading {
font-size: clamp(2.5rem, 8vw, 5rem);
line-height: 1.1;
font-weight: 800;
letter-spacing: -0.03em;
margin-bottom: 1.5rem;
}
h2.section-heading {
font-size: clamp(1.75rem, 5vw, 3rem);
line-height: 1.2;
font-weight: 700;
letter-spacing: -0.02em;
margin-bottom: 1.25rem;
}
/* Verbesserte Glasmorphismus-Stile */
.glass-morphism {
background: var(--dark-card-bg);
backdrop-filter: blur(var(--blur-amount));
-webkit-backdrop-filter: blur(var(--blur-amount));
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: var(--card-border-radius);
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.35);
transition: all 0.3s ease;
}
.glass-morphism:hover {
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.45);
transform: translateY(-2px);
}
.glass-morphism-light {
background: var(--light-card-bg);
backdrop-filter: blur(var(--blur-amount));
-webkit-backdrop-filter: blur(var(--blur-amount));
border: 1px solid rgba(0, 0, 0, 0.08);
border-radius: var(--card-border-radius);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.12);
transition: all 0.3s ease;
}
.glass-morphism-light:hover {
border: 1px solid rgba(0, 0, 0, 0.15);
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.18);
transform: translateY(-2px);
}
/* Verbesserte Navbar-Styles */
.glass-navbar-dark {
background: rgba(14, 18, 32, 0.85);
backdrop-filter: blur(15px);
-webkit-backdrop-filter: blur(15px);
border-color: rgba(255, 255, 255, 0.1);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25);
border-radius: 0 0 20px 20px;
}
.glass-navbar-light {
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(15px);
-webkit-backdrop-filter: blur(15px);
border-color: rgba(0, 0, 0, 0.05);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08);
border-radius: 0 0 20px 20px;
}
/* Verbesserte Button-Stile mit besserer Lesbarkeit und stärkeren Farbverläufen */
.btn, button, .button, [type="button"], [type="submit"] {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: var(--button-radius);
padding: 0.75rem 1.5rem;
font-weight: 600;
letter-spacing: 0.4px;
color: rgba(255, 255, 255, 1);
background: linear-gradient(135deg, rgba(99, 102, 241, 0.8), rgba(168, 85, 247, 0.8));
border: 1px solid rgba(255, 255, 255, 0.15);
box-shadow: 0 6px 14px rgba(0, 0, 0, 0.2);
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
backdrop-filter: blur(15px);
-webkit-backdrop-filter: blur(15px);
position: relative;
overflow: hidden;
outline: none;
}
.btn:hover, button:hover, .button:hover, [type="button"]:hover, [type="submit"]:hover {
background: linear-gradient(135deg, rgba(129, 140, 248, 0.9), rgba(192, 132, 252, 0.9));
transform: translateY(-3px);
border: 1px solid rgba(255, 255, 255, 0.3);
box-shadow: 0 8px 22px rgba(0, 0, 0, 0.25), 0 0 12px rgba(179, 143, 255, 0.35);
color: white;
}
.btn:active, button:active, .button:active, [type="button"]:active, [type="submit"]:active {
transform: translateY(1px);
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.2);
}
/* Navigation Stile mit verbesserten Farbverläufen */
.nav-link {
transition: all 0.25s ease;
border-radius: var(--nav-item-radius);
padding: 0.625rem 1rem;
font-weight: 500;
letter-spacing: 0.3px;
color: rgba(255, 255, 255, 0.9);
position: relative;
overflow: visible;
}
.nav-link:hover {
background: rgba(179, 143, 255, 0.2);
color: white;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.nav-link-active {
background: linear-gradient(135deg, rgba(124, 58, 237, 0.3), rgba(139, 92, 246, 0.3));
color: white;
font-weight: 600;
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.18);
}
.nav-link-active::after {
content: '';
position: absolute;
bottom: 0;
left: 10%;
width: 80%;
height: 2px;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.7), transparent);
}
/* Light-Mode Navigation Stile */
.nav-link-light {
color: rgba(26, 32, 44, 0.85);
}
.nav-link-light:hover {
background: rgba(179, 143, 255, 0.15);
color: rgba(26, 32, 44, 1);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
}
.nav-link-light-active {
background: linear-gradient(135deg, rgba(124, 58, 237, 0.2), rgba(139, 92, 246, 0.2));
color: rgba(26, 32, 44, 1);
font-weight: 600;
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.1);
}
.nav-link-light-active::after {
background: linear-gradient(90deg, transparent, rgba(26, 32, 44, 0.5), transparent);
}
/* Entfernung von Gradient-Hintergrund überall */
.gradient-bg, .purple-gradient, .gradient-purple-bg {
background: var(--dark-bg) !important;
background-image: none !important;
}
/* Verbesserte Light-Mode-Stile für Buttons */
html.light .btn, html.light button, html.light .button,
html.light [type="button"], html.light [type="submit"] {
background: linear-gradient(135deg, rgba(124, 58, 237, 0.7), rgba(139, 92, 246, 0.7));
color: #ffffff;
border: 1px solid rgba(0, 0, 0, 0.1);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
text-shadow: none;
}
html.light .btn:hover, html.light button:hover, html.light .button:hover,
html.light [type="button"]:hover, html.light [type="submit"]:hover {
background: linear-gradient(135deg, rgba(139, 92, 246, 0.85), rgba(168, 85, 247, 0.85));
color: #ffffff;
border: 1px solid rgba(0, 0, 0, 0.08);
box-shadow: 0 8px 22px rgba(0, 0, 0, 0.12), 0 0 12px rgba(179, 143, 255, 0.2);
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}
/* Verbesserte Buttons mit Glasmorphismus */
.btn-primary {
background: linear-gradient(135deg, rgba(179, 143, 255, 0.8), rgba(88, 169, 255, 0.8));
backdrop-filter: blur(var(--blur-amount));
-webkit-backdrop-filter: blur(var(--blur-amount));
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: var(--button-radius);
color: white !important;
font-weight: 600;
padding: 0.75rem 1.5rem;
transition: all 0.3s ease;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
}
.btn-primary:hover {
transform: translateY(-3px);
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.35);
background: linear-gradient(135deg, rgba(190, 160, 255, 0.9), rgba(100, 180, 255, 0.9));
}
.btn-secondary {
background: rgba(32, 36, 55, 0.8);
backdrop-filter: blur(var(--blur-amount));
-webkit-backdrop-filter: blur(var(--blur-amount));
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: var(--button-radius);
color: white;
font-weight: 500;
padding: 0.75rem 1.5rem;
transition: all 0.3s ease;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.2);
}
.btn-secondary:hover {
transform: translateY(-3px);
box-shadow: 0 10px 28px rgba(0, 0, 0, 0.3);
background: rgba(38, 42, 65, 0.9);
border: 1px solid rgba(255, 255, 255, 0.15);
}
/* Steuerungsbutton-Stil */
.control-btn {
padding: 0.5rem 1rem;
background: rgba(32, 36, 55, 0.8);
color: rgba(255, 255, 255, 0.9);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 14px;
font-size: 0.875rem;
font-weight: 500;
transition: all 0.25s ease;
backdrop-filter: blur(15px);
-webkit-backdrop-filter: blur(15px);
}
.control-btn:hover {
background: rgba(38, 42, 65, 0.9);
border: 1px solid rgba(255, 255, 255, 0.2);
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.25);
}
/* Verbesserter Farbverlauf-Text */
.gradient-text {
background: linear-gradient(135deg, rgba(200, 170, 255, 1), rgba(100, 180, 255, 1));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
font-weight: 700;
letter-spacing: -0.02em;
text-shadow: 0 2px 10px rgba(179, 143, 255, 0.3);
filter: drop-shadow(0 2px 6px rgba(179, 143, 255, 0.3));
}
/* Globaler Hintergrund */
.full-page-bg {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: var(--dark-bg);
z-index: -10;
}
html.light .full-page-bg {
background-color: var(--light-bg);
}
/* Animationen für Hintergrundeffekte */
@keyframes float {
0% { transform: translateY(0); }
50% { transform: translateY(-12px); }
100% { transform: translateY(0); }
}
@keyframes pulse {
0% { opacity: 0.7; transform: scale(1); }
50% { opacity: 1; transform: scale(1.05); }
100% { opacity: 0.7; transform: scale(1); }
}
.animate-float {
animation: float 6s ease-in-out infinite;
}
.animate-pulse {
animation: pulse 3s ease-in-out infinite;
}
/* Verbesserter Container für konsistente Layouts */
.page-container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem 1rem;
}
/* Dark Mode Toggle Stile */
.dot {
transform: translateX(0);
transition: transform 0.3s ease-in-out, background-color 0.3s ease;
}
input:checked ~ .dot {
transform: translateX(100%);
background-color: #58a9ff;
}
input:checked ~ .block {
background-color: rgba(88, 169, 255, 0.4);
}
/* Feature Cards mit Glasmorphismus und Farbverlauf */
.feature-card {
border-radius: var(--card-border-radius);
padding: 2rem;
transition: all 0.3s ease;
background: linear-gradient(145deg, rgba(32, 36, 55, 0.7), rgba(24, 28, 45, 0.9));
backdrop-filter: blur(var(--blur-amount));
-webkit-backdrop-filter: blur(var(--blur-amount));
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
}
.feature-card:hover {
transform: translateY(-5px);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.15);
background: linear-gradient(145deg, rgba(40, 44, 65, 0.8), rgba(28, 32, 50, 0.95));
}
html.light .feature-card {
background: linear-gradient(145deg, rgba(255, 255, 255, 0.8), rgba(240, 240, 250, 0.9));
border: 1px solid rgba(0, 0, 0, 0.05);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
}
html.light .feature-card:hover {
background: linear-gradient(145deg, rgba(255, 255, 255, 0.9), rgba(245, 245, 255, 0.95));
border: 1px solid rgba(0, 0, 0, 0.08);
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.12);
}
.feature-card .icon {
width: 60px;
height: 60px;
border-radius: 18px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
margin-bottom: 1.25rem;
background: linear-gradient(135deg, rgba(124, 58, 237, 0.8), rgba(139, 92, 246, 0.6));
color: white;
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15);
}
.feature-card h3 {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 0.75rem;
color: rgba(255, 255, 255, 0.95);
}
html.light .feature-card h3 {
color: rgba(26, 32, 44, 0.95);
}
.feature-card p {
color: rgba(255, 255, 255, 0.75);
line-height: 1.6;
}
html.light .feature-card p {
color: rgba(26, 32, 44, 0.75);
}

View File

@@ -0,0 +1,125 @@
/* Cybertechnisches Netzwerk Hintergrund-Overlay */
.cyber-network-bg {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
pointer-events: none;
overflow: hidden;
}
.cyber-network-bg::before {
content: '';
position: absolute;
width: 100%;
height: 100%;
background: linear-gradient(125deg,
rgba(14, 14, 22, 0.95) 0%,
rgba(30, 30, 46, 0.98) 100%);
}
.network-grid {
position: absolute;
width: 200%;
height: 200%;
top: -50%;
left: -50%;
background-size: 40px 40px;
background-image:
linear-gradient(to right, rgba(108, 93, 211, 0.05) 1px, transparent 1px),
linear-gradient(to bottom, rgba(108, 93, 211, 0.05) 1px, transparent 1px);
transform: perspective(500px) rotateX(60deg);
animation: grid-move 20s linear infinite;
}
.node {
position: absolute;
width: 4px;
height: 4px;
background: rgba(76, 223, 255, 0.8);
border-radius: 50%;
box-shadow: 0 0 10px rgba(76, 223, 255, 0.6);
filter: blur(1px);
}
.connection {
position: absolute;
height: 1px;
background: linear-gradient(90deg,
rgba(76, 223, 255, 0.2) 0%,
rgba(108, 93, 211, 0.3) 50%,
rgba(76, 223, 255, 0.2) 100%);
transform-origin: left center;
animation: pulse 4s infinite;
}
.data-packet {
position: absolute;
width: 6px;
height: 6px;
border-radius: 50%;
background: rgba(118, 69, 217, 0.8);
filter: blur(1px);
animation: travel var(--travel-time, 6s) linear infinite;
}
.glow-overlay {
position: absolute;
width: 100%;
height: 100%;
background: radial-gradient(
circle at 50% 40%,
rgba(76, 223, 255, 0.03) 0%,
rgba(108, 93, 211, 0.03) 45%,
transparent 70%
);
opacity: 0.8;
animation: pulse-glow 8s infinite;
}
/* Animations */
@keyframes grid-move {
0% {
transform: perspective(500px) rotateX(60deg) translateY(0);
}
100% {
transform: perspective(500px) rotateX(60deg) translateY(40px);
}
}
@keyframes pulse {
0%, 100% {
opacity: 0.2;
}
50% {
opacity: 0.4;
}
}
@keyframes travel {
0% {
transform: translateX(0) translateY(0);
opacity: 0;
}
10% {
opacity: 1;
}
90% {
opacity: 1;
}
100% {
transform: translateX(var(--travel-x, 100px)) translateY(var(--travel-y, 100px));
opacity: 0;
}
}
@keyframes pulse-glow {
0%, 100% {
opacity: 0.4;
}
50% {
opacity: 0.8;
}
}

View File

@@ -1434,11 +1434,16 @@ html, body {
overflow-x: hidden; overflow-x: hidden;
background: linear-gradient(135deg, var(--background-start), var(--background-end)); background: linear-gradient(135deg, var(--background-start), var(--background-end));
background-attachment: fixed; background-attachment: fixed;
scroll-behavior: smooth;
height: 100%;
} }
/* Sticky navbar */ /* Sticky navbar */
.navbar.sticky-top { .navbar.sticky-top {
position: sticky; position: sticky;
top: 0; top: 0;
z-index: 1000; z-index: 50;
} }
/* Importiere das Cyber-Network CSS */
@import url('/static/css/src/cybernetwork-bg.css');

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -37,6 +37,12 @@ const MindMap = {
console.log('MindMap-Anwendung wird initialisiert...'); console.log('MindMap-Anwendung wird initialisiert...');
// Initialisiere den ChatGPT-Assistenten
const assistant = new ChatGPTAssistant();
assistant.init();
// Speichere als Teil von MindMap
this.assistant = assistant;
// Seiten-spezifische Initialisierer aufrufen // Seiten-spezifische Initialisierer aufrufen
if (this.currentPage && this.pageInitializers[this.currentPage]) { if (this.currentPage && this.pageInitializers[this.currentPage]) {
this.pageInitializers[this.currentPage](); this.pageInitializers[this.currentPage]();

View File

@@ -0,0 +1,95 @@
/**
* Initialisierungsmodul für den CyberNetwork-Hintergrund
* Importiert und startet die Animation
*/
import CyberNetwork from './cyber-network.js';
// Beim Laden des Dokuments starten
document.addEventListener('DOMContentLoaded', () => {
console.log('CyberNetwork: Initialisierung gestartet');
// Prüfen ob das CSS bereits geladen ist, wenn nicht, dann laden
if (!document.querySelector('link[href*="cybernetwork-bg.css"]')) {
console.log('CyberNetwork: CSS wird geladen');
const cyberNetworkCss = document.createElement('link');
cyberNetworkCss.rel = 'stylesheet';
cyberNetworkCss.href = '/static/css/src/cybernetwork-bg.css';
document.head.appendChild(cyberNetworkCss);
}
// Container-Element für das Netzwerk finden
const container = document.getElementById('cyber-background-container');
if (!container) {
console.error('CyberNetwork: Container #cyber-background-container nicht gefunden!');
return;
}
console.log('CyberNetwork: Container gefunden', container);
// Konfiguration für den Netzwerk-Hintergrund
const networkConfig = {
container: container,
nodeCount: window.innerWidth < 768 ? 15 : 30, // Weniger Nodes auf mobilen Geräten
connectionCount: window.innerWidth < 768 ? 25 : 50,
packetCount: window.innerWidth < 768 ? 8 : 15,
animationSpeed: 1.0
};
// Netzwerk erstellen und initialisieren
const cyberNetwork = new CyberNetwork(networkConfig);
cyberNetwork.init();
console.log('CyberNetwork: Netzwerk initialisiert');
// Globale Referenz für Debug-Zwecke
window.cyberNetwork = cyberNetwork;
});
// Funktion zum manuellen Initialisieren, falls notwendig
export function initCyberNetwork(config = {}) {
console.log('CyberNetwork: Manuelle Initialisierung gestartet');
// CSS laden, falls nicht vorhanden
if (!document.querySelector('link[href*="cybernetwork-bg.css"]')) {
console.log('CyberNetwork: CSS wird geladen (manuell)');
const cyberNetworkCss = document.createElement('link');
cyberNetworkCss.rel = 'stylesheet';
cyberNetworkCss.href = '/static/css/src/cybernetwork-bg.css';
document.head.appendChild(cyberNetworkCss);
}
// Container-Element für das Netzwerk finden
const container = document.getElementById('cyber-background-container');
if (!container) {
console.error('CyberNetwork: Container #cyber-background-container nicht gefunden!');
return null;
}
// Bestehende Instanz zurücksetzen, falls vorhanden
if (window.cyberNetwork) {
console.log('CyberNetwork: Bestehende Instanz wird zurückgesetzt');
window.cyberNetwork.reset();
}
// Netzwerk mit benutzerdefinierten Optionen erstellen
const networkConfig = {
container: container,
nodeCount: window.innerWidth < 768 ? 15 : 30,
connectionCount: window.innerWidth < 768 ? 25 : 50,
packetCount: window.innerWidth < 768 ? 8 : 15,
animationSpeed: 1.0,
...config
};
// Neue Instanz erstellen und initialisieren
const cyberNetwork = new CyberNetwork(networkConfig);
cyberNetwork.init();
console.log('CyberNetwork: Netzwerk manuell initialisiert');
// Globale Referenz aktualisieren
window.cyberNetwork = cyberNetwork;
return cyberNetwork;
}

View File

@@ -0,0 +1,240 @@
/**
* Cyber Network Background Animation
* Generiert dynamisch ein animiertes Netzwerk für den Hintergrund
*/
class CyberNetwork {
constructor(options = {}) {
this.options = {
container: options.container || document.body,
nodeCount: options.nodeCount || 30,
connectionCount: options.connectionCount || 50,
packetCount: options.packetCount || 15,
animationSpeed: options.animationSpeed || 1.0,
...options
};
this.nodes = [];
this.connections = [];
this.packets = [];
this.initialized = false;
this.containerElement = null;
this.networkGridElement = null;
this.glowOverlayElement = null;
}
init() {
if (this.initialized) return;
// Container erstellen
this.containerElement = document.createElement('div');
this.containerElement.className = 'cyber-network-bg';
// Grid erstellen
this.networkGridElement = document.createElement('div');
this.networkGridElement.className = 'network-grid';
this.containerElement.appendChild(this.networkGridElement);
// Glow Overlay erstellen
this.glowOverlayElement = document.createElement('div');
this.glowOverlayElement.className = 'glow-overlay';
this.containerElement.appendChild(this.glowOverlayElement);
// Nodes generieren
this.generateNodes();
// Connections generieren
this.generateConnections();
// Data packets generieren
this.generateDataPackets();
// Container zum DOM hinzufügen
if (typeof this.options.container === 'string') {
const container = document.querySelector(this.options.container);
if (container) {
container.appendChild(this.containerElement);
} else {
document.body.appendChild(this.containerElement);
}
} else {
this.options.container.appendChild(this.containerElement);
}
this.initialized = true;
// Animation starten
window.addEventListener('resize', this.handleResize.bind(this));
this.startAnimationCycle();
}
generateNodes() {
const containerWidth = window.innerWidth;
const containerHeight = window.innerHeight;
for (let i = 0; i < this.options.nodeCount; i++) {
const x = Math.random() * containerWidth;
const y = Math.random() * containerHeight;
const node = document.createElement('div');
node.className = 'node';
node.style.left = `${x}px`;
node.style.top = `${y}px`;
// Größen-Variation für visuelle Tiefe
const size = 2 + Math.random() * 4;
node.style.width = `${size}px`;
node.style.height = `${size}px`;
// Speichern der Position für spätere Referenz
node._data = { x, y, size };
this.containerElement.appendChild(node);
this.nodes.push(node);
}
}
generateConnections() {
for (let i = 0; i < this.options.connectionCount; i++) {
// Zufällige Nodes auswählen
const startNodeIndex = Math.floor(Math.random() * this.nodes.length);
let endNodeIndex;
do {
endNodeIndex = Math.floor(Math.random() * this.nodes.length);
} while (endNodeIndex === startNodeIndex);
const startNode = this.nodes[startNodeIndex];
const endNode = this.nodes[endNodeIndex];
const startData = startNode._data;
const endData = endNode._data;
// Verbindung erstellen
const connection = document.createElement('div');
connection.className = 'connection';
// Position und Rotation berechnen
const dx = endData.x - startData.x;
const dy = endData.y - startData.y;
const length = Math.sqrt(dx * dx + dy * dy);
const angle = Math.atan2(dy, dx) * (180 / Math.PI);
connection.style.width = `${length}px`;
connection.style.left = `${startData.x}px`;
connection.style.top = `${startData.y}px`;
connection.style.transform = `rotate(${angle}deg)`;
// Variation in der Animations-Geschwindigkeit
connection.style.animationDuration = `${3 + Math.random() * 4}s`;
// Speichern der verbundenen Nodes
connection._data = {
startNode: startNodeIndex,
endNode: endNodeIndex,
length
};
this.containerElement.appendChild(connection);
this.connections.push(connection);
}
}
generateDataPackets() {
for (let i = 0; i < this.options.packetCount; i++) {
this.createNewDataPacket();
}
}
createNewDataPacket() {
if (this.connections.length === 0) return;
// Zufällige Verbindung auswählen
const connectionIndex = Math.floor(Math.random() * this.connections.length);
const connection = this.connections[connectionIndex];
const connectionData = connection._data;
const startNode = this.nodes[connectionData.startNode];
const startData = startNode._data;
// Data Packet erstellen
const packet = document.createElement('div');
packet.className = 'data-packet';
// Position auf dem Startknoten
packet.style.left = `${startData.x}px`;
packet.style.top = `${startData.y}px`;
// Zufällige Geschwindigkeit
const travelTime = (4 + Math.random() * 4) / this.options.animationSpeed;
packet.style.setProperty('--travel-time', `${travelTime}s`);
// Ziel-Koordinaten berechnen
const endNode = this.nodes[connectionData.endNode];
const endData = endNode._data;
const travelX = endData.x - startData.x;
const travelY = endData.y - startData.y;
packet.style.setProperty('--travel-x', `${travelX}px`);
packet.style.setProperty('--travel-y', `${travelY}px`);
// Farb-Variation
if (Math.random() > 0.5) {
packet.style.background = 'rgba(76, 223, 255, 0.8)'; // Akzentfarbe
}
this.containerElement.appendChild(packet);
this.packets.push(packet);
// Nach Ende der Animation neues Paket erstellen
setTimeout(() => {
if (this.containerElement.contains(packet)) {
this.containerElement.removeChild(packet);
}
const index = this.packets.indexOf(packet);
if (index > -1) {
this.packets.splice(index, 1);
this.createNewDataPacket();
}
}, travelTime * 1000);
}
handleResize() {
if (!this.initialized) return;
// Bei Größenänderung alles neu generieren
this.reset();
this.init();
}
reset() {
if (!this.initialized) return;
// Alle Elemente entfernen
this.nodes.forEach(node => node.remove());
this.connections.forEach(connection => connection.remove());
this.packets.forEach(packet => packet.remove());
this.nodes = [];
this.connections = [];
this.packets = [];
if (this.containerElement) {
this.containerElement.remove();
}
this.initialized = false;
}
startAnimationCycle() {
// Regelmäßig neue Pakete erstellen für mehr Dynamik
setInterval(() => {
if (this.packets.length < this.options.packetCount * 1.5) {
this.createNewDataPacket();
}
}, 1000 / this.options.animationSpeed);
}
}
// Exportieren als Modul
export default CyberNetwork;

View File

@@ -32,6 +32,9 @@ class MindMapVisualization {
this.tooltipDiv = null; this.tooltipDiv = null;
this.isLoading = true; this.isLoading = true;
// Flash-Nachrichten-Container
this.flashContainer = null;
// Erweiterte Farbpalette für Knotentypen // Erweiterte Farbpalette für Knotentypen
this.colorPalette = { this.colorPalette = {
'default': '#b38fff', 'default': '#b38fff',
@@ -57,6 +60,7 @@ class MindMapVisualization {
if (this.container.node()) { if (this.container.node()) {
this.init(); this.init();
this.setupDefaultNodes(); this.setupDefaultNodes();
this.setupFlashMessages();
// Sofortige Datenladung // Sofortige Datenladung
window.setTimeout(() => { window.setTimeout(() => {
@@ -67,6 +71,183 @@ class MindMapVisualization {
} }
} }
// Flash-Nachrichten-System einrichten
setupFlashMessages() {
// Flash-Container erstellen, falls er noch nicht existiert
if (!document.getElementById('mindmap-flash-container')) {
this.flashContainer = document.createElement('div');
this.flashContainer.id = 'mindmap-flash-container';
this.flashContainer.className = 'mindmap-flash-container';
this.flashContainer.style.position = 'fixed';
this.flashContainer.style.top = '20px';
this.flashContainer.style.right = '20px';
this.flashContainer.style.zIndex = '1000';
this.flashContainer.style.maxWidth = '350px';
this.flashContainer.style.display = 'flex';
this.flashContainer.style.flexDirection = 'column';
this.flashContainer.style.gap = '10px';
document.body.appendChild(this.flashContainer);
} else {
this.flashContainer = document.getElementById('mindmap-flash-container');
}
// Prüfen, ob Server-seitige Flash-Nachrichten existieren und anzeigen
this.checkForServerFlashMessages();
}
// Prüft auf Server-seitige Flash-Nachrichten
async checkForServerFlashMessages() {
try {
const response = await fetch('/api/get_flash_messages');
if (response.ok) {
const messages = await response.json();
messages.forEach(message => {
this.showFlash(message.message, message.category);
});
}
} catch (err) {
console.error('Fehler beim Abrufen der Flash-Nachrichten:', err);
}
}
// Zeigt eine Flash-Nachricht an
showFlash(message, type = 'info', duration = 5000) {
if (!this.flashContainer) return;
const flashElement = document.createElement('div');
flashElement.className = `mindmap-flash flash-${type}`;
flashElement.style.padding = '12px 18px';
flashElement.style.borderRadius = '8px';
flashElement.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.15)';
flashElement.style.display = 'flex';
flashElement.style.alignItems = 'center';
flashElement.style.justifyContent = 'space-between';
flashElement.style.fontSize = '14px';
flashElement.style.fontWeight = '500';
flashElement.style.backdropFilter = 'blur(10px)';
flashElement.style.opacity = '0';
flashElement.style.transform = 'translateY(-20px)';
flashElement.style.transition = 'all 0.3s ease';
// Spezifische Stile je nach Nachrichtentyp
switch(type) {
case 'success':
flashElement.style.backgroundColor = 'rgba(34, 197, 94, 0.9)';
flashElement.style.borderLeft = '5px solid #16a34a';
flashElement.style.color = 'white';
break;
case 'error':
flashElement.style.backgroundColor = 'rgba(239, 68, 68, 0.9)';
flashElement.style.borderLeft = '5px solid #dc2626';
flashElement.style.color = 'white';
break;
case 'warning':
flashElement.style.backgroundColor = 'rgba(245, 158, 11, 0.9)';
flashElement.style.borderLeft = '5px solid #d97706';
flashElement.style.color = 'white';
break;
default: // info
flashElement.style.backgroundColor = 'rgba(59, 130, 246, 0.9)';
flashElement.style.borderLeft = '5px solid #2563eb';
flashElement.style.color = 'white';
}
// Icon je nach Nachrichtentyp
let icon = '';
switch(type) {
case 'success':
icon = '<i class="fas fa-check-circle"></i>';
break;
case 'error':
icon = '<i class="fas fa-exclamation-circle"></i>';
break;
case 'warning':
icon = '<i class="fas fa-exclamation-triangle"></i>';
break;
default:
icon = '<i class="fas fa-info-circle"></i>';
}
// Inhalt der Nachricht mit Icon
const contentWrapper = document.createElement('div');
contentWrapper.style.display = 'flex';
contentWrapper.style.alignItems = 'center';
contentWrapper.style.gap = '12px';
const iconElement = document.createElement('div');
iconElement.className = 'flash-icon';
iconElement.innerHTML = icon;
const textElement = document.createElement('div');
textElement.className = 'flash-text';
textElement.textContent = message;
contentWrapper.appendChild(iconElement);
contentWrapper.appendChild(textElement);
// Schließen-Button
const closeButton = document.createElement('button');
closeButton.className = 'flash-close';
closeButton.innerHTML = '<i class="fas fa-times"></i>';
closeButton.style.background = 'none';
closeButton.style.border = 'none';
closeButton.style.color = 'currentColor';
closeButton.style.cursor = 'pointer';
closeButton.style.marginLeft = '15px';
closeButton.style.padding = '3px';
closeButton.style.fontSize = '14px';
closeButton.style.opacity = '0.7';
closeButton.style.transition = 'opacity 0.2s';
closeButton.addEventListener('mouseover', () => {
closeButton.style.opacity = '1';
});
closeButton.addEventListener('mouseout', () => {
closeButton.style.opacity = '0.7';
});
closeButton.addEventListener('click', () => {
this.removeFlash(flashElement);
});
// Zusammenfügen
flashElement.appendChild(contentWrapper);
flashElement.appendChild(closeButton);
// Zum Container hinzufügen
this.flashContainer.appendChild(flashElement);
// Animation einblenden
setTimeout(() => {
flashElement.style.opacity = '1';
flashElement.style.transform = 'translateY(0)';
}, 10);
// Automatisches Ausblenden nach der angegebenen Zeit
if (duration > 0) {
setTimeout(() => {
this.removeFlash(flashElement);
}, duration);
}
return flashElement;
}
// Entfernt eine Flash-Nachricht mit Animation
removeFlash(flashElement) {
if (!flashElement) return;
flashElement.style.opacity = '0';
flashElement.style.transform = 'translateY(-20px)';
setTimeout(() => {
if (flashElement.parentNode) {
flashElement.parentNode.removeChild(flashElement);
}
}, 300);
}
// Standardknoten als Fallback einrichten, falls die API nicht reagiert // Standardknoten als Fallback einrichten, falls die API nicht reagiert
setupDefaultNodes() { setupDefaultNodes() {
// Basis-Mindmap mit Hauptthemen // Basis-Mindmap mit Hauptthemen
@@ -282,12 +463,19 @@ class MindMapVisualization {
// Zeige Lade-Animation // Zeige Lade-Animation
this.showLoading(); this.showLoading();
// Demo-Logik: Verwende direkt die Standardknoten // API-Aufruf durchführen, um die Kategorien und ihre Knoten zu laden
this.nodes = this.defaultNodes; const response = await fetch('/api/mindmap/public');
this.links = this.defaultLinks; if (!response.ok) {
throw new Error('API-Fehler: ' + response.statusText);
}
// Simuliere einen API-Aufruf (in einer echten Anwendung würde hier ein Fetch stehen) const data = await response.json();
await new Promise(resolve => setTimeout(resolve, 1000)); console.log('Geladene Mindmap-Daten:', data);
// Verarbeite die hierarchischen Daten in flache Knoten und Links
const processed = this.processApiData(data);
this.nodes = processed.nodes;
this.links = processed.links;
// Visualisierung aktualisieren // Visualisierung aktualisieren
this.updateVisualization(); this.updateVisualization();
@@ -295,8 +483,8 @@ class MindMapVisualization {
// Lade-Animation ausblenden // Lade-Animation ausblenden
this.hideLoading(); this.hideLoading();
// Zufällige Knoten pulsieren lassen // Erfolgreiche Ladung melden
this.pulseRandomNodes(); this.showFlash('Mindmap-Daten erfolgreich geladen', 'success');
} catch (error) { } catch (error) {
console.error('Fehler beim Laden der Mindmap-Daten:', error); console.error('Fehler beim Laden der Mindmap-Daten:', error);
@@ -304,64 +492,141 @@ class MindMapVisualization {
this.nodes = this.defaultNodes; this.nodes = this.defaultNodes;
this.links = this.defaultLinks; this.links = this.defaultLinks;
// Fehler anzeigen
this.showError('Mindmap-Daten konnten nicht geladen werden. Verwende Standarddaten.');
this.showFlash('Fehler beim Laden der Mindmap-Daten. Standarddaten werden angezeigt.', 'error');
// Visualisierung auch im Fehlerfall aktualisieren // Visualisierung auch im Fehlerfall aktualisieren
this.updateVisualization(); this.updateVisualization();
this.hideLoading(); this.hideLoading();
} }
} }
// Startet ein zufälliges Pulsen von Knoten für visuelle Aufmerksamkeit // Verarbeitet die API-Daten in das benötigte Format
pulseRandomNodes() { processApiData(apiData) {
// Zufälligen Knoten auswählen // Erstelle einen Root-Knoten, der alle Kategorien verbindet
const randomNode = () => { const rootNode = {
const randomIndex = Math.floor(Math.random() * this.nodes.length); id: "root",
return this.nodes[randomIndex]; name: "Wissen",
description: "Zentrale Wissensbasis",
thought_count: 0
}; };
// Initiales Pulsen starten let nodes = [rootNode];
const initialPulse = () => { let links = [];
const node = randomNode();
this.pulseNode(node);
// Nächstes Pulsen in 3-7 Sekunden // Für jede Kategorie Knoten und Verbindungen erstellen
setTimeout(() => { apiData.forEach(category => {
const nextNode = randomNode(); // Kategorie als Knoten hinzufügen
this.pulseNode(nextNode); const categoryNode = {
id: `category_${category.id}`,
// Regelmäßig wiederholen name: category.name,
setInterval(() => { description: category.description,
const pulseNode = randomNode(); color_code: category.color_code,
this.pulseNode(pulseNode); icon: category.icon,
}, 5000 + Math.random() * 5000); thought_count: 0,
}, 3000 + Math.random() * 4000); type: 'category'
}; };
// Verzögertes Starten nach vollständigem Laden nodes.push(categoryNode);
setTimeout(initialPulse, 1000);
// Mit Root-Knoten verbinden
links.push({
source: "root",
target: categoryNode.id
});
// Alle Knoten aus dieser Kategorie hinzufügen
if (category.nodes && category.nodes.length > 0) {
category.nodes.forEach(node => {
// Zähle die Gedanken für die Kategorie
categoryNode.thought_count += node.thought_count || 0;
const mindmapNode = {
id: `node_${node.id}`,
name: node.name,
description: node.description || '',
color_code: node.color_code || category.color_code,
thought_count: node.thought_count || 0,
type: 'node',
categoryId: category.id
};
nodes.push(mindmapNode);
// Mit Kategorie-Knoten verbinden
links.push({
source: categoryNode.id,
target: mindmapNode.id
});
});
} }
// Lässt einen Knoten pulsieren für visuelle Hervorhebung // Rekursiv Unterkategorien verarbeiten
pulseNode(node) { if (category.children && category.children.length > 0) {
if (!this.nodeElements) return; this.processSubcategories(category.children, nodes, links, categoryNode.id);
const nodeElement = this.nodeElements.filter(d => d.id === node.id);
if (nodeElement.size() > 0) {
const circle = nodeElement.select('circle');
// Speichern des ursprünglichen Radius
const originalRadius = circle.attr('r');
// Animiertes Pulsieren
circle.transition()
.duration(600)
.attr('r', originalRadius * 1.3)
.attr('filter', 'url(#pulse-effect)')
.transition()
.duration(600)
.attr('r', originalRadius)
.attr('filter', 'url(#glass-effect)');
} }
});
// Root-Knoten-Gedankenzähler aktualisieren
rootNode.thought_count = nodes.reduce((sum, node) => sum + (node.thought_count || 0), 0);
return { nodes, links };
}
// Verarbeitet Unterkategorien rekursiv
processSubcategories(subcategories, nodes, links, parentId) {
subcategories.forEach(category => {
// Kategorie als Knoten hinzufügen
const categoryNode = {
id: `category_${category.id}`,
name: category.name,
description: category.description,
color_code: category.color_code,
icon: category.icon,
thought_count: 0,
type: 'subcategory'
};
nodes.push(categoryNode);
// Mit Eltern-Kategorie verbinden
links.push({
source: parentId,
target: categoryNode.id
});
// Alle Knoten aus dieser Kategorie hinzufügen
if (category.nodes && category.nodes.length > 0) {
category.nodes.forEach(node => {
// Zähle die Gedanken für die Kategorie
categoryNode.thought_count += node.thought_count || 0;
const mindmapNode = {
id: `node_${node.id}`,
name: node.name,
description: node.description || '',
color_code: node.color_code || category.color_code,
thought_count: node.thought_count || 0,
type: 'node',
categoryId: category.id
};
nodes.push(mindmapNode);
// Mit Kategorie-Knoten verbinden
links.push({
source: categoryNode.id,
target: mindmapNode.id
});
});
}
// Rekursiv Unterkategorien verarbeiten
if (category.children && category.children.length > 0) {
this.processSubcategories(category.children, nodes, links, categoryNode.id);
}
});
} }
// Zeigt den Ladebildschirm an // Zeigt den Ladebildschirm an
@@ -586,11 +851,20 @@ class MindMapVisualization {
} }
} }
// Farbe basierend auf Knotentyp erhalten // Bestimmt die Farbe eines Knotens basierend auf seinem Typ oder direkt angegebener Farbe
getNodeColor(node) { getNodeColor(node) {
// Verwende die ID als Typ, falls vorhanden // Direkt angegebene Farbe verwenden, wenn vorhanden
const nodeType = node.id.toLowerCase(); if (node.color_code) {
return this.colorPalette[nodeType] || this.colorPalette.default; return node.color_code;
}
// Kategorietyp-basierte Färbung
if (node.type === 'category' || node.type === 'subcategory') {
return this.colorPalette.root;
}
// Fallback für verschiedene Knotentypen
return this.colorPalette[node.id] || this.colorPalette.default;
} }
// Aktualisiert die Positionen in jedem Simulationsschritt // Aktualisiert die Positionen in jedem Simulationsschritt
@@ -903,6 +1177,9 @@ class MindMapVisualization {
window.onNodeDeselected(); window.onNodeDeselected();
} }
// Flash-Nachricht für abgewählten Knoten
this.showFlash('Knotenauswahl aufgehoben', 'info', 2000);
return; return;
} }
@@ -994,6 +1271,7 @@ class MindMapVisualization {
if (!thoughtContainer || !thoughtsList) { if (!thoughtContainer || !thoughtsList) {
console.error('Gedanken-Container nicht gefunden'); console.error('Gedanken-Container nicht gefunden');
this.showFlash('Fehler: Gedanken-Container nicht gefunden', 'error');
return; return;
} }
@@ -1029,9 +1307,12 @@ class MindMapVisualization {
thoughtsList.innerHTML = ''; thoughtsList.innerHTML = '';
} }
// Flash-Nachricht über ausgewählten Knoten
this.showFlash(`Knoten "${node.name}" ausgewählt`, 'info');
// Verzögerung für Animation // Verzögerung für Animation
setTimeout(() => { setTimeout(() => {
// API-Aufruf simulieren (später durch echten Aufruf ersetzen) // API-Aufruf für echte Daten aus der Datenbank
this.fetchThoughtsForNode(node.id) this.fetchThoughtsForNode(node.id)
.then(thoughts => { .then(thoughts => {
// Ladeanimation ausblenden // Ladeanimation ausblenden
@@ -1044,6 +1325,7 @@ class MindMapVisualization {
this.renderThoughts(thoughts, thoughtsList); this.renderThoughts(thoughts, thoughtsList);
} else { } else {
this.renderEmptyThoughts(thoughtsList, node); this.renderEmptyThoughts(thoughtsList, node);
this.showFlash(`Keine Gedanken zu "${node.name}" gefunden`, 'warning');
} }
}) })
.catch(error => { .catch(error => {
@@ -1052,10 +1334,142 @@ class MindMapVisualization {
loadingIndicator.style.display = 'none'; loadingIndicator.style.display = 'none';
} }
this.renderErrorState(thoughtsList); this.renderErrorState(thoughtsList);
this.showFlash('Fehler beim Laden der Gedanken. Bitte versuche es später erneut.', 'error');
}); });
}, 600); // Verzögerung für bessere UX }, 600); // Verzögerung für bessere UX
} }
// Holt Gedanken für einen Knoten aus der Datenbank
async fetchThoughtsForNode(nodeId) {
try {
// Extrahiere die tatsächliche ID aus dem nodeId Format (z.B. "node_123" oder "category_456")
const id = nodeId.toString().split('_')[1];
if (!id) {
console.warn('Ungültige Node-ID: ', nodeId);
this.showFlash('Ungültige Knoten-ID: ' + nodeId, 'warning');
return [];
}
// API-Aufruf an den entsprechenden Endpunkt
const response = await fetch(`/api/nodes/${id}/thoughts`);
if (!response.ok) {
throw new Error(`API-Fehler: ${response.statusText}`);
}
const thoughts = await response.json();
console.log('Geladene Gedanken für Knoten:', thoughts);
if (thoughts.length > 0) {
this.showFlash(`${thoughts.length} Gedanken zum Thema geladen`, 'info');
} else {
this.showFlash('Keine Gedanken für diesen Knoten gefunden', 'info');
}
return thoughts;
} catch (error) {
console.error('Fehler beim Laden der Gedanken für Knoten:', error);
this.showFlash('Fehler beim Laden der Gedanken', 'error');
return [];
}
}
// Rendert die Gedanken in der UI
renderThoughts(thoughts, container) {
if (!container) return;
container.innerHTML = '';
thoughts.forEach(thought => {
const thoughtCard = document.createElement('div');
thoughtCard.className = 'thought-card';
thoughtCard.setAttribute('data-id', thought.id);
const cardColor = thought.color_code || this.colorPalette.default;
thoughtCard.innerHTML = `
<div class="thought-card-header" style="border-left: 4px solid ${cardColor}">
<h3 class="thought-title">${thought.title}</h3>
<div class="thought-meta">
<span class="thought-date">${new Date(thought.created_at).toLocaleDateString('de-DE')}</span>
${thought.author ? `<span class="thought-author">von ${thought.author.username}</span>` : ''}
</div>
</div>
<div class="thought-content">
<p>${thought.abstract || thought.content.substring(0, 150) + '...'}</p>
</div>
<div class="thought-footer">
<div class="thought-keywords">
${thought.keywords ? thought.keywords.split(',').map(kw =>
`<span class="keyword">${kw.trim()}</span>`).join('') : ''}
</div>
<a href="/thoughts/${thought.id}" class="thought-link">Mehr lesen </a>
</div>
`;
// Event-Listener für Klick auf Gedanken
thoughtCard.addEventListener('click', (e) => {
// Verhindern, dass der Link-Klick den Kartenklick auslöst
if (e.target.tagName === 'A') return;
window.location.href = `/thoughts/${thought.id}`;
});
container.appendChild(thoughtCard);
});
}
// Rendert eine Leermeldung, wenn keine Gedanken vorhanden sind
renderEmptyThoughts(container, node) {
if (!container) return;
const emptyState = document.createElement('div');
emptyState.className = 'empty-thoughts-state';
emptyState.innerHTML = `
<div class="empty-icon">
<i class="fas fa-lightbulb"></i>
</div>
<h3>Keine Gedanken verknüpft</h3>
<p>Zu "${node.name}" sind noch keine Gedanken verknüpft.</p>
<div class="empty-actions">
<a href="/add-thought?node=${node.id}" class="btn btn-primary">
<i class="fas fa-plus-circle"></i> Gedanken hinzufügen
</a>
</div>
`;
container.appendChild(emptyState);
}
// Rendert einen Fehlerzustand
renderErrorState(container) {
if (!container) return;
const errorState = document.createElement('div');
errorState.className = 'error-thoughts-state';
errorState.innerHTML = `
<div class="error-icon">
<i class="fas fa-exclamation-triangle"></i>
</div>
<h3>Fehler beim Laden</h3>
<p>Die Gedanken konnten nicht geladen werden. Bitte versuche es später erneut.</p>
<button class="btn btn-secondary retry-button">
<i class="fas fa-redo"></i> Erneut versuchen
</button>
`;
// Event-Listener für Retry-Button
const retryButton = errorState.querySelector('.retry-button');
if (retryButton && this.selectedNode) {
retryButton.addEventListener('click', () => {
this.loadThoughtsForNode(this.selectedNode);
});
}
container.appendChild(errorState);
}
// Zentriert einen Knoten in der Ansicht // Zentriert einen Knoten in der Ansicht
centerNodeInView(node) { centerNodeInView(node) {
// Sanfter Übergang zur Knotenzentrierüng // Sanfter Übergang zur Knotenzentrierüng
@@ -1071,10 +1485,16 @@ class MindMapVisualization {
d3.zoom().transform, d3.zoom().transform,
d3.zoomIdentity.translate(x, y).scale(scale) d3.zoomIdentity.translate(x, y).scale(scale)
); );
// Flash-Nachricht für Zentrierung
if (node && node.name) {
this.showFlash(`Ansicht auf "${node.name}" zentriert`, 'info', 2000);
}
} }
// Fehlermeldung anzeigen // Fehlermeldung anzeigen
showError(message) { showError(message) {
// Standard-Fehlermeldung als Banner
const errorBanner = d3.select('body').selectAll('.error-banner').data([0]); const errorBanner = d3.select('body').selectAll('.error-banner').data([0]);
const errorEnter = errorBanner.enter() const errorEnter = errorBanner.enter()
@@ -1103,12 +1523,18 @@ class MindMapVisualization {
.delay(5000) .delay(5000)
.duration(500) .duration(500)
.style('bottom', '-100px'); .style('bottom', '-100px');
// Auch als Flash-Nachricht anzeigen
this.showFlash(message, 'error');
} }
// Fokussieren auf einen bestimmten Knoten per ID // Fokussieren auf einen bestimmten Knoten per ID
focusNode(nodeId) { focusNode(nodeId) {
const targetNode = this.nodes.find(n => n.id === nodeId); const targetNode = this.nodes.find(n => n.id === nodeId);
if (!targetNode) return; if (!targetNode) {
this.showFlash(`Knoten mit ID "${nodeId}" nicht gefunden`, 'error');
return;
}
// Ausgewählten Zustand zurücksetzen // Ausgewählten Zustand zurücksetzen
this.selectedNode = null; this.selectedNode = null;
@@ -1128,6 +1554,8 @@ class MindMapVisualization {
d3.zoom().transform, d3.zoom().transform,
transform transform
); );
this.showFlash(`Fokus auf Knoten "${targetNode.name}" gesetzt`, 'success');
} }
} }
@@ -1147,6 +1575,8 @@ class MindMapVisualization {
.style('display', 'block') .style('display', 'block')
.style('stroke-opacity', 0.5); .style('stroke-opacity', 0.5);
this.showFlash('Suchfilter zurückgesetzt', 'info', 2000);
return; return;
} }
@@ -1182,6 +1612,9 @@ class MindMapVisualization {
// Wenn mehr als ein Knoten gefunden wurde, Simulation mit reduzierter Stärke neu starten // Wenn mehr als ein Knoten gefunden wurde, Simulation mit reduzierter Stärke neu starten
if (matchingNodes.length > 1) { if (matchingNodes.length > 1) {
this.simulation.alpha(0.3).restart(); this.simulation.alpha(0.3).restart();
this.showFlash(`${matchingNodes.length} Knoten für "${searchTerm}" gefunden`, 'success');
} else if (matchingNodes.length === 0) {
this.showFlash(`Keine Knoten für "${searchTerm}" gefunden`, 'warning');
} }
} }
} }

View File

@@ -75,7 +75,7 @@ document.addEventListener('DOMContentLoaded', function() {
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
background-image: url('/static/network-bg.jpg'); background: rgba(179, 143, 255, 0.05);
background-size: cover; background-size: cover;
background-position: center; background-position: center;
opacity: 0.15; opacity: 0.15;

View File

@@ -11,6 +11,8 @@ let scaleDirection = 1;
let opacityDirection = 1; let opacityDirection = 1;
let animationFrameId = null; let animationFrameId = null;
let isDarkMode = document.documentElement.classList.contains('dark'); let isDarkMode = document.documentElement.classList.contains('dark');
let loadAttempts = 0;
const MAX_LOAD_ATTEMPTS = 2;
// Initialize the canvas and load the image // Initialize the canvas and load the image
function initNetworkBackground() { function initNetworkBackground() {
@@ -36,20 +38,14 @@ function initNetworkBackground() {
// Get context with alpha enabled // Get context with alpha enabled
ctx = canvas.getContext('2d', { alpha: true }); ctx = canvas.getContext('2d', { alpha: true });
// Load the network image // Load the network image - versuche zuerst die SVG-Version
networkImage = new Image(); networkImage = new Image();
networkImage.crossOrigin = "anonymous"; // Vermeidet CORS-Probleme networkImage.crossOrigin = "anonymous"; // Vermeidet CORS-Probleme
networkImage.src = '/static/network-bg.jpg';
// Fallback auf lokalen Pfad, falls der absolute Pfad fehlschlägt // Keine Bilder laden, direkt Fallback-Hintergrund verwenden
networkImage.onerror = function() { console.log("Verwende einfachen Hintergrund ohne Bilddateien");
networkImage.src = 'static/network-bg.jpg'; isImageLoaded = true; // Animation ohne Hintergrundbild starten
};
networkImage.onload = function() {
isImageLoaded = true;
startAnimation(); startAnimation();
};
// Handle window resize // Handle window resize
window.addEventListener('resize', debounce(resizeCanvas, 250)); window.addEventListener('resize', debounce(resizeCanvas, 250));
@@ -102,9 +98,6 @@ function resizeCanvas() {
// Start animation // Start animation
function startAnimation() { function startAnimation() {
if (!isImageLoaded) return;
// Cancel any existing animation
if (animationFrameId) { if (animationFrameId) {
cancelAnimationFrame(animationFrameId); cancelAnimationFrame(animationFrameId);
} }
@@ -115,7 +108,7 @@ function startAnimation() {
// Draw network image // Draw network image
function drawNetworkImage() { function drawNetworkImage() {
if (!isImageLoaded || !ctx) return; if (!ctx) return;
// Clear canvas with proper clear method // Clear canvas with proper clear method
ctx.clearRect(0, 0, canvas.width / (window.devicePixelRatio || 1), canvas.height / (window.devicePixelRatio || 1)); ctx.clearRect(0, 0, canvas.width / (window.devicePixelRatio || 1), canvas.height / (window.devicePixelRatio || 1));
@@ -135,6 +128,7 @@ function drawNetworkImage() {
// Set global opacity, angepasst für Dark Mode // Set global opacity, angepasst für Dark Mode
ctx.globalAlpha = isDarkMode ? opacity : opacity * 0.8; ctx.globalAlpha = isDarkMode ? opacity : opacity * 0.8;
if (isImageLoaded && networkImage.complete) {
// Bildgröße berechnen, um den Bildschirm abzudecken // Bildgröße berechnen, um den Bildschirm abzudecken
const imgAspect = networkImage.width / networkImage.height; const imgAspect = networkImage.width / networkImage.height;
const canvasAspect = canvas.width / canvas.height; const canvasAspect = canvas.width / canvas.height;
@@ -157,11 +151,34 @@ function drawNetworkImage() {
drawWidth, drawWidth,
drawHeight drawHeight
); );
} else {
// Fallback: Zeichne einen einfachen Hintergrund mit Punkten
drawFallbackBackground();
}
// Restore context state // Restore context state
ctx.restore(); ctx.restore();
} }
// Fallback-Hintergrund mit Punkten und Linien
function drawFallbackBackground() {
const width = canvas.width / (window.devicePixelRatio || 1);
const height = canvas.height / (window.devicePixelRatio || 1);
// Zeichne einige zufällige Punkte
ctx.fillStyle = isDarkMode ? 'rgba(139, 92, 246, 0.2)' : 'rgba(139, 92, 246, 0.1)';
for (let i = 0; i < 50; i++) {
const x = Math.random() * width;
const y = Math.random() * height;
const radius = Math.random() * 3 + 1;
ctx.beginPath();
ctx.arc(x - width/2, y - height/2, radius, 0, Math.PI * 2);
ctx.fill();
}
}
// Animation loop // Animation loop
function animate() { function animate() {
// Update animation parameters // Update animation parameters

View File

@@ -64,30 +64,30 @@ module.exports = {
'50%': { transform: 'translateY(-10px)' }, '50%': { transform: 'translateY(-10px)' },
} }
}, },
typography: (theme) => ({ typography: {
DEFAULT: { DEFAULT: {
css: { css: {
color: theme('colors.gray.800'), color: 'rgb(31, 41, 55)',
a: { a: {
color: theme('colors.primary.500'), color: 'rgb(41, 112, 255)',
'&:hover': { '&:hover': {
color: theme('colors.primary.700'), color: 'rgb(22, 84, 246)',
}, },
}, },
}, },
}, },
dark: { dark: {
css: { css: {
color: theme('colors.gray.200'), color: 'rgb(229, 231, 235)',
a: { a: {
color: theme('colors.primary.400'), color: 'rgb(90, 147, 255)',
'&:hover': { '&:hover': {
color: theme('colors.primary.300'), color: 'rgb(142, 184, 255)',
},
}, },
}, },
}, },
}, },
}),
boxShadow: { boxShadow: {
'soft': '0 4px 15px rgba(0, 0, 0, 0.05)', 'soft': '0 4px 15px rgba(0, 0, 0, 0.05)',
'glow': '0 0 15px rgba(32, 92, 245, 0.3)' 'glow': '0 0 15px rgba(32, 92, 245, 0.3)'
@@ -95,7 +95,6 @@ module.exports = {
}, },
}, },
plugins: [ plugins: [
require('@tailwindcss/typography'), // Typography and forms plugins removed, we'll implement their basic functionality in CSS
require('@tailwindcss/forms'),
], ],
} }

535
templates/base.html Normal file
View File

@@ -0,0 +1,535 @@
<!DOCTYPE html>
<html lang="de" class="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Systades - {% block title %}{% endblock %}</title>
<!-- Favicon -->
<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">
<meta name="keywords" content="systades, wissen, visualisierung, lernen, gedanken, theorie">
<meta name="author" content="Systades-Team">
<!-- Tailwind CSS über CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
fontFamily: {
sans: ['Inter', 'ui-sans-serif', 'system-ui', '-apple-system', 'sans-serif'],
mono: ['JetBrains Mono', 'ui-monospace', 'monospace']
},
colors: {
primary: {
50: '#f5f3ff',
100: '#ede9fe',
200: '#ddd6fe',
300: '#c4b5fd',
400: '#a78bfa',
500: '#8b5cf6',
600: '#7c3aed',
700: '#6d28d9',
800: '#5b21b6',
900: '#4c1d95'
},
secondary: {
50: '#ecfdf5',
100: '#d1fae5',
200: '#a7f3d0',
300: '#6ee7b7',
400: '#34d399',
500: '#10b981',
600: '#059669',
700: '#047857',
800: '#065f46',
900: '#064e3b'
},
dark: {
500: '#374151',
600: '#1f2937',
700: '#111827',
800: '#0e1220',
900: '#0a0e19'
}
}
}
}
}
</script>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
<!-- Icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Assistent CSS -->
<link href="{{ url_for('static', filename='css/assistant.css') }}" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', filename='css/src/cybernetwork-bg.css') }}">
<!-- Basis-Stylesheet -->
<link href="{{ url_for('static', filename='style.css') }}" rel="stylesheet">
<!-- Base-Styles ausgelagert in eigene Datei -->
<link href="{{ url_for('static', filename='css/base-styles.css') }}" rel="stylesheet">
<!-- Alpine.js -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.12.3/dist/cdn.min.js"></script>
<!-- Network Background Script -->
<script src="{{ url_for('static', filename='network-background.js') }}"></script>
<!-- Hauptmodul laden (als ES6 Modul) -->
<script type="module">
import MindMap from "{{ url_for('static', filename='js/main.js') }}";
// Alpine.js-Integration
document.addEventListener('alpine:init', () => {
Alpine.data('layout', () => ({
darkMode: false,
mobileMenuOpen: false,
userMenuOpen: false,
showSettingsModal: false,
init() {
this.fetchDarkModeFromSession();
},
fetchDarkModeFromSession() {
// Lade den Dark Mode-Status vom Server
fetch('/get_dark_mode')
.then(response => response.json())
.then(data => {
if (data.success) {
this.darkMode = data.darkMode === 'true';
document.querySelector('html').classList.toggle('dark', this.darkMode);
}
})
.catch(error => {
console.error('Fehler beim Laden der Dark Mode-Einstellung:', error);
});
},
toggleDarkMode() {
this.darkMode = !this.darkMode;
document.querySelector('html').classList.toggle('dark', this.darkMode);
// Speichere den Dark Mode-Status auf dem Server
fetch('/set_dark_mode', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ darkMode: this.darkMode })
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Zusätzlich im localStorage speichern für sofortige Reaktion bei Seitenwechsel
localStorage.setItem('darkMode', this.darkMode ? 'dark' : 'light');
// Event auslösen für andere Komponenten
document.dispatchEvent(new CustomEvent('darkModeToggled', {
detail: { isDark: this.darkMode }
}));
} else {
console.error('Fehler beim Speichern der Dark Mode-Einstellung:', data.error);
}
})
.catch(error => {
console.error('Fehler beim Speichern der Dark Mode-Einstellung:', error);
});
}
}));
});
// MindMap global verfügbar machen (für Alpine.js und andere nicht-Module Skripte)
window.MindMap = MindMap;
</script>
<!-- Seitenspezifische Styles -->
{% block extra_css %}{% endblock %}
<!-- Cybertechnisches Netzwerk-Hintergrund -->
<script type="module" src="{{ url_for('static', filename='js/modules/cyber-network-init.js') }}"></script>
</head>
<body data-page="{{ request.endpoint }}" class="relative overflow-x-hidden">
<!-- Cybertechnisches Netzwerk-Hintergrund Container (wird via JavaScript befüllt) -->
<div id="cyber-background-container" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: -1; pointer-events: none; overflow: hidden;"></div>
<!-- Globaler Hintergrund -->
<div class="full-page-bg"></div>
<!-- Statischer Fallback-Hintergrund (wird nur angezeigt, wenn JavaScript deaktiviert ist) -->
<div class="fixed inset-0 z-[-9] bg-cover bg-center opacity-50"></div>
<!-- App-Container -->
<div id="app-container" class="flex flex-col min-h-screen" x-data="layout">
<!-- Hauptnavigation -->
<nav class="sticky top-0 left-0 right-0 z-50 transition-all duration-300 py-4 px-5 border-b glass-morphism"
x-bind:class="darkMode ? 'glass-navbar-dark border-white/10' : 'glass-navbar-light border-gray-200/50'">
<div class="container mx-auto flex justify-between items-center">
<!-- Logo -->
<a href="{{ url_for('index') }}" class="flex items-center group">
<span class="text-2xl font-bold gradient-text transform transition-transform group-hover:scale-105">Systades</span>
</a>
<!-- Hauptnavigation - Desktop -->
<div class="hidden md:flex items-center space-x-5">
<a href="{{ url_for('index') }}"
class="nav-link flex items-center"
x-bind:class="darkMode
? '{{ 'nav-link-active' if request.endpoint == 'index' else '' }}'
: '{{ 'nav-link-light-active' if request.endpoint == 'index' else 'nav-link-light' }}'">
<i class="fa-solid fa-home mr-2"></i>Start
</a>
<a href="{{ url_for('mindmap') }}"
class="nav-link flex items-center"
x-bind:class="darkMode
? '{{ 'nav-link-active' if request.endpoint == 'mindmap' else '' }}'
: '{{ 'nav-link-light-active' if request.endpoint == 'mindmap' else 'nav-link-light' }}'">
<i class="fa-solid fa-diagram-project mr-2"></i>Mindmap
</a>
<a href="{{ url_for('search_thoughts_page') }}"
class="nav-link flex items-center"
x-bind:class="darkMode
? '{{ 'nav-link-active' if request.endpoint == 'search_thoughts_page' else '' }}'
: '{{ 'nav-link-light-active' if request.endpoint == 'search_thoughts_page' else 'nav-link-light' }}'">
<i class="fa-solid fa-search mr-2"></i>Suche
</a>
<!-- KI-Assistent Button -->
<button onclick="window.MindMap && window.MindMap.assistant && window.MindMap.assistant.toggleAssistant(true)"
class="nav-link flex items-center"
x-bind:class="darkMode
? 'bg-gradient-to-r from-purple-600/80 to-blue-500/80 text-white font-medium px-4 py-2 rounded-xl hover:shadow-lg transition-all duration-300 hover:-translate-y-0.5'
: 'bg-gradient-to-r from-purple-500/20 to-blue-400/20 text-gray-800 font-medium px-4 py-2 rounded-xl hover:shadow-md transition-all duration-300 hover:-translate-y-0.5'">
<i class="fa-solid fa-robot mr-2"></i>KI-Chat
</button>
{% if current_user.is_authenticated %}
<a href="{{ url_for('profile') }}"
class="nav-link flex items-center"
x-bind:class="darkMode
? '{{ 'nav-link-active' if request.endpoint == 'profile' else '' }}'
: '{{ 'nav-link-light-active' if request.endpoint == 'profile' else 'nav-link-light' }}'">
<i class="fa-solid fa-user mr-2"></i>Profil
</a>
{% endif %}
</div>
<!-- Rechte Seite -->
<div class="flex items-center space-x-4">
<!-- 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-blue-400/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-blue-500 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 }">
<button @click="open = !open"
class="flex items-center space-x-2 p-2 rounded-full transition-all duration-300 cursor-pointer"
x-bind:class="darkMode
? 'bg-gray-800/80 text-white/90 hover:bg-gray-700/80'
: 'bg-gray-200/80 text-gray-700 hover:bg-gray-300/80'">
<div class="w-9 h-9 rounded-full flex items-center justify-center text-white font-medium text-sm overflow-hidden"
style="background: linear-gradient(135deg, #8b5cf6, #6366f1);">
{% if current_user.avatar %}
<img src="{{ current_user.avatar }}" alt="{{ current_user.username }}" class="w-full h-full object-cover">
{% else %}
{{ current_user.username[0].upper() }}
{% endif %}
</div>
<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ü -->
<div x-show="open"
@click.away="open = false"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
class="absolute right-0 mt-2 w-52 rounded-2xl overflow-hidden shadow-lg transform origin-top-right z-50"
x-bind:class="darkMode
? 'bg-gray-800/95 backdrop-blur-md border border-white/10'
: 'bg-white/95 backdrop-blur-md border border-gray-200/50'">
<a href="{{ url_for('profile') }}"
class="block px-4 py-3 transition-colors duration-200 flex items-center"
x-bind:class="darkMode
? 'text-white/90 hover:bg-purple-500/20'
: 'text-gray-700 hover:bg-purple-500/10'">
<i class="fa-solid fa-user mr-2 text-purple-400"></i>Profil
</a>
<a href="{{ url_for('my_account') }}"
class="block px-4 py-3 transition-colors duration-200 flex items-center"
x-bind:class="darkMode
? 'text-white/90 hover:bg-purple-500/20'
: 'text-gray-700 hover:bg-purple-500/10'">
<i class="fa-solid fa-bookmark mr-2 text-purple-400"></i>Meine Merkliste
</a>
<a href="{{ url_for('settings') }}"
class="block px-4 py-3 transition-colors duration-200 flex items-center"
x-bind:class="darkMode
? 'text-white/90 hover:bg-purple-500/20'
: 'text-gray-700 hover:bg-purple-500/10'">
<i class="fa-solid fa-gear mr-2 text-purple-400"></i>Einstellungen
</a>
<div class="my-2 h-px" x-bind:class="darkMode ? 'bg-white/10' : 'bg-gray-200'"></div>
<a href="{{ url_for('logout') }}"
class="block px-4 py-3 transition-colors duration-200 flex items-center"
x-bind:class="darkMode
? 'text-white/90 hover:bg-red-500/20'
: 'text-gray-700 hover:bg-red-500/10'">
<i class="fa-solid fa-right-from-bracket mr-2 text-red-400"></i>Abmelden
</a>
</div>
</div>
{% else %}
<a href="{{ url_for('login') }}"
class="flex items-center px-4 py-2.5 rounded-xl font-medium transition-all duration-300"
x-bind:class="darkMode
? 'bg-gray-800/80 text-white hover:bg-gray-700/80 shadow-md hover:shadow-lg hover:-translate-y-0.5'
: 'bg-gray-200/80 text-gray-800 hover:bg-gray-300/80 shadow-sm hover:shadow-md hover:-translate-y-0.5'">
<i class="fa-solid fa-user mr-2"></i>Mein Konto
</a>
{% endif %}
<!-- Mobilmenü-Button -->
<button @click="mobileMenuOpen = !mobileMenuOpen"
class="md:hidden rounded-xl p-2.5 transition-colors duration-200 focus:outline-none"
x-bind:class="darkMode
? 'text-white/90 hover:bg-gray-700/50'
: 'text-gray-700 hover:bg-gray-200/80'">
<i class="fa-solid" :class="mobileMenuOpen ? 'fa-times' : 'fa-bars'"></i>
</button>
</div>
</div>
</nav>
<!-- Mobile Menü -->
<div x-show="mobileMenuOpen"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 -translate-y-4"
x-transition:enter-end="opacity-100 translate-y-0"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100 translate-y-0"
x-transition:leave-end="opacity-0 -translate-y-4"
class="md:hidden w-full z-40 border-b"
x-bind:class="darkMode
? 'bg-gray-900/90 backdrop-blur-lg border-white/10'
: 'bg-white/90 backdrop-blur-lg border-gray-200'">
<div class="px-4 py-4 space-y-3">
<a href="{{ url_for('index') }}"
class="block py-3.5 px-4 rounded-xl transition-all duration-200 flex items-center"
x-bind:class="darkMode
? '{{ 'bg-purple-500/20 text-white' if request.endpoint == 'index' else 'text-white/80 hover:bg-gray-800/80 hover:text-white' }}'
: '{{ 'bg-purple-500/10 text-gray-900' if request.endpoint == 'index' else 'text-gray-700 hover:bg-gray-100 hover:text-gray-900' }}'">
<i class="fa-solid fa-home w-5 mr-3"></i>Start
</a>
<a href="{{ url_for('mindmap') }}"
class="block py-3.5 px-4 rounded-xl transition-all duration-200 flex items-center"
x-bind:class="darkMode
? '{{ 'bg-purple-500/20 text-white' if request.endpoint == 'mindmap' else 'text-white/80 hover:bg-gray-800/80 hover:text-white' }}'
: '{{ 'bg-purple-500/10 text-gray-900' if request.endpoint == 'mindmap' else 'text-gray-700 hover:bg-gray-100 hover:text-gray-900' }}'">
<i class="fa-solid fa-diagram-project w-5 mr-3"></i>Mindmap
</a>
<a href="{{ url_for('search_thoughts_page') }}"
class="block py-3.5 px-4 rounded-xl transition-all duration-200 flex items-center"
x-bind:class="darkMode
? '{{ 'bg-purple-500/20 text-white' if request.endpoint == 'search_thoughts_page' else 'text-white/80 hover:bg-gray-800/80 hover:text-white' }}'
: '{{ 'bg-purple-500/10 text-gray-900' if request.endpoint == 'search_thoughts_page' else 'text-gray-700 hover:bg-gray-100 hover:text-gray-900' }}'">
<i class="fa-solid fa-search w-5 mr-3"></i>Suche
</a>
<!-- KI-Button für Mobilmenü -->
<button onclick="window.MindMap && window.MindMap.assistant && window.MindMap.assistant.toggleAssistant(true); mobileMenuOpen = false;"
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-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 %}
<a href="{{ url_for('profile') }}"
class="block py-3.5 px-4 rounded-xl transition-all duration-200 flex items-center"
x-bind:class="darkMode
? '{{ 'bg-purple-500/20 text-white' if request.endpoint == 'profile' else 'text-white/80 hover:bg-gray-800/80 hover:text-white' }}'
: '{{ 'bg-purple-500/10 text-gray-900' if request.endpoint == 'profile' else 'text-gray-700 hover:bg-gray-100 hover:text-gray-900' }}'">
<i class="fa-solid fa-user w-5 mr-3"></i>Profil
</a>
{% endif %}
</div>
</div>
<!-- Hauptinhalt -->
<main class="flex-grow pt-6">
{% block content %}{% endblock %}
</main>
<!-- Footer -->
<footer class="mt-12 py-10 transition-colors duration-300 rounded-t-3xl mx-4 sm:mx-6 md:mx-8"
:class="darkMode ? 'bg-gray-900/60 backdrop-blur-xl border-t border-white/10' : 'bg-white/60 backdrop-blur-xl border-t border-gray-200/50'">
<div class="container mx-auto px-4">
<div class="grid grid-cols-1 md:grid-cols-3 gap-10">
<!-- Logo und Beschreibung -->
<div class="text-center md:text-left flex flex-col">
<a href="{{ url_for('index') }}" class="text-2xl font-bold mb-4 gradient-text inline-block transform transition-transform hover:scale-105">Systades</a>
<p class="mt-2 text-sm max-w-md"
:class="darkMode ? 'text-gray-300' : 'text-gray-600'">
Eine interaktive Plattform zum Visualisieren, Erforschen und Teilen von Wissen und Gedanken in einem strukturierten Format.
</p>
<!-- Social Media Icons -->
<div class="flex items-center space-x-4 mt-6 justify-center md:justify-start">
<a href="#" class="transition-all duration-200 transform hover:scale-110 hover:-translate-y-1"
:class="darkMode ? 'text-purple-400 hover:text-purple-300' : 'text-purple-600 hover:text-purple-500'">
<i class="fab fa-twitter text-xl"></i>
</a>
<a href="#" class="transition-all duration-200 transform hover:scale-110 hover:-translate-y-1"
:class="darkMode ? 'text-purple-400 hover:text-purple-300' : 'text-purple-600 hover:text-purple-500'">
<i class="fab fa-linkedin text-xl"></i>
</a>
<a href="#" class="transition-all duration-200 transform hover:scale-110 hover:-translate-y-1"
:class="darkMode ? 'text-purple-400 hover:text-purple-300' : 'text-purple-600 hover:text-purple-500'">
<i class="fab fa-github text-xl"></i>
</a>
<a href="#" class="transition-all duration-200 transform hover:scale-110 hover:-translate-y-1"
:class="darkMode ? 'text-purple-400 hover:text-purple-300' : 'text-purple-600 hover:text-purple-500'">
<i class="fab fa-discord text-xl"></i>
</a>
</div>
</div>
<!-- Links -->
<div class="grid grid-cols-2 gap-8">
<div class="flex flex-col space-y-3">
<h3 class="font-semibold text-lg mb-2"
:class="darkMode ? 'text-white' : 'text-gray-800'">Navigation</h3>
<a href="{{ url_for('index') }}" class="text-sm transition-all duration-200"
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
Startseite
</a>
<a href="{{ url_for('mindmap') }}" class="text-sm transition-all duration-200"
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
Mindmap
</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'">
Profil
</a>
<a href="{{ url_for('my_account') }}" class="text-sm transition-all duration-200"
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
Meine Merkliste
</a>
{% else %}
<a href="{{ url_for('login') }}" class="text-sm transition-all duration-200"
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
Anmelden
</a>
<a href="{{ url_for('register') }}" class="text-sm transition-all duration-200"
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
Registrieren
</a>
{% endif %}
</div>
<div class="flex flex-col space-y-3">
<h3 class="font-semibold text-lg mb-2"
:class="darkMode ? 'text-white' : 'text-gray-800'">Rechtliches</h3>
<a href="{{ url_for('impressum') }}" class="text-sm transition-all duration-200"
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
Impressum
</a>
<a href="{{ url_for('datenschutz') }}" class="text-sm transition-all duration-200"
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
Datenschutz
</a>
<a href="{{ url_for('agb') }}" class="text-sm transition-all duration-200"
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
AGB
</a>
</div>
</div>
<!-- Newsletter Anmeldung -->
<div class="flex flex-col">
<h3 class="font-semibold text-lg mb-4"
:class="darkMode ? 'text-white' : 'text-gray-800'">Newsletter</h3>
<p class="text-sm mb-4"
:class="darkMode ? 'text-gray-300' : 'text-gray-600'">
Bleibe auf dem Laufenden mit unseren neuesten Funktionen und Updates.
</p>
<form class="flex flex-col space-y-3">
<input type="email" placeholder="Deine E-Mail Adresse"
class="px-4 py-2.5 rounded-xl transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-purple-500"
:class="darkMode ? 'bg-gray-800/80 text-white border border-gray-700 focus:bg-gray-800' : 'bg-white/80 text-gray-800 border border-gray-300 focus:bg-white'" />
<button type="submit"
class="px-4 py-2.5 rounded-xl font-medium transition-all duration-300 bg-gradient-to-r from-purple-600 to-indigo-600 text-white shadow-md hover:shadow-lg hover:-translate-y-0.5">
Abonnieren
</button>
</form>
</div>
</div>
<!-- Untere Linie -->
<div class="mt-10 pt-6 border-t flex flex-col md:flex-row justify-between items-center"
:class="darkMode ? 'border-gray-800/50 text-gray-400' : 'border-gray-300/50 text-gray-600'">
<div class="text-xs md:text-sm mb-3 md:mb-0">
&copy; {{ current_year }} Systades. Alle Rechte vorbehalten.
</div>
<div class="text-xs md:text-sm">
Designed with <i class="fas fa-heart text-pink-500"></i> in Deutschland
</div>
</div>
</div>
</footer>
</div>
<!-- Hilfsscripts -->
{% block scripts %}{% endblock %}
<!-- KI-Chat Initialisierung -->
<script type="module">
// Importiere und initialisiere den ChatGPT-Assistenten direkt, um sicherzustellen,
// dass er auf jeder Seite verfügbar ist, selbst wenn MindMap nicht geladen ist
import ChatGPTAssistant from "{{ url_for('static', filename='js/modules/chatgpt-assistant.js') }}";
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>
</body>
</html>

603
templates/index.html Normal file
View File

@@ -0,0 +1,603 @@
{% extends "base.html" %}
{% block title %}Wissensnetzwerk{% endblock %}
{% block extra_css %}
<style>
/* Hintergrund über die gesamte Seite erstrecken */
html, body {
min-height: 100vh;
width: 100%;
margin: 0;
padding: 0;
overflow-x: hidden;
}
/* Entferne den Gradient-Hintergrund vollständig */
.hero-gradient, .bg-fade {
background: none !important;
clip-path: none !important;
}
.tech-line {
height: 1px;
background: linear-gradient(to right, transparent, rgba(100, 100, 100, 0.1), transparent);
}
.tech-dot {
width: 4px;
height: 4px;
border-radius: 50%;
background-color: rgba(100, 100, 100, 0.2);
position: absolute;
}
.dark .tech-line {
background: linear-gradient(to right, transparent, rgba(255, 255, 255, 0.1), transparent);
}
.dark .tech-dot {
background-color: rgba(255, 255, 255, 0.3);
}
@keyframes pulse {
0% { r: 10; opacity: 0.7; }
50% { r: 12; opacity: 1; }
100% { r: 10; opacity: 0.7; }
}
.animate-pulse {
animation: pulse 3s ease-in-out infinite;
}
@keyframes iconPulse {
0% { transform: scale(1); }
50% { transform: scale(1.1); }
100% { transform: scale(1); }
}
.icon-pulse {
animation: iconPulse 3s ease-in-out infinite;
display: inline-block;
}
/* Volle Seitenbreite für Container */
#app-container, .container, main, .mx-auto, .py-12 {
width: 100%;
}
/* Sicherstellen dass der Hintergrund die ganze Seite abdeckt */
.full-page-bg {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: -1;
}
/* Chat-Animationen */
.typing-dots span {
animation-duration: 1.2s;
animation-iteration-count: infinite;
}
/* Chat-Nachrichten-Animation */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translate3d(0, 10px, 0);
}
to {
opacity: 1;
transform: translate3d(0, 0, 0);
}
}
#embedded-chat-messages > div {
animation: fadeInUp 0.3s ease-out forwards;
}
/* Sanftes Scrollen im Chat */
#embedded-chat-messages {
scroll-behavior: smooth;
}
/* Benutzerdefinierter Scrollbar für den Chat */
#embedded-chat-messages::-webkit-scrollbar {
width: 6px;
}
#embedded-chat-messages::-webkit-scrollbar-track {
background-color: rgba(0, 0, 0, 0.05);
border-radius: 10px;
}
#embedded-chat-messages::-webkit-scrollbar-thumb {
background-color: rgba(139, 92, 246, 0.3);
border-radius: 10px;
}
.dark #embedded-chat-messages::-webkit-scrollbar-thumb {
background-color: rgba(139, 92, 246, 0.5);
}
/* Hover-Effekt für Quick-Query-Buttons */
.quick-query-btn:hover {
cursor: pointer;
background: linear-gradient(to right, rgba(139, 92, 246, 0.1), rgba(96, 165, 250, 0.1));
}
.dark .quick-query-btn:hover {
background: linear-gradient(to right, rgba(139, 92, 246, 0.2), rgba(96, 165, 250, 0.2));
}
</style>
{% endblock %}
{% block content %}
<!-- Hintergrund für die gesamte Seite -->
<div class="full-page-bg gradient-background"></div>
<!-- Hero Section -->
<section class="relative pt-20 pb-32">
<!-- Hero Content -->
<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">
<span class="gradient-text inline-block transform transition-all duration-700 hover:scale-105">Wissen</span> neu
<div class="mt-2 relative">
<span class="relative inline-block">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>
<p class="text-xl md:text-2xl text-gray-700 dark:text-gray-300 max-w-3xl mx-auto mb-12">
Erkunde komplexe Ideen visuell, schaffe Verbindungen und teile deine Gedanken
in einem interaktiven Wissensnetzwerk.
</p>
<div class="flex flex-col sm:flex-row gap-5 justify-center">
<a href="{{ url_for('mindmap') }}" class="group transition-all duration-300 bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-700 hover:to-indigo-700 text-white font-medium text-lg px-8 py-4 rounded-2xl shadow-lg hover:shadow-xl hover:shadow-purple-500/20 transform hover:-translate-y-1">
<span class="flex items-center justify-center">
<i class="fa-solid fa-diagram-project mr-3 text-purple-200 group-hover:text-white transition-all duration-300 animate-pulse"></i>
<span class="relative">
Mindmap erkunden
<span class="absolute -bottom-1 left-0 w-0 h-0.5 bg-white group-hover:w-full transition-all duration-300"></span>
</span>
</span>
</a>
{% if not current_user.is_authenticated %}
<a href="{{ url_for('register') }}" class="group transition-all duration-300 bg-gradient-to-r from-blue-500 to-cyan-500 hover:from-blue-600 hover:to-cyan-600 text-white font-medium text-lg px-8 py-4 rounded-2xl shadow-lg hover:shadow-xl hover:shadow-blue-500/20 transform hover:-translate-y-1">
<span class="flex items-center justify-center">
<i class="fa-solid fa-user-plus mr-3 text-blue-200 group-hover:text-white transition-all duration-300 icon-pulse"></i>
<span class="relative">
Konto erstellen
<span class="absolute -bottom-1 left-0 w-0 h-0.5 bg-white group-hover:w-full transition-all duration-300"></span>
</span>
</span>
</a>
{% endif %}
</div>
</div>
<!-- Tech illustration -->
<div class="relative w-full max-w-4xl mx-auto h-80 sm:h-96">
<div class="absolute inset-0 flex items-center justify-center">
<div class="hidden md:block 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>
</div>
<!-- Network Visualization with SVG -->
<svg class="absolute inset-0 w-full h-full" viewBox="0 0 800 600" preserveAspectRatio="xMidYMid meet">
<!-- Glossy Nodes and Lines -->
<defs>
<radialGradient id="nodeGradient" cx="50%" cy="50%" r="50%" fx="50%" fy="50%">
<stop offset="0%" stop-color="rgba(139, 92, 246, 0.9)" />
<stop offset="100%" stop-color="rgba(79, 70, 229, 0.5)" />
</radialGradient>
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="5" result="blur" />
<feComposite in="SourceGraphic" in2="blur" operator="over" />
</filter>
</defs>
<!-- Network Lines -->
<g class="lines">
<!-- Connection network -->
<line x1="200" y1="250" x2="400" y2="150" stroke="rgba(0,0,0,0.1)" stroke-width="1" class="dark:hidden" />
<line x1="400" y1="150" x2="600" y2="250" stroke="rgba(0,0,0,0.1)" stroke-width="1" class="dark:hidden" />
<line x1="600" y1="250" x2="400" y2="350" stroke="rgba(0,0,0,0.1)" stroke-width="1" class="dark:hidden" />
<line x1="400" y1="350" x2="200" y2="250" stroke="rgba(0,0,0,0.1)" stroke-width="1" class="dark:hidden" />
<line x1="400" y1="150" x2="400" y2="350" stroke="rgba(0,0,0,0.1)" stroke-width="1" class="dark:hidden" />
<line x1="200" y1="250" x2="600" y2="250" stroke="rgba(0,0,0,0.1)" stroke-width="1" class="dark:hidden" />
<!-- Dark mode connections -->
<line x1="200" y1="250" x2="400" y2="150" stroke="rgba(255,255,255,0.1)" stroke-width="1" class="hidden dark:inline" />
<line x1="400" y1="150" x2="600" y2="250" stroke="rgba(255,255,255,0.1)" stroke-width="1" class="hidden dark:inline" />
<line x1="600" y1="250" x2="400" y2="350" stroke="rgba(255,255,255,0.1)" stroke-width="1" class="hidden dark:inline" />
<line x1="400" y1="350" x2="200" y2="250" stroke="rgba(255,255,255,0.1)" stroke-width="1" class="hidden dark:inline" />
<line x1="400" y1="150" x2="400" y2="350" stroke="rgba(255,255,255,0.1)" stroke-width="1" class="hidden dark:inline" />
<line x1="200" y1="250" x2="600" y2="250" stroke="rgba(255,255,255,0.1)" stroke-width="1" class="hidden dark:inline" />
<!-- Pulse animation for some lines -->
<line class="animate-pulse" x1="400" y1="150" x2="300" y2="200" stroke="rgba(139, 92, 246, 0.5)" stroke-width="2" />
<line class="animate-pulse" x1="400" y1="350" x2="500" y2="300" stroke="rgba(168, 85, 247, 0.5)" stroke-width="2" />
</g>
<!-- Network Nodes -->
<g class="nodes">
<circle cx="400" cy="150" r="15" fill="url(#nodeGradient)" filter="url(#glow)" class="animate-pulse float-animation" />
<circle cx="200" cy="250" r="10" fill="url(#nodeGradient)" class="float-animation" />
<circle cx="600" cy="250" r="10" fill="url(#nodeGradient)" class="float-animation" />
<circle cx="400" cy="350" r="15" fill="url(#nodeGradient)" filter="url(#glow)" class="animate-pulse float-animation" />
<circle cx="300" cy="200" r="8" fill="url(#nodeGradient)" class="float-animation" />
<circle cx="500" cy="300" r="8" fill="url(#nodeGradient)" class="float-animation" />
</g>
</svg>
</div>
</div>
</div>
</section>
<!-- Features Section -->
<section class="py-20 relative">
<div class="tech-line absolute top-0 left-1/2 transform -translate-x-1/2 w-1/3"></div>
<div class="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-16">
<h2 class="section-heading mb-4 text-gray-900 dark:text-white">Was ist <span class="gradient-text">Systades?</span></h2>
<p class="text-lg text-gray-700 dark:text-gray-300 max-w-3xl mx-auto">
Ein modernes Werkzeug zum Visualisieren, Erforschen und Teilen von Wissen
in einer intuitiven, interaktiven Umgebung.
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
<!-- Feature Card 1 -->
<div class="feature-card p-8 rounded-3xl hover:-translate-y-3 transform transition-all duration-300">
<div class="icon mb-6 rounded-2xl shadow-lg">
<i class="fa-solid fa-brain"></i>
</div>
<h3 class="text-xl font-semibold mb-3">Visualisiere Wissen</h3>
<p>
Sieh Wissen als vernetztes System, entdecke Zusammenhänge und erkenne überraschende
Verbindungen zwischen verschiedenen Themengebieten.
</p>
</div>
<!-- Feature Card 2 -->
<div class="feature-card p-8 rounded-3xl hover:-translate-y-3 transform transition-all duration-300">
<div class="icon mb-6 rounded-2xl shadow-lg">
<i class="fa-solid fa-lightbulb"></i>
</div>
<h3 class="text-xl font-semibold mb-3">Teile Gedanken</h3>
<p>
Füge deine eigenen Ideen und Perspektiven hinzu. Erstelle Verbindungen zu
vorhandenen Gedanken und bereichere die wachsende Wissensbasis.
</p>
</div>
<!-- Feature Card 3 -->
<div class="feature-card p-8 rounded-3xl hover:-translate-y-3 transform transition-all duration-300">
<div class="icon mb-6 rounded-2xl shadow-lg">
<i class="fa-solid fa-users"></i>
</div>
<h3 class="text-xl font-semibold mb-3">Community</h3>
<p>
Sei Teil einer Gemeinschaft, die gemeinsam ein verteiltes Wissensarchiv aufbaut
und sich in thematisch fokussierten Bereichen austauscht.
</p>
</div>
<!-- Feature Card 4 -->
<div class="feature-card p-8 rounded-3xl hover:-translate-y-3 transform transition-all duration-300">
<div class="icon mb-6 rounded-2xl shadow-lg">
<i class="fa-solid fa-robot"></i>
</div>
<h3 class="text-xl font-semibold mb-3">KI-Assistenz</h3>
<p>
Lass dir von künstlicher Intelligenz helfen, neue Zusammenhänge zu entdecken,
Inhalte zusammenzufassen und Fragen zu beantworten.
</p>
</div>
<!-- Feature Card 5 -->
<div class="feature-card p-8 rounded-3xl hover:-translate-y-3 transform transition-all duration-300">
<div class="icon mb-6 rounded-2xl shadow-lg">
<i class="fa-solid fa-search"></i>
</div>
<h3 class="text-xl font-semibold mb-3">Intelligente Suche</h3>
<p>
Finde genau die Informationen, die du suchst, mit fortschrittlichen Such- und
Filterfunktionen für eine präzise Navigation durch das Wissen.
</p>
</div>
<!-- Feature Card 6 -->
<div class="feature-card p-8 rounded-3xl hover:-translate-y-3 transform transition-all duration-300">
<div class="icon mb-6 rounded-2xl shadow-lg">
<i class="fa-solid fa-route"></i>
</div>
<h3 class="text-xl font-semibold mb-3">Geführte Pfade</h3>
<p>
Folge kuratierten Lernpfaden durch komplexe Themen oder erschaffe selbst
Routen für andere, die deinen Gedankengängen folgen möchten.
</p>
</div>
</div>
</div>
</section>
<!-- Call to Action Section -->
<section class="py-16 sm:py-20 md:py-24 relative overflow-hidden">
<div class="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
<div class="glass-effect p-6 sm:p-8 md:p-12 rounded-3xl transform transition-all duration-500 hover:-translate-y-2 hover:shadow-2xl bg-gradient-to-br from-purple-500/15 to-blue-500/15 backdrop-blur-xl border border-white/10 shadow-lg">
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-6">
<div class="md:w-2/3">
<h2 class="text-2xl sm:text-3xl lg:text-4xl font-bold mb-3 text-gray-900 dark:text-white leading-tight">
Bereit, <span class="gradient-text bg-clip-text text-transparent bg-gradient-to-r from-purple-500 to-blue-500">Wissen</span> neu zu entdecken?
</h2>
<p class="text-gray-700 dark:text-gray-300 text-base sm:text-lg mb-6 md:mb-0 max-w-2xl">
Starte jetzt deine Reise durch das Wissensnetzwerk und erschließe neue Perspektiven.
</p>
</div>
<div class="md:w-1/3 text-center md:text-right">
<a href="{{ url_for('mindmap') }}" class="inline-flex items-center justify-center w-full md:w-auto btn-primary font-bold py-3 sm:py-3.5 px-6 sm:px-8 rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1 hover:scale-105 bg-gradient-to-r from-purple-600 to-blue-600 text-white">
<span class="flex items-center justify-center">
<i class="fa-solid fa-arrow-right mr-2"></i>
<span>Zur Mindmap</span>
</span>
</a>
</div>
</div>
</div>
</div>
</section>
<!-- Quick Access Section -->
<section class="py-16 sm:py-20">
<div class="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 lg:gap-8">
<!-- Themen-Übersicht -->
<div class="glass-morphism p-6 sm:p-8 rounded-3xl transition-all duration-500 hover:-translate-y-3 hover:shadow-xl border border-white/10 backdrop-blur-md">
<h3 class="text-xl font-bold mb-4 flex items-center text-gray-800 dark:text-white">
<div class="w-10 h-10 sm:w-12 sm:h-12 rounded-2xl bg-gradient-to-r from-violet-500 to-fuchsia-500 flex items-center justify-center mr-3 sm:mr-4 shadow-md transform transition-transform duration-300 hover:scale-110">
<i class="fa-solid fa-fire text-white text-base sm:text-lg"></i>
</div>
<span class="text-lg sm:text-xl md:text-2xl">Themen-Übersicht</span>
</h3>
<div class="space-y-3 sm:space-y-4 mb-6">
<a href="{{ url_for('mindmap') }}" class="flex items-center p-3 sm:p-3.5 rounded-xl hover:bg-gray-100/50 dark:hover:bg-white/5 transition-all duration-200 group">
<div class="w-3 h-3 rounded-full bg-purple-400 mr-3 group-hover:scale-125 transition-transform"></div>
<div class="flex-grow">
<p class="font-medium text-gray-800 dark:text-gray-200">Wissensbereiche <span class="text-xs text-gray-500">(12)</span></p>
<p class="text-xs sm:text-sm text-gray-500 dark:text-gray-400">Überblick über Themenbereiche</p>
</div>
<i class="fa-solid fa-chevron-right text-gray-500 group-hover:translate-x-1 transition-transform"></i>
</a>
<a href="{{ url_for('search_thoughts_page') }}" class="flex items-center p-3 sm:p-3.5 rounded-xl hover:bg-gray-100/50 dark:hover:bg-white/5 transition-all duration-200 group">
<div class="w-3 h-3 rounded-full bg-blue-400 mr-3 group-hover:scale-125 transition-transform"></div>
<div class="flex-grow">
<p class="font-medium text-gray-800 dark:text-gray-200">Gedanken <span class="text-xs text-gray-500">(87)</span></p>
<p class="text-xs sm:text-sm text-gray-500 dark:text-gray-400">Konkrete Einträge durchsuchen</p>
</div>
<i class="fa-solid fa-chevron-right text-gray-500 group-hover:translate-x-1 transition-transform"></i>
</a>
<a href="#" class="flex items-center p-3 sm:p-3.5 rounded-xl hover:bg-gray-100/50 dark:hover:bg-white/5 transition-all duration-200 group">
<div class="w-3 h-3 rounded-full bg-green-400 mr-3 group-hover:scale-125 transition-transform"></div>
<div class="flex-grow">
<p class="font-medium text-gray-800 dark:text-gray-200">Verbindungen <span class="text-xs text-gray-500">(34)</span></p>
<p class="text-xs sm:text-sm text-gray-500 dark:text-gray-400">Beziehungen zwischen Gedanken</p>
</div>
<i class="fa-solid fa-chevron-right text-gray-500 group-hover:translate-x-1 transition-transform"></i>
</a>
</div>
<a href="{{ url_for('search_thoughts_page') }}" class="btn-primary w-full text-center rounded-xl py-3 sm:py-3.5 transform transition-all duration-300 hover:-translate-y-1 hover:shadow-lg flex items-center justify-center">
<span>Alle Themen entdecken</span>
<i class="fa-solid fa-arrow-right ml-2"></i>
</a>
</div>
<!-- KI-Assistent mit eingebettetem Chat -->
<div class="glass-morphism p-6 sm:p-8 rounded-3xl transition-all duration-500 hover:-translate-y-1 hover:shadow-xl backdrop-blur-md border border-white/10">
<h3 class="text-xl md:text-2xl font-bold mb-4 flex flex-wrap sm:flex-nowrap items-center text-gray-800 dark:text-white">
<div class="w-10 h-10 sm:w-12 sm:h-12 rounded-2xl bg-gradient-to-r from-purple-600 to-blue-600 flex items-center justify-center mr-3 sm:mr-4 shadow-lg transform transition-transform duration-300 hover:scale-110">
<i class="fa-solid fa-robot text-white text-base sm:text-lg"></i>
</div>
<span class="mt-1 sm:mt-0">KI-Assistent</span>
</h3>
<!-- Eingebettetes Chat-Interface -->
<div id="embedded-assistant" class="rounded-xl border border-gray-200/50 dark:border-gray-700/50 overflow-hidden flex flex-col h-[300px]">
<!-- Chat Verlauf -->
<div id="embedded-chat-messages" class="flex-grow p-4 overflow-y-auto space-y-3 bg-white/70 dark:bg-gray-800/70">
<!-- Begrüßungsnachricht -->
<div class="flex items-start space-x-2">
<div class="w-8 h-8 rounded-full bg-gradient-to-r from-purple-600 to-blue-600 flex items-center justify-center flex-shrink-0">
<i class="fa-solid fa-robot text-white text-xs"></i>
</div>
<div class="max-w-[85%] bg-purple-100 dark:bg-gray-700 p-3 rounded-xl rounded-tl-none shadow-sm">
<p class="text-sm text-gray-700 dark:text-gray-200">Hallo! Ich bin dein KI-Assistent. Wie kann ich dir helfen?</p>
</div>
</div>
</div>
<!-- Chat Eingabe -->
<div class="p-3 border-t border-gray-200/70 dark:border-gray-700/70 bg-gray-50/90 dark:bg-gray-800/90">
<form id="embedded-chat-form" class="flex items-center space-x-2">
<input type="text" id="embedded-chat-input"
placeholder="Stelle eine Frage..."
class="flex-grow px-4 py-2 rounded-xl border bg-white/90 dark:bg-gray-700/90 border-gray-300 dark:border-gray-600 shadow-sm focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-all duration-200 placeholder-gray-400 dark:placeholder-gray-500 text-gray-700 dark:text-gray-200">
<button type="submit"
class="p-2 rounded-xl bg-gradient-to-r from-purple-600 to-blue-600 text-white shadow-md hover:shadow-lg transition-all duration-200 hover:-translate-y-0.5">
<i class="fa-solid fa-paper-plane"></i>
</button>
</form>
</div>
</div>
<!-- Schnelllinks unter dem Chat -->
<div class="mt-4 flex flex-wrap gap-2">
<button class="quick-query-btn px-2 sm:px-3 py-1 sm:py-1.5 bg-gray-100 hover:bg-gray-200 dark:bg-gray-800/70 dark:hover:bg-gray-700/80 rounded-lg sm:rounded-xl text-xs text-gray-700 dark:text-gray-300 transition-all duration-200 hover:-translate-y-0.5 shadow-sm hover:shadow">
Was ist Systades?
</button>
<button class="quick-query-btn px-2 sm:px-3 py-1 sm:py-1.5 bg-gray-100 hover:bg-gray-200 dark:bg-gray-800/70 dark:hover:bg-gray-700/80 rounded-lg sm:rounded-xl text-xs text-gray-700 dark:text-gray-300 transition-all duration-200 hover:-translate-y-0.5 shadow-sm hover:shadow">
Wie erstelle ich eine Mindmap?
</button>
<button class="quick-query-btn px-2 sm:px-3 py-1 sm:py-1.5 bg-gray-100 hover:bg-gray-200 dark:bg-gray-800/70 dark:hover:bg-gray-700/80 rounded-lg sm:rounded-xl text-xs text-gray-700 dark:text-gray-300 transition-all duration-200 hover:-translate-y-0.5 shadow-sm hover:shadow">
Zeige neueste Gedanken
</button>
</div>
<!-- Vollständigen KI-Chat öffnen -->
<button onclick="window.MindMap.assistant.toggleAssistant(true)" class="mt-4 btn-primary w-full text-center rounded-xl py-2 sm:py-2.5 shadow-md hover:shadow-lg transition-all duration-300 hover:-translate-y-1 flex items-center justify-center">
<i class="fa-solid fa-expand mr-2"></i>
<span>Chat in Vollansicht öffnen</span>
</button>
</div>
</div>
</div>
</section>
{% endblock %}
<!-- JavaScript für eingebetteten Chat -->
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Warten bis MindMap und der Assistent initialisiert sind
const waitForAssistant = setInterval(() => {
if (window.MindMap && window.MindMap.assistant) {
clearInterval(waitForAssistant);
initEmbeddedChat();
}
}, 200);
function initEmbeddedChat() {
const chatForm = document.getElementById('embedded-chat-form');
const chatInput = document.getElementById('embedded-chat-input');
const messagesContainer = document.getElementById('embedded-chat-messages');
const quickQueryBtns = document.querySelectorAll('.quick-query-btn');
// Event-Listener für das Chat-Formular
chatForm.addEventListener('submit', function(e) {
e.preventDefault();
const userMessage = chatInput.value.trim();
if (!userMessage) return;
// Nachricht des Benutzers anzeigen
appendMessage('user', userMessage);
chatInput.value = '';
// Anzeigen, dass der Assistent antwortet
const typingIndicator = appendTypingIndicator();
// API-Anfrage an den Assistenten senden
sendToAssistant(userMessage)
.then(response => {
// Entferne Tipp-Indikator
typingIndicator.remove();
// Zeige Antwort des Assistenten an
appendMessage('assistant', response);
})
.catch(error => {
typingIndicator.remove();
appendMessage('assistant', 'Es tut mir leid, ich konnte deine Nachricht nicht verarbeiten. Bitte versuche es später noch einmal.');
console.error('Fehler bei der Kommunikation mit dem Assistenten:', error);
});
});
// Schnellabfragen-Buttons
quickQueryBtns.forEach(btn => {
btn.addEventListener('click', function() {
const query = this.textContent.trim();
chatInput.value = query;
chatForm.dispatchEvent(new Event('submit'));
});
});
// Funktion zum Hinzufügen einer Nachricht zum Chat
function appendMessage(sender, message) {
const messageElement = document.createElement('div');
messageElement.className = 'flex items-start space-x-2';
if (sender === 'user') {
messageElement.innerHTML = `
<div class="flex-grow"></div>
<div class="max-w-[85%] bg-blue-100 dark:bg-blue-900/40 p-3 rounded-xl rounded-tr-none shadow-sm">
<p class="text-sm text-gray-700 dark:text-gray-200">${message}</p>
</div>
<div class="w-8 h-8 rounded-full bg-gradient-to-r from-blue-500 to-indigo-500 flex items-center justify-center flex-shrink-0">
<i class="fa-solid fa-user text-white text-xs"></i>
</div>
`;
} else {
messageElement.innerHTML = `
<div class="w-8 h-8 rounded-full bg-gradient-to-r from-purple-600 to-blue-600 flex items-center justify-center flex-shrink-0">
<i class="fa-solid fa-robot text-white text-xs"></i>
</div>
<div class="max-w-[85%] bg-purple-100 dark:bg-gray-700 p-3 rounded-xl rounded-tl-none shadow-sm">
<p class="text-sm text-gray-700 dark:text-gray-200">${message}</p>
</div>
`;
}
messagesContainer.appendChild(messageElement);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
// Tipp-Indikator für "Assistent schreibt..."
function appendTypingIndicator() {
const indicatorElement = document.createElement('div');
indicatorElement.className = 'flex items-start space-x-2 typing-indicator';
indicatorElement.innerHTML = `
<div class="w-8 h-8 rounded-full bg-gradient-to-r from-purple-600 to-blue-600 flex items-center justify-center flex-shrink-0">
<i class="fa-solid fa-robot text-white text-xs"></i>
</div>
<div class="max-w-[85%] bg-purple-100 dark:bg-gray-700 p-3 rounded-xl rounded-tl-none shadow-sm">
<p class="text-sm text-gray-500 dark:text-gray-400 flex items-center">
<span class="mr-1">Tipp</span>
<span class="typing-dots flex space-x-1">
<span class="w-1.5 h-1.5 bg-gray-500 dark:bg-gray-400 rounded-full animate-bounce" style="animation-delay: 0ms;"></span>
<span class="w-1.5 h-1.5 bg-gray-500 dark:bg-gray-400 rounded-full animate-bounce" style="animation-delay: 150ms;"></span>
<span class="w-1.5 h-1.5 bg-gray-500 dark:bg-gray-400 rounded-full animate-bounce" style="animation-delay: 300ms;"></span>
</span>
</p>
</div>
`;
messagesContainer.appendChild(indicatorElement);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
return indicatorElement;
}
// Sende Nachricht an den Assistenten und erhalte Antwort
async function sendToAssistant(message) {
try {
const response = await fetch('/api/assistant', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
messages: [
{ role: "system", content: "Du bist ein hilfreicher Assistent für das Wissensnetzwerk Systades." },
{ role: "user", content: message }
]
})
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Unbekannter Fehler');
}
return data.response || data.answer || 'Ich habe keine Antwort erhalten.';
} catch (error) {
console.error('Fehler bei der API-Anfrage:', error);
throw error;
}
}
}
});
</script>
{% endblock %}

55
templates/login.html Normal file
View File

@@ -0,0 +1,55 @@
{% extends "base.html" %}
{% block title %}Anmelden{% endblock %}
{% block content %}
<div class="flex justify-center items-center min-h-screen px-4 py-12">
<div class="w-full max-w-md">
<div class="bg-white bg-opacity-20 backdrop-blur-lg rounded-xl shadow-xl overflow-hidden transition-all duration-300 hover:shadow-2xl">
<div class="p-6 sm:p-8">
<h2 class="text-center text-2xl font-bold text-gray-800 mb-6">
<i class="fas fa-sign-in-alt mr-2"></i>
Anmelden
</h2>
<form method="POST" action="{{ url_for('login') }}" class="space-y-6">
<div class="space-y-2">
<label for="username" class="block text-sm font-medium text-gray-700">Benutzername</label>
<div class="relative rounded-md shadow-sm">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fas fa-user text-gray-400"></i>
</div>
<input type="text" id="username" name="username" required
class="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
placeholder="Benutzername eingeben">
</div>
</div>
<div class="space-y-2">
<label for="password" class="block text-sm font-medium text-gray-700">Passwort</label>
<div class="relative rounded-md shadow-sm">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fas fa-lock text-gray-400"></i>
</div>
<input type="password" id="password" name="password" required
class="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
placeholder="Passwort eingeben">
</div>
</div>
<div>
<button type="submit"
class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors duration-200">
<i class="fas fa-sign-in-alt mr-2"></i> Anmelden
</button>
</div>
<div class="text-center text-sm text-gray-600">
<p>Noch kein Konto? <a href="{{ url_for('register') }}" class="font-medium text-blue-600 hover:text-blue-500 transition-colors">Registrieren</a></p>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -516,9 +516,7 @@
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
background-image: url('/static/network-bg.jpg'); background-image: none;
background-size: cover;
background-position: center;
opacity: 0.2; opacity: 0.2;
z-index: -1; z-index: -1;
animation: pulse 10s ease-in-out infinite alternate; animation: pulse 10s ease-in-out infinite alternate;
@@ -579,13 +577,38 @@
<!-- Mindmap-Container - Jetzt größer --> <!-- Mindmap-Container - Jetzt größer -->
<div class="glass-card overflow-hidden mb-12"> <div class="glass-card overflow-hidden mb-12">
<div id="mindmap-container" class="relative" style="height: 80vh; min-height: 700px;"> <div id="mindmap-container" class="relative" style="height: 80vh; min-height: 700px;">
<!-- Lade-Overlay --> <!-- SVG Filters for node effects -->
<div class="mindmap-loading absolute inset-0 flex items-center justify-center z-10" style="background: rgba(14, 18, 32, 0.7); backdrop-filter: blur(5px);"> <svg width="0" height="0" style="position: absolute;">
<defs>
<!-- Glasmorphismus-Effekt für Knoten -->
<filter id="glass-effect" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur in="SourceAlpha" stdDeviation="4" result="blur" />
<feColorMatrix in="blur" mode="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 18 -7" result="glow" />
<feBlend in="SourceGraphic" in2="glow" mode="normal" />
</filter>
<!-- Hover-Glow-Effekt -->
<filter id="hover-glow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur in="SourceAlpha" stdDeviation="5" result="blur" />
<feColorMatrix in="blur" mode="matrix" values="1 0 0 0 0.5 0 1 0 0 0.5 0 0 1 0 1 0 0 0 18 -7" result="glow" />
<feBlend in="SourceGraphic" in2="glow" mode="normal" />
</filter>
<!-- Ausgewählter-Knoten-Glow-Effekt -->
<filter id="selected-glow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur in="SourceAlpha" stdDeviation="8" result="blur" />
<feColorMatrix in="blur" mode="matrix" values="1 0 0 0 0.7 0 1 0 0 0.2 0 0 1 0 1 0 0 0 18 -7" result="glow" />
<feBlend in="SourceGraphic" in2="glow" mode="normal" />
</filter>
</defs>
</svg>
<!-- Lade-Overlay mit verbesserter Animation und Transition -->
<div class="mindmap-loading absolute inset-0 flex items-center justify-center z-10" style="background: rgba(14, 18, 32, 0.8); backdrop-filter: blur(10px); transition: opacity 0.5s ease-in-out;">
<div class="text-center"> <div class="text-center">
<div class="inline-block animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-purple-500 mb-4"></div> <div class="inline-block animate-spin rounded-full h-16 w-16 border-t-3 border-b-3 border-purple-500 mb-6"></div>
<p class="text-white text-lg">Wissenslandschaft wird geladen...</p> <p class="text-white text-xl font-semibold mb-3">Wissenslandschaft wird geladen...</p>
<div class="w-64 h-2 bg-gray-700 rounded-full mt-4 overflow-hidden"> <p class="text-gray-300 text-sm mb-4">Daten werden aus der Datenbank abgerufen</p>
<div class="loading-progress h-full bg-gradient-to-r from-purple-500 to-blue-500 rounded-full" style="width: 0%"></div> <div class="w-80 h-3 bg-gray-800 rounded-full mt-2 overflow-hidden">
<div class="loading-progress h-full bg-gradient-to-r from-purple-500 via-blue-500 to-purple-500 rounded-full" style="width: 0%; transition: width 0.3s ease-in-out;"></div>
</div> </div>
</div> </div>
</div> </div>
@@ -648,240 +671,381 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Modal zum Hinzufügen eines neuen Gedanken -->
<div id="add-thought-modal" class="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50 hidden" style="backdrop-filter: blur(5px);">
<div class="glass-card w-full max-w-md p-6">
<div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-bold text-white">Neuen Gedanken hinzufügen</h3>
<button class="text-gray-400 hover:text-white" onclick="document.getElementById('add-thought-modal').classList.add('hidden')">
<i class="fas fa-times"></i>
</button>
</div>
<form id="add-thought-form" method="POST">
<input type="hidden" id="thought-node-id" name="node_id">
<div class="mb-4">
<label class="block text-gray-300 mb-2" for="thought-title">Titel</label>
<input class="w-full bg-gray-800 text-white border border-gray-700 rounded-lg py-2 px-3" id="thought-title" name="title" placeholder="Titel des Gedankens" required>
</div>
<div class="mb-4">
<label class="block text-gray-300 mb-2" for="thought-content">Inhalt</label>
<textarea class="w-full bg-gray-800 text-white border border-gray-700 rounded-lg py-2 px-3 h-32" id="thought-content" name="content" placeholder="Beschreibe deinen Gedanken..." required></textarea>
</div>
<div class="flex justify-end space-x-3">
<button type="button" class="py-2 px-4 bg-gray-700 text-white rounded-lg" onclick="document.getElementById('add-thought-modal').classList.add('hidden')">Abbrechen</button>
<button type="submit" class="py-2 px-4 bg-gradient-to-r from-purple-600 to-blue-500 text-white rounded-lg">Speichern</button>
</div>
</form>
</div>
</div>
</div> </div>
{% endblock %} {% endblock %}
{% block extra_js %} {% block scripts %}
<!-- D3.js für die Mindmap-Visualisierung --> <!-- D3.js Library -->
<script src="https://d3js.org/d3.v7.min.js"></script> <script src="https://d3js.org/d3.v7.min.js"></script>
<!-- Tippy.js für verbesserte Tooltips -->
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.6/dist/umd/popper.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/tippy.js@6.3.7/dist/tippy.umd.min.js"></script>
<!-- D3-Erweiterungen für spezifische Effekte -->
<script src="{{ url_for('static', filename='d3-extensions.js') }}"></script>
<!-- Mindmap JS -->
<script src="{{ url_for('static', filename='mindmap.js') }}"></script>
<script src="{{ url_for('static', filename='network-animation.js') }}"></script>
<!-- Tippy.js für Tooltips -->
<script src="https://unpkg.com/@popperjs/core@2"></script>
<script src="https://unpkg.com/tippy.js@6"></script>
<!-- Mindmap scripts -->
<script src="{{ url_for('static', filename='js/modules/mindmap.js') }}"></script>
<!-- Initialization Script -->
<script> <script>
// Dynamische Neuronen-Netz-Animation im Hintergrund
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Animation des neuronalen Netzwerks hinzufügen // Mindmap-Container holen
const neuralBg = document.querySelector('.neural-universe-bg');
// Neuronenpunkte erstellen
const neuronCount = 100;
for (let i = 0; i < neuronCount; i++) {
const neuron = document.createElement('div');
neuron.className = 'neuron-point';
// Zufällige Position
const posX = Math.random() * 100;
const posY = Math.random() * 100;
const size = Math.random() * 3 + 1;
const animDuration = Math.random() * 50 + 20;
// Styling mit Glasmorphismus
neuron.style.cssText = `
position: absolute;
left: ${posX}%;
top: ${posY}%;
width: ${size}px;
height: ${size}px;
background: rgba(255, 255, 255, ${Math.random() * 0.3 + 0.1});
border-radius: 50%;
filter: blur(${Math.random() * 1}px);
box-shadow: 0 0 ${Math.random() * 10 + 5}px rgba(179, 143, 255, 0.5);
animation: pulse ${animDuration}s infinite alternate ease-in-out;
`;
neuralBg.appendChild(neuron);
}
// Verbindungen zwischen Neuronen erstellen
const connectionCount = 40;
for (let i = 0; i < connectionCount; i++) {
const connection = document.createElement('div');
connection.className = 'neuron-connection';
// Zufällige Position und Rotation für Verbindungen
const posX = Math.random() * 100;
const posY = Math.random() * 100;
const width = Math.random() * 150 + 50;
const height = Math.random() * 1 + 0.5;
const rotation = Math.random() * 360;
const opacity = Math.random() * 0.2 + 0.05;
const animDuration = Math.random() * 20 + 10;
connection.style.cssText = `
position: absolute;
left: ${posX}%;
top: ${posY}%;
width: ${width}px;
height: ${height}px;
background: linear-gradient(90deg, transparent, rgba(179, 143, 255, ${opacity}), transparent);
transform: rotate(${rotation}deg);
animation: flash ${animDuration}s infinite alternate ease-in-out;
opacity: ${opacity};
`;
neuralBg.appendChild(connection);
}
// Initialisiere die Mindmap-Visualisierung
const mindmapContainer = document.getElementById('mindmap-container'); const mindmapContainer = document.getElementById('mindmap-container');
const containerWidth = mindmapContainer.clientWidth;
const containerHeight = mindmapContainer.clientHeight;
const mindmap = new MindMapVisualization('#mindmap-container', { // Options für die Visualisierung
width: containerWidth, const options = {
height: containerHeight, width: mindmapContainer.clientWidth,
height: mindmapContainer.clientHeight,
nodeRadius: 22, nodeRadius: 22,
selectedNodeRadius: 28, selectedNodeRadius: 28,
linkDistance: 160, linkDistance: 150,
chargeStrength: -1200, chargeStrength: -1000,
centerForce: 0.1, centerForce: 0.15,
tooltipEnabled: true, tooltipEnabled: true,
onNodeClick: function(node) { onNodeClick: function(node) {
console.log('Node clicked:', node); console.log('Node clicked:', node);
// Hier können spezifische Aktionen für Knotenklicks definiert werden
// Gedanken zu diesem Knoten laden
fetch(`/api/nodes/${node.id}/thoughts`)
.then(response => {
if (!response.ok) {
throw new Error('Netzwerkantwort war nicht ok');
}
return response.json();
})
.then(data => {
console.log('Gedanken zu diesem Knoten:', data);
// Gedanken im Seitenbereich anzeigen
const thoughtsContainer = document.getElementById('thoughts-container');
if (thoughtsContainer) {
thoughtsContainer.innerHTML = '';
if (data.thoughts && data.thoughts.length > 0) {
data.thoughts.forEach(thought => {
const thoughtElement = document.createElement('div');
thoughtElement.className = 'thought-item bg-gray-800 rounded-lg p-4 mb-3';
thoughtElement.innerHTML = `
<h3 class="text-lg font-semibold text-white">${thought.title}</h3>
<p class="text-gray-300 mt-2">${thought.content}</p>
<div class="flex justify-between mt-3">
<span class="text-sm text-gray-400">${new Date(thought.created_at).toLocaleDateString('de-DE')}</span>
<button class="text-blue-400 hover:text-blue-300" data-thought-id="${thought.id}">
<i class="fas fa-bookmark"></i>
</button>
</div>
`;
thoughtsContainer.appendChild(thoughtElement);
});
} else {
thoughtsContainer.innerHTML = '<p class="text-gray-400">Keine Gedanken für diesen Knoten vorhanden.</p>';
}
}
// Aktualisiere das Formular zum Hinzufügen von Gedanken
document.getElementById('thought-node-id').value = node.id;
})
.catch(error => {
console.error('Fehler beim Laden der Gedanken:', error);
// Benutzer über den Fehler informieren
if (window.mindmap && window.mindmap.showFlash) {
window.mindmap.showFlash('Fehler beim Laden der Gedanken', 'error');
} }
}); });
}
};
// Event-Listener für Steuerungsbuttons // Ladebalken-Animation starten
const progressBar = document.querySelector('.loading-progress');
let progress = 0;
const loadingInterval = setInterval(() => {
progress += 5;
if (progress > 100) progress = 100;
progressBar.style.width = `${progress}%`;
if (progress === 100) {
clearInterval(loadingInterval);
}
}, 150);
// Mindmap erstellen und initialisieren
window.mindmap = new MindMapVisualization('#mindmap-container', options);
// API-Aufruf, um echte Daten zu laden
fetch('/api/mindmap')
.then(response => {
if (!response.ok) {
throw new Error('Netzwerkantwort war nicht ok');
}
return response.json();
})
.then(data => {
// Ladebalken auf 100% setzen
progressBar.style.width = '100%';
// Lade-Overlay nach kurzer Verzögerung ausblenden
setTimeout(() => {
const loadingOverlay = document.querySelector('.mindmap-loading');
if (loadingOverlay) {
loadingOverlay.style.opacity = '0';
setTimeout(() => {
loadingOverlay.style.display = 'none';
}, 500);
}
}, 500);
console.log('Mindmap-Daten geladen:', data);
})
.catch(error => {
console.error('Fehler beim Laden der Mindmap-Daten:', error);
// Fehlerbehandlung: Zeige trotzdem die Standard-Mindmap
progressBar.style.width = '100%';
setTimeout(() => {
const loadingOverlay = document.querySelector('.mindmap-loading');
if (loadingOverlay) {
loadingOverlay.style.opacity = '0';
setTimeout(() => {
loadingOverlay.style.display = 'none';
}, 500);
}
}, 500);
});
// UI Event-Handler einrichten
document.getElementById('zoom-in-btn').addEventListener('click', function() { document.getElementById('zoom-in-btn').addEventListener('click', function() {
// Zoom-In-Funktionalität if (window.mindmap) {
const svg = d3.select('#mindmap-container svg'); const transform = d3.zoomTransform(window.mindmap.svg.node());
const currentZoom = d3.zoomTransform(svg.node()); window.mindmap.svg.call(
const newScale = currentZoom.k * 1.3;
svg.transition().duration(300).call(
d3.zoom().transform, d3.zoom().transform,
d3.zoomIdentity.translate(currentZoom.x, currentZoom.y).scale(newScale) d3.zoomIdentity.translate(transform.x, transform.y).scale(transform.k * 1.3)
); );
}
}); });
document.getElementById('zoom-out-btn').addEventListener('click', function() { document.getElementById('zoom-out-btn').addEventListener('click', function() {
// Zoom-Out-Funktionalität if (window.mindmap) {
const svg = d3.select('#mindmap-container svg'); const transform = d3.zoomTransform(window.mindmap.svg.node());
const currentZoom = d3.zoomTransform(svg.node()); window.mindmap.svg.call(
const newScale = currentZoom.k / 1.3;
svg.transition().duration(300).call(
d3.zoom().transform, d3.zoom().transform,
d3.zoomIdentity.translate(currentZoom.x, currentZoom.y).scale(newScale) d3.zoomIdentity.translate(transform.x, transform.y).scale(transform.k / 1.3)
); );
}
}); });
document.getElementById('center-btn').addEventListener('click', function() { document.getElementById('center-btn').addEventListener('click', function() {
// Zentrieren-Funktionalität if (window.mindmap) {
const svg = d3.select('#mindmap-container svg'); window.mindmap.svg.call(
svg.transition().duration(500).call(
d3.zoom().transform, d3.zoom().transform,
d3.zoomIdentity.scale(1) d3.zoomIdentity
); );
}
}); });
// Add Thought Button
document.getElementById('add-thought-btn').addEventListener('click', function() { document.getElementById('add-thought-btn').addEventListener('click', function() {
// Implementierung für das Hinzufügen eines neuen Gedankens if (window.mindmap && window.mindmap.selectedNode) {
if (mindmap.selectedNode) { const nodeId = window.mindmap.selectedNode.id;
const newNodeName = prompt('Gedanke eingeben:'); const modal = document.getElementById('add-thought-modal');
if (newNodeName && newNodeName.trim() !== '') { if (modal) {
const newNodeId = 'node_' + Date.now(); // Modal öffnen, wenn vorhanden
const newNode = { modal.classList.remove('hidden');
id: newNodeId, // Node-ID in ein verstecktes Feld setzen
name: newNodeName, const nodeIdField = document.getElementById('thought-node-id');
description: 'Neuer Gedanke', if (nodeIdField) nodeIdField.value = nodeId;
thought_count: 0 } else {
}; // Simpler Dialog, wenn kein Modal existiert
const thoughtText = prompt('Neuen Gedanken eingeben:');
if (thoughtText) {
// Gedanken über API hinzufügen
fetch(`/api/nodes/${nodeId}/thoughts`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
title: thoughtText,
content: thoughtText
})
})
.then(response => response.json())
.then(data => {
// Erfolgsmeldung anzeigen
const notification = document.createElement('div');
notification.className = 'fixed top-4 right-4 bg-green-600 text-white p-4 rounded-lg shadow-lg z-50 animate-fade-in';
notification.innerHTML = `
<div class="flex items-center">
<i class="fas fa-check-circle mr-2"></i>
<p>Gedanke wurde erfolgreich hinzugefügt!</p>
</div>
`;
document.body.appendChild(notification);
// Node zur Mindmap hinzufügen // Notification nach 3 Sekunden ausblenden
mindmap.nodes.push(newNode); setTimeout(() => {
notification.classList.add('animate-fade-out');
setTimeout(() => document.body.removeChild(notification), 500);
}, 3000);
// Link zum ausgewählten Knoten erstellen // Aktualisiere den Gedankenzähler am Knoten, falls vorhanden
mindmap.links.push({ const nodeElement = document.getElementById(`node-${window.mindmap.selectedNode.id}`);
source: mindmap.selectedNode.id, const countElement = nodeElement.querySelector('.thought-count');
target: newNodeId if (countElement) {
const currentCount = parseInt(countElement.textContent);
countElement.textContent = (currentCount + 1).toString();
}
})
.catch(error => {
console.error('Fehler beim Hinzufügen des Gedankens:', error);
alert('Fehler beim Hinzufügen des Gedankens.');
}); });
}
// Mindmap aktualisieren
mindmap.updateVisualization();
} }
} else { } else {
alert('Bitte zuerst einen Knoten auswählen, um einen Gedanken hinzuzufügen.'); alert('Bitte wähle zuerst einen Knoten aus.');
} }
}); });
// Connect Button
document.getElementById('connect-btn').addEventListener('click', function() { document.getElementById('connect-btn').addEventListener('click', function() {
// Implementierung für das Verbinden von Knoten if (window.mindmap && window.mindmap.selectedNode) {
if (mindmap.selectedNode && mindmap.mouseoverNode && mindmap.selectedNode !== mindmap.mouseoverNode) { // Speichere den ersten ausgewählten Knoten
// Prüfen, ob Verbindung bereits existiert window.mindmap.sourceNode = window.mindmap.selectedNode;
const existingLink = mindmap.links.find(link =>
(link.source.id === mindmap.selectedNode.id && link.target.id === mindmap.mouseoverNode.id) || // Visuelles Feedback für den Benutzer
(link.source.id === mindmap.mouseoverNode.id && link.target.id === mindmap.selectedNode.id) const selectedCircle = d3.select(`#node-${window.mindmap.selectedNode.id} circle`);
selectedCircle.classed('connection-source', true);
// Benutzerfreundlichere Benachrichtigung mit Statusanzeige
const notification = document.createElement('div');
notification.id = 'connection-notification';
notification.className = 'fixed top-4 right-4 bg-purple-600 text-white p-4 rounded-lg shadow-lg z-50';
notification.innerHTML = `
<p class="font-bold mb-1">Verbindungsmodus aktiv</p>
<p class="text-sm">Wähle einen zweiten Knoten aus, um eine Verbindung herzustellen</p>
<button id="cancel-connection" class="mt-2 px-3 py-1 bg-purple-800 rounded hover:bg-purple-900 text-sm">Abbrechen</button>
`;
document.body.appendChild(notification);
// Abbrechen-Button-Funktionalität
document.getElementById('cancel-connection').addEventListener('click', function() {
window.mindmap.connectMode = false;
window.mindmap.sourceNode = null;
selectedCircle.classed('connection-source', false);
document.body.removeChild(notification);
});
// Aktiviere den Verbindungsmodus
window.mindmap.connectMode = true;
// Cursor-Stil ändern, um den Verbindungsmodus anzuzeigen
document.getElementById('mindmap-container').style.cursor = 'crosshair';
} else {
alert('Bitte wähle zuerst einen Knoten aus.');
}
});
// Formular zum Hinzufügen neuer Gedanken behandeln
const thoughtForm = document.getElementById('add-thought-form');
if (thoughtForm) {
thoughtForm.addEventListener('submit', function(event) {
event.preventDefault();
const nodeId = document.getElementById('thought-node-id').value;
const title = document.getElementById('thought-title').value;
const content = document.getElementById('thought-content').value;
if (!nodeId || !title || !content) {
alert('Bitte fülle alle Felder aus.');
return;
}
// Gedanken über API hinzufügen
fetch(`/api/nodes/${nodeId}/thoughts`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
title: title,
content: content,
branch: 'main' // Default-Zweig
})
})
.then(response => {
if (!response.ok) {
throw new Error('Fehler beim Hinzufügen des Gedankens');
}
return response.json();
})
.then(data => {
// Modal schließen
document.getElementById('add-thought-modal').classList.add('hidden');
// Formular zurücksetzen
thoughtForm.reset();
// Erfolgsmeldung anzeigen
alert('Gedanke wurde erfolgreich hinzugefügt!');
// Optional: Knoten in der Mindmap aktualisieren (z.B. Zähler erhöhen)
if (window.mindmap && window.mindmap.selectedNode) {
window.mindmap.selectedNode.thought_count += 1;
window.mindmap.updateNodeLabels();
}
})
.catch(error => {
console.error('Fehler beim Speichern des Gedankens:', error);
alert('Fehler beim Speichern des Gedankens. Bitte versuche es erneut.');
});
});
}
// Fenstergrößen-Änderung behandeln
let resizeTimeout;
window.addEventListener('resize', function() {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(function() {
if (window.mindmap) {
window.mindmap.width = mindmapContainer.clientWidth;
window.mindmap.height = mindmapContainer.clientHeight;
window.mindmap.svg
.attr('width', '100%')
.attr('height', window.mindmap.height)
.attr('viewBox', `0 0 ${window.mindmap.width} ${window.mindmap.height}`);
// Mittelpunkt-Kraft aktualisieren
window.mindmap.simulation.force('center',
d3.forceCenter(window.mindmap.width / 2, window.mindmap.height / 2)
); );
if (!existingLink) { // Simulation neu starten
// Link erstellen window.mindmap.simulation.alpha(0.3).restart();
mindmap.links.push({ }
source: mindmap.selectedNode.id, }, 250);
target: mindmap.mouseoverNode.id
}); });
// Mindmap aktualisieren
mindmap.updateVisualization();
} else {
alert('Diese Verbindung existiert bereits.');
}
} else {
alert('Bitte wähle zwei verschiedene Knoten aus, um sie zu verbinden.');
}
}); });
// Responsive Anpassung bei Fenstergrößenänderung
window.addEventListener('resize', function() {
const newWidth = mindmapContainer.clientWidth;
const newHeight = mindmapContainer.clientHeight;
if (mindmap.svg) {
mindmap.svg
.attr('width', newWidth)
.attr('height', newHeight);
mindmap.width = newWidth;
mindmap.height = newHeight;
// Force-Simulation aktualisieren
if (mindmap.simulation) {
mindmap.simulation
.force('center', d3.forceCenter(newWidth / 2, newHeight / 2))
.restart();
}
}
});
// Aktualisiere das Aussehen von Bookmarks, sobald die Mindmap vollständig geladen ist
setTimeout(() => {
if (mindmap && typeof mindmap.updateAllBookmarkedNodes === 'function') {
mindmap.updateAllBookmarkedNodes();
}
}, 1000);
});
// Animationen für die Hintergrundeffekte
document.head.insertAdjacentHTML('beforeend', `
<style>
@keyframes pulse {
0% { transform: scale(1); opacity: 0.5; }
100% { transform: scale(1.5); opacity: 0.2; }
}
@keyframes flash {
0% { opacity: 0.02; }
50% { opacity: 0.2; }
100% { opacity: 0.08; }
}
</style>
`);
</script> </script>
{% endblock %} {% endblock %}

115
templates/register.html Normal file
View File

@@ -0,0 +1,115 @@
{% extends "base.html" %}
{% block title %}Registrieren{% endblock %}
{% block content %}
<div class="flex justify-center items-center mt-10 px-4">
<div class="w-full max-w-md">
<div class="bg-white bg-opacity-80 backdrop-blur-lg rounded-lg shadow-md border border-white border-opacity-30 p-6 md:p-8 transition-all duration-300 transform hover:shadow-lg">
<h2 class="text-center mb-6 text-gray-800 font-bold text-2xl flex items-center justify-center">
<i class="fas fa-user-plus mr-2 text-blue-600"></i>
Registrieren
</h2>
<form method="POST" action="{{ url_for('register') }}" class="needs-validation space-y-6" novalidate>
<div class="space-y-2">
<label for="username" class="block text-gray-700 font-medium text-sm">Benutzername</label>
<div class="relative flex items-center">
<span class="absolute left-3 text-blue-600">
<i class="fas fa-user"></i>
</span>
<input type="text" class="pl-10 w-full rounded-md border border-gray-300 py-2 px-4 focus:border-blue-500 focus:ring focus:ring-blue-200 focus:ring-opacity-50 transition-all duration-200"
id="username" name="username" placeholder="Dein Benutzername" required>
</div>
<div class="invalid-feedback text-red-600 text-sm hidden">
Bitte gib einen Benutzernamen ein.
</div>
</div>
<div class="space-y-2">
<label for="email" class="block text-gray-700 font-medium text-sm">E-Mail</label>
<div class="relative flex items-center">
<span class="absolute left-3 text-blue-600">
<i class="fas fa-envelope"></i>
</span>
<input type="email" class="pl-10 w-full rounded-md border border-gray-300 py-2 px-4 focus:border-blue-500 focus:ring focus:ring-blue-200 focus:ring-opacity-50 transition-all duration-200"
id="email" name="email" placeholder="name@beispiel.de" required>
</div>
<div class="invalid-feedback text-red-600 text-sm hidden">
Bitte gib eine gültige E-Mail-Adresse ein.
</div>
</div>
<div class="space-y-2">
<label for="password" class="block text-gray-700 font-medium text-sm">Passwort</label>
<div class="relative flex items-center">
<span class="absolute left-3 text-blue-600">
<i class="fas fa-lock"></i>
</span>
<input type="password" class="pl-10 w-full rounded-md border border-gray-300 py-2 px-4 focus:border-blue-500 focus:ring focus:ring-blue-200 focus:ring-opacity-50 transition-all duration-200"
id="password" name="password" placeholder="Mindestens 8 Zeichen" required>
<button class="absolute right-2 text-gray-500 hover:text-gray-700 focus:outline-none" type="button" id="togglePassword">
<i class="fas fa-eye"></i>
</button>
</div>
<div class="invalid-feedback text-red-600 text-sm hidden">
Bitte gib ein sicheres Passwort ein.
</div>
</div>
<div class="pt-2">
<button type="submit" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-md shadow-sm transition-all duration-200 transform hover:scale-[1.02] focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50">
<i class="fas fa-user-plus mr-2"></i> Konto erstellen
</button>
</div>
<div class="text-center mt-4 text-gray-700 text-sm">
<p>Bereits registriert? <a href="{{ url_for('login') }}" class="text-blue-600 hover:text-blue-800 font-medium transition-colors duration-200">Anmelden</a></p>
</div>
</form>
</div>
</div>
</div>
<script>
// Formularvalidierung aktivieren
(function() {
'use strict';
var forms = document.querySelectorAll('.needs-validation');
Array.prototype.slice.call(forms).forEach(function(form) {
form.addEventListener('submit', function(event) {
if (!form.checkValidity()) {
event.preventDefault();
event.stopPropagation();
// Zeige Fehlermeldungen an
form.querySelectorAll(':invalid').forEach(function(input) {
input.parentNode.nextElementSibling.classList.remove('hidden');
});
}
form.classList.add('was-validated');
}, false);
// Verstecke Fehlermeldungen bei Eingabe
form.querySelectorAll('input').forEach(function(input) {
input.addEventListener('input', function() {
if (this.checkValidity()) {
this.parentNode.nextElementSibling.classList.add('hidden');
}
});
});
});
// Passwort-Sichtbarkeit umschalten
const togglePassword = document.querySelector('#togglePassword');
const password = document.querySelector('#password');
togglePassword.addEventListener('click', function() {
const type = password.getAttribute('type') === 'password' ? 'text' : 'password';
password.setAttribute('type', type);
this.querySelector('i').classList.toggle('fa-eye');
this.querySelector('i').classList.toggle('fa-eye-slash');
});
})();
</script>
{% endblock %}

105
templates/search.html Normal file
View File

@@ -0,0 +1,105 @@
{% extends "base.html" %}
{% block title %}Suche{% endblock %}
{% block content %}
<div class="flex flex-col md:flex-row gap-6">
<!-- Filter-Sidebar -->
<div class="w-full md:w-1/3 lg:w-1/4 mb-6">
<div class="bg-white/10 backdrop-blur-md rounded-xl p-6 shadow-lg">
<h4 class="text-xl font-semibold mb-4">Erweiterte Suche</h4>
<form id="search-form">
<div class="mb-4">
<label class="block text-sm font-medium mb-1">Suchbegriff</label>
<div class="flex items-center border rounded-lg overflow-hidden bg-white/5">
<span class="px-3 py-2 text-gray-400">
<i class="fas fa-search"></i>
</span>
<input type="text" class="w-full bg-transparent border-0 focus:ring-0 py-2 px-1" name="q" placeholder="Suche...">
</div>
</div>
<div class="mb-4">
<label class="block text-sm font-medium mb-1">Schlagworte</label>
<div class="flex items-center border rounded-lg overflow-hidden bg-white/5">
<span class="px-3 py-2 text-gray-400">
<i class="fas fa-tags"></i>
</span>
<input type="text" class="w-full bg-transparent border-0 focus:ring-0 py-2 px-1" name="keywords" placeholder="Schlagworte (kommagetrennt)">
</div>
</div>
<div class="mb-4">
<label class="block text-sm font-medium mb-1">Minimale Bewertung</label>
<div class="flex items-center border rounded-lg overflow-hidden bg-white/5">
<span class="px-3 py-2 text-gray-400">
<i class="fas fa-star"></i>
</span>
<select class="w-full bg-transparent border-0 focus:ring-0 py-2 px-1" name="min_rating">
<option value="">Alle</option>
<option value="4">4+ Sterne</option>
<option value="3">3+ Sterne</option>
<option value="2">2+ Sterne</option>
<option value="1">1+ Stern</option>
</select>
</div>
</div>
<div class="mb-4">
<label class="block text-sm font-medium mb-1">Quellentyp</label>
<div class="flex items-center border rounded-lg overflow-hidden bg-white/5">
<span class="px-3 py-2 text-gray-400">
<i class="fas fa-file-alt"></i>
</span>
<select class="w-full bg-transparent border-0 focus:ring-0 py-2 px-1" name="source_type">
<option value="">Alle</option>
<option value="PDF">PDF</option>
<option value="Markdown">Markdown</option>
<option value="Text">Text</option>
</select>
</div>
</div>
<div class="mb-5">
<label class="block text-sm font-medium mb-1">Beziehungstyp</label>
<div class="flex items-center border rounded-lg overflow-hidden bg-white/5">
<span class="px-3 py-2 text-gray-400">
<i class="fas fa-project-diagram"></i>
</span>
<select class="w-full bg-transparent border-0 focus:ring-0 py-2 px-1" name="relation_type">
<option value="">Alle</option>
<option value="SUPPORTS">Stützt</option>
<option value="CONTRADICTS">Widerspricht</option>
<option value="BUILDS_UPON">Baut auf auf</option>
<option value="GENERALIZES">Verallgemeinert</option>
<option value="SPECIFIES">Spezifiziert</option>
<option value="INSPIRES">Inspiriert</option>
</select>
</div>
</div>
<button type="submit" class="w-full bg-gradient-to-r from-blue-500 to-indigo-600 text-white py-2 px-4 rounded-lg hover:opacity-90 transition-all flex items-center justify-center">
<i class="fas fa-search mr-2"></i> Suchen
</button>
</form>
</div>
</div>
<!-- Suchergebnisse -->
<div class="w-full md:w-2/3 lg:w-3/4">
<div class="bg-white/10 backdrop-blur-md rounded-xl p-6 shadow-lg mb-6">
<h3 class="text-2xl font-semibold mb-2">Suchergebnisse</h3>
<p class="text-gray-300 text-sm">Nutze die Filter links, um deine Suche zu präzisieren.</p>
</div>
<div id="search-results" class="mb-6">
<!-- Suchergebnisse werden hier dynamisch eingefügt -->
<div class="bg-white/10 backdrop-blur-md rounded-xl p-8 shadow-lg text-center">
<i class="fas fa-search text-5xl mb-6 text-blue-400"></i>
<h5 class="text-xl font-medium mb-3">Wissen entdecken</h5>
<p class="text-gray-300">Gib einen Suchbegriff ein, um in der wissenschaftlichen Wissensdatenbank zu suchen.</p>
</div>
</div>
</div>
</div>
{% endblock %}

34
utils/__init__.py Executable file
View File

@@ -0,0 +1,34 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Utility functions for the website application.
This package contains various utilities for database management,
user management, and server administration.
"""
from .db_fix import fix_database_schema
from .db_rebuild import rebuild_database
from .db_test import test_database_connection, test_models, print_database_stats, run_all_tests
from .user_manager import list_users, create_user, reset_password, delete_user, create_admin_user
from .server import run_development_server
__all__ = [
# Database utilities
'fix_database_schema',
'rebuild_database',
'test_database_connection',
'test_models',
'print_database_stats',
'run_all_tests',
# User management
'list_users',
'create_user',
'reset_password',
'delete_user',
'create_admin_user',
# Server management
'run_development_server',
]

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

78
utils/db_fix.py Executable file
View File

@@ -0,0 +1,78 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import sqlite3
from datetime import datetime
import sys
import importlib.util
# Add the parent directory to path so we can import the app
parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, parent_dir)
from app import app, db_path
from models import db
def ensure_db_dir():
"""Make sure the database directory exists."""
os.makedirs(os.path.dirname(db_path), exist_ok=True)
def fix_database_schema():
"""Fix the database schema by adding missing columns."""
with app.app_context():
# Ensure directory exists
ensure_db_dir()
# Check if database exists, create tables if needed
if not os.path.exists(db_path):
print("Database doesn't exist. Creating all tables from scratch...")
db.create_all()
print("Database tables created successfully!")
return
# Connect to existing database
print(f"Connecting to database: {db_path}")
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
# Check if User table exists
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='user'")
if not cursor.fetchone():
print("User table doesn't exist. Creating all tables from scratch...")
conn.close()
db.create_all()
print("Database tables created successfully!")
return
# Check existing columns
cursor.execute("PRAGMA table_info(user)")
columns = cursor.fetchall()
column_names = [col[1] for col in columns]
print("Existing columns in User table:", column_names)
# Add missing columns
if 'created_at' not in column_names:
print("Adding 'created_at' column to User table...")
cursor.execute("ALTER TABLE user ADD COLUMN created_at TIMESTAMP")
if 'last_login' not in column_names:
print("Adding 'last_login' column to User table...")
cursor.execute("ALTER TABLE user ADD COLUMN last_login TIMESTAMP")
if 'avatar' not in column_names:
print("Adding 'avatar' column to User table...")
cursor.execute("ALTER TABLE user ADD COLUMN avatar VARCHAR(200)")
if 'bio' not in column_names:
print("Adding 'bio' column to User table...")
cursor.execute("ALTER TABLE user ADD COLUMN bio TEXT")
# Commit the changes
conn.commit()
conn.close()
print("Database schema updated successfully!")
return True
if __name__ == "__main__":
fix_database_schema()

81
utils/db_rebuild.py Executable file
View File

@@ -0,0 +1,81 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import sqlite3
from datetime import datetime
import sys
import importlib.util
# Add the parent directory to path so we can import the app
parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, parent_dir)
from app import app, db_path, create_default_categories
from models import db, User, Category
def rebuild_database():
"""Completely rebuilds the database by dropping and recreating all tables."""
with app.app_context():
print(f"Database path: {db_path}")
# Back up existing database if it exists
if os.path.exists(db_path):
backup_path = db_path + '.backup'
try:
import shutil
shutil.copy2(db_path, backup_path)
print(f"Backed up existing database to {backup_path}")
except Exception as e:
print(f"Warning: Could not create backup - {str(e)}")
# Ensure directory exists
os.makedirs(os.path.dirname(db_path), exist_ok=True)
# Drop all tables and recreate them
print("Dropping all tables...")
db.drop_all()
print("Creating all tables...")
db.create_all()
# Create admin user
print("Creating admin user...")
admin = User(
username='admin',
email='admin@example.com',
is_admin=True,
created_at=datetime.utcnow()
)
admin.set_password('admin')
db.session.add(admin)
# Create regular user
print("Creating regular user...")
user = User(
username='user',
email='user@example.com',
is_admin=False,
created_at=datetime.utcnow()
)
user.set_password('user')
db.session.add(user)
try:
# Commit to generate user IDs
db.session.commit()
print("Users created successfully!")
# Create default categories
print("Creating default categories...")
create_default_categories()
print("Database rebuild completed successfully!")
return True
except Exception as e:
db.session.rollback()
print(f"Error during database rebuild: {str(e)}")
raise
if __name__ == "__main__":
rebuild_database()

120
utils/db_test.py Executable file
View File

@@ -0,0 +1,120 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import sys
import sqlite3
# Add the parent directory to path so we can import the app
parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, parent_dir)
from app import app, db_path
from models import db, User, Thought, MindMapNode, Category
def test_database_connection():
"""Test if the database exists and can be connected to."""
try:
if not os.path.exists(db_path):
print(f"Database file does not exist: {db_path}")
return False
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
cursor.execute("PRAGMA integrity_check")
result = cursor.fetchone()
conn.close()
if result and result[0] == "ok":
print(f"Database integrity check passed: {db_path}")
return True
else:
print(f"Database integrity check failed: {result}")
return False
except Exception as e:
print(f"Error testing database connection: {e}")
return False
def test_models():
"""Test if all models are properly defined and can be queried."""
with app.app_context():
try:
print("\nTesting User model...")
user_count = User.query.count()
print(f" Found {user_count} users")
print("\nTesting Category model...")
category_count = Category.query.count()
print(f" Found {category_count} categories")
print("\nTesting MindMapNode model...")
node_count = MindMapNode.query.count()
print(f" Found {node_count} mindmap nodes")
print("\nTesting Thought model...")
thought_count = Thought.query.count()
print(f" Found {thought_count} thoughts")
if user_count == 0:
print("\nWARNING: No users found in the database. You might need to create an admin user.")
return True
except Exception as e:
print(f"Error testing models: {e}")
return False
def print_database_stats():
"""Print database statistics."""
with app.app_context():
try:
stats = []
stats.append(("Users", User.query.count()))
stats.append(("Categories", Category.query.count()))
stats.append(("Mindmap Nodes", MindMapNode.query.count()))
stats.append(("Thoughts", Thought.query.count()))
print("\nDatabase Statistics:")
print("-" * 40)
for name, count in stats:
print(f"{name:<20} : {count}")
print("-" * 40)
return True
except Exception as e:
print(f"Error generating database statistics: {e}")
return False
def run_all_tests():
"""Run all database tests."""
success = True
print("=" * 60)
print("STARTING DATABASE TESTS")
print("=" * 60)
# Test database connection
print("\n1. Testing database connection...")
if not test_database_connection():
success = False
# Test models
print("\n2. Testing database models...")
if not test_models():
success = False
# Print statistics
print("\n3. Database statistics:")
if not print_database_stats():
success = False
print("\n" + "=" * 60)
if success:
print("All database tests completed successfully!")
else:
print("Some database tests failed. Check the output above for details.")
print("=" * 60)
return success
if __name__ == "__main__":
run_all_tests()

34
utils/server.py Executable file
View File

@@ -0,0 +1,34 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import sys
# Add the parent directory to path so we can import the app
parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, parent_dir)
from app import app
def run_development_server(host='127.0.0.1', port=5000, debug=True):
"""Run the Flask development server."""
try:
print(f"Starting development server on http://{host}:{port}")
print("Press CTRL+C to stop the server")
app.run(host=host, port=port, debug=debug)
return True
except Exception as e:
print(f"Error starting development server: {e}")
return False
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description='Run the development server')
parser.add_argument('--host', default='127.0.0.1', help='Host to bind to')
parser.add_argument('--port', type=int, default=5000, help='Port to bind to')
parser.add_argument('--debug', action='store_true', default=True, help='Enable debug mode')
args = parser.parse_args()
run_development_server(host=args.host, port=args.port, debug=args.debug)

159
utils/user_manager.py Executable file
View File

@@ -0,0 +1,159 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import sys
from datetime import datetime
# Add the parent directory to path so we can import the app
parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, parent_dir)
from app import app
from models import db, User
def list_users():
"""List all users in the database."""
with app.app_context():
try:
users = User.query.all()
if not users:
print("No users found in the database.")
return []
print("Found {} users:".format(len(users)))
print("-" * 60)
print("{:<5} {:<20} {:<30} {:<10}".format("ID", "Username", "Email", "Admin"))
print("-" * 60)
for user in users:
print("{:<5} {:<20} {:<30} {:<10}".format(
user.id, user.username, user.email, "Yes" if user.is_admin else "No"
))
return users
except Exception as e:
print(f"Error listing users: {e}")
return []
def create_user(username, email, password, is_admin=False):
"""Create a new user in the database."""
with app.app_context():
try:
# Check if user already exists
existing_user = User.query.filter_by(username=username).first()
if existing_user:
print(f"User with username '{username}' already exists.")
return False
# Check if email already exists
existing_email = User.query.filter_by(email=email).first()
if existing_email:
print(f"User with email '{email}' already exists.")
return False
# Create new user
user = User(
username=username,
email=email,
is_admin=is_admin,
created_at=datetime.utcnow()
)
user.set_password(password)
db.session.add(user)
db.session.commit()
print(f"User '{username}' created successfully!")
return True
except Exception as e:
db.session.rollback()
print(f"Error creating user: {e}")
return False
def reset_password(username, new_password):
"""Reset password for a user."""
with app.app_context():
try:
user = User.query.filter_by(username=username).first()
if not user:
print(f"User '{username}' not found.")
return False
user.set_password(new_password)
db.session.commit()
print(f"Password for user '{username}' reset successfully!")
return True
except Exception as e:
db.session.rollback()
print(f"Error resetting password: {e}")
return False
def delete_user(username):
"""Delete a user from the database."""
with app.app_context():
try:
user = User.query.filter_by(username=username).first()
if not user:
print(f"User '{username}' not found.")
return False
db.session.delete(user)
db.session.commit()
print(f"User '{username}' deleted successfully!")
return True
except Exception as e:
db.session.rollback()
print(f"Error deleting user: {e}")
return False
def create_admin_user():
"""Create an admin user in the database."""
return create_user('admin', 'admin@example.com', 'admin', is_admin=True)
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description='User management utility')
subparsers = parser.add_subparsers(dest='command', help='Command to execute')
# List users command
list_parser = subparsers.add_parser('list', help='List all users')
# Create user command
create_parser = subparsers.add_parser('create', help='Create a new user')
create_parser.add_argument('--username', '-u', required=True, help='Username')
create_parser.add_argument('--email', '-e', required=True, help='Email address')
create_parser.add_argument('--password', '-p', required=True, help='Password')
create_parser.add_argument('--admin', '-a', action='store_true', help='Make user an admin')
# Reset password command
reset_parser = subparsers.add_parser('reset-password', help='Reset a user password')
reset_parser.add_argument('--username', '-u', required=True, help='Username')
reset_parser.add_argument('--password', '-p', required=True, help='New password')
# Delete user command
delete_parser = subparsers.add_parser('delete', help='Delete a user')
delete_parser.add_argument('--username', '-u', required=True, help='Username to delete')
# Create admin command (shortcut)
admin_parser = subparsers.add_parser('create-admin', help='Create the default admin user')
args = parser.parse_args()
if args.command == 'list':
list_users()
elif args.command == 'create':
create_user(args.username, args.email, args.password, args.admin)
elif args.command == 'reset-password':
reset_password(args.username, args.password)
elif args.command == 'delete':
delete_user(args.username)
elif args.command == 'create-admin':
create_admin_user()
else:
parser.print_help()

View File

@@ -1 +0,0 @@
OPENAI_API_KEY=sk-placeholder

View File

@@ -1,96 +0,0 @@
# MindMapProjekt - Roadmap
## Projektübersicht
Das MindMapProjekt ist eine interaktive Plattform zum Visualisieren, Erforschen und Teilen von Wissen. Das Projekt wird umfassend überarbeitet, um ein modernes, benutzerfreundliches Design und erweiterte Funktionalitäten zu bieten.
## Technischer Stack
- **Backend**: Python/Flask
- **Frontend**:
- Tailwind CSS für moderne UI
- SVG-Bibliotheken für Visualisierungen (D3.js)
- JavaScript/Alpine.js für interaktive Komponenten
- **Datenbank**: SQLite mit SQLAlchemy
- **KI-Integration**: OpenAI API für intelligente Assistenz
## Roadmap der Überarbeitung
### Phase 1: Grundlegende Infrastruktur ✅
- [x] Bestandsaufnahme des aktuellen Projekts
- [x] Erstellung der Roadmap
- [x] Aktualisierung der Abhängigkeiten
- [x] Integration von Tailwind CSS
- [x] Einrichtung der SVG-Bibliotheken (D3.js)
- [x] Favicon erstellen
- [x] Setup-Skript für einfache Installation
### Phase 2: Design-Überarbeitung 🔄
- [x] Implementierung des Dark Mode
- [x] Erstellung eines modernen, minimalistischen UI mit Tech-Ästhetik
- [x] Responsive Design für alle Geräte
- [ ] Gestaltung der Landing Page mit großer Typografie
### Phase 3: Mindmap-Funktionalitäten 🔄
- [x] Verbesserte Visualisierung mit SVG und D3.js
- [x] Implementierung der Mouseover-Funktion
- [x] Entwicklung der Suchfunktion für Knoten
- [ ] Tagging-System für Inhalte
- [ ] Quellenmanagement und -verlinkung
- [ ] Upload-Funktionalität an Knotenpunkten
### Phase 4: Kernseitenentwicklung
- [ ] Überarbeitung der Startseite mit neuen Features
- [ ] Entwicklung der "Wer sind wir?"-Seite
- [ ] Implementierung von Impressum und Datenschutzerklärung
- [ ] Erstellung der Kontaktseite mit FAQs
- [ ] Überarbeitung des Benutzerprofilbereichs
### Phase 5: Community-Features
- [ ] Entwicklung des Autorenbereichs
- [ ] Implementierung von Community-Bereichen für Themenbereiche
- [ ] Verbesserter Kommentarbereich
- [ ] Benutzerrechtemanagement
### Phase 6: KI-Integration
- [ ] Implementierung des Frage-Antwort-Systems
- [ ] KI-generierte Themeneinleitungen
- [ ] Intelligente Suchunterstützung
- [ ] Geführte Pfade durch Themenbereiche
- [ ] Vorgeschlagene Chat-Möglichkeiten
### Phase 7: Benutzerprofilfunktionen
- [ ] Speichern von Thematiken
- [ ] Persönliche Mindmap/Pinboard
- [ ] Beitragsmanagement
- [ ] Benutzerstatistiken und -aktivitäten
### Phase 8: Testing und Optimierung
- [ ] Umfassende Tests aller Funktionen
- [ ] Performance-Optimierung
- [ ] SEO-Implementierung
- [ ] Barrierefreiheit prüfen und verbessern
### Phase 9: Dokumentation und Einführung
- [ ] Erstellung von Benutzeranleitungen
- [ ] Entwicklerdokumentation
- [ ] Administratorenhandbuch
- [ ] Guided Tour für neue Benutzer
## Aktueller Status
- **Phase 1**: ✅ Abgeschlossen
- **Phase 2**: 🔄 In Bearbeitung (75% abgeschlossen)
- **Phase 3**: 🔄 In Bearbeitung (50% abgeschlossen)
## Aktuelle Fortschritte
- Grundlegende UI modernisiert mit Tailwind CSS und Dark Mode
- Neues Favicon für bessere visuelle Identität erstellt
- Setup-Prozess vereinfacht mit einem Shell-Skript
- Mindmap-Visualisierung komplett überarbeitet mit D3.js für eine interaktivere Erfahrung
- Responsive Design für optimale Darstellung auf allen Geräten
## Nächste Schritte
- Fertigstellung der Landing Page
- Erstellung der "Wer sind wir?"-Seite
- Implementierung des Tagging-Systems für Gedanken
- Verbesserung der Gedankenansicht im Mindmap-Bereich
*Zuletzt aktualisiert: 01.06.2024*

View File

@@ -1,836 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
from datetime import datetime, timedelta
from flask import Flask, render_template, request, redirect, url_for, flash, jsonify, session
from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user
from flask_sqlalchemy import SQLAlchemy
from werkzeug.security import generate_password_hash, check_password_hash
import json
from enum import Enum
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, TextAreaField, SelectField, HiddenField
from wtforms.validators import DataRequired, Email, Length, EqualTo, ValidationError
from functools import wraps
import secrets
from sqlalchemy.sql import func
import openai
from dotenv import load_dotenv
# Lade .env-Datei
load_dotenv()
app = Flask(__name__)
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'default-dev-key')
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///mindmap.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=365) # Langlebige Session für Dark Mode-Einstellung
# OpenAI API-Konfiguration
openai.api_key = os.environ.get('OPENAI_API_KEY')
# Context processor für globale Template-Variablen
@app.context_processor
def inject_globals():
"""Inject global variables into all templates."""
return {
'current_year': datetime.now().year
}
# Kontext-Prozessor für alle Templates
@app.context_processor
def inject_current_year():
return {'current_year': datetime.now().year}
db = SQLAlchemy(app)
login_manager = LoginManager(app)
login_manager.login_view = 'login'
# Benutzerdefinierter Decorator für Admin-Zugriff
def admin_required(f):
@wraps(f)
@login_required
def decorated_function(*args, **kwargs):
if not current_user.is_admin:
flash('Zugriff verweigert. Nur Administratoren dürfen diese Seite aufrufen.', 'error')
return redirect(url_for('index'))
return f(*args, **kwargs)
return decorated_function
class RelationType(Enum):
SUPPORTS = "stützt"
CONTRADICTS = "widerspricht"
BUILDS_UPON = "baut auf auf"
GENERALIZES = "verallgemeinert"
SPECIFIES = "spezifiziert"
INSPIRES = "inspiriert"
class ThoughtRelation(db.Model):
id = db.Column(db.Integer, primary_key=True)
source_id = db.Column(db.Integer, db.ForeignKey('thought.id'), nullable=False)
target_id = db.Column(db.Integer, db.ForeignKey('thought.id'), nullable=False)
relation_type = db.Column(db.Enum(RelationType), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
created_by_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
class ThoughtRating(db.Model):
id = db.Column(db.Integer, primary_key=True)
thought_id = db.Column(db.Integer, db.ForeignKey('thought.id'), nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
relevance_score = db.Column(db.Integer, nullable=False) # 1-5
created_at = db.Column(db.DateTime, default=datetime.utcnow)
__table_args__ = (
db.UniqueConstraint('thought_id', 'user_id', name='unique_thought_rating'),
)
# Database Models
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
password_hash = db.Column(db.String(128))
is_admin = db.Column(db.Boolean, default=False)
thoughts = db.relationship('Thought', backref='author', lazy=True)
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
class Thought(db.Model):
id = db.Column(db.Integer, primary_key=True)
content = db.Column(db.Text, nullable=False)
timestamp = db.Column(db.DateTime, default=datetime.utcnow)
branch = db.Column(db.String(100), nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
title = db.Column(db.String(200), nullable=False)
abstract = db.Column(db.Text)
keywords = db.Column(db.String(500))
color_code = db.Column(db.String(7)) # Hex color code
source_type = db.Column(db.String(50)) # PDF, Markdown, Text etc.
comments = db.relationship('Comment', backref='thought', lazy=True, cascade="all, delete-orphan")
ratings = db.relationship('ThoughtRating', backref='thought', lazy=True)
outgoing_relations = db.relationship(
'ThoughtRelation',
foreign_keys=[ThoughtRelation.source_id],
backref='source_thought',
lazy=True
)
incoming_relations = db.relationship(
'ThoughtRelation',
foreign_keys=[ThoughtRelation.target_id],
backref='target_thought',
lazy=True
)
@property
def average_rating(self):
if not self.ratings:
return 0
return sum(r.relevance_score for r in self.ratings) / len(self.ratings)
class Comment(db.Model):
id = db.Column(db.Integer, primary_key=True)
content = db.Column(db.Text, nullable=False)
timestamp = db.Column(db.DateTime, default=datetime.utcnow)
thought_id = db.Column(db.Integer, db.ForeignKey('thought.id'), nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
author = db.relationship('User', backref='comments')
class MindMapNode(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False)
parent_id = db.Column(db.Integer, db.ForeignKey('mind_map_node.id'), nullable=True)
children = db.relationship('MindMapNode', backref=db.backref('parent', remote_side=[id]))
thoughts = db.relationship('Thought', secondary='node_thought_association', backref='nodes')
# Association table for many-to-many relationship between MindMapNode and Thought
node_thought_association = db.Table('node_thought_association',
db.Column('node_id', db.Integer, db.ForeignKey('mind_map_node.id'), primary_key=True),
db.Column('thought_id', db.Integer, db.ForeignKey('thought.id'), primary_key=True)
)
@login_manager.user_loader
def load_user(id):
return User.query.get(int(id))
# Routes for authentication
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
user = User.query.filter_by(username=username).first()
if user and user.check_password(password):
login_user(user)
next_page = request.args.get('next')
return redirect(next_page or url_for('index'))
flash('Ungültiger Benutzername oder Passwort')
return render_template('login.html')
@app.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
username = request.form.get('username')
email = request.form.get('email')
password = request.form.get('password')
if User.query.filter_by(username=username).first():
flash('Benutzername existiert bereits')
return redirect(url_for('register'))
if User.query.filter_by(email=email).first():
flash('E-Mail ist bereits registriert')
return redirect(url_for('register'))
user = User(username=username, email=email)
user.set_password(password)
db.session.add(user)
db.session.commit()
login_user(user)
return redirect(url_for('index'))
return render_template('register.html')
@app.route('/logout')
@login_required
def logout():
logout_user()
return redirect(url_for('index'))
# Route for the homepage
@app.route('/')
def index():
return render_template('index.html')
# Route for the mindmap page
@app.route('/mindmap')
def mindmap():
return render_template('mindmap.html')
# Route for user profile
@app.route('/profile')
@login_required
def profile():
thoughts = Thought.query.filter_by(user_id=current_user.id).order_by(Thought.timestamp.desc()).all()
return render_template('profile.html', thoughts=thoughts, user=current_user)
# Route für Benutzereinstellungen
@app.route('/settings', methods=['GET', 'POST'])
@login_required
def settings():
if request.method == 'POST':
# Formular-Daten verarbeiten
current_password = request.form.get('current_password')
new_password = request.form.get('new_password')
confirm_password = request.form.get('confirm_password')
# Änderungen an Benutzerdaten
if new_password:
if not current_user.check_password(current_password):
flash('Aktuelles Passwort ist falsch', 'error')
return redirect(url_for('settings'))
if new_password != confirm_password:
flash('Neue Passwörter stimmen nicht überein', 'error')
return redirect(url_for('settings'))
if len(new_password) < 6:
flash('Neues Passwort muss mindestens 6 Zeichen lang sein', 'error')
return redirect(url_for('settings'))
# Passwort aktualisieren
current_user.set_password(new_password)
db.session.commit()
flash('Passwort erfolgreich aktualisiert', 'success')
# E-Mail-Benachrichtigungen und andere Präferenzen könnten hier aktualisiert werden
return redirect(url_for('settings'))
return render_template('settings.html')
# Routes für rechtliche Seiten
@app.route('/impressum/')
def impressum():
return render_template('impressum.html')
@app.route('/datenschutz/')
def datenschutz():
return render_template('datenschutz.html')
@app.route('/agb/')
def agb():
return render_template('agb.html')
# API routes for mindmap and thoughts
@app.route('/api/mindmap')
def get_mindmap():
"""API-Endpunkt zur Bereitstellung der Mindmap-Daten in hierarchischer Form."""
# Alle root-Nodes (ohne parent) abrufen
root_nodes = MindMapNode.query.filter_by(parent_id=None).all()
if not root_nodes:
# Wenn keine Nodes existieren, erstelle Beispieldaten
create_sample_mindmap()
root_nodes = MindMapNode.query.filter_by(parent_id=None).all()
# Ergebnisse in hierarchischer Struktur zurückgeben
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."""
# Gedankenzähler abrufen von der many-to-many Beziehung
thought_count = len(node.thoughts)
# Daten für aktuellen Knoten
node_data = {
"id": node.id,
"name": node.name,
"description": f"Knoten mit {thought_count} Gedanken",
"thought_count": thought_count,
"children": []
}
# Rekursiv Kinder hinzufügen
child_nodes = MindMapNode.query.filter_by(parent_id=node.id).all()
for child_node in child_nodes:
child_data = build_node_tree(child_node)
node_data["children"].append(child_data)
return node_data
@app.route('/api/nodes/<int:node_id>/thoughts')
def get_node_thoughts(node_id):
"""API-Endpunkt zur Abfrage aller Gedanken eines Knotens."""
# Node existiert?
node = MindMapNode.query.get_or_404(node_id)
# Alle Gedanken des Knotens abrufen über die many-to-many Beziehung
thoughts = node.thoughts
# Gedanken in JSON konvertieren
thought_list = []
for thought in thoughts:
# Autor ermitteln
author_name = "Anonym"
if thought.user_id:
user = User.query.get(thought.user_id)
if user:
author_name = user.username
# Zeitstempel formatieren
timestamp = thought.timestamp.strftime("%d.%m.%Y %H:%M")
thought_list.append({
"id": thought.id,
"title": thought.title,
"content": thought.content,
"keywords": thought.keywords,
"author": author_name,
"timestamp": timestamp
})
return jsonify(thought_list)
@app.route('/api/nodes/<int:node_id>/thoughts', methods=['POST'])
@login_required
def add_node_thought(node_id):
"""API-Endpunkt zum Hinzufügen eines Gedankens zu einem Knoten."""
# Node existiert?
node = MindMapNode.query.get_or_404(node_id)
# Daten aus Anfrage extrahieren
data = request.json
if not data or not data.get('content'):
return jsonify({"error": "Inhalt ist erforderlich"}), 400
# Neuen Gedanken erstellen
thought = Thought(
title=data.get('title', f"Gedanke zu {node.name}"),
content=data.get('content'),
keywords=data.get('keywords', ''),
branch=node.name, # Branch auf den Node-Namen setzen
user_id=current_user.id
)
# In Datenbank speichern
db.session.add(thought)
# Dem Knoten zuordnen über die many-to-many Beziehung
node.thoughts.append(thought)
db.session.commit()
# Autor ermitteln (für die Antwort)
author_name = current_user.username
# Zeitstempel formatieren
timestamp = thought.timestamp.strftime("%d.%m.%Y %H:%M")
# Gedankenobjekt zurückgeben
return jsonify({
"id": thought.id,
"title": thought.title,
"content": thought.content,
"keywords": thought.keywords,
"author": author_name,
"timestamp": timestamp
})
@app.route('/api/thoughts/<int:thought_id>', methods=['GET'])
def get_thought(thought_id):
thought = Thought.query.get_or_404(thought_id)
return jsonify({
'id': thought.id,
'content': thought.content,
'author': thought.author.username,
'timestamp': thought.timestamp.strftime('%d.%m.%Y, %H:%M'),
'branch': thought.branch,
'comments_count': len(thought.comments)
})
@app.route('/api/thoughts', methods=['POST'])
@login_required
def add_thought():
data = request.json
node_id = data.get('node_id')
content = data.get('content')
title = data.get('title')
abstract = data.get('abstract')
keywords = data.get('keywords')
source_type = data.get('source_type')
color_code = data.get('color_code')
if not all([node_id, content, title]):
return jsonify({'error': 'Pflichtfelder fehlen'}), 400
node = MindMapNode.query.get_or_404(node_id)
thought = Thought(
content=content,
title=title,
abstract=abstract,
keywords=keywords,
source_type=source_type,
color_code=color_code,
branch=node.name,
user_id=current_user.id
)
db.session.add(thought)
node.thoughts.append(thought)
db.session.commit()
return jsonify({
'id': thought.id,
'title': thought.title,
'content': thought.content,
'abstract': thought.abstract,
'keywords': thought.keywords,
'color_code': thought.color_code,
'author': thought.author.username,
'timestamp': thought.timestamp.strftime('%d.%m.%Y, %H:%M'),
'branch': thought.branch,
'average_rating': thought.average_rating
})
@app.route('/api/comments/<int:thought_id>', methods=['GET'])
def get_comments(thought_id):
thought = Thought.query.get_or_404(thought_id)
comments = [
{
'id': comment.id,
'content': comment.content,
'author': comment.author.username,
'timestamp': comment.timestamp.strftime('%d.%m.%Y, %H:%M')
}
for comment in thought.comments
]
return jsonify(comments)
@app.route('/api/comments', methods=['POST'])
@login_required
def add_comment():
data = request.json
thought_id = data.get('thought_id')
content = data.get('content')
if not thought_id or not content:
return jsonify({'error': 'Fehlende Daten'}), 400
thought = Thought.query.get_or_404(thought_id)
comment = Comment(
content=content,
thought_id=thought_id,
user_id=current_user.id
)
db.session.add(comment)
db.session.commit()
return jsonify({
'id': comment.id,
'content': comment.content,
'author': comment.author.username,
'timestamp': comment.timestamp.strftime('%d.%m.%Y, %H:%M')
})
# Admin routes
@app.route('/admin')
@admin_required
def admin():
users = User.query.all()
nodes = MindMapNode.query.all()
thoughts = Thought.query.all()
# Aktuelles Datum für Logs
now = datetime.now()
return render_template('admin.html', users=users, nodes=nodes, thoughts=thoughts, now=now, active_tab='users')
# Zusätzliche Route für die Admin-Dashboard-Seite
@app.route('/admin/dashboard')
@admin_required
def admin_dashboard():
users = User.query.all()
nodes = MindMapNode.query.all()
thoughts = Thought.query.all()
now = datetime.now()
return render_template('admin.html', users=users, nodes=nodes, thoughts=thoughts, now=now, active_tab='dashboard')
# Zusätzliche Route für die Admin-Benutzer-Seite
@app.route('/admin/users')
@admin_required
def admin_users():
users = User.query.all()
nodes = MindMapNode.query.all()
thoughts = Thought.query.all()
now = datetime.now()
return render_template('admin.html', users=users, nodes=nodes, thoughts=thoughts, now=now, active_tab='users')
# Zusätzliche Route für die Admin-Gedanken-Seite
@app.route('/admin/thoughts')
@admin_required
def admin_thoughts():
users = User.query.all()
nodes = MindMapNode.query.all()
thoughts = Thought.query.all()
now = datetime.now()
return render_template('admin.html', users=users, nodes=nodes, thoughts=thoughts, now=now, active_tab='thoughts')
# Zusätzliche Route für die Admin-Mindmap-Seite
@app.route('/admin/mindmap')
@admin_required
def admin_mindmap():
users = User.query.all()
nodes = MindMapNode.query.all()
thoughts = Thought.query.all()
now = datetime.now()
return render_template('admin.html', users=users, nodes=nodes, thoughts=thoughts, now=now, active_tab='nodes')
@app.route('/api/thoughts/<int:thought_id>/relations', methods=['GET'])
def get_thought_relations(thought_id):
thought = Thought.query.get_or_404(thought_id)
relations = []
for relation in thought.outgoing_relations:
relations.append({
'id': relation.id,
'source_id': relation.source_id,
'target_id': relation.target_id,
'relation_type': relation.relation_type.value,
'created_by': User.query.get(relation.created_by_id).username,
'created_at': relation.created_at.strftime('%d.%m.%Y, %H:%M')
})
return jsonify(relations)
@app.route('/api/relations', methods=['POST'])
@login_required
def create_relation():
data = request.json
source_id = data.get('source_id')
target_id = data.get('target_id')
relation_type = data.get('relation_type')
if not all([source_id, target_id, relation_type]):
return jsonify({'error': 'Pflichtfelder fehlen'}), 400
try:
relation_type_enum = RelationType[relation_type.upper()]
except KeyError:
return jsonify({'error': 'Ungültiger Relationstyp'}), 400
relation = ThoughtRelation(
source_id=source_id,
target_id=target_id,
relation_type=relation_type_enum,
created_by_id=current_user.id
)
db.session.add(relation)
db.session.commit()
return jsonify({
'id': relation.id,
'source_id': relation.source_id,
'target_id': relation.target_id,
'relation_type': relation.relation_type.value,
'created_by': current_user.username,
'created_at': relation.created_at.strftime('%d.%m.%Y, %H:%M')
})
@app.route('/api/thoughts/<int:thought_id>/rate', methods=['POST'])
@login_required
def rate_thought(thought_id):
data = request.json
score = data.get('score')
if not isinstance(score, int) or score < 1 or score > 5:
return jsonify({'error': 'Bewertung muss zwischen 1 und 5 liegen'}), 400
rating = ThoughtRating.query.filter_by(
thought_id=thought_id,
user_id=current_user.id
).first()
if rating:
rating.relevance_score = score
else:
rating = ThoughtRating(
thought_id=thought_id,
user_id=current_user.id,
relevance_score=score
)
db.session.add(rating)
db.session.commit()
thought = Thought.query.get(thought_id)
return jsonify({
'thought_id': thought_id,
'average_rating': thought.average_rating,
'total_ratings': len(thought.ratings)
})
@app.route('/api/thoughts/search', methods=['GET'])
def search_thoughts():
query = request.args.get('q', '')
keywords = request.args.getlist('keywords')
min_rating = request.args.get('min_rating', type=float)
source_type = request.args.get('source_type')
thoughts_query = Thought.query
if query:
thoughts_query = thoughts_query.filter(
db.or_(
Thought.title.ilike(f'%{query}%'),
Thought.content.ilike(f'%{query}%'),
Thought.abstract.ilike(f'%{query}%')
)
)
if keywords:
for keyword in keywords:
thoughts_query = thoughts_query.filter(Thought.keywords.ilike(f'%{keyword}%'))
if source_type:
thoughts_query = thoughts_query.filter_by(source_type=source_type)
thoughts = thoughts_query.all()
if min_rating is not None:
thoughts = [t for t in thoughts if t.average_rating >= min_rating]
result = []
for thought in thoughts:
result.append({
'id': thought.id,
'title': thought.title,
'abstract': thought.abstract,
'keywords': thought.keywords,
'color_code': thought.color_code,
'author': thought.author.username,
'timestamp': thought.timestamp.strftime('%d.%m.%Y, %H:%M'),
'branch': thought.branch,
'average_rating': thought.average_rating,
'total_ratings': len(thought.ratings)
})
return jsonify(result)
@app.route('/search')
def search_thoughts_page():
return render_template('search.html')
# Hilfsfunktion zum Erstellen einer Beispiel-Mindmap, falls keine Daten existieren
def create_sample_mindmap():
try:
if MindMapNode.query.count() > 0:
return # Wenn bereits Daten existieren, nichts tun
# Wurzelknoten erstellen
root = MindMapNode(name="Wissen")
db.session.add(root)
db.session.flush() # Flush zur Generierung der ID
# Hauptkategorien erstellen
philosophy = MindMapNode(name="Philosophie", parent_id=root.id)
science = MindMapNode(name="Wissenschaft", parent_id=root.id)
technology = MindMapNode(name="Technologie", parent_id=root.id)
arts = MindMapNode(name="Künste", parent_id=root.id)
db.session.add_all([philosophy, science, technology, arts])
db.session.flush()
# Unterkategorien erstellen
# Philosophie
db.session.add_all([
MindMapNode(name="Ethik", parent_id=philosophy.id),
MindMapNode(name="Logik", parent_id=philosophy.id),
MindMapNode(name="Metaphysik", parent_id=philosophy.id)
])
# Wissenschaft
db.session.add_all([
MindMapNode(name="Physik", parent_id=science.id),
MindMapNode(name="Biologie", parent_id=science.id),
MindMapNode(name="Chemie", parent_id=science.id)
])
# Technologie
db.session.add_all([
MindMapNode(name="Informatik", parent_id=technology.id),
MindMapNode(name="Künstliche Intelligenz", parent_id=technology.id),
MindMapNode(name="Robotik", parent_id=technology.id)
])
# Künste
db.session.add_all([
MindMapNode(name="Musik", parent_id=arts.id),
MindMapNode(name="Literatur", parent_id=arts.id),
MindMapNode(name="Malerei", parent_id=arts.id)
])
db.session.commit()
print("Beispiel-Mindmap erfolgreich erstellt.")
except Exception as e:
db.session.rollback()
print(f"Fehler beim Erstellen der Beispiel-Mindmap: {str(e)}")
@app.route('/set_dark_mode', methods=['POST'])
def set_dark_mode():
try:
data = request.json
if data and 'darkMode' in data:
# Speichere den Dark Mode-Status in der Session
session['darkMode'] = str(data['darkMode']).lower()
# Mache die Session permanent (bleibt auch nach Browser-Schließung bestehen)
session.permanent = True
return jsonify({'success': True, 'darkMode': session['darkMode']})
return jsonify({'success': False, 'error': 'Ungültige Anfrage'})
except Exception as e:
app.logger.error(f"Fehler beim Setzen des Dark Mode: {str(e)}")
return jsonify({'success': False, 'error': str(e)})
@app.route('/get_dark_mode', methods=['GET'])
def get_dark_mode():
try:
dark_mode = session.get('darkMode', 'false')
return jsonify({'success': True, 'darkMode': dark_mode})
except Exception as e:
app.logger.error(f"Fehler beim Abrufen des Dark Mode: {str(e)}")
return jsonify({'success': False, 'error': str(e)})
# Fehlerseiten-Handler
@app.errorhandler(404)
def page_not_found(e):
"""Handler für 404 Fehler - Seite nicht gefunden."""
return render_template('errors/404.html'), 404
@app.errorhandler(403)
def forbidden(e):
"""Handler für 403 Fehler - Zugriff verweigert."""
return render_template('errors/403.html'), 403
@app.errorhandler(500)
def internal_server_error(e):
"""Handler für 500 Fehler - Interner Serverfehler."""
app.logger.error(f"500 Fehler: {str(e)}")
return render_template('errors/500.html'), 500
@app.errorhandler(429)
def too_many_requests(e):
"""Handler für 429 Fehler - Zu viele Anfragen."""
return render_template('errors/429.html'), 429
# Route für den KI-Assistenten API-Endpunkt
@app.route('/api/assistant', methods=['POST'])
def chat_with_assistant():
try:
# Daten aus der Anfrage extrahieren
data = request.json
messages = data.get('messages', [])
# Formatiere die Nachrichten für die OpenAI API
formatted_messages = []
for message in messages:
role = message['role']
if role == 'user':
formatted_messages.append({"role": "user", "content": message['content']})
elif role == 'assistant':
formatted_messages.append({"role": "assistant", "content": message['content']})
# Standard-Systemnachricht hinzufügen
formatted_messages.insert(0, {
"role": "system",
"content": "Du bist ein hilfreicher Assistent namens 'MindMap KI', der Benutzer bei ihren Fragen " +
"rund um Wissen, Lernen und dem Finden von Verbindungen zwischen Ideen unterstützt. " +
"Sei präzise, freundlich und hilfsbereit. Versuche, deine Antworten prägnant zu halten, " +
"aber biete dennoch wertvolle Informationen. Wenn du eine Frage nicht beantworten kannst, " +
"sag es ehrlich. Antworte auf Deutsch."
})
# Anfrage an die OpenAI API senden
response = openai.chat.completions.create(
model="gpt-3.5-turbo",
messages=formatted_messages,
max_tokens=500,
temperature=0.7,
)
# Antwort extrahieren
assistant_reply = response.choices[0].message.content
return jsonify({"success": True, "response": assistant_reply})
except Exception as e:
# Log-Fehler für die Serverkonsole
print(f"Fehler bei der KI-Anfrage: {str(e)}")
return jsonify({"success": False, "error": "Fehler bei der Verarbeitung der Anfrage"}), 500
# Route für Benutzer-Merkliste und persönliche Mindmap
@app.route('/my-account')
@login_required
def my_account():
return render_template('my_account.html', current_year=current_year)
# Flask starten
if __name__ == '__main__':
with app.app_context():
# Make sure tables exist
db.create_all()
app.run(host="0.0.0.0", debug=True)

View File

@@ -1,133 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from app import app, db, User, Thought, Comment, MindMapNode, ThoughtRelation, ThoughtRating, RelationType
import os
def init_database():
with app.app_context():
# Datenbank löschen und neu erstellen
if os.path.exists('instance/mindmap.db'):
os.remove('instance/mindmap.db')
db.create_all()
# Admin-Benutzer erstellen
admin = User(username='admin', email='admin@example.com', is_admin=True)
admin.set_password('admin')
db.session.add(admin)
# Beispiel-Benutzer erstellen
user = User(username='user', email='user@example.com')
user.set_password('user')
db.session.add(user)
# Commit um IDs zu generieren
db.session.commit()
# Grundlegende Mindmap-Struktur erstellen
root = MindMapNode(name='Wissen')
db.session.add(root)
# Hauptzweige erstellen
branches = [
'Philosophie',
'Wissenschaft',
'Technologie',
'Kunst',
'Geschichte'
]
branch_nodes = {}
for branch in branches:
node = MindMapNode(name=branch, parent=root)
branch_nodes[branch] = node
db.session.add(node)
# Commit um IDs zu generieren
db.session.commit()
# Beispiel-Gedanken erstellen
thoughts = [
{
'title': 'Künstliche Intelligenz und Bewusstsein',
'content': 'Die Frage nach maschinellem Bewusstsein ist fundamental für die KI-Ethik.',
'abstract': 'Eine Untersuchung der philosophischen Implikationen von KI-Bewusstsein.',
'keywords': 'KI, Bewusstsein, Ethik, Philosophie',
'branch': 'Philosophie',
'color_code': '#FF5733',
'source_type': 'Markdown'
},
{
'title': 'Quantenmechanik und Realität',
'content': 'Die Kopenhagener Deutung und ihre Auswirkungen auf unser Verständnis der Realität.',
'abstract': 'Eine Analyse verschiedener Interpretationen der Quantenmechanik.',
'keywords': 'Quantenmechanik, Physik, Realität',
'branch': 'Wissenschaft',
'color_code': '#33FF57',
'source_type': 'PDF'
}
]
thought_objects = []
for t in thoughts:
thought = Thought(
title=t['title'],
content=t['content'],
abstract=t['abstract'],
keywords=t['keywords'],
branch=t['branch'],
color_code=t['color_code'],
source_type=t['source_type'],
user_id=user.id # Hier wird die user_id gesetzt
)
branch_node = branch_nodes[t['branch']]
branch_node.thoughts.append(thought)
thought_objects.append(thought)
db.session.add(thought)
# Commit um IDs zu generieren
db.session.commit()
# Beispiel-Relation erstellen
relation = ThoughtRelation(
source_id=thought_objects[0].id,
target_id=thought_objects[1].id,
relation_type=RelationType.INSPIRES,
created_by_id=user.id
)
db.session.add(relation)
# Beispiel-Bewertung erstellen
rating = ThoughtRating(
thought_id=thought_objects[0].id,
user_id=admin.id,
relevance_score=5
)
db.session.add(rating)
# Beispiel-Kommentare erstellen
for thought in thought_objects:
comment = Comment(
content=f'Interessante Perspektive zu {thought.title}!',
thought_id=thought.id,
user_id=admin.id
)
db.session.add(comment)
# Finale Commit
db.session.commit()
print("Datenbank wurde erfolgreich initialisiert!")
def init_db():
# Alias für die Kompatibilität mit älteren Scripts
init_database()
if __name__ == '__main__':
init_database()
print("Datenbank wurde erfolgreich initialisiert!")
print("Sie können die Anwendung jetzt mit 'python app.py' starten")
print("Anmelden mit:")
print(" Admin: username=admin, password=admin")
print(" User: username=user, password=user")

Binary file not shown.

View File

@@ -1 +0,0 @@
../autoprefixer/bin/autoprefixer

View File

@@ -1 +0,0 @@
../browserslist/cli.js

1
website/node_modules/.bin/cssesc generated vendored
View File

@@ -1 +0,0 @@
../cssesc/bin/cssesc

1
website/node_modules/.bin/csv2json generated vendored
View File

@@ -1 +0,0 @@
../d3-dsv/bin/dsv2json.js

1
website/node_modules/.bin/csv2tsv generated vendored
View File

@@ -1 +0,0 @@
../d3-dsv/bin/dsv2dsv.js

1
website/node_modules/.bin/dsv2dsv generated vendored
View File

@@ -1 +0,0 @@
../d3-dsv/bin/dsv2dsv.js

1
website/node_modules/.bin/dsv2json generated vendored
View File

@@ -1 +0,0 @@
../d3-dsv/bin/dsv2json.js

1
website/node_modules/.bin/glob generated vendored
View File

@@ -1 +0,0 @@
../glob/dist/esm/bin.mjs

1
website/node_modules/.bin/jiti generated vendored
View File

@@ -1 +0,0 @@
../jiti/bin/jiti.js

1
website/node_modules/.bin/json2csv generated vendored
View File

@@ -1 +0,0 @@
../d3-dsv/bin/json2dsv.js

1
website/node_modules/.bin/json2dsv generated vendored
View File

@@ -1 +0,0 @@
../d3-dsv/bin/json2dsv.js

1
website/node_modules/.bin/json2tsv generated vendored
View File

@@ -1 +0,0 @@
../d3-dsv/bin/json2dsv.js

1
website/node_modules/.bin/marked generated vendored
View File

@@ -1 +0,0 @@
../marked/bin/marked.js

View File

@@ -1 +0,0 @@
../mini-svg-data-uri/cli.js

1
website/node_modules/.bin/nanoid generated vendored
View File

@@ -1 +0,0 @@
../nanoid/bin/nanoid.cjs

View File

@@ -1 +0,0 @@
../which/bin/node-which

1
website/node_modules/.bin/resolve generated vendored
View File

@@ -1 +0,0 @@
../resolve/bin/resolve

1
website/node_modules/.bin/sucrase generated vendored
View File

@@ -1 +0,0 @@
../sucrase/bin/sucrase

View File

@@ -1 +0,0 @@
../sucrase/bin/sucrase-node

1
website/node_modules/.bin/tailwind generated vendored
View File

@@ -1 +0,0 @@
../tailwindcss/lib/cli.js

Some files were not shown because too many files have changed in this diff Show More