Compare commits
16 Commits
74307ba345
...
till-v2
| Author | SHA1 | Date | |
|---|---|---|---|
| b8ad3aea13 | |||
| edf3049e42 | |||
| d117978005 | |||
| 48d8463481 | |||
| 08314ec703 | |||
| 0bb7d8d0dc | |||
| 4a28c2c453 | |||
| 66d987857a | |||
| d58aba26c2 | |||
| 8f0a6d4372 | |||
| 5372fe220e | |||
| 11ab15127c | |||
| 0705ecce59 | |||
| 1c59b0b616 | |||
| d42c43db50 | |||
| e46264b201 |
13
.env
Normal file
13
.env
Normal 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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
|
||||||
10
Dockerfile
10
Dockerfile
@@ -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
165
README.md
@@ -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
102
ROADMAP.md
Normal 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
125
TOOLS.py
Executable 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()
|
||||||
BIN
__pycache__/app.cpython-311.pyc
Normal file
BIN
__pycache__/app.cpython-311.pyc
Normal file
Binary file not shown.
BIN
__pycache__/models.cpython-311.pyc
Normal file
BIN
__pycache__/models.cpython-311.pyc
Normal file
Binary file not shown.
4
cookies.txt
Normal file
4
cookies.txt
Normal 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.
|
||||||
|
|
||||||
@@ -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
BIN
database/systades.db
Normal file
Binary file not shown.
BIN
database/systades.db.backup
Normal file
BIN
database/systades.db.backup
Normal file
Binary file not shown.
@@ -1,7 +0,0 @@
|
|||||||
version: "3.9"
|
|
||||||
services:
|
|
||||||
web:
|
|
||||||
build: .
|
|
||||||
ports:
|
|
||||||
- "5000:5000"
|
|
||||||
restart: always
|
|
||||||
@@ -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
258
init_db.py
Executable 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
230
models.py
Executable 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)
|
||||||
@@ -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
426
static/css/base-styles.css
Normal 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);
|
||||||
|
}
|
||||||
125
static/css/src/cybernetwork-bg.css
Normal file
125
static/css/src/cybernetwork-bg.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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');
|
||||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -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]();
|
||||||
95
static/js/modules/cyber-network-init.js
Normal file
95
static/js/modules/cyber-network-init.js
Normal 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;
|
||||||
|
}
|
||||||
240
static/js/modules/cyber-network.js
Normal file
240
static/js/modules/cyber-network.js
Normal 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;
|
||||||
@@ -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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
@@ -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
|
||||||
@@ -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
535
templates/base.html
Normal 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">
|
||||||
|
© {{ 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
603
templates/index.html
Normal 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
55
templates/login.html
Normal 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 %}
|
||||||
@@ -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
115
templates/register.html
Normal 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
105
templates/search.html
Normal 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
34
utils/__init__.py
Executable 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',
|
||||||
|
]
|
||||||
BIN
utils/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
utils/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/db_fix.cpython-311.pyc
Normal file
BIN
utils/__pycache__/db_fix.cpython-311.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/db_rebuild.cpython-311.pyc
Normal file
BIN
utils/__pycache__/db_rebuild.cpython-311.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/db_test.cpython-311.pyc
Normal file
BIN
utils/__pycache__/db_test.cpython-311.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/server.cpython-311.pyc
Normal file
BIN
utils/__pycache__/server.cpython-311.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/user_manager.cpython-311.pyc
Normal file
BIN
utils/__pycache__/user_manager.cpython-311.pyc
Normal file
Binary file not shown.
78
utils/db_fix.py
Executable file
78
utils/db_fix.py
Executable 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
81
utils/db_rebuild.py
Executable 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
120
utils/db_test.py
Executable 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
34
utils/server.py
Executable 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
159
utils/user_manager.py
Executable 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()
|
||||||
@@ -1 +0,0 @@
|
|||||||
OPENAI_API_KEY=sk-placeholder
|
|
||||||
@@ -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*
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
836
website/app.py
836
website/app.py
@@ -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)
|
|
||||||
@@ -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.
1
website/node_modules/.bin/autoprefixer
generated
vendored
1
website/node_modules/.bin/autoprefixer
generated
vendored
@@ -1 +0,0 @@
|
|||||||
../autoprefixer/bin/autoprefixer
|
|
||||||
1
website/node_modules/.bin/browserslist
generated
vendored
1
website/node_modules/.bin/browserslist
generated
vendored
@@ -1 +0,0 @@
|
|||||||
../browserslist/cli.js
|
|
||||||
1
website/node_modules/.bin/cssesc
generated
vendored
1
website/node_modules/.bin/cssesc
generated
vendored
@@ -1 +0,0 @@
|
|||||||
../cssesc/bin/cssesc
|
|
||||||
1
website/node_modules/.bin/csv2json
generated
vendored
1
website/node_modules/.bin/csv2json
generated
vendored
@@ -1 +0,0 @@
|
|||||||
../d3-dsv/bin/dsv2json.js
|
|
||||||
1
website/node_modules/.bin/csv2tsv
generated
vendored
1
website/node_modules/.bin/csv2tsv
generated
vendored
@@ -1 +0,0 @@
|
|||||||
../d3-dsv/bin/dsv2dsv.js
|
|
||||||
1
website/node_modules/.bin/dsv2dsv
generated
vendored
1
website/node_modules/.bin/dsv2dsv
generated
vendored
@@ -1 +0,0 @@
|
|||||||
../d3-dsv/bin/dsv2dsv.js
|
|
||||||
1
website/node_modules/.bin/dsv2json
generated
vendored
1
website/node_modules/.bin/dsv2json
generated
vendored
@@ -1 +0,0 @@
|
|||||||
../d3-dsv/bin/dsv2json.js
|
|
||||||
1
website/node_modules/.bin/glob
generated
vendored
1
website/node_modules/.bin/glob
generated
vendored
@@ -1 +0,0 @@
|
|||||||
../glob/dist/esm/bin.mjs
|
|
||||||
1
website/node_modules/.bin/jiti
generated
vendored
1
website/node_modules/.bin/jiti
generated
vendored
@@ -1 +0,0 @@
|
|||||||
../jiti/bin/jiti.js
|
|
||||||
1
website/node_modules/.bin/json2csv
generated
vendored
1
website/node_modules/.bin/json2csv
generated
vendored
@@ -1 +0,0 @@
|
|||||||
../d3-dsv/bin/json2dsv.js
|
|
||||||
1
website/node_modules/.bin/json2dsv
generated
vendored
1
website/node_modules/.bin/json2dsv
generated
vendored
@@ -1 +0,0 @@
|
|||||||
../d3-dsv/bin/json2dsv.js
|
|
||||||
1
website/node_modules/.bin/json2tsv
generated
vendored
1
website/node_modules/.bin/json2tsv
generated
vendored
@@ -1 +0,0 @@
|
|||||||
../d3-dsv/bin/json2dsv.js
|
|
||||||
1
website/node_modules/.bin/marked
generated
vendored
1
website/node_modules/.bin/marked
generated
vendored
@@ -1 +0,0 @@
|
|||||||
../marked/bin/marked.js
|
|
||||||
1
website/node_modules/.bin/mini-svg-data-uri
generated
vendored
1
website/node_modules/.bin/mini-svg-data-uri
generated
vendored
@@ -1 +0,0 @@
|
|||||||
../mini-svg-data-uri/cli.js
|
|
||||||
1
website/node_modules/.bin/nanoid
generated
vendored
1
website/node_modules/.bin/nanoid
generated
vendored
@@ -1 +0,0 @@
|
|||||||
../nanoid/bin/nanoid.cjs
|
|
||||||
1
website/node_modules/.bin/node-which
generated
vendored
1
website/node_modules/.bin/node-which
generated
vendored
@@ -1 +0,0 @@
|
|||||||
../which/bin/node-which
|
|
||||||
1
website/node_modules/.bin/resolve
generated
vendored
1
website/node_modules/.bin/resolve
generated
vendored
@@ -1 +0,0 @@
|
|||||||
../resolve/bin/resolve
|
|
||||||
1
website/node_modules/.bin/sucrase
generated
vendored
1
website/node_modules/.bin/sucrase
generated
vendored
@@ -1 +0,0 @@
|
|||||||
../sucrase/bin/sucrase
|
|
||||||
1
website/node_modules/.bin/sucrase-node
generated
vendored
1
website/node_modules/.bin/sucrase-node
generated
vendored
@@ -1 +0,0 @@
|
|||||||
../sucrase/bin/sucrase-node
|
|
||||||
1
website/node_modules/.bin/tailwind
generated
vendored
1
website/node_modules/.bin/tailwind
generated
vendored
@@ -1 +0,0 @@
|
|||||||
../tailwindcss/lib/cli.js
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user