Compare commits
171 Commits
till-v2
...
be767e9f27
| Author | SHA1 | Date | |
|---|---|---|---|
| be767e9f27 | |||
| 6aaf073ffb | |||
| b6080f96cf | |||
| 9ebf4b7abd | |||
| 5d35983f15 | |||
| 7278ece2b8 | |||
| f677e98795 | |||
| 40c3f6d9b4 | |||
| 9939db731b | |||
| d0f32a8355 | |||
| 02d1801fc9 | |||
| c51a8e23ca | |||
| 1600647bc4 | |||
| 82d03f6c48 | |||
| d1352286b7 | |||
| e7b3374c53 | |||
| 4bf046c657 | |||
| 892a1212d9 | |||
| 8440b7c30d | |||
| 74c2783b1a | |||
| fcd82eb5c9 | |||
| c654986f65 | |||
| f4ab617c59 | |||
| 9c36179f29 | |||
| f292cf1ce5 | |||
| 3a20ea0282 | |||
| 44986bfa23 | |||
| 41195a44cb | |||
| e1cd23230d | |||
| 77095e91b6 | |||
| 6322e046c5 | |||
| 2584bae149 | |||
| c0bd7a3986 | |||
| dec4a57b89 | |||
| 6a3b3a81c1 | |||
| 629813c486 | |||
| 7cb2bf1ed0 | |||
| ed1d41d316 | |||
| fe3cf81bc7 | |||
| 2e68ae30b8 | |||
| 858fdf5c44 | |||
| 4948f3ad2a | |||
| 52954e51f1 | |||
| 14f1356551 | |||
| 44c7183e97 | |||
| d99cae4956 | |||
| 3ae5f2527c | |||
| 412dabd5c1 | |||
| 5ade301f80 | |||
| 118f8ed132 | |||
| 121f46df01 | |||
| 4b0613eb6b | |||
| dd172d8596 | |||
| 653b3abe91 | |||
| ec50886145 | |||
| c888dcc452 | |||
| acceec4352 | |||
| f093a6211c | |||
| 58a5ea00bd | |||
| aeb829e36a | |||
| 49e5e19b7c | |||
| 903e095b66 | |||
| 2d083f5c0a | |||
| cbe8dc3bd0 | |||
| 7c1533c20d | |||
| c285b7d8dc | |||
| 21ddd38e13 | |||
| 1cf7bfbf76 | |||
| 40b28134fc | |||
| d5fababd49 | |||
| 7c742debdf | |||
| 4a4271a23c | |||
| c1038b479f | |||
| cd0083544a | |||
| a03bec2dff | |||
| 997479581d | |||
| 8153390e35 | |||
| bfa155628e | |||
| 700a8a3b89 | |||
| 808481ffe7 | |||
| e2c8cfaacf | |||
| 78e37fa717 | |||
| b2cf50626a | |||
| 7f48526315 | |||
| 84f8a6bf31 | |||
| 7003c89447 | |||
| d0821db983 | |||
| f0c4c514c4 | |||
| 304a399b85 | |||
| a5396c0d6e | |||
| 9cc4e70cba | |||
| a8cac08d30 | |||
| 42a7485ce1 | |||
| 54a5ccc224 | |||
| a99f82d4cf | |||
| 699127f41f | |||
| e8d356a27a | |||
| daf2704253 | |||
| 084059449f | |||
| c9bbc6ff25 | |||
| 742e3fda20 | |||
| 54aa246b79 | |||
| 505fb9aa47 | |||
| e4e6541b8c | |||
| e724181915 | |||
| 460c3f987e | |||
| 7f33dea278 | |||
| 726d9c9c70 | |||
| 81170fbd3d | |||
| eff3fda1ca | |||
| d49b266d96 | |||
| 34a08c4a6a | |||
| 7918de1723 | |||
| a0e4cd2208 | |||
| 2199d6007c | |||
| 7fb9452d09 | |||
| 1f3e60efde | |||
| 5e97381c8f | |||
| 4c402423c0 | |||
| 6d2595e3a6 | |||
| 29b44e5c52 | |||
| 693e542d5f | |||
| 4c3e476338 | |||
| 613c38ccb2 | |||
| 91fdd43fe0 | |||
| f36dd5ffaa | |||
| 2e1c3ce8b0 | |||
| d80c4c9aec | |||
| 3b0bea959c | |||
| cb3bfe0e6a | |||
| fd63810845 | |||
| 883973fe7b | |||
| 027e632856 | |||
| 406289e54f | |||
| 71b33e6cec | |||
| c74d3164bb | |||
| 4982cddeef | |||
| 631619ccb4 | |||
| f9881b678d | |||
| 259ce3cf69 | |||
| 9f4743eaea | |||
| de0f837cfd | |||
| 1c49ddfb19 | |||
| 46c16e5f01 | |||
| 84667bca00 | |||
| 779449559d | |||
| 721a10e861 | |||
| a431873ca2 | |||
| e4ab1e1bb5 | |||
| f69356473b | |||
| 38ac13e87c | |||
| 0afb8cb6e2 | |||
| 5d282d2108 | |||
| 4aba72efa2 | |||
| 89476d5353 | |||
| 0f7a33340a | |||
| 73501e7cda | |||
| 9f8eba6736 | |||
| b6bf9f387d | |||
| d9fe1f8efc | |||
| fd7bc59851 | |||
| 55f1f87509 | |||
| 03f8761312 | |||
| 506748fda7 | |||
| 6d069f68cd | |||
| 4310239a7a | |||
| e9fe907af0 | |||
| 0c69d9aba3 | |||
| 6da85cdece | |||
| a073b09115 | |||
| f1f4870989 |
34
.cursor/rules/ai-integration.mdc
Normal file
34
.cursor/rules/ai-integration.mdc
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
description:
|
||||||
|
globs:
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
# KI-Integration
|
||||||
|
|
||||||
|
Die Anwendung integriert OpenAI für KI-Funktionalitäten:
|
||||||
|
|
||||||
|
## Konfiguration
|
||||||
|
- [app.py](mdc:app.py): OpenAI-Client-Initialisierung
|
||||||
|
- [requirements.txt](mdc:requirements.txt): OpenAI SDK als Abhängigkeit
|
||||||
|
|
||||||
|
## Endpunkte
|
||||||
|
- `/api/assistant`: Hauptendpunkt für KI-Anfragen
|
||||||
|
|
||||||
|
## Funktionalitäten
|
||||||
|
- Chatbot-Integration: Benutzer können mit einem KI-Assistenten kommunizieren
|
||||||
|
- Inhaltsanalyse: KI kann Gedanken und Konzepte analysieren
|
||||||
|
- Vorschläge: Kontextbezogene Vorschläge basierend auf dem Benutzerkontext
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
- Verwendet den OpenAI SDK für API-Aufrufe
|
||||||
|
- Kontextübergabe für konsistente Konversationen
|
||||||
|
- Streaming-Antworten für bessere Benutzererfahrung
|
||||||
|
|
||||||
|
## Konfigurationsparameter
|
||||||
|
- `OPENAI_API_KEY`: API-Schlüssel (in .env-Datei)
|
||||||
|
- Das System verwendet vorwiegend das Chat-Completion-API
|
||||||
|
|
||||||
|
## Sicherheitsmaßnahmen
|
||||||
|
- API-Schlüssel werden sicher über Umgebungsvariablen geladen
|
||||||
|
- Ratenbegrenzung und Fehlerbehandlung für API-Aufrufe
|
||||||
|
- Eingabevalidierung vor API-Anfragen
|
||||||
36
.cursor/rules/authentication.mdc
Normal file
36
.cursor/rules/authentication.mdc
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
description:
|
||||||
|
globs:
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
# Authentifizierung und Benutzerrollen
|
||||||
|
|
||||||
|
Die Anwendung nutzt Flask-Login für das Authentifizierungssystem:
|
||||||
|
|
||||||
|
## Hauptkomponenten
|
||||||
|
- [LoginManager](mdc:app.py): Konfiguration im app.py
|
||||||
|
- [User Model](mdc:models.py): Die User-Klasse implementiert UserMixin für Flask-Login
|
||||||
|
- Passwort-Hashing: Verwendet Werkzeug Security für sichere Passwort-Speicherung
|
||||||
|
|
||||||
|
## Authentifizierungsrouten
|
||||||
|
- `/login`: Benutzeranmeldung (GET/POST)
|
||||||
|
- `/register`: Benutzerregistrierung (GET/POST)
|
||||||
|
- `/logout`: Benutzerabmeldung
|
||||||
|
|
||||||
|
## Benutzerrollen
|
||||||
|
- Reguläre Benutzer: Grundlegende Funktionen
|
||||||
|
- Administratoren (`is_admin=True`): Erweiterte Privilegien
|
||||||
|
|
||||||
|
## Zugriffskontrollen
|
||||||
|
- `@login_required`: Decorator für routenspezifischen Authentifizierungsschutz
|
||||||
|
- `@admin_required`: Benutzerdefinierter Decorator für Admin-Zugriffskontrolle
|
||||||
|
|
||||||
|
## Sitzungsverwaltung
|
||||||
|
- Tracking von Anmeldezeit (`last_login`)
|
||||||
|
- Langlebige Sitzungen für Präferenzen (z.B. Dark Mode)
|
||||||
|
- Angepasste Flash-Nachrichten
|
||||||
|
|
||||||
|
## Profilmanagement
|
||||||
|
- `/settings`: Benutzereinstellungen aktualisieren
|
||||||
|
- Passwortänderung
|
||||||
|
- Profildetails (Biografie, Avatar, etc.)
|
||||||
31
.cursor/rules/configuration.mdc
Normal file
31
.cursor/rules/configuration.mdc
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
description:
|
||||||
|
globs:
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
# Konfiguration und Umgebungsvariablen
|
||||||
|
|
||||||
|
Die Anwendung verwendet Umgebungsvariablen für die Konfiguration:
|
||||||
|
|
||||||
|
## Konfigurationsdateien
|
||||||
|
- [.env](mdc:.env): Haupt-Umgebungsvariablen (nicht in Git)
|
||||||
|
- [example.env](mdc:example.env): Beispiel-Konfiguration als Vorlage
|
||||||
|
|
||||||
|
## Wichtige Konfigurationsparameter
|
||||||
|
- `SECRET_KEY`: Geheimer Schlüssel für Flask-Sitzungen
|
||||||
|
- `SQLALCHEMY_DATABASE_URI`: Datenbankverbindung
|
||||||
|
- `OPENAI_API_KEY`: API-Schlüssel für OpenAI-Integration
|
||||||
|
|
||||||
|
## Anwendungsinitialisierung
|
||||||
|
- [run.py](mdc:run.py): Lädt Umgebungsvariablen und startet die Anwendung
|
||||||
|
- [app.py](mdc:app.py): Konfiguriert Flask mit den geladenen Umgebungsvariablen
|
||||||
|
- [init_db.py](mdc:init_db.py): Initialisiert die Datenbank mit Beispieldaten
|
||||||
|
|
||||||
|
## Datenbank-Konfiguration
|
||||||
|
- SQLite-Datenbank im `/database`-Verzeichnis
|
||||||
|
- Automatische Erstellung der Datenbankstruktur bei Anwendungsstart
|
||||||
|
- Beispieldaten werden mit `init_database()` erstellt
|
||||||
|
|
||||||
|
## Ausführung der Anwendung
|
||||||
|
- Entwicklungsserver: `python run.py`
|
||||||
|
- In Produktion: Nutzung von Gunicorn (siehe requirements.txt)
|
||||||
31
.cursor/rules/data-models.mdc
Normal file
31
.cursor/rules/data-models.mdc
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
description:
|
||||||
|
globs:
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
# Datenmodelle
|
||||||
|
|
||||||
|
Die Anwendung verwendet SQLAlchemy als ORM mit folgenden Hauptmodellen:
|
||||||
|
|
||||||
|
## Benutzer und Authentifizierung
|
||||||
|
- [User](mdc:models.py): Benutzermodell mit Authentifizierung und Profildaten
|
||||||
|
|
||||||
|
## Mind-Mapping und Wissensorganisation
|
||||||
|
- [Category](mdc:models.py): Wissenschaftliche Kategorien zur Organisation der Mindmap
|
||||||
|
- [MindMapNode](mdc:models.py): Knoten in der öffentlichen Mindmap
|
||||||
|
- [UserMindmap](mdc:models.py): Benutzerspezifische Mindmaps
|
||||||
|
- [UserMindmapNode](mdc:models.py): Speichert Positionen von Knoten in Benutzer-Mindmaps
|
||||||
|
- [MindmapNote](mdc:models.py): Private Notizen zu Mindmap-Elementen
|
||||||
|
|
||||||
|
## Gedanken und Inhalte
|
||||||
|
- [Thought](mdc:models.py): Gedanken und Konzepte, die in Mindmaps verknüpft werden
|
||||||
|
- [ThoughtRelation](mdc:models.py): Verknüpfungen zwischen verschiedenen Gedanken
|
||||||
|
- [ThoughtRating](mdc:models.py): Bewertungen von Gedanken durch Benutzer
|
||||||
|
- [Comment](mdc:models.py): Kommentare zu Gedanken
|
||||||
|
|
||||||
|
## Hauptbeziehungen
|
||||||
|
- Benutzer → Gedanken: 1-zu-n (Autor)
|
||||||
|
- Benutzer → MindMaps: 1-zu-n
|
||||||
|
- Gedanken ↔ MindMapNodes: n-zu-m
|
||||||
|
- Kategorien → MindMapNodes: 1-zu-n
|
||||||
|
- Gedanken ↔ Gedanken: über ThoughtRelation (gerichtete Beziehungen)
|
||||||
32
.cursor/rules/development-workflow.mdc
Normal file
32
.cursor/rules/development-workflow.mdc
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
description:
|
||||||
|
globs:
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
# Entwicklungs-Workflow
|
||||||
|
|
||||||
|
## Grundlegende Entwicklungsschritte
|
||||||
|
1. Umgebung einrichten: Python 3.11 und Abhängigkeiten installieren
|
||||||
|
2. `.env`-Datei basierend auf `example.env` erstellen
|
||||||
|
3. Datenbank initialisieren: `python init_db.py`
|
||||||
|
4. Entwicklungsserver starten: `python run.py`
|
||||||
|
|
||||||
|
## Datenbankentwicklung
|
||||||
|
- Models in [models.py](mdc:models.py) definieren
|
||||||
|
- Migrationen bei Schemaänderungen durchführen
|
||||||
|
- Testdaten über [init_db.py](mdc:init_db.py) bereitstellen
|
||||||
|
|
||||||
|
## Anwendungsentwicklung
|
||||||
|
- Neue Routen in [app.py](mdc:app.py) hinzufügen
|
||||||
|
- Frontend-Templates in `/templates` erstellen/anpassen
|
||||||
|
- API-Endpoints für AJAX/Frontend-Integration implementieren
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
- Tests mit pytest schreiben (siehe requirements.txt)
|
||||||
|
- Flask-Testumgebung für Integrationstest verwenden
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
- Immer auf Datenbankmodelle zurückgreifen (kein Raw-SQL)
|
||||||
|
- API-Endpunkte mit Authentifizierung schützen
|
||||||
|
- Flash-Nachrichten für Benutzerrückmeldungen verwenden
|
||||||
|
- Code-Dokumentation in deutscher Sprache halten
|
||||||
41
.cursor/rules/frontend-structure.mdc
Normal file
41
.cursor/rules/frontend-structure.mdc
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
---
|
||||||
|
description:
|
||||||
|
globs:
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
# Frontend-Struktur
|
||||||
|
|
||||||
|
Die Anwendung verwendet ein Flask-Jinja2-Template-System mit JavaScript-Erweiterungen:
|
||||||
|
|
||||||
|
## Template-Struktur
|
||||||
|
- `/templates`: Hauptverzeichnis für Jinja2-Templates
|
||||||
|
- `/templates/errors`: Fehlerseiten (404, 500, etc.)
|
||||||
|
- Layout-Templates für einheitliches Design
|
||||||
|
|
||||||
|
## Frontend-Assets
|
||||||
|
- `/static/css`: CSS-Dateien (mit Tailwind)
|
||||||
|
- `/static/css/src`: Quell-CSS-Dateien
|
||||||
|
- `/static/js`: JavaScript-Dateien
|
||||||
|
- `/static/js/modules`: Modulare JS-Komponenten
|
||||||
|
- `/static/img`: Bilder und grafische Elemente
|
||||||
|
|
||||||
|
## JavaScript-Funktionalität
|
||||||
|
- API-Integration: Asynchrone Kommunikation mit Backend
|
||||||
|
- Mindmap-Visualisierung: Interaktive Darstellung von Konzepten
|
||||||
|
- Benutzeroberflächen-Interaktivität: Drag & Drop, Tooltips, Modals
|
||||||
|
|
||||||
|
## CSS-Framework
|
||||||
|
- Tailwind CSS für responsive Design-Elemente
|
||||||
|
- TAILWIND CDN verwenden, nicht manuell build!
|
||||||
|
|
||||||
|
## Responsive Design
|
||||||
|
- Mobile-first Ansatz für verschiedene Gerätetypen
|
||||||
|
- Anpassungsfähiges Layout für verschiedene Bildschirmgrößen
|
||||||
|
|
||||||
|
## Zugänglichkeit
|
||||||
|
- Semantisches HTML für bessere Zugänglichkeit
|
||||||
|
- ARIA-Attribute für Screenreader-Unterstützung
|
||||||
|
|
||||||
|
## Internationalisierung
|
||||||
|
- Deutsche Benutzeroberfläche als Standard
|
||||||
|
- Vorbereitet für mehrsprachige Unterstützung
|
||||||
27
.cursor/rules/project-structure.mdc
Normal file
27
.cursor/rules/project-structure.mdc
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
description:
|
||||||
|
globs:
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
# Projekt-Struktur (Systades)
|
||||||
|
|
||||||
|
## Hauptkomponenten
|
||||||
|
Diese Python-Flask-Webanwendung implementiert ein Mind-Mapping und Gedanken-Management System:
|
||||||
|
|
||||||
|
- [app.py](mdc:app.py): Hauptanwendungsdatei mit allen Routen und Endpunkten
|
||||||
|
- [models.py](mdc:models.py): Datenbankmodelle und Beziehungen
|
||||||
|
- [run.py](mdc:run.py): Startpunkt der Anwendung
|
||||||
|
- [init_db.py](mdc:init_db.py): Initialisiert die Datenbank mit Beispieldaten
|
||||||
|
|
||||||
|
## Projektstruktur
|
||||||
|
- `/database`: Enthält SQLite-Datenbank
|
||||||
|
- `/docs`: Dokumentation
|
||||||
|
- `/static`: Frontend-Ressourcen (CSS, JS, Bilder)
|
||||||
|
- `/templates`: Jinja2-Templates für die Webseiten
|
||||||
|
- `/utils`: Hilfsfunktionen und -klassen
|
||||||
|
|
||||||
|
## Hauptfunktionalität
|
||||||
|
- Mind-Mapping: Visualisierung von Wissen und Beziehungen
|
||||||
|
- Gedanken-Management: Erfassung und Organisation von Ideen und Konzepten
|
||||||
|
- Benutzer-Management: Registrierung, Login, Profile
|
||||||
|
- API-Endpunkte: RESTful-Schnittstellen für Frontend-Integration
|
||||||
43
.cursor/rules/routing.mdc
Normal file
43
.cursor/rules/routing.mdc
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
---
|
||||||
|
description:
|
||||||
|
globs:
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
# Routing und API-Endpunkte
|
||||||
|
|
||||||
|
## Hauptrouten (Webseiten)
|
||||||
|
- `/`: Startseite
|
||||||
|
- `/login`, `/register`, `/logout`: Authentifizierung
|
||||||
|
- `/mindmap`: Öffentliche Mindmap-Ansicht
|
||||||
|
- `/profile`: Benutzerprofil
|
||||||
|
- `/settings`: Benutzereinstellungen
|
||||||
|
- `/search`: Suchfunktion
|
||||||
|
- `/my_account`: Kontoübersicht
|
||||||
|
|
||||||
|
## API-Endpunkte
|
||||||
|
### Mindmap-Verwaltung
|
||||||
|
- `/api/mindmap`: Öffentliche Mindmap-Daten abrufen
|
||||||
|
- `/api/mindmap/public`: Öffentliche Mindmap abrufen
|
||||||
|
- `/api/mindmap/user/<id>`: Benutzer-Mindmap abrufen
|
||||||
|
- `/api/mindmap/<id>/add_node`: Knoten hinzufügen
|
||||||
|
- `/api/mindmap/<id>/remove_node/<node_id>`: Knoten entfernen
|
||||||
|
- `/api/mindmap/<id>/update_node_position`: Knotenposition aktualisieren
|
||||||
|
- `/api/mindmap/<id>/notes`: Notizen verwalten
|
||||||
|
|
||||||
|
### Gedanken und Inhalte
|
||||||
|
- `/api/thoughts`: Gedanken erstellen
|
||||||
|
- `/api/thoughts/<id>`: Gedanken abrufen, aktualisieren, löschen
|
||||||
|
- `/api/thoughts/<id>/bookmark`: Lesezeichen setzen/entfernen
|
||||||
|
- `/api/nodes/<id>/thoughts`: Gedanken zu einem Knoten abrufen/hinzufügen
|
||||||
|
|
||||||
|
### System und Benutzereinstellungen
|
||||||
|
- `/api/set_dark_mode`, `/api/get_dark_mode`: Erscheinungsbild-Einstellungen
|
||||||
|
- `/api/assistant`: KI-Assistent-Kommunikation
|
||||||
|
- `/api/categories`: Kategorien abrufen
|
||||||
|
- `/api/get_flash_messages`: Flash-Nachrichten für AJAX-Anfragen
|
||||||
|
|
||||||
|
## Fehlerbehandlung
|
||||||
|
- 404: Page Not Found
|
||||||
|
- 403: Forbidden
|
||||||
|
- 500: Internal Server Error
|
||||||
|
- 429: Too Many Requests
|
||||||
8
.env
8
.env
@@ -2,12 +2,14 @@
|
|||||||
# Kopiere diese Datei zu .env und passe die Werte an
|
# Kopiere diese Datei zu .env und passe die Werte an
|
||||||
|
|
||||||
# Flask
|
# Flask
|
||||||
SECRET_KEY=dein-geheimer-schluessel-hier
|
FLASK_APP=app.py
|
||||||
|
FLASK_ENV=development
|
||||||
|
SECRET_KEY=your-secret-key-replace-in-production
|
||||||
|
|
||||||
# OpenAI API
|
# OpenAI API
|
||||||
OPENAI_API_KEY=sk-dein-openai-api-schluessel-hier
|
OPENAI_API_KEY=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
|
||||||
|
|
||||||
# Datenbank
|
# Datenbank
|
||||||
# Bei Bedarf kann hier eine andere Datenbank-URL angegeben werden
|
# Bei Bedarf kann hier eine andere Datenbank-URL angegeben werden
|
||||||
# Der Pfad wird relativ zum Projektverzeichnis angegeben
|
# Der Pfad wird relativ zum Projektverzeichnis angegeben
|
||||||
# SQLALCHEMY_DATABASE_URI=sqlite:////absoluter/pfad/zu/database/systades.db OPENAI_API_KEY=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
|
# SQLALCHEMY_DATABASE_URI=sqlite:////absoluter/pfad/zu/database/systades.db
|
||||||
8
.vscode/jsconfig.json
vendored
Normal file
8
.vscode/jsconfig.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "esnext",
|
||||||
|
"lib": [
|
||||||
|
"esnext"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
68
.vscode/main.js
vendored
Normal file
68
.vscode/main.js
vendored
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
/// <reference types="vscode" />
|
||||||
|
// @ts-check
|
||||||
|
// API: https://code.visualstudio.com/api/references/vscode-api
|
||||||
|
// @ts-ignore
|
||||||
|
const vscode = require('vscode');
|
||||||
|
* @typedef {import('vscode').ExtensionContext} ExtensionContext
|
||||||
|
* @typedef {import('vscode').commands} commands
|
||||||
|
* @typedef {import('vscode').window} window
|
||||||
|
* @typedef {import('vscode').TextEditor} TextEditor
|
||||||
|
* @typedef {import('vscode').TextDocument} TextDocument
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aktiviert die Erweiterung und registriert den Auto-Resume-Befehl
|
||||||
|
* @param {vscode.ExtensionContext} context - Der Erweiterungskontext
|
||||||
|
*/
|
||||||
|
function activate(context) {
|
||||||
|
const disposable = vscode.commands.registerCommand('extension.autoResume', () => {
|
||||||
|
const editor = vscode.window.activeTextEditor;
|
||||||
|
if (!editor) return;
|
||||||
|
|
||||||
|
const document = editor.document;
|
||||||
|
const text = document.getText();
|
||||||
|
|
||||||
|
// Track last click time to avoid multiple clicks
|
||||||
|
let lastClickTime = 0;
|
||||||
|
|
||||||
|
// Main function that looks for and clicks the resume link
|
||||||
|
function clickResumeLink() {
|
||||||
|
// Prevent clicking too frequently (3 second cooldown)
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - lastClickTime < 3000) return;
|
||||||
|
|
||||||
|
// Check if text contains rate limit text
|
||||||
|
if (text.includes('stop the agent after 25 tool calls') ||
|
||||||
|
text.includes('Note: we default stop')) {
|
||||||
|
|
||||||
|
// Find the resume link position
|
||||||
|
const resumePos = text.indexOf('resume the conversation');
|
||||||
|
if (resumePos !== -1) {
|
||||||
|
vscode.window.showInformationMessage('Auto-resuming conversation...');
|
||||||
|
lastClickTime = now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Führe periodisch aus
|
||||||
|
const interval = global.setInterval(clickResumeLink, 1000);
|
||||||
|
|
||||||
|
// Speichere das Intervall in den Subscriptions
|
||||||
|
context.subscriptions.push({
|
||||||
|
dispose: () => global.clearInterval(interval)
|
||||||
|
});
|
||||||
|
// Führe die Funktion sofort aus
|
||||||
|
clickResumeLink();
|
||||||
|
});
|
||||||
|
|
||||||
|
context.subscriptions.push(disposable);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deactivate() {
|
||||||
|
// Cleanup if needed
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
activate,
|
||||||
|
deactivate
|
||||||
|
}
|
||||||
239
COMMON_ERRORS.md
Normal file
239
COMMON_ERRORS.md
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
# ABSOLUTE DON'TS:
|
||||||
|
- Verwendung von npm anstelle der Tailwind CDN
|
||||||
|
- Implementierung von Content Security Policy (CSP) - UNTER KEINEN UMSTÄNDEN!
|
||||||
|
- Implementierung von Cross-Site Request Forgery (CSRF) Schutz
|
||||||
|
- Implementierung von Security Headers
|
||||||
|
- Verwendung von TypeScript anstelle von JavaScript
|
||||||
|
- Implementierung von OAuth oder anderen externen Authentifizierungsmethoden
|
||||||
|
|
||||||
|
# HÄUFIGE FEHLER:
|
||||||
|
- Verwendung der falschen Datenbank (die korrekte ist: database/systades.db)
|
||||||
|
- Falsche Pfadangaben bei statischen Dateien
|
||||||
|
- Vergessen der deutschen Spracheinstellungen in Templates
|
||||||
|
- Nicht beachten der vorhandenen Projektstruktur
|
||||||
|
- Falsche Einbindung der Neural Network Background Animation
|
||||||
|
- Verwendung von englischen Variablennamen in deutschen Funktionen
|
||||||
|
- Vergessen der Mindmap-Datenstruktur gemäß der Roadmap
|
||||||
|
|
||||||
|
# Häufige Fehler und Lösungen
|
||||||
|
|
||||||
|
## Datenbankfehler
|
||||||
|
|
||||||
|
### Fehler: "no such column: user.password"
|
||||||
|
|
||||||
|
**Fehlerbeschreibung:**
|
||||||
|
```
|
||||||
|
sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) no such column: user.password
|
||||||
|
[SQL: SELECT user.id AS user_id, user.username AS user_username, user.email AS user_email, user.password AS user_password, user.created_at AS user_created_at, user.is_active AS user_is_active, user.role AS user_role
|
||||||
|
FROM user
|
||||||
|
WHERE user.id = ?]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ursache:**
|
||||||
|
Die Spalte `password` fehlt in der Tabelle `user` der SQLite-Datenbank. Dies kann durch eine unvollständige Datenbankinitialisierung oder ein fehlerhaftes Schema-Update verursacht worden sein.
|
||||||
|
|
||||||
|
**Lösung:**
|
||||||
|
|
||||||
|
1. **Datenbank reparieren mit dem Fix-Skript**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python fix_user_table.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Dieses Skript:
|
||||||
|
- Prüft, ob die Tabelle `user` existiert und erstellt sie, falls nicht
|
||||||
|
- Prüft, ob die Spalte `password` existiert und fügt sie hinzu, falls nicht
|
||||||
|
|
||||||
|
2. **Standardbenutzer erstellen**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python create_default_users.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Dieses Skript:
|
||||||
|
- Erstellt Standardbenutzer (admin, user), falls keine vorhanden sind
|
||||||
|
- Setzt Passwörter mit korrektem Hashing
|
||||||
|
|
||||||
|
3. **Datenbank testen**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python test_app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Dieses Skript überprüft:
|
||||||
|
- Ob die Datenbank existiert
|
||||||
|
- Ob die Tabelle `user` korrekt konfiguriert ist
|
||||||
|
- Ob Benutzer vorhanden sind
|
||||||
|
|
||||||
|
## Häufige Ursachen für Datenbankfehler
|
||||||
|
|
||||||
|
1. **Inkonsistente Datenbankschemas**
|
||||||
|
- Unterschiede zwischen dem SQLAlchemy-Modell und der tatsächlichen Datenbankstruktur
|
||||||
|
- Fehlende Spalten, die in den Modellen definiert sind
|
||||||
|
|
||||||
|
2. **Falsche Datenbankinitialisierung**
|
||||||
|
- Die Datenbank wurde nicht korrekt initialisiert
|
||||||
|
- Fehler bei der Migration oder dem Schema-Update
|
||||||
|
|
||||||
|
3. **Datenbankdatei-Korrumpierung**
|
||||||
|
- Die SQLite-Datenbankdatei wurde beschädigt
|
||||||
|
- Lösung: Sicherung wiederherstellen oder Datenbank neu erstellen
|
||||||
|
|
||||||
|
## Vorbeugende Maßnahmen
|
||||||
|
|
||||||
|
1. **Regelmäßige Backups**
|
||||||
|
- Tägliche Sicherung der Datenbankdatei
|
||||||
|
|
||||||
|
2. **Schema-Validierung**
|
||||||
|
- Regelmäßige Überprüfung der Datenbankstruktur
|
||||||
|
- Automatisierte Tests für Datenbankschema
|
||||||
|
|
||||||
|
3. **Datenbankmigration**
|
||||||
|
- Verwenden Sie Flask-Migrate für strukturierte Datenbank-Updates
|
||||||
|
- Dokumentieren Sie alle Schemaänderungen
|
||||||
|
|
||||||
|
## Content Security Policy (CSP)
|
||||||
|
|
||||||
|
### Problem: Externe Ressourcen werden nicht geladen
|
||||||
|
**Fehler:** Externe Ressourcen wie CDNs werden nicht korrekt geladen.
|
||||||
|
|
||||||
|
**Lösung:**
|
||||||
|
1. Stellen Sie sicher, dass die URLs in den Templates korrekt sind:
|
||||||
|
```html
|
||||||
|
<link href="https://cdn.tailwindcss.com" rel="stylesheet">
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Überprüfen Sie die Netzwerkverbindung und ob die CDN-Domains erreichbar sind.
|
||||||
|
|
||||||
|
3. Verwenden Sie lokale Ressourcen als Alternative:
|
||||||
|
```html
|
||||||
|
<link href="{{ url_for('static', filename='css/tailwind.min.css') }}" rel="stylesheet">
|
||||||
|
```
|
||||||
|
|
||||||
|
### Problem: CSP-Fehler (script-src-elem 'none', font-src 'none', etc.)
|
||||||
|
**Fehler:** Skripte werden aufgrund von CSP-Einschränkungen blockiert.
|
||||||
|
|
||||||
|
**Lösung:**
|
||||||
|
1. WICHTIG: CSP-Einschränkungen NICHT implementieren! Diese sind im Projekt unerwünscht.
|
||||||
|
|
||||||
|
2. Falls CSP-Header bereits vorhanden sind, entfernen Sie diese aus der Anwendung:
|
||||||
|
```python
|
||||||
|
# Entfernen Sie diesen Code, falls vorhanden:
|
||||||
|
# @app.after_request
|
||||||
|
# def add_security_headers(response):
|
||||||
|
# response.headers['Content-Security-Policy'] = '...'
|
||||||
|
# return response
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Stellen Sie sicher, dass externe Ressourcen direkt geladen werden:
|
||||||
|
```html
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||||
|
<script src="{{ url_for('static', filename='js/alpine.min.js') }}" defer></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Für Inline-Skripte keine Nonce verwenden:
|
||||||
|
```html
|
||||||
|
<script>
|
||||||
|
// Inline-JavaScript-Code ohne Einschränkungen
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Problem: Tailwind CSS CDN wird blockiert
|
||||||
|
**Fehler:** Tailwind CSS kann nicht von CDN geladen werden.
|
||||||
|
|
||||||
|
**Lösung:**
|
||||||
|
1. Verwenden Sie die lokale Version von Tailwind CSS:
|
||||||
|
```html
|
||||||
|
<link href="{{ url_for('static', filename='css/tailwind.min.css') }}" rel="stylesheet">
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Alternativ können Sie die CDN-Version direkt im Template einbinden:
|
||||||
|
```html
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Stellen Sie sicher, dass die Datei `static/css/tailwind.min.css` existiert und aktuell ist.
|
||||||
|
|
||||||
|
## Authentifizierung
|
||||||
|
|
||||||
|
### Problem: Login funktioniert nicht
|
||||||
|
**Fehler:** Benutzer kann sich nicht einloggen.
|
||||||
|
|
||||||
|
**Lösung:**
|
||||||
|
1. Standard-Admin-Benutzer erstellen: `python TOOLS.py user:admin`
|
||||||
|
2. Passwort zurücksetzen: `python TOOLS.py user:reset-pw -u USERNAME -p NEWPASSWORD`
|
||||||
|
|
||||||
|
## Neural Network Background
|
||||||
|
|
||||||
|
### Problem: Hintergrund-Animation wird nicht angezeigt
|
||||||
|
**Fehler:** Die Neural Network Animation im Hintergrund erscheint nicht.
|
||||||
|
|
||||||
|
**Lösung:**
|
||||||
|
1. Überprüfen Sie, ob die Datei `static/neural-network-background.js` korrekt eingebunden ist:
|
||||||
|
```html
|
||||||
|
<script src="{{ url_for('static', filename='neural-network-background.js') }}"></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Initialisieren Sie die Animation im Template:
|
||||||
|
```html
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const background = new NeuralNetworkBackground();
|
||||||
|
background.initialize();
|
||||||
|
background.animate();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Stellen Sie sicher, dass keine CSS-Regeln die Animation überdecken:
|
||||||
|
```css
|
||||||
|
#neural-network-background {
|
||||||
|
z-index: -10;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mindmap-Funktionalität
|
||||||
|
|
||||||
|
### Problem: Mindmap-Daten werden nicht geladen
|
||||||
|
**Fehler:** Die dynamische Mindmap zeigt keine Daten an.
|
||||||
|
|
||||||
|
**Lösung:**
|
||||||
|
1. Überprüfen Sie die API-Endpunkte für die Mindmap-Daten:
|
||||||
|
```python
|
||||||
|
@app.route('/api/mindmap/nodes', methods=['GET'])
|
||||||
|
def get_mindmap_nodes():
|
||||||
|
# Implementierung...
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Stellen Sie sicher, dass die AJAX-Anfragen korrekt implementiert sind:
|
||||||
|
```javascript
|
||||||
|
fetch('/api/mindmap/nodes')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
// Verarbeitung der Mindmap-Daten
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Überprüfen Sie die Datenbankeinträge für Mindmap-Knoten und -Verbindungen.
|
||||||
|
|
||||||
|
## ChatGPT-Assistent
|
||||||
|
|
||||||
|
### Problem: Assistent reagiert nicht auf Eingaben
|
||||||
|
**Fehler:** Der ChatGPT-Assistent verarbeitet keine Benutzereingaben.
|
||||||
|
|
||||||
|
**Lösung:**
|
||||||
|
1. Überprüfen Sie die Einbindung der JavaScript-Datei:
|
||||||
|
```html
|
||||||
|
<script src="{{ url_for('static', filename='js/modules/chatgpt-assistant.js') }}"></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Stellen Sie sicher, dass der Assistent korrekt initialisiert wird:
|
||||||
|
```javascript
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const assistant = new ChatGPTAssistant();
|
||||||
|
assistant.initialize();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Überprüfen Sie die API-Endpunkte für die Kommunikation mit dem Assistenten.
|
||||||
33
Dockerfile
Normal file
33
Dockerfile
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# Dockerfile
|
||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
# Arbeitsverzeichnis in Container
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Systemabhängigkeiten installieren und Verzeichnisse anlegen
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y --no-install-recommends gcc && \
|
||||||
|
rm -rf /var/lib/apt/lists/* && \
|
||||||
|
mkdir -p /app/database
|
||||||
|
|
||||||
|
# pip auf den neuesten Stand bringen und Requirements installieren
|
||||||
|
COPY requirements.txt ./
|
||||||
|
RUN pip install --upgrade pip && \
|
||||||
|
pip install --no-cache-dir -U -r requirements.txt
|
||||||
|
|
||||||
|
# Anwendungscode kopieren
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Berechtigungen für database-Ordner
|
||||||
|
RUN chmod -R 777 /app/database
|
||||||
|
|
||||||
|
# Exponiere Port 5000 für Flask
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
|
# Setze Umgebungsvariablen
|
||||||
|
ENV FLASK_APP=app.py
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
|
|
||||||
|
# Startkommando mit spezifischen Flags für Produktion
|
||||||
|
CMD ["python", "app.py"]
|
||||||
80
README.md
80
README.md
@@ -6,9 +6,10 @@ Das MindMapProjekt ist eine interaktive Plattform zum Visualisieren, Erforschen
|
|||||||
## Technischer Stack
|
## Technischer Stack
|
||||||
- **Backend**: Python/Flask
|
- **Backend**: Python/Flask
|
||||||
- **Frontend**:
|
- **Frontend**:
|
||||||
- Tailwind CSS für moderne UI
|
- Tailwind CSS (via CDN) für moderne UI
|
||||||
- SVG-Bibliotheken für Visualisierungen (D3.js)
|
- SVG-Bibliotheken für Visualisierungen (D3.js)
|
||||||
- JavaScript/Alpine.js für interaktive Komponenten
|
- JavaScript/Alpine.js für interaktive Komponenten
|
||||||
|
- WebGL für animierte Hintergrundeffekte
|
||||||
- **Datenbank**: SQLite mit SQLAlchemy
|
- **Datenbank**: SQLite mit SQLAlchemy
|
||||||
- **KI-Integration**: OpenAI API für intelligente Assistenz
|
- **KI-Integration**: OpenAI API für intelligente Assistenz
|
||||||
|
|
||||||
@@ -61,16 +62,20 @@ Für detaillierte Hilfe: `python TOOLS.py -h`
|
|||||||
- [x] Favicon erstellen
|
- [x] Favicon erstellen
|
||||||
- [x] Setup-Skript für einfache Installation
|
- [x] Setup-Skript für einfache Installation
|
||||||
|
|
||||||
### Phase 2: Design-Überarbeitung 🔄
|
### Phase 2: Design-Überarbeitung ✅
|
||||||
- [x] Implementierung des Dark Mode
|
- [x] Implementierung des Dark Mode
|
||||||
- [x] Erstellung eines modernen, minimalistischen UI mit Tech-Ästhetik
|
- [x] Erstellung eines modernen, minimalistischen UI mit Tech-Ästhetik
|
||||||
- [x] Responsive Design für alle Geräte
|
- [x] Responsive Design für alle Geräte
|
||||||
- [ ] Gestaltung der Landing Page mit großer Typografie
|
- [x] Gestaltung der Landing Page mit großer Typografie
|
||||||
|
- [x] Animierter Neurales Netzwerk-Hintergrund mit WebGL
|
||||||
|
|
||||||
### Phase 3: Mindmap-Funktionalitäten 🔄
|
### Phase 3: Mindmap-Funktionalitäten 🔄
|
||||||
- [x] Verbesserte Visualisierung mit SVG und D3.js
|
- [x] Verbesserte Visualisierung mit SVG und D3.js
|
||||||
- [x] Implementierung der Mouseover-Funktion
|
- [x] Implementierung der Mouseover-Funktion
|
||||||
- [x] Entwicklung der Suchfunktion für Knoten
|
- [x] Entwicklung der Suchfunktion für Knoten
|
||||||
|
- [x] Clustertopologie für neuronale Netzwerkdarstellung
|
||||||
|
- [x] Fehlerbehandlung für robuste Datenverarbeitung und Knotenverweise
|
||||||
|
- [x] Verbesserte Verbindungserkennung zwischen Knoten
|
||||||
- [ ] Tagging-System für Inhalte
|
- [ ] Tagging-System für Inhalte
|
||||||
- [ ] Quellenmanagement und -verlinkung
|
- [ ] Quellenmanagement und -verlinkung
|
||||||
- [ ] Upload-Funktionalität an Knotenpunkten
|
- [ ] Upload-Funktionalität an Knotenpunkten
|
||||||
@@ -115,8 +120,8 @@ Für detaillierte Hilfe: `python TOOLS.py -h`
|
|||||||
|
|
||||||
## Aktueller Status
|
## Aktueller Status
|
||||||
- **Phase 1**: ✅ Abgeschlossen
|
- **Phase 1**: ✅ Abgeschlossen
|
||||||
- **Phase 2**: 🔄 In Bearbeitung (75% abgeschlossen)
|
- **Phase 2**: ✅ Abgeschlossen
|
||||||
- **Phase 3**: 🔄 In Bearbeitung (50% abgeschlossen)
|
- **Phase 3**: 🔄 In Bearbeitung (75% abgeschlossen)
|
||||||
|
|
||||||
## Aktuelle Fortschritte
|
## Aktuelle Fortschritte
|
||||||
- Grundlegende UI modernisiert mit Tailwind CSS und Dark Mode
|
- Grundlegende UI modernisiert mit Tailwind CSS und Dark Mode
|
||||||
@@ -124,11 +129,68 @@ Für detaillierte Hilfe: `python TOOLS.py -h`
|
|||||||
- Setup-Prozess vereinfacht mit einem Shell-Skript
|
- Setup-Prozess vereinfacht mit einem Shell-Skript
|
||||||
- Mindmap-Visualisierung komplett überarbeitet mit D3.js für eine interaktivere Erfahrung
|
- Mindmap-Visualisierung komplett überarbeitet mit D3.js für eine interaktivere Erfahrung
|
||||||
- Responsive Design für optimale Darstellung auf allen Geräten
|
- Responsive Design für optimale Darstellung auf allen Geräten
|
||||||
|
- Animierter neuronaler Netzwerk-Hintergrund mit WebGL implementiert
|
||||||
|
- Verbesserte neuronale Cluster-Darstellung in der MindMap-Ansicht
|
||||||
|
- Behebung von kritischen Fehlern in der Knotenvisualisierung und Verbindungserkennung
|
||||||
|
- Robustere Datenverarbeitung für Mindmap-Knoten implementiert
|
||||||
|
- Fehlerbehandlung für verschiedene API-Datenformate verbessert
|
||||||
|
|
||||||
|
## Neuronaler Netzwerk-Hintergrund
|
||||||
|
|
||||||
|
Ein wesentliches neues Feature ist der animierte Hintergrund, der ein neuronales Netzwerk simuliert:
|
||||||
|
|
||||||
|
- **WebGL-basierte Rendering-Engine** für hohe Performance
|
||||||
|
- **Dynamische Knoten und Verbindungen** mit realistischem Bewegungsverhalten
|
||||||
|
- **Neuronenfeuer-Simulation** mit Signalweiterleitung zwischen Knoten
|
||||||
|
- **Clustertopologie** für realistisches Erscheinungsbild
|
||||||
|
- **Anpassbare Farbgebung und Animationsparameter**
|
||||||
|
- **Flüssige Animationen** mit über 100 Knotenpunkten
|
||||||
|
|
||||||
|
Die Animation ist vollständig responsiv und passt sich automatisch an verschiedene Bildschirmgrößen an, ohne die Browser-Performance zu beeinträchtigen.
|
||||||
|
|
||||||
|
## Mindmap-Verbesserungen
|
||||||
|
|
||||||
|
Die Mindmap-Darstellung wurde grundlegend überarbeitet:
|
||||||
|
|
||||||
|
- **D3.js Force-Directed Graph** für intuitive Knotenpositionierung
|
||||||
|
- **Verbesserte Fehlerbehandlung** für robustere Datenverarbeitung
|
||||||
|
- **Neuronale Cluster-Gruppierung** von thematisch zusammengehörigen Inhalten
|
||||||
|
- **Glasmorphismus-Effekte** für moderne visuelle Darstellung
|
||||||
|
- **Verbesserte Hover- und Selektionseffekte**
|
||||||
|
- **Flüssige Animationen** bei Knotenauswahl und -fokussierung
|
||||||
|
|
||||||
## Nächste Schritte
|
## Nächste Schritte
|
||||||
- Fertigstellung der Landing Page
|
- Fertigstellung des Tagging-Systems für Gedanken
|
||||||
- Erstellung der "Wer sind wir?"-Seite
|
|
||||||
- Implementierung des Tagging-Systems für Gedanken
|
|
||||||
- Verbesserung der Gedankenansicht im Mindmap-Bereich
|
- Verbesserung der Gedankenansicht im Mindmap-Bereich
|
||||||
|
- Implementierung von Quellenmanagement
|
||||||
|
- Überarbeitung der Startseite mit neuen Features
|
||||||
|
|
||||||
*Zuletzt aktualisiert: 01.06.2024*
|
## Content Security Policy (CSP)
|
||||||
|
|
||||||
|
Die Anwendung implementiert eine Content Security Policy, um die Sicherheit zu erhöhen und unerwünschte externe Ressourcen zu blockieren. CSP wird in `app.py` konfiguriert und schränkt ein, welche Ressourcen geladen werden dürfen.
|
||||||
|
|
||||||
|
### Aktualisierung (06.06.2024)
|
||||||
|
Die Anwendung verwendet nun die Tailwind CSS CDN für vereinfachte Entwicklung. Die CSP wurde entsprechend angepasst, um die Domain `cdn.tailwindcss.com` zu erlauben.
|
||||||
|
|
||||||
|
### Lokale und CDN-Ressourcen
|
||||||
|
|
||||||
|
Die Anwendung nutzt eine Mischung aus lokalen Ressourcen und CDNs:
|
||||||
|
- **CDN-Ressourcen**:
|
||||||
|
- Tailwind CSS (cdn.tailwindcss.com)
|
||||||
|
- **Lokale Ressourcen**:
|
||||||
|
- Alpine.js
|
||||||
|
- Font Awesome
|
||||||
|
- Google Fonts (Inter und JetBrains Mono)
|
||||||
|
- WebGL-Animation (neural-network-background.js)
|
||||||
|
|
||||||
|
### CSP-Nonces
|
||||||
|
|
||||||
|
Die Anwendung verwendet Nonces für Inline-Skripte. In den Templates wird `{{ csp_nonce }}` verwendet, um den Nonce-Wert einzufügen:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script nonce="{{ csp_nonce }}">
|
||||||
|
// JavaScript Code
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
*Zuletzt aktualisiert: 15.06.2024*
|
||||||
98
ROADMAP.md
98
ROADMAP.md
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Diese Roadmap beschreibt die geplante Entwicklung der dynamischen, benutzerorientierten Mindmap-Funktionalität für das Systades-Projekt.
|
Diese Roadmap beschreibt die geplante Entwicklung der dynamischen, benutzerorientierten Mindmap-Funktionalität für das Systades-Projekt.
|
||||||
|
|
||||||
## Phase 1: Grundlegendes Datenmodell und Backend (Abgeschlossen)
|
## Phase 1: Grundlegendes Datenmodell und Backend (Abgeschlossen ✅)
|
||||||
|
|
||||||
- [x] Entwurf des Datenbankschemas für benutzerorientierte Mindmaps
|
- [x] Entwurf des Datenbankschemas für benutzerorientierte Mindmaps
|
||||||
- [x] Implementierung der Modelle in models.py
|
- [x] Implementierung der Modelle in models.py
|
||||||
@@ -10,31 +10,54 @@ Diese Roadmap beschreibt die geplante Entwicklung der dynamischen, benutzerorien
|
|||||||
- [x] Integration mit der bestehenden Benutzerauthentifizierung
|
- [x] Integration mit der bestehenden Benutzerauthentifizierung
|
||||||
- [x] Seed-Daten für die Entwicklung und Tests
|
- [x] Seed-Daten für die Entwicklung und Tests
|
||||||
|
|
||||||
## Phase 2: Dynamische Mindmap-Visualisierung (Aktuell)
|
## Phase 2: Dynamische Mindmap-Visualisierung (Abgeschlossen ✅)
|
||||||
|
|
||||||
- [ ] Anpassung des Frontend-Codes zur Verwendung der DB-Daten anstelle des SVG
|
- [x] Anpassung des Frontend-Codes zur Verwendung der DB-Daten anstelle des SVG
|
||||||
- [ ] Implementierung von AJAX-Anfragen zum Laden der Mindmap-Daten
|
- [x] Implementierung von AJAX-Anfragen zum Laden der Mindmap-Daten
|
||||||
- [ ] Dynamisches Rendering der Knoten, Verbindungen und Labels
|
- [x] Dynamisches Rendering der Knoten, Verbindungen und Labels
|
||||||
- [ ] Drag-and-Drop-Funktionalität für die Bewegung von Knoten
|
- [x] Drag-and-Drop-Funktionalität für die Bewegung von Knoten
|
||||||
- [ ] Zoom- und Pan-Funktionalität mit Persistenz der Ansicht
|
- [x] Zoom- und Pan-Funktionalität mit Persistenz der Ansicht
|
||||||
|
- [x] Verbesserte Fehlerbehandlung in der Knotenvisualisierung
|
||||||
|
- [x] Robustere Verbindungserkennung zwischen Knoten
|
||||||
|
- [x] Implementierung von Glasmorphismus-Effekten für moderneres UI
|
||||||
|
|
||||||
## Phase 3: Benutzerdefinierte Mindmaps
|
## Phase 3: Visuelles Design und UX (Abgeschlossen ✅)
|
||||||
|
|
||||||
- [ ] UI für das Erstellen, Bearbeiten und Löschen eigener Mindmaps
|
- [x] Implementierung des Dark Mode
|
||||||
|
- [x] Entwicklung eines modernen, minimalistischen UI
|
||||||
|
- [x] Animierter neuronaler Netzwerk-Hintergrund mit WebGL
|
||||||
|
- [x] Responsive Design für alle Geräte
|
||||||
|
- [x] Verbesserte Hover- und Selektionseffekte
|
||||||
|
- [x] Clustertopologie für neuronale Netzwerkdarstellung
|
||||||
|
- [x] Animierte Neuronenfeuer-Simulation mit Signalweiterleitung
|
||||||
|
|
||||||
|
## Phase 4: Benutzerdefinierte Mindmaps (Aktuell 🔄)
|
||||||
|
|
||||||
|
- [x] UI für das Betrachten bestehender Mindmaps
|
||||||
|
- [ ] UI für das Erstellen und Bearbeiten eigener Mindmaps
|
||||||
- [ ] Funktion zum Hinzufügen/Entfernen von Knoten aus der öffentlichen Mindmap
|
- [ ] Funktion zum Hinzufügen/Entfernen von Knoten aus der öffentlichen Mindmap
|
||||||
- [ ] Speichern der Knotenpositionen und Ansichtseinstellungen
|
- [ ] Speichern der Knotenpositionen und Ansichtseinstellungen
|
||||||
- [ ] Benutzerspezifische Visualisierungseinstellungen
|
- [ ] Benutzerspezifische Visualisierungseinstellungen
|
||||||
- [ ] Dashboard mit Übersicht aller Mindmaps des Benutzers
|
- [ ] Dashboard mit Übersicht aller Mindmaps des Benutzers
|
||||||
|
|
||||||
## Phase 4: Notizen und Annotationen
|
## Phase 5: Notizen und Annotationen
|
||||||
|
|
||||||
|
- [x] Anzeige von Gedanken zu Mindmap-Knoten
|
||||||
- [ ] UI für das Hinzufügen privater Notizen zu Knoten
|
- [ ] UI für das Hinzufügen privater Notizen zu Knoten
|
||||||
- [ ] Visuelle Anzeige von Notizen in der Mindmap
|
- [ ] Visuelle Anzeige von Notizen in der Mindmap
|
||||||
- [ ] Texteditor mit Markdown-Unterstützung für Notizen
|
- [ ] Texteditor mit Markdown-Unterstützung für Notizen
|
||||||
- [ ] Kategorisierung und Farbkodierung von Notizen
|
- [ ] Kategorisierung und Farbkodierung von Notizen
|
||||||
- [ ] Suchfunktion für Notizen
|
- [ ] Suchfunktion für Notizen
|
||||||
|
|
||||||
## Phase 5: Integrationen und Erweiterungen
|
## Phase 6: Tagging und Quellenmanagement
|
||||||
|
|
||||||
|
- [ ] Tagging-System für Inhalte implementieren
|
||||||
|
- [ ] Verknüpfen von Quellen mit Mindmap-Knoten
|
||||||
|
- [ ] Upload-Funktionalität für Dateien und Medien
|
||||||
|
- [ ] Verwaltung von Zitaten und Referenzen
|
||||||
|
- [ ] Visuelles Feedback für Tags und Quellen in der Mindmap
|
||||||
|
|
||||||
|
## Phase 7: Integrationen und Erweiterungen
|
||||||
|
|
||||||
- [ ] Import/Export-Funktionalität für Mindmaps (JSON, PNG)
|
- [ ] Import/Export-Funktionalität für Mindmaps (JSON, PNG)
|
||||||
- [ ] Teilen von Mindmaps (öffentlich/privat/mit bestimmten Benutzern)
|
- [ ] Teilen von Mindmaps (öffentlich/privat/mit bestimmten Benutzern)
|
||||||
@@ -42,7 +65,7 @@ Diese Roadmap beschreibt die geplante Entwicklung der dynamischen, benutzerorien
|
|||||||
- [ ] Verknüpfung mit externen Ressourcen (Links, Dateien)
|
- [ ] Verknüpfung mit externen Ressourcen (Links, Dateien)
|
||||||
- [ ] Versionierung von Mindmaps
|
- [ ] Versionierung von Mindmaps
|
||||||
|
|
||||||
## Phase 6: KI-Integration und Analyse
|
## Phase 8: KI-Integration und Analyse
|
||||||
|
|
||||||
- [ ] KI-gestützte Vorschläge für Verbindungen zwischen Knoten
|
- [ ] KI-gestützte Vorschläge für Verbindungen zwischen Knoten
|
||||||
- [ ] Automatische Kategorisierung von Inhalten
|
- [ ] Automatische Kategorisierung von Inhalten
|
||||||
@@ -50,7 +73,7 @@ Diese Roadmap beschreibt die geplante Entwicklung der dynamischen, benutzerorien
|
|||||||
- [ ] Mindmap-Statistiken und Analysen
|
- [ ] Mindmap-Statistiken und Analysen
|
||||||
- [ ] KI-basierte Zusammenfassung von Teilbereichen der Mindmap
|
- [ ] KI-basierte Zusammenfassung von Teilbereichen der Mindmap
|
||||||
|
|
||||||
## Phase 7: Optimierung und Skalierung
|
## Phase 9: Optimierung und Skalierung
|
||||||
|
|
||||||
- [ ] Performance-Optimierung für große Mindmaps
|
- [ ] Performance-Optimierung für große Mindmaps
|
||||||
- [ ] Verbesserung der Benutzerfreundlichkeit basierend auf Feedback
|
- [ ] Verbesserung der Benutzerfreundlichkeit basierend auf Feedback
|
||||||
@@ -66,6 +89,20 @@ Diese Roadmap beschreibt die geplante Entwicklung der dynamischen, benutzerorien
|
|||||||
- [ ] Caching-Strategien für bessere Performance
|
- [ ] Caching-Strategien für bessere Performance
|
||||||
- [ ] Verbesserte Fehlerbehandlung und Logging
|
- [ ] Verbesserte Fehlerbehandlung und Logging
|
||||||
|
|
||||||
|
## KI-Integration
|
||||||
|
|
||||||
|
### Aktuelle Implementation
|
||||||
|
- Integration von OpenAI mit dem gpt-4o-mini-Modell für den KI-Assistenten
|
||||||
|
- Datenbankzugriff für den KI-Assistenten, um direkt Informationen aus der Datenbank abzufragen
|
||||||
|
- Verbesserte Benutzeroberfläche für den KI-Assistenten mit kontextbezogenen Vorschlägen
|
||||||
|
|
||||||
|
### Zukünftige Verbesserungen
|
||||||
|
- Implementierung von Vektorsuche für präzisere Datenbank-Abfragen durch die KI
|
||||||
|
- Erweiterung der KI-Funktionalität für tiefere Analyse von Zusammenhängen zwischen Gedanken
|
||||||
|
- KI-gestützte Vorschläge für neue Verbindungen zwischen Gedanken basierend auf Inhaltsanalyse
|
||||||
|
- Finetuning des KI-Modells auf die spezifischen Anforderungen der Anwendung
|
||||||
|
- Erweiterung auf multimodale Fähigkeiten (Bild- und Textanalyse)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Implementierungsdetails
|
## Implementierungsdetails
|
||||||
@@ -85,6 +122,7 @@ Das Datenbankschema umfasst folgende Hauptentitäten:
|
|||||||
### Frontend-Technologien
|
### Frontend-Technologien
|
||||||
|
|
||||||
- D3.js für die Visualisierung der Mindmap
|
- D3.js für die Visualisierung der Mindmap
|
||||||
|
- WebGL für den neuronalen Netzwerk-Hintergrund
|
||||||
- AJAX für dynamisches Laden von Daten
|
- AJAX für dynamisches Laden von Daten
|
||||||
- Interaktive Bedienelemente mit JavaScript
|
- Interaktive Bedienelemente mit JavaScript
|
||||||
- Responsive Design mit Tailwind CSS
|
- Responsive Design mit Tailwind CSS
|
||||||
@@ -99,4 +137,36 @@ Die implementierten API-Endpunkte umfassen:
|
|||||||
- `/api/mindmap/<id>/remove_node/<node_id>` - Entfernen eines Knotens
|
- `/api/mindmap/<id>/remove_node/<node_id>` - Entfernen eines Knotens
|
||||||
- `/api/mindmap/<id>/update_node_position` - Aktualisierung von Knotenpositionen
|
- `/api/mindmap/<id>/update_node_position` - Aktualisierung von Knotenpositionen
|
||||||
- `/api/mindmap/<id>/notes` - Verwaltung von Notizen
|
- `/api/mindmap/<id>/notes` - Verwaltung von Notizen
|
||||||
- `/api/nodes/<id>/thoughts` - Abrufen und Hinzufügen von Gedanken zu Knoten
|
- `/api/nodes/<id>/thoughts` - Abrufen und Hinzufügen von Gedanken zu Knoten
|
||||||
|
- `/api/get_dark_mode` - Abrufen der Dark Mode Einstellung
|
||||||
|
|
||||||
|
## Neuronaler Netzwerk-Hintergrund
|
||||||
|
|
||||||
|
Der neue WebGL-basierte Hintergrund bietet:
|
||||||
|
|
||||||
|
- WebGL-basierte Rendering-Engine für optimale Performance
|
||||||
|
- Dynamische Knoten und Verbindungen mit realistischem Verhalten
|
||||||
|
- Clustering von neuronalen Knoten für natürlicheres Erscheinungsbild
|
||||||
|
- Simulation von neuronaler Aktivität und Signalweiterleitung
|
||||||
|
- Anpassbare visuelle Parameter (Helligkeit, Dichte, Geschwindigkeit)
|
||||||
|
- Vollständig responsives Design für alle Bildschirmgrößen
|
||||||
|
|
||||||
|
## Aktuelle Verbesserungen
|
||||||
|
- Tailwind CSS wurde auf CDN-Version aktualisiert (06.06.2024)
|
||||||
|
- Content Security Policy (CSP) für Tailwind CSS CDN und WebGL konfiguriert
|
||||||
|
- Behebung kritischer Fehler in der Mindmap-Knotenvisualisierung (15.06.2024)
|
||||||
|
- Verbesserte Verbindungserkennung zwischen Knoten implementiert
|
||||||
|
- Robuste Fehlerbehandlung für verschiedene API-Datenformate
|
||||||
|
|
||||||
|
## Zukünftige Aufgaben (Q3 2024)
|
||||||
|
- Implementierung des Tagging-Systems für Gedanken
|
||||||
|
- Quellenmanagement für Mindmap-Knoten
|
||||||
|
- Erweiterte Benutzerprofilfunktionen
|
||||||
|
- Verbesserung der mobilen Benutzererfahrung
|
||||||
|
- Integration von Exportfunktionen für Mindmaps
|
||||||
|
|
||||||
|
*Zuletzt aktualisiert: 15.06.2024*
|
||||||
|
|
||||||
|
## [Entfernt] CORS-Unterstützung (flask-cors)
|
||||||
|
- Die flask-cors-Bibliothek und alle zugehörigen Initialisierungen wurden entfernt.
|
||||||
|
- CORS wird nicht mehr unterstützt oder benötigt.
|
||||||
Binary file not shown.
BIN
__pycache__/app.cpython-313.pyc
Normal file
BIN
__pycache__/app.cpython-313.pyc
Normal file
Binary file not shown.
BIN
__pycache__/app.cpython-36.pyc
Normal file
BIN
__pycache__/app.cpython-36.pyc
Normal file
Binary file not shown.
BIN
__pycache__/init_db.cpython-311.pyc
Normal file
BIN
__pycache__/init_db.cpython-311.pyc
Normal file
Binary file not shown.
BIN
__pycache__/init_db.cpython-313.pyc
Normal file
BIN
__pycache__/init_db.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
__pycache__/models.cpython-313.pyc
Normal file
BIN
__pycache__/models.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backup/archiv_0.1.zip
Normal file
BIN
backup/archiv_0.1.zip
Normal file
Binary file not shown.
@@ -1,4 +0,0 @@
|
|||||||
# Netscape HTTP Cookie File
|
|
||||||
# https://curl.se/docs/http-cookies.html
|
|
||||||
# This file was generated by libcurl! Edit at your own risk.
|
|
||||||
|
|
||||||
BIN
database/__pycache__/models.cpython-313.pyc
Normal file
BIN
database/__pycache__/models.cpython-313.pyc
Normal file
Binary file not shown.
BIN
database/systades.V2.db.backup
Normal file
BIN
database/systades.V2.db.backup
Normal file
Binary file not shown.
Binary file not shown.
103
db_operations.py
Normal file
103
db_operations.py
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import sqlite3
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
|
||||||
|
# Verbindung zur Datenbank herstellen
|
||||||
|
db_path = os.path.join(os.getcwd(), 'database', 'systades.db')
|
||||||
|
conn = sqlite3.connect(db_path)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Schema der mind_map_node Tabelle anzeigen
|
||||||
|
cursor.execute("PRAGMA table_info(mind_map_node)")
|
||||||
|
columns = cursor.fetchall()
|
||||||
|
print("Tabellenschema mind_map_node:")
|
||||||
|
for column in columns:
|
||||||
|
print(f"{column[1]} ({column[2]})")
|
||||||
|
|
||||||
|
# Existierende Knoten anzeigen
|
||||||
|
cursor.execute("SELECT id, name, description, color_code FROM mind_map_node LIMIT 5")
|
||||||
|
existing_nodes = cursor.fetchall()
|
||||||
|
print("\nBestehende Knoten:")
|
||||||
|
for node in existing_nodes:
|
||||||
|
print(f"ID: {node[0]}, Name: {node[1]}, Beschreibung: {node[2]}")
|
||||||
|
|
||||||
|
# Mögliche Kategorien abrufen (für die Verknüpfung)
|
||||||
|
cursor.execute("SELECT id, name FROM category")
|
||||||
|
categories = cursor.fetchall()
|
||||||
|
print("\nVerfügbare Kategorien:")
|
||||||
|
for category in categories:
|
||||||
|
print(f"ID: {category[0]}, Name: {category[1]}")
|
||||||
|
|
||||||
|
# Wissenschaftliche Themengebiete für neue Knoten
|
||||||
|
scientific_nodes = [
|
||||||
|
{
|
||||||
|
"name": "Quantenphysik",
|
||||||
|
"description": "Die Quantenphysik befasst sich mit dem Verhalten von Materie und Energie auf atomarer und subatomarer Ebene.",
|
||||||
|
"color_code": "#4B0082", # Indigo
|
||||||
|
"icon": "fa-atom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Neurowissenschaften",
|
||||||
|
"description": "Interdisziplinäre Wissenschaft, die sich mit der Struktur und Funktion des Nervensystems und des Gehirns beschäftigt.",
|
||||||
|
"color_code": "#FF4500", # Orange-Rot
|
||||||
|
"icon": "fa-brain"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Künstliche Intelligenz",
|
||||||
|
"description": "Forschungsgebiet der Informatik, das sich mit der Automatisierung intelligenten Verhaltens befasst.",
|
||||||
|
"color_code": "#008080", # Teal
|
||||||
|
"icon": "fa-robot"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Klimaforschung",
|
||||||
|
"description": "Wissenschaftliche Untersuchung des Klimas, seiner Variationen und Veränderungen auf allen zeitlichen und räumlichen Skalen.",
|
||||||
|
"color_code": "#2E8B57", # Seegrün
|
||||||
|
"icon": "fa-cloud-sun"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Genetik",
|
||||||
|
"description": "Teilgebiet der Biologie, das sich mit Vererbung sowie der Funktion und Wirkung von Genen beschäftigt.",
|
||||||
|
"color_code": "#800080", # Lila
|
||||||
|
"icon": "fa-dna"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Astrophysik",
|
||||||
|
"description": "Zweig der Astronomie, der sich mit den physikalischen Eigenschaften des Universums befasst.",
|
||||||
|
"color_code": "#191970", # Mitternachtsblau
|
||||||
|
"icon": "fa-star"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
# Neue Knoten hinzufügen
|
||||||
|
print("\nFüge neue wissenschaftliche Knoten hinzu...")
|
||||||
|
for node in scientific_nodes:
|
||||||
|
# Prüfen, ob der Knoten bereits existiert
|
||||||
|
cursor.execute("SELECT id FROM mind_map_node WHERE name = ?", (node["name"],))
|
||||||
|
existing = cursor.fetchone()
|
||||||
|
if existing:
|
||||||
|
print(f"Knoten '{node['name']}' existiert bereits mit ID {existing[0]}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Zufällige Kategorie wählen, wenn vorhanden
|
||||||
|
category_id = None
|
||||||
|
if categories:
|
||||||
|
category_id = random.choice(categories)[0]
|
||||||
|
|
||||||
|
# Neuen Knoten einfügen
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO mind_map_node (name, description, color_code, icon, is_public, category_id)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
""", (
|
||||||
|
node["name"],
|
||||||
|
node["description"],
|
||||||
|
node["color_code"],
|
||||||
|
node["icon"],
|
||||||
|
True,
|
||||||
|
category_id
|
||||||
|
))
|
||||||
|
print(f"Knoten '{node['name']}' hinzugefügt")
|
||||||
|
|
||||||
|
# Änderungen übernehmen und Verbindung schließen
|
||||||
|
conn.commit()
|
||||||
|
print("\nDatenbank erfolgreich aktualisiert!")
|
||||||
|
conn.close()
|
||||||
17
docker-compose.yml
Normal file
17
docker-compose.yml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
version: '3.9'
|
||||||
|
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
build: .
|
||||||
|
image: systades_app:latest
|
||||||
|
container_name: systades_app
|
||||||
|
restart: always
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
ports:
|
||||||
|
- "5000:5000"
|
||||||
|
volumes:
|
||||||
|
- ./database:/app/database
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
db_data:
|
||||||
@@ -2,12 +2,14 @@
|
|||||||
# Kopiere diese Datei zu .env und passe die Werte an
|
# Kopiere diese Datei zu .env und passe die Werte an
|
||||||
|
|
||||||
# Flask
|
# Flask
|
||||||
SECRET_KEY=dein-geheimer-schluessel-hier
|
FLASK_APP=app.py
|
||||||
|
FLASK_ENV=development
|
||||||
|
SECRET_KEY=mein-sicherer-schluessel-fuer-entwicklung
|
||||||
|
|
||||||
# OpenAI API
|
# OpenAI API
|
||||||
OPENAI_API_KEY=sk-dein-openai-api-schluessel-hier
|
OPENAI_API_KEY=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
|
||||||
|
|
||||||
# Datenbank
|
# Datenbank
|
||||||
# Bei Bedarf kann hier eine andere Datenbank-URL angegeben werden
|
# Bei Bedarf kann hier eine andere Datenbank-URL angegeben werden
|
||||||
# Der Pfad wird relativ zum Projektverzeichnis angegeben
|
# Der Pfad wird relativ zum Projektverzeichnis angegeben
|
||||||
# SQLALCHEMY_DATABASE_URI=sqlite:////absoluter/pfad/zu/database/systades.db
|
SQLALCHEMY_DATABASE_URI=sqlite:///database/systades.db
|
||||||
470
init_db.py
Executable file → Normal file
470
init_db.py
Executable file → Normal file
@@ -5,252 +5,240 @@ from app import app, initialize_database, db_path
|
|||||||
from models import db, User, Thought, Comment, MindMapNode, ThoughtRelation, ThoughtRating, RelationType
|
from models import db, User, Thought, Comment, MindMapNode, ThoughtRelation, ThoughtRating, RelationType
|
||||||
from models import Category, UserMindmap, UserMindmapNode, MindmapNote
|
from models import Category, UserMindmap, UserMindmapNode, MindmapNote
|
||||||
import os
|
import os
|
||||||
|
import sqlite3
|
||||||
|
from flask import Flask
|
||||||
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
def init_database():
|
# Erstelle eine temporäre Flask-App, um die Datenbank zu initialisieren
|
||||||
"""Initialisiert die Datenbank mit Beispieldaten."""
|
app = Flask(__name__)
|
||||||
with app.app_context():
|
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///database/systades.db'
|
||||||
# Datenbank löschen und neu erstellen
|
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||||
if os.path.exists(db_path):
|
db.init_app(app)
|
||||||
os.remove(db_path)
|
|
||||||
|
|
||||||
# Stellen Sie sicher, dass das Verzeichnis existiert
|
|
||||||
os.makedirs(os.path.dirname(db_path), exist_ok=True)
|
|
||||||
|
|
||||||
db.create_all()
|
|
||||||
|
|
||||||
# Admin-Benutzer erstellen
|
|
||||||
admin = User(username='admin', email='admin@example.com', is_admin=True)
|
|
||||||
admin.set_password('admin')
|
|
||||||
db.session.add(admin)
|
|
||||||
|
|
||||||
# Beispiel-Benutzer erstellen
|
|
||||||
user = User(username='user', email='user@example.com')
|
|
||||||
user.set_password('user')
|
|
||||||
db.session.add(user)
|
|
||||||
|
|
||||||
# Commit, um IDs zu generieren
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
# Wissenschaftliche Kategorien erstellen
|
|
||||||
science = Category(name='Wissenschaft', description='Wissenschaftliche Erkenntnisse',
|
|
||||||
color_code='#4CAF50', icon='flask')
|
|
||||||
db.session.add(science)
|
|
||||||
|
|
||||||
philosophy = Category(name='Philosophie', description='Philosophische Theorien und Gedanken',
|
|
||||||
color_code='#9C27B0', icon='lightbulb')
|
|
||||||
db.session.add(philosophy)
|
|
||||||
|
|
||||||
technology = Category(name='Technologie', description='Technologische Entwicklungen',
|
|
||||||
color_code='#FF9800', icon='microchip')
|
|
||||||
db.session.add(technology)
|
|
||||||
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
# Wissenschaftliche Unterkategorien
|
|
||||||
physics = Category(name='Physik', description='Studium der Materie und Energie',
|
|
||||||
color_code='#81C784', icon='atom', parent_id=science.id)
|
|
||||||
biology = Category(name='Biologie', description='Studium lebender Organismen',
|
|
||||||
color_code='#66BB6A', icon='leaf', parent_id=science.id)
|
|
||||||
chemistry = Category(name='Chemie', description='Studium der Stoffe und ihrer Reaktionen',
|
|
||||||
color_code='#A5D6A7', icon='vial', parent_id=science.id)
|
|
||||||
|
|
||||||
db.session.add_all([physics, biology, chemistry])
|
|
||||||
|
|
||||||
# Technologie-Unterkategorien
|
|
||||||
informatics = Category(name='Informatik', description='Studium der Informationsverarbeitung',
|
|
||||||
color_code='#FFB74D', icon='laptop-code', parent_id=technology.id)
|
|
||||||
ai = Category(name='Künstliche Intelligenz', description='Entwicklung intelligenter Systeme',
|
|
||||||
color_code='#FFA726', icon='robot', parent_id=technology.id)
|
|
||||||
|
|
||||||
db.session.add_all([informatics, ai])
|
|
||||||
|
|
||||||
# Philosophie-Unterkategorien
|
|
||||||
ethics = Category(name='Ethik', description='Moralphilosophie und Wertesysteme',
|
|
||||||
color_code='#BA68C8', icon='balance-scale', parent_id=philosophy.id)
|
|
||||||
logic = Category(name='Logik', description='Studie der gültigen Schlussfolgerungen',
|
|
||||||
color_code='#AB47BC', icon='project-diagram', parent_id=philosophy.id)
|
|
||||||
|
|
||||||
db.session.add_all([ethics, logic])
|
|
||||||
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
# Knoten für die öffentliche Mindmap erstellen
|
|
||||||
nodes = {
|
|
||||||
'quantenmechanik': MindMapNode(
|
|
||||||
name='Quantenmechanik',
|
|
||||||
description='Physikalische Theorie zur Beschreibung der Materie auf atomarer Ebene',
|
|
||||||
color_code='#81C784',
|
|
||||||
category_id=physics.id,
|
|
||||||
created_by_id=admin.id
|
|
||||||
),
|
|
||||||
'relativitaetstheorie': MindMapNode(
|
|
||||||
name='Relativitätstheorie',
|
|
||||||
description='Einsteins Theorien zur Raumzeit und Gravitation',
|
|
||||||
color_code='#81C784',
|
|
||||||
category_id=physics.id,
|
|
||||||
created_by_id=admin.id
|
|
||||||
),
|
|
||||||
'genetik': MindMapNode(
|
|
||||||
name='Genetik',
|
|
||||||
description='Wissenschaft der Gene und Vererbung',
|
|
||||||
color_code='#66BB6A',
|
|
||||||
category_id=biology.id,
|
|
||||||
created_by_id=admin.id
|
|
||||||
),
|
|
||||||
'machine_learning': MindMapNode(
|
|
||||||
name='Machine Learning',
|
|
||||||
description='Algorithmen, die aus Daten lernen können',
|
|
||||||
color_code='#FFA726',
|
|
||||||
category_id=ai.id,
|
|
||||||
created_by_id=admin.id
|
|
||||||
),
|
|
||||||
'ki_ethik': MindMapNode(
|
|
||||||
name='KI-Ethik',
|
|
||||||
description='Moralische Implikationen künstlicher Intelligenz',
|
|
||||||
color_code='#BA68C8',
|
|
||||||
category_id=ethics.id,
|
|
||||||
created_by_id=user.id
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
for node in nodes.values():
|
|
||||||
db.session.add(node)
|
|
||||||
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
# Verknüpfungen zwischen Knoten herstellen (Hierarchie)
|
|
||||||
nodes['machine_learning'].parents.append(nodes['ki_ethik'])
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
# Gedanken erstellen
|
|
||||||
thoughts = [
|
|
||||||
{
|
|
||||||
'title': 'Künstliche Intelligenz und Bewusstsein',
|
|
||||||
'content': 'Die Frage nach maschinellem Bewusstsein ist fundamental für die KI-Ethik. Aktuelle KI-Systeme haben kein Bewusstsein, aber fortschrittliche KI könnte in Zukunft Eigenschaften entwickeln, die diesem nahekommen.',
|
|
||||||
'abstract': 'Eine Untersuchung der philosophischen Implikationen von KI-Bewusstsein.',
|
|
||||||
'keywords': 'KI, Bewusstsein, Ethik, Philosophie',
|
|
||||||
'branch': 'Philosophie',
|
|
||||||
'color_code': '#BA68C8',
|
|
||||||
'source_type': 'Markdown',
|
|
||||||
'user_id': user.id,
|
|
||||||
'node': nodes['ki_ethik']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'title': 'Quantenmechanik und Realität',
|
|
||||||
'content': 'Die Kopenhagener Deutung und ihre Auswirkungen auf unser Verständnis der Realität. Quantenmechanik stellt grundlegende Annahmen über Determinismus und Lokalität in Frage.',
|
|
||||||
'abstract': 'Eine Analyse verschiedener Interpretationen der Quantenmechanik.',
|
|
||||||
'keywords': 'Quantenmechanik, Physik, Realität',
|
|
||||||
'branch': 'Physik',
|
|
||||||
'color_code': '#81C784',
|
|
||||||
'source_type': 'PDF',
|
|
||||||
'user_id': admin.id,
|
|
||||||
'node': nodes['quantenmechanik']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'title': 'Deep Learning Fortschritte',
|
|
||||||
'content': 'Die neuesten Fortschritte im Deep Learning haben zu beeindruckenden Ergebnissen in Bereichen wie Computer Vision, Natural Language Processing und Reinforcement Learning geführt.',
|
|
||||||
'abstract': 'Überblick über aktuelle Deep Learning-Techniken und ihre Anwendungen.',
|
|
||||||
'keywords': 'Deep Learning, Neural Networks, AI',
|
|
||||||
'branch': 'Technologie',
|
|
||||||
'color_code': '#FFA726',
|
|
||||||
'source_type': 'Webpage',
|
|
||||||
'user_id': admin.id,
|
|
||||||
'node': nodes['machine_learning']
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
thought_objects = []
|
|
||||||
for t_data in thoughts:
|
|
||||||
node = t_data.pop('node')
|
|
||||||
thought = Thought(**t_data)
|
|
||||||
node.thoughts.append(thought)
|
|
||||||
thought_objects.append(thought)
|
|
||||||
db.session.add(thought)
|
|
||||||
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
# Beziehungen zwischen Gedanken
|
|
||||||
relation = ThoughtRelation(
|
|
||||||
source_id=thought_objects[0].id,
|
|
||||||
target_id=thought_objects[2].id,
|
|
||||||
relation_type=RelationType.INSPIRES,
|
|
||||||
created_by_id=user.id
|
|
||||||
)
|
|
||||||
db.session.add(relation)
|
|
||||||
|
|
||||||
# Bewertungen erstellen
|
|
||||||
rating1 = ThoughtRating(
|
|
||||||
thought_id=thought_objects[0].id,
|
|
||||||
user_id=admin.id,
|
|
||||||
relevance_score=5
|
|
||||||
)
|
|
||||||
rating2 = ThoughtRating(
|
|
||||||
thought_id=thought_objects[2].id,
|
|
||||||
user_id=user.id,
|
|
||||||
relevance_score=4
|
|
||||||
)
|
|
||||||
db.session.add_all([rating1, rating2])
|
|
||||||
|
|
||||||
# Kommentare erstellen
|
|
||||||
for thought in thought_objects:
|
|
||||||
comment = Comment(
|
|
||||||
content=f'Interessante Perspektive zu {thought.title}!',
|
|
||||||
thought_id=thought.id,
|
|
||||||
user_id=admin.id if thought.user_id != admin.id else user.id
|
|
||||||
)
|
|
||||||
db.session.add(comment)
|
|
||||||
|
|
||||||
# Benutzer-Mindmaps erstellen
|
|
||||||
user_mindmap = UserMindmap(
|
|
||||||
name='Meine KI-Forschung',
|
|
||||||
description='Meine persönliche Sammlung zu KI und Ethik',
|
|
||||||
user_id=user.id
|
|
||||||
)
|
|
||||||
db.session.add(user_mindmap)
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
# Knoten zur Benutzer-Mindmap hinzufügen
|
|
||||||
user_mindmap_nodes = [
|
|
||||||
UserMindmapNode(
|
|
||||||
user_mindmap_id=user_mindmap.id,
|
|
||||||
node_id=nodes['machine_learning'].id,
|
|
||||||
x_position=200,
|
|
||||||
y_position=300
|
|
||||||
),
|
|
||||||
UserMindmapNode(
|
|
||||||
user_mindmap_id=user_mindmap.id,
|
|
||||||
node_id=nodes['ki_ethik'].id,
|
|
||||||
x_position=500,
|
|
||||||
y_position=200
|
|
||||||
)
|
|
||||||
]
|
|
||||||
db.session.add_all(user_mindmap_nodes)
|
|
||||||
|
|
||||||
# Private Notizen
|
|
||||||
note = MindmapNote(
|
|
||||||
user_id=user.id,
|
|
||||||
mindmap_id=user_mindmap.id,
|
|
||||||
node_id=nodes['ki_ethik'].id,
|
|
||||||
content="Recherchiere mehr über aktuelle ethische Richtlinien für KI-Entwicklung!",
|
|
||||||
color_code="#FFF59D"
|
|
||||||
)
|
|
||||||
db.session.add(note)
|
|
||||||
|
|
||||||
# Gedanken zu Bookmarks hinzufügen
|
|
||||||
user.bookmarked_thoughts.append(thought_objects[0])
|
|
||||||
admin.bookmarked_thoughts.append(thought_objects[1])
|
|
||||||
|
|
||||||
# Finaler Commit
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
print("Datenbank wurde erfolgreich initialisiert!")
|
|
||||||
|
|
||||||
def init_db():
|
def init_db():
|
||||||
"""Alias für Kompatibilität mit älteren Scripts."""
|
with app.app_context():
|
||||||
init_database()
|
print("Initialisiere Datenbank...")
|
||||||
|
|
||||||
|
# Tabellen erstellen
|
||||||
|
db.create_all()
|
||||||
|
print("Tabellen wurden erstellt.")
|
||||||
|
|
||||||
|
# Standardbenutzer erstellen, falls keine vorhanden sind
|
||||||
|
if User.query.count() == 0:
|
||||||
|
print("Erstelle Standardbenutzer...")
|
||||||
|
create_default_users()
|
||||||
|
|
||||||
|
# Standardkategorien erstellen, falls keine vorhanden sind
|
||||||
|
if Category.query.count() == 0:
|
||||||
|
print("Erstelle Standardkategorien...")
|
||||||
|
create_default_categories()
|
||||||
|
|
||||||
|
# Beispiel-Mindmap erstellen, falls keine Knoten vorhanden sind
|
||||||
|
if MindMapNode.query.count() == 0:
|
||||||
|
print("Erstelle Beispiel-Mindmap...")
|
||||||
|
create_sample_mindmap()
|
||||||
|
|
||||||
|
print("Datenbankinitialisierung abgeschlossen.")
|
||||||
|
|
||||||
|
def create_default_users():
|
||||||
|
"""Erstellt Standardbenutzer für die Anwendung"""
|
||||||
|
users = [
|
||||||
|
{
|
||||||
|
'username': 'admin',
|
||||||
|
'email': 'admin@example.com',
|
||||||
|
'password': 'admin',
|
||||||
|
'role': 'admin'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'username': 'user',
|
||||||
|
'email': 'user@example.com',
|
||||||
|
'password': 'user',
|
||||||
|
'role': 'user'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
for user_data in users:
|
||||||
|
password = user_data.pop('password')
|
||||||
|
user = User(**user_data)
|
||||||
|
user.set_password(password)
|
||||||
|
db.session.add(user)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
print(f"{len(users)} Benutzer wurden erstellt.")
|
||||||
|
|
||||||
|
def create_default_categories():
|
||||||
|
"""Erstellt die Standardkategorien für die Mindmap"""
|
||||||
|
categories = [
|
||||||
|
{
|
||||||
|
'name': 'Konzept',
|
||||||
|
'description': 'Abstrakte Ideen und theoretische Konzepte',
|
||||||
|
'color_code': '#6366f1',
|
||||||
|
'icon': 'lightbulb'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Technologie',
|
||||||
|
'description': 'Hardware, Software, Tools und Plattformen',
|
||||||
|
'color_code': '#10b981',
|
||||||
|
'icon': 'cpu'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Prozess',
|
||||||
|
'description': 'Workflows, Methodologien und Vorgehensweisen',
|
||||||
|
'color_code': '#f59e0b',
|
||||||
|
'icon': 'git-branch'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Person',
|
||||||
|
'description': 'Personen, Teams und Organisationen',
|
||||||
|
'color_code': '#ec4899',
|
||||||
|
'icon': 'user'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Dokument',
|
||||||
|
'description': 'Dokumentationen, Referenzen und Ressourcen',
|
||||||
|
'color_code': '#3b82f6',
|
||||||
|
'icon': 'file-text'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
for cat_data in categories:
|
||||||
|
category = Category(**cat_data)
|
||||||
|
db.session.add(category)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
print(f"{len(categories)} Kategorien wurden erstellt.")
|
||||||
|
|
||||||
|
def create_sample_mindmap():
|
||||||
|
"""Erstellt eine Beispiel-Mindmap mit Knoten und Beziehungen"""
|
||||||
|
|
||||||
|
# Kategorien für die Zuordnung
|
||||||
|
categories = Category.query.all()
|
||||||
|
category_map = {cat.name: cat for cat in categories}
|
||||||
|
|
||||||
|
# Beispielknoten erstellen
|
||||||
|
nodes = [
|
||||||
|
{
|
||||||
|
'name': 'Wissensmanagement',
|
||||||
|
'description': 'Systematische Erfassung, Speicherung und Nutzung von Wissen in Organisationen.',
|
||||||
|
'color_code': '#6366f1',
|
||||||
|
'icon': 'database',
|
||||||
|
'category': category_map.get('Konzept'),
|
||||||
|
'x': 0,
|
||||||
|
'y': 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Mind-Mapping',
|
||||||
|
'description': 'Technik zur visuellen Darstellung von Informationen und Zusammenhängen.',
|
||||||
|
'color_code': '#10b981',
|
||||||
|
'icon': 'git-branch',
|
||||||
|
'category': category_map.get('Prozess'),
|
||||||
|
'x': 200,
|
||||||
|
'y': -150
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Cytoscape.js',
|
||||||
|
'description': 'JavaScript-Bibliothek für die Visualisierung und Manipulation von Graphen.',
|
||||||
|
'color_code': '#3b82f6',
|
||||||
|
'icon': 'code',
|
||||||
|
'category': category_map.get('Technologie'),
|
||||||
|
'x': 350,
|
||||||
|
'y': -50
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Socket.IO',
|
||||||
|
'description': 'Bibliothek für Echtzeit-Kommunikation zwischen Client und Server.',
|
||||||
|
'color_code': '#3b82f6',
|
||||||
|
'icon': 'zap',
|
||||||
|
'category': category_map.get('Technologie'),
|
||||||
|
'x': 350,
|
||||||
|
'y': 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Kollaboration',
|
||||||
|
'description': 'Zusammenarbeit mehrerer Benutzer an gemeinsamen Inhalten.',
|
||||||
|
'color_code': '#f59e0b',
|
||||||
|
'icon': 'users',
|
||||||
|
'category': category_map.get('Prozess'),
|
||||||
|
'x': 200,
|
||||||
|
'y': 150
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'SQLite',
|
||||||
|
'description': 'Leichtgewichtige relationale Datenbank, die ohne Server-Prozess auskommt.',
|
||||||
|
'color_code': '#3b82f6',
|
||||||
|
'icon': 'database',
|
||||||
|
'category': category_map.get('Technologie'),
|
||||||
|
'x': 0,
|
||||||
|
'y': 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Flask',
|
||||||
|
'description': 'Leichtgewichtiges Python-Webframework für die Entwicklung von Webanwendungen.',
|
||||||
|
'color_code': '#3b82f6',
|
||||||
|
'icon': 'server',
|
||||||
|
'category': category_map.get('Technologie'),
|
||||||
|
'x': -200,
|
||||||
|
'y': 150
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'REST API',
|
||||||
|
'description': 'Architekturstil für verteilte Systeme, insbesondere Webanwendungen.',
|
||||||
|
'color_code': '#10b981',
|
||||||
|
'icon': 'link',
|
||||||
|
'category': category_map.get('Konzept'),
|
||||||
|
'x': -200,
|
||||||
|
'y': -150
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Dokumentation',
|
||||||
|
'description': 'Strukturierte Erfassung und Beschreibung von Informationen und Prozessen.',
|
||||||
|
'color_code': '#ec4899',
|
||||||
|
'icon': 'file-text',
|
||||||
|
'category': category_map.get('Dokument'),
|
||||||
|
'x': -350,
|
||||||
|
'y': 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
# Knoten in die Datenbank einfügen
|
||||||
|
node_objects = {}
|
||||||
|
for node_data in nodes:
|
||||||
|
category = node_data.pop('category', None)
|
||||||
|
x = node_data.pop('x', 0)
|
||||||
|
y = node_data.pop('y', 0)
|
||||||
|
node = MindMapNode(**node_data)
|
||||||
|
if category:
|
||||||
|
node.category_id = category.id
|
||||||
|
db.session.add(node)
|
||||||
|
db.session.flush() # Generiert IDs für neue Objekte
|
||||||
|
node_objects[node.name] = node
|
||||||
|
|
||||||
|
# Beziehungen erstellen
|
||||||
|
relationships = [
|
||||||
|
('Wissensmanagement', 'Mind-Mapping'),
|
||||||
|
('Wissensmanagement', 'Kollaboration'),
|
||||||
|
('Wissensmanagement', 'Dokumentation'),
|
||||||
|
('Mind-Mapping', 'Cytoscape.js'),
|
||||||
|
('Kollaboration', 'Socket.IO'),
|
||||||
|
('Wissensmanagement', 'SQLite'),
|
||||||
|
('SQLite', 'Flask'),
|
||||||
|
('Flask', 'REST API'),
|
||||||
|
('REST API', 'Socket.IO'),
|
||||||
|
('REST API', 'Dokumentation')
|
||||||
|
]
|
||||||
|
|
||||||
|
for parent_name, child_name in relationships:
|
||||||
|
parent = node_objects.get(parent_name)
|
||||||
|
child = node_objects.get(child_name)
|
||||||
|
if parent and child:
|
||||||
|
parent.children.append(child)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
print(f"{len(nodes)} Knoten und {len(relationships)} Beziehungen wurden erstellt.")
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
init_database()
|
init_db()
|
||||||
print("Datenbank wurde erfolgreich initialisiert!")
|
print("Datenbank wurde erfolgreich initialisiert!")
|
||||||
print("Sie können die Anwendung jetzt mit 'python app.py' starten")
|
print("Sie können die Anwendung jetzt mit 'python app.py' starten")
|
||||||
print("Anmelden mit:")
|
print("Anmelden mit:")
|
||||||
|
|||||||
651
logs/app.log
Normal file
651
logs/app.log
Normal file
@@ -0,0 +1,651 @@
|
|||||||
|
2025-05-10 23:12:44,110 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:74]
|
||||||
|
2025-05-10 23:12:45,854 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:74]
|
||||||
|
2025-05-10 23:12:45,854 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:74]
|
||||||
|
2025-05-10 23:13:27,379 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:74]
|
||||||
|
2025-05-10 23:13:29,289 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:74]
|
||||||
|
2025-05-10 23:13:29,289 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:74]
|
||||||
|
2025-05-10 23:13:35,686 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:75]
|
||||||
|
2025-05-10 23:13:37,640 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:75]
|
||||||
|
2025-05-10 23:13:37,640 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:75]
|
||||||
|
2025-05-10 23:14:35,907 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:75]
|
||||||
|
2025-05-10 23:14:37,804 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:75]
|
||||||
|
2025-05-10 23:14:37,804 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:75]
|
||||||
|
2025-05-10 23:14:44,251 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:75]
|
||||||
|
2025-05-10 23:14:46,088 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:75]
|
||||||
|
2025-05-10 23:14:46,088 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:75]
|
||||||
|
2025-05-10 23:15:14,106 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:75]
|
||||||
|
2025-05-10 23:15:15,855 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:75]
|
||||||
|
2025-05-10 23:15:15,855 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:75]
|
||||||
|
2025-05-10 23:15:30,739 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||||
|
2025-05-10 23:15:32,667 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||||
|
2025-05-10 23:15:32,667 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||||
|
2025-05-10 23:16:55,581 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||||
|
2025-05-10 23:16:57,283 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||||
|
2025-05-10 23:16:57,283 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||||
|
2025-05-10 23:17:04,727 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||||
|
2025-05-10 23:17:06,698 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||||
|
2025-05-10 23:17:06,698 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||||
|
2025-05-10 23:23:26,898 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||||
|
2025-05-10 23:23:28,862 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||||
|
2025-05-10 23:23:28,862 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||||
|
2025-05-10 23:24:45,296 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||||
|
2025-05-10 23:24:47,176 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||||
|
2025-05-10 23:24:47,176 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||||
|
2025-05-10 23:26:06,881 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||||
|
2025-05-10 23:26:08,727 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||||
|
2025-05-10 23:26:08,727 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||||
|
2025-05-10 23:26:12,865 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||||
|
2025-05-10 23:26:14,599 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||||
|
2025-05-10 23:26:14,599 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||||
|
2025-05-10 23:26:24,367 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||||
|
2025-05-10 23:26:26,054 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||||
|
2025-05-10 23:26:26,054 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||||
|
2025-05-10 23:26:27,900 ERROR: Fehler 500: 405 Method Not Allowed: The method is not allowed for the requested URL.
|
||||||
|
Endpoint: /api/thoughts, Method: GET, IP: 127.0.0.1
|
||||||
|
User: 1 (admin)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1823, in full_dispatch_request
|
||||||
|
rv = self.dispatch_request()
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1788, in dispatch_request
|
||||||
|
self.raise_routing_exception(req)
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1770, in raise_routing_exception
|
||||||
|
raise request.routing_exception # type: ignore
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\ctx.py", line 351, in match_request
|
||||||
|
result = self.url_adapter.match(return_rule=True) # type: ignore
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\werkzeug\routing\map.py", line 619, in match
|
||||||
|
raise MethodNotAllowed(valid_methods=list(e.have_match_for)) from None
|
||||||
|
werkzeug.exceptions.MethodNotAllowed: 405 Method Not Allowed: The method is not allowed for the requested URL.
|
||||||
|
[in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:92]
|
||||||
|
2025-05-10 23:26:27,900 ERROR: Fehler 500: 405 Method Not Allowed: The method is not allowed for the requested URL.
|
||||||
|
Endpoint: /api/thoughts, Method: GET, IP: 127.0.0.1
|
||||||
|
User: 1 (admin)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1823, in full_dispatch_request
|
||||||
|
rv = self.dispatch_request()
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1788, in dispatch_request
|
||||||
|
self.raise_routing_exception(req)
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1770, in raise_routing_exception
|
||||||
|
raise request.routing_exception # type: ignore
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\ctx.py", line 351, in match_request
|
||||||
|
result = self.url_adapter.match(return_rule=True) # type: ignore
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\werkzeug\routing\map.py", line 619, in match
|
||||||
|
raise MethodNotAllowed(valid_methods=list(e.have_match_for)) from None
|
||||||
|
werkzeug.exceptions.MethodNotAllowed: 405 Method Not Allowed: The method is not allowed for the requested URL.
|
||||||
|
[in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:92]
|
||||||
|
2025-05-10 23:26:31,236 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||||
|
2025-05-10 23:26:33,032 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||||
|
2025-05-10 23:26:33,032 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||||
|
2025-05-10 23:26:35,635 ERROR: Fehler 404: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
Endpoint: /api/mindmap/root, Method: GET, IP: 127.0.0.1
|
||||||
|
User: 1 (admin)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1823, in full_dispatch_request
|
||||||
|
rv = self.dispatch_request()
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1788, in dispatch_request
|
||||||
|
self.raise_routing_exception(req)
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1770, in raise_routing_exception
|
||||||
|
raise request.routing_exception # type: ignore
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\ctx.py", line 351, in match_request
|
||||||
|
result = self.url_adapter.match(return_rule=True) # type: ignore
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\werkzeug\routing\map.py", line 624, in match
|
||||||
|
raise NotFound() from None
|
||||||
|
werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
[in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:92]
|
||||||
|
2025-05-10 23:26:35,635 ERROR: Fehler 404: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
Endpoint: /api/mindmap/root, Method: GET, IP: 127.0.0.1
|
||||||
|
User: 1 (admin)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1823, in full_dispatch_request
|
||||||
|
rv = self.dispatch_request()
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1788, in dispatch_request
|
||||||
|
self.raise_routing_exception(req)
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1770, in raise_routing_exception
|
||||||
|
raise request.routing_exception # type: ignore
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\ctx.py", line 351, in match_request
|
||||||
|
result = self.url_adapter.match(return_rule=True) # type: ignore
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\werkzeug\routing\map.py", line 624, in match
|
||||||
|
raise NotFound() from None
|
||||||
|
werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
[in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:92]
|
||||||
|
2025-05-10 23:26:37,188 ERROR: Fehler 404: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
Endpoint: /api/mindmap/science, Method: GET, IP: 127.0.0.1
|
||||||
|
User: 1 (admin)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1823, in full_dispatch_request
|
||||||
|
rv = self.dispatch_request()
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1788, in dispatch_request
|
||||||
|
self.raise_routing_exception(req)
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1770, in raise_routing_exception
|
||||||
|
raise request.routing_exception # type: ignore
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\ctx.py", line 351, in match_request
|
||||||
|
result = self.url_adapter.match(return_rule=True) # type: ignore
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\werkzeug\routing\map.py", line 624, in match
|
||||||
|
raise NotFound() from None
|
||||||
|
werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
[in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:92]
|
||||||
|
2025-05-10 23:26:37,188 ERROR: Fehler 404: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
Endpoint: /api/mindmap/science, Method: GET, IP: 127.0.0.1
|
||||||
|
User: 1 (admin)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1823, in full_dispatch_request
|
||||||
|
rv = self.dispatch_request()
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1788, in dispatch_request
|
||||||
|
self.raise_routing_exception(req)
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1770, in raise_routing_exception
|
||||||
|
raise request.routing_exception # type: ignore
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\ctx.py", line 351, in match_request
|
||||||
|
result = self.url_adapter.match(return_rule=True) # type: ignore
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\werkzeug\routing\map.py", line 624, in match
|
||||||
|
raise NotFound() from None
|
||||||
|
werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
[in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:92]
|
||||||
|
2025-05-10 23:26:38,359 ERROR: Fehler 404: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
Endpoint: /api/mindmap/science, Method: GET, IP: 127.0.0.1
|
||||||
|
User: 1 (admin)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1823, in full_dispatch_request
|
||||||
|
rv = self.dispatch_request()
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1788, in dispatch_request
|
||||||
|
self.raise_routing_exception(req)
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1770, in raise_routing_exception
|
||||||
|
raise request.routing_exception # type: ignore
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\ctx.py", line 351, in match_request
|
||||||
|
result = self.url_adapter.match(return_rule=True) # type: ignore
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\werkzeug\routing\map.py", line 624, in match
|
||||||
|
raise NotFound() from None
|
||||||
|
werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
[in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:92]
|
||||||
|
2025-05-10 23:26:38,359 ERROR: Fehler 404: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
Endpoint: /api/mindmap/science, Method: GET, IP: 127.0.0.1
|
||||||
|
User: 1 (admin)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1823, in full_dispatch_request
|
||||||
|
rv = self.dispatch_request()
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1788, in dispatch_request
|
||||||
|
self.raise_routing_exception(req)
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1770, in raise_routing_exception
|
||||||
|
raise request.routing_exception # type: ignore
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\ctx.py", line 351, in match_request
|
||||||
|
result = self.url_adapter.match(return_rule=True) # type: ignore
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\werkzeug\routing\map.py", line 624, in match
|
||||||
|
raise NotFound() from None
|
||||||
|
werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
[in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:92]
|
||||||
|
2025-05-10 23:27:24,242 ERROR: Fehler 404: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
Endpoint: /api/mindmap/root, Method: GET, IP: 127.0.0.1
|
||||||
|
User: 1 (admin)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1823, in full_dispatch_request
|
||||||
|
rv = self.dispatch_request()
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1788, in dispatch_request
|
||||||
|
self.raise_routing_exception(req)
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1770, in raise_routing_exception
|
||||||
|
raise request.routing_exception # type: ignore
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\ctx.py", line 351, in match_request
|
||||||
|
result = self.url_adapter.match(return_rule=True) # type: ignore
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\werkzeug\routing\map.py", line 624, in match
|
||||||
|
raise NotFound() from None
|
||||||
|
werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
[in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:92]
|
||||||
|
2025-05-10 23:27:24,242 ERROR: Fehler 404: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
Endpoint: /api/mindmap/root, Method: GET, IP: 127.0.0.1
|
||||||
|
User: 1 (admin)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1823, in full_dispatch_request
|
||||||
|
rv = self.dispatch_request()
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1788, in dispatch_request
|
||||||
|
self.raise_routing_exception(req)
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1770, in raise_routing_exception
|
||||||
|
raise request.routing_exception # type: ignore
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\ctx.py", line 351, in match_request
|
||||||
|
result = self.url_adapter.match(return_rule=True) # type: ignore
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\werkzeug\routing\map.py", line 624, in match
|
||||||
|
raise NotFound() from None
|
||||||
|
werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
[in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:92]
|
||||||
|
2025-05-10 23:27:26,086 ERROR: Fehler 404: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
Endpoint: /api/mindmap/philosophy, Method: GET, IP: 127.0.0.1
|
||||||
|
User: 1 (admin)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1823, in full_dispatch_request
|
||||||
|
rv = self.dispatch_request()
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1788, in dispatch_request
|
||||||
|
self.raise_routing_exception(req)
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1770, in raise_routing_exception
|
||||||
|
raise request.routing_exception # type: ignore
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\ctx.py", line 351, in match_request
|
||||||
|
result = self.url_adapter.match(return_rule=True) # type: ignore
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\werkzeug\routing\map.py", line 624, in match
|
||||||
|
raise NotFound() from None
|
||||||
|
werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
[in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:92]
|
||||||
|
2025-05-10 23:27:26,086 ERROR: Fehler 404: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
Endpoint: /api/mindmap/philosophy, Method: GET, IP: 127.0.0.1
|
||||||
|
User: 1 (admin)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1823, in full_dispatch_request
|
||||||
|
rv = self.dispatch_request()
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1788, in dispatch_request
|
||||||
|
self.raise_routing_exception(req)
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1770, in raise_routing_exception
|
||||||
|
raise request.routing_exception # type: ignore
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\ctx.py", line 351, in match_request
|
||||||
|
result = self.url_adapter.match(return_rule=True) # type: ignore
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\werkzeug\routing\map.py", line 624, in match
|
||||||
|
raise NotFound() from None
|
||||||
|
werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
[in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:92]
|
||||||
|
2025-05-10 23:27:26,806 ERROR: Fehler 404: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
Endpoint: /api/mindmap/philosophy, Method: GET, IP: 127.0.0.1
|
||||||
|
User: 1 (admin)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1823, in full_dispatch_request
|
||||||
|
rv = self.dispatch_request()
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1788, in dispatch_request
|
||||||
|
self.raise_routing_exception(req)
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1770, in raise_routing_exception
|
||||||
|
raise request.routing_exception # type: ignore
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\ctx.py", line 351, in match_request
|
||||||
|
result = self.url_adapter.match(return_rule=True) # type: ignore
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\werkzeug\routing\map.py", line 624, in match
|
||||||
|
raise NotFound() from None
|
||||||
|
werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
[in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:92]
|
||||||
|
2025-05-10 23:27:26,806 ERROR: Fehler 404: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
Endpoint: /api/mindmap/philosophy, Method: GET, IP: 127.0.0.1
|
||||||
|
User: 1 (admin)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1823, in full_dispatch_request
|
||||||
|
rv = self.dispatch_request()
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1788, in dispatch_request
|
||||||
|
self.raise_routing_exception(req)
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1770, in raise_routing_exception
|
||||||
|
raise request.routing_exception # type: ignore
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\ctx.py", line 351, in match_request
|
||||||
|
result = self.url_adapter.match(return_rule=True) # type: ignore
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\werkzeug\routing\map.py", line 624, in match
|
||||||
|
raise NotFound() from None
|
||||||
|
werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
[in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:92]
|
||||||
|
2025-05-10 23:27:27,018 ERROR: Fehler 404: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
Endpoint: /api/mindmap/philosophy, Method: GET, IP: 127.0.0.1
|
||||||
|
User: 1 (admin)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1823, in full_dispatch_request
|
||||||
|
rv = self.dispatch_request()
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1788, in dispatch_request
|
||||||
|
self.raise_routing_exception(req)
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1770, in raise_routing_exception
|
||||||
|
raise request.routing_exception # type: ignore
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\ctx.py", line 351, in match_request
|
||||||
|
result = self.url_adapter.match(return_rule=True) # type: ignore
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\werkzeug\routing\map.py", line 624, in match
|
||||||
|
raise NotFound() from None
|
||||||
|
werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
[in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:92]
|
||||||
|
2025-05-10 23:27:27,018 ERROR: Fehler 404: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
Endpoint: /api/mindmap/philosophy, Method: GET, IP: 127.0.0.1
|
||||||
|
User: 1 (admin)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1823, in full_dispatch_request
|
||||||
|
rv = self.dispatch_request()
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1788, in dispatch_request
|
||||||
|
self.raise_routing_exception(req)
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1770, in raise_routing_exception
|
||||||
|
raise request.routing_exception # type: ignore
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\ctx.py", line 351, in match_request
|
||||||
|
result = self.url_adapter.match(return_rule=True) # type: ignore
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\werkzeug\routing\map.py", line 624, in match
|
||||||
|
raise NotFound() from None
|
||||||
|
werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
[in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:92]
|
||||||
|
2025-05-10 23:31:23,240 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||||
|
2025-05-10 23:31:25,125 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||||
|
2025-05-10 23:31:25,125 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||||
|
2025-05-10 23:31:28,852 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||||
|
2025-05-10 23:31:30,696 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||||
|
2025-05-10 23:31:30,696 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||||
|
2025-05-10 23:31:35,223 ERROR: Fehler 404: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
Endpoint: /api/mindmap/root, Method: GET, IP: 127.0.0.1
|
||||||
|
User: 1 (admin)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1823, in full_dispatch_request
|
||||||
|
rv = self.dispatch_request()
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1788, in dispatch_request
|
||||||
|
self.raise_routing_exception(req)
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1770, in raise_routing_exception
|
||||||
|
raise request.routing_exception # type: ignore
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\ctx.py", line 351, in match_request
|
||||||
|
result = self.url_adapter.match(return_rule=True) # type: ignore
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\werkzeug\routing\map.py", line 624, in match
|
||||||
|
raise NotFound() from None
|
||||||
|
werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
[in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:92]
|
||||||
|
2025-05-10 23:31:35,223 ERROR: Fehler 404: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
Endpoint: /api/mindmap/root, Method: GET, IP: 127.0.0.1
|
||||||
|
User: 1 (admin)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1823, in full_dispatch_request
|
||||||
|
rv = self.dispatch_request()
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1788, in dispatch_request
|
||||||
|
self.raise_routing_exception(req)
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1770, in raise_routing_exception
|
||||||
|
raise request.routing_exception # type: ignore
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\ctx.py", line 351, in match_request
|
||||||
|
result = self.url_adapter.match(return_rule=True) # type: ignore
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\werkzeug\routing\map.py", line 624, in match
|
||||||
|
raise NotFound() from None
|
||||||
|
werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
[in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:92]
|
||||||
|
2025-05-10 23:31:38,338 ERROR: Fehler 404: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
Endpoint: /.well-known/appspecific/com.chrome.devtools.json, Method: GET, IP: 127.0.0.1
|
||||||
|
User: 1 (admin)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1823, in full_dispatch_request
|
||||||
|
rv = self.dispatch_request()
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1788, in dispatch_request
|
||||||
|
self.raise_routing_exception(req)
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1770, in raise_routing_exception
|
||||||
|
raise request.routing_exception # type: ignore
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\ctx.py", line 351, in match_request
|
||||||
|
result = self.url_adapter.match(return_rule=True) # type: ignore
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\werkzeug\routing\map.py", line 624, in match
|
||||||
|
raise NotFound() from None
|
||||||
|
werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
[in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:92]
|
||||||
|
2025-05-10 23:31:38,338 ERROR: Fehler 404: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
Endpoint: /.well-known/appspecific/com.chrome.devtools.json, Method: GET, IP: 127.0.0.1
|
||||||
|
User: 1 (admin)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1823, in full_dispatch_request
|
||||||
|
rv = self.dispatch_request()
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1788, in dispatch_request
|
||||||
|
self.raise_routing_exception(req)
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1770, in raise_routing_exception
|
||||||
|
raise request.routing_exception # type: ignore
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\ctx.py", line 351, in match_request
|
||||||
|
result = self.url_adapter.match(return_rule=True) # type: ignore
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\werkzeug\routing\map.py", line 624, in match
|
||||||
|
raise NotFound() from None
|
||||||
|
werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
[in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:92]
|
||||||
|
2025-05-10 23:40:36,881 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||||
|
2025-05-10 23:40:38,667 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||||
|
2025-05-10 23:40:38,667 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||||
|
2025-05-10 23:40:52,761 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||||
|
2025-05-10 23:40:54,529 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||||
|
2025-05-10 23:40:54,529 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||||
|
2025-05-10 23:41:07,200 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||||
|
2025-05-10 23:41:08,976 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||||
|
2025-05-10 23:41:08,976 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||||
|
2025-05-10 23:41:21,428 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||||
|
2025-05-10 23:41:23,210 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||||
|
2025-05-10 23:41:23,210 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||||
|
2025-05-10 23:42:37,123 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||||
|
2025-05-10 23:42:38,804 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||||
|
2025-05-10 23:42:38,804 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||||
|
2025-05-10 23:50:59,126 ERROR: Fehler 404: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
Endpoint: /api/mindmap/root, Method: GET, IP: 127.0.0.1
|
||||||
|
User: 1 (admin)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1823, in full_dispatch_request
|
||||||
|
rv = self.dispatch_request()
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1788, in dispatch_request
|
||||||
|
self.raise_routing_exception(req)
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1770, in raise_routing_exception
|
||||||
|
raise request.routing_exception # type: ignore
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\ctx.py", line 351, in match_request
|
||||||
|
result = self.url_adapter.match(return_rule=True) # type: ignore
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\werkzeug\routing\map.py", line 624, in match
|
||||||
|
raise NotFound() from None
|
||||||
|
werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
[in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:92]
|
||||||
|
2025-05-10 23:50:59,126 ERROR: Fehler 404: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
Endpoint: /api/mindmap/root, Method: GET, IP: 127.0.0.1
|
||||||
|
User: 1 (admin)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1823, in full_dispatch_request
|
||||||
|
rv = self.dispatch_request()
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1788, in dispatch_request
|
||||||
|
self.raise_routing_exception(req)
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1770, in raise_routing_exception
|
||||||
|
raise request.routing_exception # type: ignore
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\ctx.py", line 351, in match_request
|
||||||
|
result = self.url_adapter.match(return_rule=True) # type: ignore
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\werkzeug\routing\map.py", line 624, in match
|
||||||
|
raise NotFound() from None
|
||||||
|
werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
[in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:92]
|
||||||
|
2025-05-10 23:51:08,006 INFO: Anwendung gestartet [in c:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||||
|
2025-05-10 23:51:09,703 INFO: Anwendung gestartet [in c:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||||
|
2025-05-10 23:51:09,703 INFO: Anwendung gestartet [in c:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||||
|
2025-05-10 23:51:27,276 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||||
|
2025-05-10 23:51:29,021 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||||
|
2025-05-10 23:51:29,021 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||||
|
2025-05-10 23:51:32,757 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||||
|
2025-05-10 23:51:34,502 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||||
|
2025-05-10 23:51:34,502 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||||
|
2025-05-10 23:51:45,531 ERROR: Fehler 404: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
Endpoint: /api/mindmap/root, Method: GET, IP: 127.0.0.1
|
||||||
|
User: 1 (admin)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1823, in full_dispatch_request
|
||||||
|
rv = self.dispatch_request()
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1788, in dispatch_request
|
||||||
|
self.raise_routing_exception(req)
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1770, in raise_routing_exception
|
||||||
|
raise request.routing_exception # type: ignore
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\ctx.py", line 351, in match_request
|
||||||
|
result = self.url_adapter.match(return_rule=True) # type: ignore
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\werkzeug\routing\map.py", line 624, in match
|
||||||
|
raise NotFound() from None
|
||||||
|
werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
[in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:92]
|
||||||
|
2025-05-10 23:51:45,531 ERROR: Fehler 404: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
Endpoint: /api/mindmap/root, Method: GET, IP: 127.0.0.1
|
||||||
|
User: 1 (admin)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1823, in full_dispatch_request
|
||||||
|
rv = self.dispatch_request()
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1788, in dispatch_request
|
||||||
|
self.raise_routing_exception(req)
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1770, in raise_routing_exception
|
||||||
|
raise request.routing_exception # type: ignore
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\ctx.py", line 351, in match_request
|
||||||
|
result = self.url_adapter.match(return_rule=True) # type: ignore
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\werkzeug\routing\map.py", line 624, in match
|
||||||
|
raise NotFound() from None
|
||||||
|
werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
[in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:92]
|
||||||
|
2025-05-10 23:51:47,801 ERROR: Fehler 404: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
Endpoint: /api/mindmap/arts, Method: GET, IP: 127.0.0.1
|
||||||
|
User: 1 (admin)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1823, in full_dispatch_request
|
||||||
|
rv = self.dispatch_request()
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1788, in dispatch_request
|
||||||
|
self.raise_routing_exception(req)
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1770, in raise_routing_exception
|
||||||
|
raise request.routing_exception # type: ignore
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\ctx.py", line 351, in match_request
|
||||||
|
result = self.url_adapter.match(return_rule=True) # type: ignore
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\werkzeug\routing\map.py", line 624, in match
|
||||||
|
raise NotFound() from None
|
||||||
|
werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
[in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:92]
|
||||||
|
2025-05-10 23:51:47,801 ERROR: Fehler 404: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
Endpoint: /api/mindmap/arts, Method: GET, IP: 127.0.0.1
|
||||||
|
User: 1 (admin)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1823, in full_dispatch_request
|
||||||
|
rv = self.dispatch_request()
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1788, in dispatch_request
|
||||||
|
self.raise_routing_exception(req)
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1770, in raise_routing_exception
|
||||||
|
raise request.routing_exception # type: ignore
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\ctx.py", line 351, in match_request
|
||||||
|
result = self.url_adapter.match(return_rule=True) # type: ignore
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\werkzeug\routing\map.py", line 624, in match
|
||||||
|
raise NotFound() from None
|
||||||
|
werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
[in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:92]
|
||||||
|
2025-05-10 23:51:48,521 ERROR: Fehler 404: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
Endpoint: /api/mindmap/arts, Method: GET, IP: 127.0.0.1
|
||||||
|
User: 1 (admin)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1823, in full_dispatch_request
|
||||||
|
rv = self.dispatch_request()
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1788, in dispatch_request
|
||||||
|
self.raise_routing_exception(req)
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1770, in raise_routing_exception
|
||||||
|
raise request.routing_exception # type: ignore
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\ctx.py", line 351, in match_request
|
||||||
|
result = self.url_adapter.match(return_rule=True) # type: ignore
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\werkzeug\routing\map.py", line 624, in match
|
||||||
|
raise NotFound() from None
|
||||||
|
werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
[in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:92]
|
||||||
|
2025-05-10 23:51:48,521 ERROR: Fehler 404: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
Endpoint: /api/mindmap/arts, Method: GET, IP: 127.0.0.1
|
||||||
|
User: 1 (admin)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1823, in full_dispatch_request
|
||||||
|
rv = self.dispatch_request()
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1788, in dispatch_request
|
||||||
|
self.raise_routing_exception(req)
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1770, in raise_routing_exception
|
||||||
|
raise request.routing_exception # type: ignore
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\ctx.py", line 351, in match_request
|
||||||
|
result = self.url_adapter.match(return_rule=True) # type: ignore
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\werkzeug\routing\map.py", line 624, in match
|
||||||
|
raise NotFound() from None
|
||||||
|
werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
[in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:92]
|
||||||
|
2025-05-10 23:51:48,713 ERROR: Fehler 404: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
Endpoint: /api/mindmap/arts, Method: GET, IP: 127.0.0.1
|
||||||
|
User: 1 (admin)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1823, in full_dispatch_request
|
||||||
|
rv = self.dispatch_request()
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1788, in dispatch_request
|
||||||
|
self.raise_routing_exception(req)
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1770, in raise_routing_exception
|
||||||
|
raise request.routing_exception # type: ignore
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\ctx.py", line 351, in match_request
|
||||||
|
result = self.url_adapter.match(return_rule=True) # type: ignore
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\werkzeug\routing\map.py", line 624, in match
|
||||||
|
raise NotFound() from None
|
||||||
|
werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
[in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:92]
|
||||||
|
2025-05-10 23:51:48,713 ERROR: Fehler 404: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
Endpoint: /api/mindmap/arts, Method: GET, IP: 127.0.0.1
|
||||||
|
User: 1 (admin)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1823, in full_dispatch_request
|
||||||
|
rv = self.dispatch_request()
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1788, in dispatch_request
|
||||||
|
self.raise_routing_exception(req)
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1770, in raise_routing_exception
|
||||||
|
raise request.routing_exception # type: ignore
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\ctx.py", line 351, in match_request
|
||||||
|
result = self.url_adapter.match(return_rule=True) # type: ignore
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\werkzeug\routing\map.py", line 624, in match
|
||||||
|
raise NotFound() from None
|
||||||
|
werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
[in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:92]
|
||||||
|
2025-05-10 23:51:51,763 ERROR: Fehler 404: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
Endpoint: /.well-known/appspecific/com.chrome.devtools.json, Method: GET, IP: 127.0.0.1
|
||||||
|
User: 1 (admin)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1823, in full_dispatch_request
|
||||||
|
rv = self.dispatch_request()
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1788, in dispatch_request
|
||||||
|
self.raise_routing_exception(req)
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1770, in raise_routing_exception
|
||||||
|
raise request.routing_exception # type: ignore
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\ctx.py", line 351, in match_request
|
||||||
|
result = self.url_adapter.match(return_rule=True) # type: ignore
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\werkzeug\routing\map.py", line 624, in match
|
||||||
|
raise NotFound() from None
|
||||||
|
werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
[in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:92]
|
||||||
|
2025-05-10 23:51:51,763 ERROR: Fehler 404: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
Endpoint: /.well-known/appspecific/com.chrome.devtools.json, Method: GET, IP: 127.0.0.1
|
||||||
|
User: 1 (admin)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1823, in full_dispatch_request
|
||||||
|
rv = self.dispatch_request()
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1788, in dispatch_request
|
||||||
|
self.raise_routing_exception(req)
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\app.py", line 1770, in raise_routing_exception
|
||||||
|
raise request.routing_exception # type: ignore
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\flask\ctx.py", line 351, in match_request
|
||||||
|
result = self.url_adapter.match(return_rule=True) # type: ignore
|
||||||
|
File "C:\Users\TTOMCZA.EMEA\AppData\Roaming\Python\Python313\site-packages\werkzeug\routing\map.py", line 624, in match
|
||||||
|
raise NotFound() from None
|
||||||
|
werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
[in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:92]
|
||||||
1
migrations/README
Normal file
1
migrations/README
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Single-database configuration for Flask.
|
||||||
BIN
migrations/__pycache__/env.cpython-311.pyc
Normal file
BIN
migrations/__pycache__/env.cpython-311.pyc
Normal file
Binary file not shown.
BIN
migrations/__pycache__/env.cpython-313.pyc
Normal file
BIN
migrations/__pycache__/env.cpython-313.pyc
Normal file
Binary file not shown.
50
migrations/alembic.ini
Normal file
50
migrations/alembic.ini
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# A generic, single database configuration.
|
||||||
|
|
||||||
|
[alembic]
|
||||||
|
# template used to generate migration files
|
||||||
|
# file_template = %%(rev)s_%%(slug)s
|
||||||
|
|
||||||
|
# set to 'true' to run the environment during
|
||||||
|
# the 'revision' command, regardless of autogenerate
|
||||||
|
# revision_environment = false
|
||||||
|
|
||||||
|
|
||||||
|
# Logging configuration
|
||||||
|
[loggers]
|
||||||
|
keys = root,sqlalchemy,alembic,flask_migrate
|
||||||
|
|
||||||
|
[handlers]
|
||||||
|
keys = console
|
||||||
|
|
||||||
|
[formatters]
|
||||||
|
keys = generic
|
||||||
|
|
||||||
|
[logger_root]
|
||||||
|
level = WARN
|
||||||
|
handlers = console
|
||||||
|
qualname =
|
||||||
|
|
||||||
|
[logger_sqlalchemy]
|
||||||
|
level = WARN
|
||||||
|
handlers =
|
||||||
|
qualname = sqlalchemy.engine
|
||||||
|
|
||||||
|
[logger_alembic]
|
||||||
|
level = INFO
|
||||||
|
handlers =
|
||||||
|
qualname = alembic
|
||||||
|
|
||||||
|
[logger_flask_migrate]
|
||||||
|
level = INFO
|
||||||
|
handlers =
|
||||||
|
qualname = flask_migrate
|
||||||
|
|
||||||
|
[handler_console]
|
||||||
|
class = StreamHandler
|
||||||
|
args = (sys.stderr,)
|
||||||
|
level = NOTSET
|
||||||
|
formatter = generic
|
||||||
|
|
||||||
|
[formatter_generic]
|
||||||
|
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||||
|
datefmt = %H:%M:%S
|
||||||
113
migrations/env.py
Normal file
113
migrations/env.py
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import logging
|
||||||
|
from logging.config import fileConfig
|
||||||
|
|
||||||
|
from flask import current_app
|
||||||
|
|
||||||
|
from alembic import context
|
||||||
|
|
||||||
|
# this is the Alembic Config object, which provides
|
||||||
|
# access to the values within the .ini file in use.
|
||||||
|
config = context.config
|
||||||
|
|
||||||
|
# Interpret the config file for Python logging.
|
||||||
|
# This line sets up loggers basically.
|
||||||
|
fileConfig(config.config_file_name)
|
||||||
|
logger = logging.getLogger('alembic.env')
|
||||||
|
|
||||||
|
|
||||||
|
def get_engine():
|
||||||
|
try:
|
||||||
|
# this works with Flask-SQLAlchemy<3 and Alchemical
|
||||||
|
return current_app.extensions['migrate'].db.get_engine()
|
||||||
|
except (TypeError, AttributeError):
|
||||||
|
# this works with Flask-SQLAlchemy>=3
|
||||||
|
return current_app.extensions['migrate'].db.engine
|
||||||
|
|
||||||
|
|
||||||
|
def get_engine_url():
|
||||||
|
try:
|
||||||
|
return get_engine().url.render_as_string(hide_password=False).replace(
|
||||||
|
'%', '%%')
|
||||||
|
except AttributeError:
|
||||||
|
return str(get_engine().url).replace('%', '%%')
|
||||||
|
|
||||||
|
|
||||||
|
# add your model's MetaData object here
|
||||||
|
# for 'autogenerate' support
|
||||||
|
# from myapp import mymodel
|
||||||
|
# target_metadata = mymodel.Base.metadata
|
||||||
|
config.set_main_option('sqlalchemy.url', get_engine_url())
|
||||||
|
target_db = current_app.extensions['migrate'].db
|
||||||
|
|
||||||
|
# other values from the config, defined by the needs of env.py,
|
||||||
|
# can be acquired:
|
||||||
|
# my_important_option = config.get_main_option("my_important_option")
|
||||||
|
# ... etc.
|
||||||
|
|
||||||
|
|
||||||
|
def get_metadata():
|
||||||
|
if hasattr(target_db, 'metadatas'):
|
||||||
|
return target_db.metadatas[None]
|
||||||
|
return target_db.metadata
|
||||||
|
|
||||||
|
|
||||||
|
def run_migrations_offline():
|
||||||
|
"""Run migrations in 'offline' mode.
|
||||||
|
|
||||||
|
This configures the context with just a URL
|
||||||
|
and not an Engine, though an Engine is acceptable
|
||||||
|
here as well. By skipping the Engine creation
|
||||||
|
we don't even need a DBAPI to be available.
|
||||||
|
|
||||||
|
Calls to context.execute() here emit the given string to the
|
||||||
|
script output.
|
||||||
|
|
||||||
|
"""
|
||||||
|
url = config.get_main_option("sqlalchemy.url")
|
||||||
|
context.configure(
|
||||||
|
url=url, target_metadata=get_metadata(), literal_binds=True
|
||||||
|
)
|
||||||
|
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
|
def run_migrations_online():
|
||||||
|
"""Run migrations in 'online' mode.
|
||||||
|
|
||||||
|
In this scenario we need to create an Engine
|
||||||
|
and associate a connection with the context.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# this callback is used to prevent an auto-migration from being generated
|
||||||
|
# when there are no changes to the schema
|
||||||
|
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
|
||||||
|
def process_revision_directives(context, revision, directives):
|
||||||
|
if getattr(config.cmd_opts, 'autogenerate', False):
|
||||||
|
script = directives[0]
|
||||||
|
if script.upgrade_ops.is_empty():
|
||||||
|
directives[:] = []
|
||||||
|
logger.info('No changes in schema detected.')
|
||||||
|
|
||||||
|
conf_args = current_app.extensions['migrate'].configure_args
|
||||||
|
if conf_args.get("process_revision_directives") is None:
|
||||||
|
conf_args["process_revision_directives"] = process_revision_directives
|
||||||
|
|
||||||
|
connectable = get_engine()
|
||||||
|
|
||||||
|
with connectable.connect() as connection:
|
||||||
|
context.configure(
|
||||||
|
connection=connection,
|
||||||
|
target_metadata=get_metadata(),
|
||||||
|
**conf_args
|
||||||
|
)
|
||||||
|
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
|
if context.is_offline_mode():
|
||||||
|
run_migrations_offline()
|
||||||
|
else:
|
||||||
|
run_migrations_online()
|
||||||
24
migrations/script.py.mako
Normal file
24
migrations/script.py.mako
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
"""${message}
|
||||||
|
|
||||||
|
Revision ID: ${up_revision}
|
||||||
|
Revises: ${down_revision | comma,n}
|
||||||
|
Create Date: ${create_date}
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
${imports if imports else ""}
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = ${repr(up_revision)}
|
||||||
|
down_revision = ${repr(down_revision)}
|
||||||
|
branch_labels = ${repr(branch_labels)}
|
||||||
|
depends_on = ${repr(depends_on)}
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
${upgrades if upgrades else "pass"}
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
${downgrades if downgrades else "pass"}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
38
migrations/versions/add_mindmap_shares_table.py
Normal file
38
migrations/versions/add_mindmap_shares_table.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
"""add mindmap shares table
|
||||||
|
|
||||||
|
Revision ID: add_mindmap_shares
|
||||||
|
Revises: add_missing_user_fields
|
||||||
|
Create Date: 2025-05-10 23:20:00.000000
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import sqlite
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'add_mindmap_shares'
|
||||||
|
down_revision = 'add_missing_user_fields'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# Erstelle PermissionType Enum
|
||||||
|
op.create_table('mindmap_share',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('mindmap_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('shared_by_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('shared_with_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('permission_type', sa.String(20), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('last_accessed', sa.DateTime(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['mindmap_id'], ['user_mindmap.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['shared_by_id'], ['user.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['shared_with_id'], ['user.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('mindmap_id', 'shared_with_id', name='unique_mindmap_share')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
op.drop_table('mindmap_share')
|
||||||
40
migrations/versions/add_missing_user_fields.py
Normal file
40
migrations/versions/add_missing_user_fields.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
"""Add missing user fields
|
||||||
|
|
||||||
|
Revision ID: 5a23f8c6db37
|
||||||
|
Revises: d4406f5b12f7
|
||||||
|
Create Date: 2025-05-02 10:45:00.000000
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '5a23f8c6db37'
|
||||||
|
down_revision = 'd4406f5b12f7'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('user', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('bio', sa.Text(), nullable=True))
|
||||||
|
batch_op.add_column(sa.Column('location', sa.String(length=100), nullable=True))
|
||||||
|
batch_op.add_column(sa.Column('website', sa.String(length=200), nullable=True))
|
||||||
|
batch_op.add_column(sa.Column('avatar', sa.String(length=200), nullable=True))
|
||||||
|
batch_op.add_column(sa.Column('last_login', sa.DateTime(), nullable=True))
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('user', schema=None) as batch_op:
|
||||||
|
batch_op.drop_column('last_login')
|
||||||
|
batch_op.drop_column('avatar')
|
||||||
|
batch_op.drop_column('website')
|
||||||
|
batch_op.drop_column('location')
|
||||||
|
batch_op.drop_column('bio')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
"""Add password column to user
|
||||||
|
|
||||||
|
Revision ID: d4406f5b12f7
|
||||||
|
Revises:
|
||||||
|
Create Date: 2025-04-28 21:26:37.430823
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'd4406f5b12f7'
|
||||||
|
down_revision = None
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('user', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('password', sa.String(length=512), nullable=False, server_default="changeme"))
|
||||||
|
batch_op.add_column(sa.Column('is_active', sa.Boolean(), nullable=True))
|
||||||
|
batch_op.add_column(sa.Column('role', sa.String(length=20), nullable=True))
|
||||||
|
batch_op.drop_column('last_login')
|
||||||
|
batch_op.drop_column('bio')
|
||||||
|
batch_op.drop_column('password_hash')
|
||||||
|
batch_op.drop_column('is_admin')
|
||||||
|
batch_op.drop_column('avatar')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('user', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('avatar', sa.VARCHAR(length=200), nullable=True))
|
||||||
|
batch_op.add_column(sa.Column('is_admin', sa.BOOLEAN(), nullable=True))
|
||||||
|
batch_op.add_column(sa.Column('password_hash', sa.VARCHAR(length=128), nullable=True))
|
||||||
|
batch_op.add_column(sa.Column('bio', sa.TEXT(), nullable=True))
|
||||||
|
batch_op.add_column(sa.Column('last_login', sa.DATETIME(), nullable=True))
|
||||||
|
batch_op.drop_column('role')
|
||||||
|
batch_op.drop_column('is_active')
|
||||||
|
batch_op.drop_column('password')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
191
models.py
Executable file → Normal file
191
models.py
Executable file → Normal file
@@ -6,6 +6,8 @@ from flask_login import UserMixin
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from werkzeug.security import generate_password_hash, check_password_hash
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
import uuid as uuid_pkg
|
||||||
|
import os
|
||||||
|
|
||||||
db = SQLAlchemy()
|
db = SQLAlchemy()
|
||||||
|
|
||||||
@@ -43,30 +45,45 @@ user_thought_bookmark = db.Table('user_thought_bookmark',
|
|||||||
db.Column('created_at', db.DateTime, default=datetime.utcnow)
|
db.Column('created_at', db.DateTime, default=datetime.utcnow)
|
||||||
)
|
)
|
||||||
|
|
||||||
class User(UserMixin, db.Model):
|
class User(db.Model, UserMixin):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
username = db.Column(db.String(80), unique=True, nullable=False)
|
username = db.Column(db.String(80), unique=True, nullable=False)
|
||||||
email = db.Column(db.String(120), unique=True, nullable=False)
|
email = db.Column(db.String(120), unique=True, nullable=False)
|
||||||
password_hash = db.Column(db.String(128))
|
password = db.Column(db.String(512), nullable=False)
|
||||||
is_admin = db.Column(db.Boolean, default=False)
|
|
||||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
last_login = db.Column(db.DateTime)
|
is_active = db.Column(db.Boolean, default=True)
|
||||||
avatar = db.Column(db.String(200))
|
role = db.Column(db.String(20), default="user") # 'user', 'admin', 'moderator'
|
||||||
bio = db.Column(db.Text)
|
bio = db.Column(db.Text, nullable=True) # Profil-Bio
|
||||||
|
location = db.Column(db.String(100), nullable=True) # Standort
|
||||||
|
website = db.Column(db.String(200), nullable=True) # Website
|
||||||
|
avatar = db.Column(db.String(200), nullable=True) # Profilbild-URL
|
||||||
|
last_login = db.Column(db.DateTime, nullable=True) # Letzter Login
|
||||||
|
|
||||||
# Beziehungen
|
# Relationships
|
||||||
|
threads = db.relationship('Thread', backref='creator', lazy=True)
|
||||||
|
messages = db.relationship('Message', backref='author', lazy=True)
|
||||||
|
projects = db.relationship('Project', backref='owner', lazy=True)
|
||||||
|
mindmaps = db.relationship('UserMindmap', backref='user', lazy=True)
|
||||||
thoughts = db.relationship('Thought', backref='author', lazy=True)
|
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,
|
bookmarked_thoughts = db.relationship('Thought', secondary=user_thought_bookmark,
|
||||||
backref=db.backref('bookmarked_by', lazy='dynamic'))
|
lazy='dynamic', backref=db.backref('bookmarked_by', lazy='dynamic'))
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<User {self.username}>'
|
||||||
|
|
||||||
def set_password(self, password):
|
def set_password(self, password):
|
||||||
self.password_hash = generate_password_hash(password)
|
self.password = generate_password_hash(password)
|
||||||
|
|
||||||
def check_password(self, password):
|
def check_password(self, password):
|
||||||
return check_password_hash(self.password_hash, password)
|
return check_password_hash(self.password, password)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_admin(self):
|
||||||
|
return self.role == 'admin'
|
||||||
|
|
||||||
|
@is_admin.setter
|
||||||
|
def is_admin(self, value):
|
||||||
|
self.role = 'admin' if value else 'user'
|
||||||
|
|
||||||
class Category(db.Model):
|
class Category(db.Model):
|
||||||
"""Wissenschaftliche Kategorien für die Gliederung der öffentlichen Mindmap"""
|
"""Wissenschaftliche Kategorien für die Gliederung der öffentlichen Mindmap"""
|
||||||
@@ -81,6 +98,9 @@ class Category(db.Model):
|
|||||||
children = db.relationship('Category', backref=db.backref('parent', remote_side=[id]))
|
children = db.relationship('Category', backref=db.backref('parent', remote_side=[id]))
|
||||||
nodes = db.relationship('MindMapNode', backref='category', lazy=True)
|
nodes = db.relationship('MindMapNode', backref='category', lazy=True)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<Category {self.name}>'
|
||||||
|
|
||||||
class MindMapNode(db.Model):
|
class MindMapNode(db.Model):
|
||||||
"""Öffentliche Mindmap-Knoten, die für alle Benutzer sichtbar sind"""
|
"""Öffentliche Mindmap-Knoten, die für alle Benutzer sichtbar sind"""
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
@@ -92,7 +112,9 @@ class MindMapNode(db.Model):
|
|||||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
created_by_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True)
|
created_by_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True)
|
||||||
category_id = db.Column(db.Integer, db.ForeignKey('category.id'), nullable=True)
|
category_id = db.Column(db.Integer, db.ForeignKey('category.id'), nullable=True)
|
||||||
|
|
||||||
|
__table_args__ = {'extend_existing': True}
|
||||||
|
|
||||||
# Beziehungen für Baumstruktur (mehrere Eltern möglich)
|
# Beziehungen für Baumstruktur (mehrere Eltern möglich)
|
||||||
parents = db.relationship(
|
parents = db.relationship(
|
||||||
'MindMapNode',
|
'MindMapNode',
|
||||||
@@ -111,6 +133,20 @@ class MindMapNode(db.Model):
|
|||||||
# Beziehung zum Ersteller
|
# Beziehung zum Ersteller
|
||||||
created_by = db.relationship('User', backref='created_nodes')
|
created_by = db.relationship('User', backref='created_nodes')
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<MindMapNode {self.name}>'
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
'id': self.id,
|
||||||
|
'name': self.name,
|
||||||
|
'description': self.description,
|
||||||
|
'color_code': self.color_code,
|
||||||
|
'icon': self.icon,
|
||||||
|
'category_id': self.category_id,
|
||||||
|
'created_at': self.created_at.isoformat() if self.created_at else None
|
||||||
|
}
|
||||||
|
|
||||||
class UserMindmap(db.Model):
|
class UserMindmap(db.Model):
|
||||||
"""Benutzerspezifische Mindmap, die vom Benutzer personalisierbar ist"""
|
"""Benutzerspezifische Mindmap, die vom Benutzer personalisierbar ist"""
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
@@ -227,4 +263,129 @@ class Comment(db.Model):
|
|||||||
thought_id = db.Column(db.Integer, db.ForeignKey('thought.id'), 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)
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
last_modified = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
last_modified = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||||
|
|
||||||
|
# Thread model
|
||||||
|
class Thread(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
title = db.Column(db.String(200), nullable=False)
|
||||||
|
description = db.Column(db.Text)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
messages = db.relationship('Message', backref='thread', lazy=True, cascade="all, delete-orphan")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<Thread {self.title}>'
|
||||||
|
|
||||||
|
# Message model
|
||||||
|
class Message(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
content = db.Column(db.Text, nullable=False)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||||
|
thread_id = db.Column(db.Integer, db.ForeignKey('thread.id'), nullable=False)
|
||||||
|
role = db.Column(db.String(20), default="user") # 'user', 'assistant', 'system'
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<Message {self.id} by {self.user_id}>'
|
||||||
|
|
||||||
|
# Project model
|
||||||
|
class Project(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
title = db.Column(db.String(150), nullable=False)
|
||||||
|
description = db.Column(db.Text)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||||
|
category_id = db.Column(db.Integer, db.ForeignKey('category.id'))
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
documents = db.relationship('Document', backref='project', lazy=True, cascade="all, delete-orphan")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<Project {self.title}>'
|
||||||
|
|
||||||
|
# Document model
|
||||||
|
class Document(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
title = db.Column(db.String(150), nullable=False)
|
||||||
|
content = db.Column(db.Text)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||||
|
project_id = db.Column(db.Integer, db.ForeignKey('project.id'), nullable=False)
|
||||||
|
filename = db.Column(db.String(150), nullable=True)
|
||||||
|
file_path = db.Column(db.String(300), nullable=True)
|
||||||
|
file_type = db.Column(db.String(50), nullable=True)
|
||||||
|
file_size = db.Column(db.Integer, nullable=True)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<Document {self.title}>'
|
||||||
|
|
||||||
|
# Forum-Kategorie-Modell - entspricht den Hauptknotenpunkten der Mindmap
|
||||||
|
class ForumCategory(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
node_id = db.Column(db.Integer, db.ForeignKey('mind_map_node.id'), nullable=False)
|
||||||
|
title = db.Column(db.String(200), nullable=False)
|
||||||
|
description = db.Column(db.Text)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
is_active = db.Column(db.Boolean, default=True)
|
||||||
|
|
||||||
|
# Beziehungen
|
||||||
|
node = db.relationship('MindMapNode', backref='forum_category')
|
||||||
|
posts = db.relationship('ForumPost', backref='category', lazy=True, cascade="all, delete-orphan")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<ForumCategory {self.title}>'
|
||||||
|
|
||||||
|
# Forum-Beitrag-Modell
|
||||||
|
class ForumPost(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
title = db.Column(db.String(200), nullable=False)
|
||||||
|
content = db.Column(db.Text, nullable=False)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||||
|
category_id = db.Column(db.Integer, db.ForeignKey('forum_category.id'), nullable=False)
|
||||||
|
parent_id = db.Column(db.Integer, db.ForeignKey('forum_post.id'), nullable=True)
|
||||||
|
is_pinned = db.Column(db.Boolean, default=False)
|
||||||
|
is_locked = db.Column(db.Boolean, default=False)
|
||||||
|
view_count = db.Column(db.Integer, default=0)
|
||||||
|
|
||||||
|
# Beziehungen
|
||||||
|
author = db.relationship('User', backref='forum_posts')
|
||||||
|
replies = db.relationship('ForumPost', backref=db.backref('parent', remote_side=[id]), lazy=True)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<ForumPost {self.title}>'
|
||||||
|
|
||||||
|
# Berechtigungstypen für Mindmap-Freigaben
|
||||||
|
class PermissionType(Enum):
|
||||||
|
READ = "Nur-Lesen"
|
||||||
|
EDIT = "Bearbeiten"
|
||||||
|
ADMIN = "Administrator"
|
||||||
|
|
||||||
|
# Freigabemodell für Mindmaps
|
||||||
|
class MindmapShare(db.Model):
|
||||||
|
"""Speichert Informationen über freigegebene Mindmaps und Berechtigungen"""
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
mindmap_id = db.Column(db.Integer, db.ForeignKey('user_mindmap.id'), nullable=False)
|
||||||
|
shared_by_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||||
|
shared_with_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||||
|
permission_type = db.Column(db.Enum(PermissionType), nullable=False, default=PermissionType.READ)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
last_accessed = db.Column(db.DateTime, nullable=True)
|
||||||
|
|
||||||
|
# Beziehungen
|
||||||
|
mindmap = db.relationship('UserMindmap', backref=db.backref('shares', lazy='dynamic'))
|
||||||
|
shared_by = db.relationship('User', foreign_keys=[shared_by_id], backref=db.backref('shared_mindmaps', lazy='dynamic'))
|
||||||
|
shared_with = db.relationship('User', foreign_keys=[shared_with_id], backref=db.backref('accessible_mindmaps', lazy='dynamic'))
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
db.UniqueConstraint('mindmap_id', 'shared_with_id', name='unique_mindmap_share'),
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<MindmapShare: {self.mindmap_id} - {self.shared_with_id} - {self.permission_type.name}>'
|
||||||
@@ -5,10 +5,13 @@ email-validator
|
|||||||
python-dotenv
|
python-dotenv
|
||||||
werkzeug==2.2.3
|
werkzeug==2.2.3
|
||||||
flask-sqlalchemy==3.0.5
|
flask-sqlalchemy==3.0.5
|
||||||
openai==1.3.0
|
openai
|
||||||
requests==2.31.0
|
requests==2.31.0
|
||||||
flask-cors==4.0.0
|
|
||||||
gunicorn==21.2.0
|
gunicorn==21.2.0
|
||||||
#pillow==10.0.1
|
#pillow==10.0.1
|
||||||
pytest==7.4.0
|
pytest==7.4.0
|
||||||
pytest-flask==1.2.0
|
pytest-flask==1.2.0
|
||||||
|
Flask-Migrate
|
||||||
|
flask-socketio==5.3.6
|
||||||
|
python-engineio==4.8.2
|
||||||
|
python-socketio==5.11.1
|
||||||
53
start.sh
Normal file
53
start.sh
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
#!/usr/bin/env powershell
|
||||||
|
# Windows PowerShell-Version des Start-Skripts
|
||||||
|
# Datum: 01.05.2025
|
||||||
|
|
||||||
|
# Docker-Status prüfen
|
||||||
|
Write-Host "Prüfe Docker-Status..." -ForegroundColor Cyan
|
||||||
|
try {
|
||||||
|
$status = docker ps -q
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Host "Docker ist nicht gestartet. Bitte starten Sie Docker Desktop." -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Write-Host "Docker ist nicht verfügbar. Bitte installieren Sie Docker Desktop und starten Sie es." -ForegroundColor Red
|
||||||
|
Write-Host $_.Exception.Message
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Alte Container stoppen und entfernen
|
||||||
|
$containerExists = docker ps -a --filter "name=systades_app" -q
|
||||||
|
if ($containerExists) {
|
||||||
|
Write-Host "Stoppe und entferne alten Container..." -ForegroundColor Yellow
|
||||||
|
docker rm -f systades_app
|
||||||
|
}
|
||||||
|
|
||||||
|
# Alte Images löschen
|
||||||
|
Write-Host "Entferne altes Image..." -ForegroundColor Yellow
|
||||||
|
docker rmi -f systades_app:latest
|
||||||
|
|
||||||
|
# Stelle sicher, dass das Datenbankverzeichnis existiert
|
||||||
|
if (-not (Test-Path "database")) {
|
||||||
|
New-Item -Path "database" -ItemType Directory -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
# Docker-Compose Setup neu bauen
|
||||||
|
Write-Host "Baue Container neu..." -ForegroundColor Green
|
||||||
|
docker-compose build --no-cache
|
||||||
|
|
||||||
|
# Docker-Compose neu starten
|
||||||
|
Write-Host "Starte Container..." -ForegroundColor Green
|
||||||
|
docker-compose up -d --force-recreate
|
||||||
|
|
||||||
|
# Warte kurz und prüfe, ob der Container läuft
|
||||||
|
Write-Host "Prüfe Container-Status..." -ForegroundColor Cyan
|
||||||
|
Start-Sleep -Seconds 3
|
||||||
|
docker ps | Select-String "systades_app"
|
||||||
|
|
||||||
|
# Ausgabe
|
||||||
|
Write-Host "`nSystemstatus:" -ForegroundColor Cyan
|
||||||
|
Write-Host "----------------------------------------"
|
||||||
|
Write-Host "Systades-Anwendung ist jetzt unter http://localhost:5000 erreichbar." -ForegroundColor Green
|
||||||
|
Write-Host "Container-Logs können mit 'docker logs -f systades_app' angezeigt werden." -ForegroundColor Green
|
||||||
|
Write-Host "----------------------------------------"
|
||||||
1
static/541AABD7-4E37-44ED-B491-1459C8C19699.PNG
Normal file
1
static/541AABD7-4E37-44ED-B491-1459C8C19699.PNG
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
// Background animation with Three.js
|
|
||||||
let scene, camera, renderer, stars = [];
|
|
||||||
|
|
||||||
function initBackground() {
|
|
||||||
// Setup scene
|
|
||||||
scene = new THREE.Scene();
|
|
||||||
|
|
||||||
// Setup camera
|
|
||||||
camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 1000);
|
|
||||||
camera.position.z = 100;
|
|
||||||
|
|
||||||
// Setup renderer
|
|
||||||
renderer = new THREE.WebGLRenderer({ alpha: true });
|
|
||||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
||||||
renderer.setClearColor(0x000000, 0); // Transparent background
|
|
||||||
|
|
||||||
// Append renderer to DOM
|
|
||||||
const backgroundContainer = document.getElementById('background-container');
|
|
||||||
if (backgroundContainer) {
|
|
||||||
backgroundContainer.appendChild(renderer.domElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add stars
|
|
||||||
for (let i = 0; i < 1000; i++) {
|
|
||||||
const geometry = new THREE.SphereGeometry(0.2, 8, 8);
|
|
||||||
const material = new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true, opacity: Math.random() * 0.5 + 0.1 });
|
|
||||||
const star = new THREE.Mesh(geometry, material);
|
|
||||||
|
|
||||||
// Random position
|
|
||||||
star.position.x = Math.random() * 600 - 300;
|
|
||||||
star.position.y = Math.random() * 600 - 300;
|
|
||||||
star.position.z = Math.random() * 600 - 300;
|
|
||||||
|
|
||||||
// Store reference to move in animation
|
|
||||||
star.velocity = Math.random() * 0.02 + 0.005;
|
|
||||||
stars.push(star);
|
|
||||||
|
|
||||||
scene.add(star);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add large glowing particles
|
|
||||||
for (let i = 0; i < 15; i++) {
|
|
||||||
const size = Math.random() * 5 + 2;
|
|
||||||
const geometry = new THREE.SphereGeometry(size, 16, 16);
|
|
||||||
|
|
||||||
// Create a glowing material
|
|
||||||
const color = new THREE.Color();
|
|
||||||
color.setHSL(Math.random(), 0.7, 0.5); // Random hue
|
|
||||||
|
|
||||||
const material = new THREE.MeshBasicMaterial({
|
|
||||||
color: color,
|
|
||||||
transparent: true,
|
|
||||||
opacity: 0.2
|
|
||||||
});
|
|
||||||
|
|
||||||
const particle = new THREE.Mesh(geometry, material);
|
|
||||||
|
|
||||||
// Random position but further away
|
|
||||||
particle.position.x = Math.random() * 1000 - 500;
|
|
||||||
particle.position.y = Math.random() * 1000 - 500;
|
|
||||||
particle.position.z = Math.random() * 200 - 400;
|
|
||||||
|
|
||||||
// Store reference to move in animation
|
|
||||||
particle.velocity = Math.random() * 0.01 + 0.002;
|
|
||||||
stars.push(particle);
|
|
||||||
|
|
||||||
scene.add(particle);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle window resize
|
|
||||||
window.addEventListener('resize', onWindowResize);
|
|
||||||
|
|
||||||
// Start animation
|
|
||||||
animate();
|
|
||||||
}
|
|
||||||
|
|
||||||
function animate() {
|
|
||||||
requestAnimationFrame(animate);
|
|
||||||
|
|
||||||
// Move stars
|
|
||||||
stars.forEach(star => {
|
|
||||||
star.position.z += star.velocity;
|
|
||||||
|
|
||||||
// Reset position if star moves too close
|
|
||||||
if (star.position.z > 100) {
|
|
||||||
star.position.z = -300;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Rotate the entire scene slightly for a dreamy effect
|
|
||||||
scene.rotation.y += 0.0003;
|
|
||||||
scene.rotation.x += 0.0001;
|
|
||||||
|
|
||||||
renderer.render(scene, camera);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onWindowResize() {
|
|
||||||
camera.aspect = window.innerWidth / window.innerHeight;
|
|
||||||
camera.updateProjectionMatrix();
|
|
||||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize background when the DOM is loaded
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', initBackground);
|
|
||||||
} else {
|
|
||||||
initBackground();
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
C:\Users\firem\Downloads\background.mp4
|
|
||||||
27
static/css/all.min.css
vendored
Normal file
27
static/css/all.min.css
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* Font Awesome 6.4.0
|
||||||
|
*
|
||||||
|
* This is a placeholder file. For production, you should:
|
||||||
|
* 1. Download Font Awesome from https://fontawesome.com/download
|
||||||
|
* 2. Extract the downloaded package
|
||||||
|
* 3. Copy the 'css/all.min.css' file to this location
|
||||||
|
* 4. Copy the 'webfonts' folder to '/static/webfonts/'
|
||||||
|
*
|
||||||
|
* Alternatively, you can install via npm and copy the files:
|
||||||
|
* npm install @fortawesome/fontawesome-free
|
||||||
|
* cp -r node_modules/@fortawesome/fontawesome-free/css/all.min.css static/css/
|
||||||
|
* cp -r node_modules/@fortawesome/fontawesome-free/webfonts/ static/
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Placeholder styles for common Font Awesome icons */
|
||||||
|
.fa, .fas, .far, .fab {
|
||||||
|
display: inline-block;
|
||||||
|
width: 1em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Warning message */
|
||||||
|
body::before {
|
||||||
|
content: "Font Awesome CSS placeholder. Please replace with the actual file.";
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
@@ -1,26 +1,37 @@
|
|||||||
/* ChatGPT Assistent Styles */
|
/* ChatGPT Assistent Styles - Verbesserte Version */
|
||||||
#chatgpt-assistant {
|
#chatgpt-assistant {
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: 'Inter', sans-serif;
|
||||||
|
bottom: 5.5rem;
|
||||||
|
z-index: 100;
|
||||||
|
max-height: 85vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
#assistant-chat {
|
#assistant-chat {
|
||||||
transition: max-height 0.3s ease, opacity 0.3s ease;
|
transition: max-height 0.4s cubic-bezier(0.25, 0.1, 0.25, 1),
|
||||||
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.2);
|
opacity 0.3s cubic-bezier(0.25, 0.1, 0.25, 1);
|
||||||
|
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.25);
|
||||||
border-radius: 0.75rem;
|
border-radius: 0.75rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
max-width: calc(100vw - 2rem);
|
||||||
|
max-height: 80vh !important;
|
||||||
#assistant-toggle {
|
|
||||||
transition: transform 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
#assistant-toggle:hover {
|
|
||||||
transform: scale(1.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#assistant-history {
|
#assistant-history {
|
||||||
|
max-height: calc(80vh - 150px);
|
||||||
|
overflow-y: auto;
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: rgba(156, 163, 175, 0.5) transparent;
|
scrollbar-color: rgba(156, 163, 175, 0.5) transparent;
|
||||||
|
padding-bottom: 2rem; /* Zusätzlicher Abstand unten */
|
||||||
|
}
|
||||||
|
|
||||||
|
#assistant-toggle {
|
||||||
|
transition: transform 0.3s ease, background-color 0.2s ease;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
z-index: 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
#assistant-toggle:hover {
|
||||||
|
transform: scale(1.1) rotate(10deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
#assistant-history::-webkit-scrollbar {
|
#assistant-history::-webkit-scrollbar {
|
||||||
@@ -40,27 +51,74 @@
|
|||||||
background-color: rgba(156, 163, 175, 0.3);
|
background-color: rgba(156, 163, 175, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Verbesserte Message-Bubbles mit Schatten und Animation */
|
||||||
|
#assistant-history .flex > div {
|
||||||
|
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
|
||||||
|
animation: messageAppear 0.3s ease-out forwards;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes messageAppear {
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Verzögerte Animation für Messages */
|
||||||
|
#assistant-history .flex:nth-child(1) > div { animation-delay: 0.05s; }
|
||||||
|
#assistant-history .flex:nth-child(2) > div { animation-delay: 0.1s; }
|
||||||
|
#assistant-history .flex:nth-child(3) > div { animation-delay: 0.15s; }
|
||||||
|
#assistant-history .flex:nth-child(4) > div { animation-delay: 0.2s; }
|
||||||
|
#assistant-history .flex:nth-child(5) > div { animation-delay: 0.25s; }
|
||||||
|
|
||||||
|
/* Vorschläge styling */
|
||||||
|
#assistant-suggestions {
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestion-pill {
|
||||||
|
animation: pillAppear 0.4s ease forwards;
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.9);
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pillAppear {
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Styling für verschiedene Verzögerungen bei Vorschlägen */
|
||||||
|
#assistant-suggestions button:nth-child(1) { animation-delay: 0.1s; }
|
||||||
|
#assistant-suggestions button:nth-child(2) { animation-delay: 0.2s; }
|
||||||
|
#assistant-suggestions button:nth-child(3) { animation-delay: 0.3s; }
|
||||||
|
|
||||||
/* Mach Platz für Notifications, damit sie nicht mit dem Assistenten überlappen */
|
/* Mach Platz für Notifications, damit sie nicht mit dem Assistenten überlappen */
|
||||||
.notification-area {
|
.notification-area {
|
||||||
bottom: 5rem;
|
bottom: 5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Verbesserter Glassmorphism-Effekt */
|
/* Verbesserte Glassmorphism-Effekt */
|
||||||
.glass-morphism {
|
.glass-morphism {
|
||||||
background: rgba(255, 255, 255, 0.2);
|
background: rgba(255, 255, 255, 0.25);
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(12px);
|
||||||
-webkit-backdrop-filter: blur(10px);
|
-webkit-backdrop-filter: blur(12px);
|
||||||
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.15);
|
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.15);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .glass-morphism {
|
.dark .glass-morphism {
|
||||||
background: rgba(15, 23, 42, 0.3);
|
background: rgba(15, 23, 42, 0.35);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.4);
|
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dunkleres Dark Theme */
|
/* Verbesserte Farbpalette für Dark Theme */
|
||||||
.dark {
|
.dark {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgba(10, 15, 25, var(--tw-bg-opacity)) !important;
|
background-color: rgba(10, 15, 25, var(--tw-bg-opacity)) !important;
|
||||||
@@ -82,6 +140,62 @@
|
|||||||
background-color: rgba(23, 33, 64, var(--tw-bg-opacity)) !important;
|
background-color: rgba(23, 33, 64, var(--tw-bg-opacity)) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Typing Indicator Animation Styles */
|
||||||
|
.typing-indicator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-indicator span {
|
||||||
|
height: 8px;
|
||||||
|
width: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0 2px;
|
||||||
|
opacity: 0.6;
|
||||||
|
animation: bounce 1.4s infinite ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark .typing-indicator span {
|
||||||
|
background-color: rgba(255, 255, 255, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .typing-indicator span {
|
||||||
|
background-color: rgba(107, 114, 128, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-indicator span:nth-child(1) { animation-delay: 0s; }
|
||||||
|
.typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
|
||||||
|
.typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
|
||||||
|
|
||||||
|
@keyframes bounce {
|
||||||
|
0%, 80%, 100% { transform: translateY(0); }
|
||||||
|
40% { transform: translateY(-8px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chat Input Fokus-Effekt */
|
||||||
|
#assistant-chat input:focus {
|
||||||
|
border-color: var(--primary-500, #3B82F6);
|
||||||
|
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark #assistant-chat input:focus {
|
||||||
|
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Verbesserte Responsive Layouts */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
#assistant-chat {
|
||||||
|
width: calc(100vw - 2rem) !important;
|
||||||
|
max-height: 65vh !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chatgpt-assistant {
|
||||||
|
right: 1rem;
|
||||||
|
bottom: 6rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Footer immer unten */
|
/* Footer immer unten */
|
||||||
html, body {
|
html, body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -101,4 +215,38 @@ main {
|
|||||||
|
|
||||||
footer {
|
footer {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Verbesserte Farbkontraste für Nachrichtenblasen */
|
||||||
|
.user-message {
|
||||||
|
background-color: rgba(124, 58, 237, 0.1) !important;
|
||||||
|
color: #4B5563 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark .user-message {
|
||||||
|
background-color: rgba(124, 58, 237, 0.2) !important;
|
||||||
|
color: #F9FAFB !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-message {
|
||||||
|
background-color: #F3F4F6 !important;
|
||||||
|
color: #1F2937 !important;
|
||||||
|
border-left: 3px solid #8B5CF6;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark .assistant-message {
|
||||||
|
background-color: rgba(31, 41, 55, 0.5) !important;
|
||||||
|
color: #F9FAFB !important;
|
||||||
|
border-left: 3px solid #8B5CF6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chat-Assistent-Position im Footer-Bereich anpassen */
|
||||||
|
.chat-assistant {
|
||||||
|
max-height: 75vh;
|
||||||
|
bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-assistant .chat-messages {
|
||||||
|
max-height: calc(75vh - 180px);
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
253
static/css/mindmap.css
Normal file
253
static/css/mindmap.css
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
/* Mindmap Container Styles */
|
||||||
|
.mindmap-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 600px;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toolbar Styles */
|
||||||
|
.mindmap-toolbar {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
z-index: 1000;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .mindmap-toolbar {
|
||||||
|
background: rgba(30, 41, 59, 0.8);
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toolbar Buttons */
|
||||||
|
.mindmap-toolbar button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mindmap-toolbar button:hover {
|
||||||
|
background: var(--accent-primary);
|
||||||
|
color: white;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mindmap-toolbar button:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mindmap-toolbar button i {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Export Group Styles */
|
||||||
|
.export-group {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-options {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
right: 0;
|
||||||
|
margin-top: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
display: none;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .export-options {
|
||||||
|
background: rgba(30, 41, 59, 0.9);
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-group:hover .export-options {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-options button {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
padding: 8px 12px;
|
||||||
|
justify-content: flex-start;
|
||||||
|
font-size: 14px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Context Menu Styles */
|
||||||
|
.mindmap-context-menu {
|
||||||
|
position: fixed;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
padding: 8px;
|
||||||
|
z-index: 1000;
|
||||||
|
min-width: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .mindmap-context-menu {
|
||||||
|
background: rgba(30, 41, 59, 0.9);
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mindmap-context-menu button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mindmap-context-menu button:hover {
|
||||||
|
background: var(--accent-primary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mindmap-context-menu button i {
|
||||||
|
margin-right: 8px;
|
||||||
|
width: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Node Styles */
|
||||||
|
.mindmap-node {
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
border: 2px solid var(--accent-primary);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mindmap-node:hover {
|
||||||
|
box-shadow: 0 0 0 2px var(--accent-primary);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mindmap-node.selected {
|
||||||
|
border-color: var(--accent-secondary);
|
||||||
|
box-shadow: 0 0 0 3px var(--accent-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Edge Styles */
|
||||||
|
.mindmap-edge {
|
||||||
|
width: 2px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .mindmap-edge {
|
||||||
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mindmap-edge:hover {
|
||||||
|
width: 3px;
|
||||||
|
background-color: var(--accent-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animation Styles */
|
||||||
|
@keyframes nodeAppear {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.8);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mindmap-node-new {
|
||||||
|
animation: nodeAppear 0.3s ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Styles */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.mindmap-toolbar {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
width: calc(100% - 32px);
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-options {
|
||||||
|
left: 0;
|
||||||
|
right: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading State */
|
||||||
|
.mindmap-loading {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mindmap-loading-spinner {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: 4px solid var(--bg-secondary);
|
||||||
|
border-top-color: var(--accent-primary);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tooltip Styles */
|
||||||
|
.mindmap-tooltip {
|
||||||
|
position: absolute;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1000;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
max-width: 200px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .mindmap-tooltip {
|
||||||
|
background: rgba(30, 41, 59, 0.9);
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
106
static/css/neural-network-background.css
Normal file
106
static/css/neural-network-background.css
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
/* Neural Network Background CSS */
|
||||||
|
|
||||||
|
/* Make sure the neural network background is always visible */
|
||||||
|
#neural-network-background {
|
||||||
|
position: fixed !important;
|
||||||
|
top: 0 !important;
|
||||||
|
left: 0 !important;
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
z-index: -10 !important; /* Below content but above regular background */
|
||||||
|
pointer-events: none !important;
|
||||||
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Override any solid background colors for the body */
|
||||||
|
body, body.dark {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make sure any background color is removed */
|
||||||
|
html.dark, html {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make sure any fixed backgrounds are removed */
|
||||||
|
#app-container {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure content is properly visible over the background */
|
||||||
|
.glass-morphism {
|
||||||
|
background-color: rgba(17, 24, 39, 0.6) !important;
|
||||||
|
backdrop-filter: blur(5px) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark Mode - Navbar */
|
||||||
|
body.dark .glass-navbar-dark {
|
||||||
|
background-color: rgba(10, 14, 25, 0.7) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light Mode - Verbesserter Navbar */
|
||||||
|
body .glass-navbar-light {
|
||||||
|
background-color: rgba(255, 255, 255, 0.92) !important;
|
||||||
|
backdrop-filter: blur(10px) !important;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06) !important;
|
||||||
|
border-bottom: 1px solid rgba(220, 220, 220, 0.5) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light Mode - Verbesserte Lesbarkeit für Navbar-Elemente */
|
||||||
|
body:not(.dark) .navbar-link,
|
||||||
|
body:not(.dark) .navbar-item {
|
||||||
|
color: #1e3a8a !important; /* Dunkles Blau für bessere Lesbarkeit */
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .navbar-link:hover,
|
||||||
|
body:not(.dark) .navbar-item:hover {
|
||||||
|
color: #4f46e5 !important; /* Helles Lila beim Hover */
|
||||||
|
background-color: rgba(240, 245, 255, 0.9) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light Mode - Buttons verbessert */
|
||||||
|
body:not(.dark) .btn,
|
||||||
|
body:not(.dark) button {
|
||||||
|
background-color: #3b82f6 !important; /* Klares Blau statt Grau */
|
||||||
|
color: white !important;
|
||||||
|
border: none !important;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .btn:hover,
|
||||||
|
body:not(.dark) button:hover {
|
||||||
|
background-color: #4f46e5 !important; /* Lila beim Hover */
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.12) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Verbesserte Karten im Light Mode */
|
||||||
|
body:not(.dark) .card,
|
||||||
|
body:not(.dark) .panel {
|
||||||
|
background-color: rgba(255, 255, 255, 0.92) !important;
|
||||||
|
border: 1px solid rgba(220, 220, 220, 0.8) !important;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Verbesserte Lesbarkeit für Text im Light Mode */
|
||||||
|
body:not(.dark) {
|
||||||
|
color: #1e293b !important; /* Dunkles Blau-Grau statt Schwarz */
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) h1,
|
||||||
|
body:not(.dark) h2,
|
||||||
|
body:not(.dark) h3,
|
||||||
|
body:not(.dark) h4,
|
||||||
|
body:not(.dark) h5,
|
||||||
|
body:not(.dark) h6 {
|
||||||
|
color: #0f172a !important; /* Fast schwarz für Überschriften */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make sure footer has proper transparency and styling */
|
||||||
|
body.dark footer {
|
||||||
|
background-color: rgba(10, 14, 25, 0.7) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) footer {
|
||||||
|
background-color: rgba(249, 250, 251, 0.92) !important;
|
||||||
|
border-top: 1px solid rgba(220, 220, 220, 0.8) !important;
|
||||||
|
}
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
/* 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,16 +1434,211 @@ 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: 50;
|
z-index: 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Importiere das Cyber-Network CSS */
|
/* Light Mode Optimierungen für wichtige UI-Komponenten */
|
||||||
@import url('/static/css/src/cybernetwork-bg.css');
|
|
||||||
|
/* Buttons im Light Mode */
|
||||||
|
.btn-primary:not(.dark-mode .btn-primary) {
|
||||||
|
background-color: var(--light-primary, #3b82f6);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:not(.dark-mode .btn-primary):hover {
|
||||||
|
background-color: var(--light-primary-hover, #4f46e5);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:not(.dark-mode .btn-secondary) {
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
color: #374151;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:not(.dark-mode .btn-secondary):hover {
|
||||||
|
background-color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navbar im Light Mode */
|
||||||
|
.navbar:not(.dark-mode .navbar),
|
||||||
|
.nav:not(.dark-mode .nav) {
|
||||||
|
background-color: rgba(255, 255, 255, 0.95);
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar:not(.dark-mode .navbar) .nav-link,
|
||||||
|
.nav:not(.dark-mode .nav) .nav-link {
|
||||||
|
color: #1e3a8a;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar:not(.dark-mode .navbar) .nav-link:hover,
|
||||||
|
.nav:not(.dark-mode .nav) .nav-link:hover {
|
||||||
|
color: #4f46e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar:not(.dark-mode .navbar) .navbar-brand,
|
||||||
|
.nav:not(.dark-mode .nav) .navbar-brand {
|
||||||
|
color: #0f172a;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dropdown Menüs im Light Mode */
|
||||||
|
.dropdown-menu:not(.dark-mode .dropdown-menu) {
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05), 0 10px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item:not(.dark-mode .dropdown-item) {
|
||||||
|
color: #1e293b;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item:not(.dark-mode .dropdown-item):hover {
|
||||||
|
background-color: #f1f5f9;
|
||||||
|
color: #4f46e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Karten im Light Mode */
|
||||||
|
.card:not(.dark-mode .card) {
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.03), 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header:not(.dark-mode .card-header) {
|
||||||
|
background-color: #f8fafc;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-footer:not(.dark-mode .card-footer) {
|
||||||
|
background-color: #f8fafc;
|
||||||
|
border-top: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Formulare im Light Mode */
|
||||||
|
.form-control:not(.dark-mode .form-control) {
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:not(.dark-mode .form-control):focus {
|
||||||
|
border-color: #3b82f6;
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tabs im Light Mode */
|
||||||
|
.nav-tabs:not(.dark-mode .nav-tabs) {
|
||||||
|
border-bottom-color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tabs:not(.dark-mode .nav-tabs) .nav-link {
|
||||||
|
color: #64748b;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tabs:not(.dark-mode .nav-tabs) .nav-link:hover {
|
||||||
|
border-color: #e5e7eb #e5e7eb #e5e7eb;
|
||||||
|
color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tabs:not(.dark-mode .nav-tabs) .nav-link.active {
|
||||||
|
color: #0f172a;
|
||||||
|
background-color: white;
|
||||||
|
border-color: #e5e7eb #e5e7eb white;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Alerts im Light Mode */
|
||||||
|
.alert:not(.dark-mode .alert) {
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-primary:not(.dark-mode .alert-primary) {
|
||||||
|
background-color: #eff6ff;
|
||||||
|
border-color: #bfdbfe;
|
||||||
|
color: #1e40af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-success:not(.dark-mode .alert-success) {
|
||||||
|
background-color: #f0fdf4;
|
||||||
|
border-color: #bbf7d0;
|
||||||
|
color: #166534;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-warning:not(.dark-mode .alert-warning) {
|
||||||
|
background-color: #fffbeb;
|
||||||
|
border-color: #fef3c7;
|
||||||
|
color: #92400e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-danger:not(.dark-mode .alert-danger) {
|
||||||
|
background-color: #fef2f2;
|
||||||
|
border-color: #fecaca;
|
||||||
|
color: #b91c1c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Badges im Light Mode */
|
||||||
|
.badge:not(.dark-mode .badge) {
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 0.25em 0.6em;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-primary:not(.dark-mode .badge-primary) {
|
||||||
|
background-color: #3b82f6;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-secondary:not(.dark-mode .badge-secondary) {
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tabellen im Light Mode */
|
||||||
|
table:not(.dark-mode table) {
|
||||||
|
background-color: white;
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
table:not(.dark-mode table) th {
|
||||||
|
background-color: #f8fafc;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
color: #0f172a;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0.75rem;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
table:not(.dark-mode table) td {
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
padding: 0.75rem;
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
table:not(.dark-mode table) tr:hover {
|
||||||
|
background-color: #f8fafc;
|
||||||
|
}
|
||||||
6
static/css/tailwind.min.css
vendored
Normal file
6
static/css/tailwind.min.css
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* Failed to bundle using Rollup v2.79.2: the file imports a not supported node.js built-in module "fs".
|
||||||
|
* If you believe this to be an issue with jsDelivr, and not with the package itself, please open an issue at https://github.com/jsdelivr/jsdelivr
|
||||||
|
*/
|
||||||
|
|
||||||
|
throw new Error('Failed to bundle using Rollup v2.79.2: the file imports a not supported node.js built-in module "fs". If you believe this to be an issue with jsDelivr, and not with the package itself, please open an issue at https://github.com/jsdelivr/jsdelivr');
|
||||||
70
static/d3-extensions.js
vendored
70
static/d3-extensions.js
vendored
@@ -450,6 +450,76 @@ class D3Extensions {
|
|||||||
// Pulsanimation starten
|
// Pulsanimation starten
|
||||||
pulse();
|
pulse();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verarbeitet Daten aus der Datenbank für die Mindmap-Visualisierung
|
||||||
|
* @param {Array} databaseNodes - Knotendaten aus der Datenbank
|
||||||
|
* @param {Array} links - Verbindungsdaten oder null für automatische Extraktion
|
||||||
|
* @returns {Object} Aufbereitete Daten für D3.js
|
||||||
|
*/
|
||||||
|
static processDbNodesForVisualization(databaseNodes, links = null) {
|
||||||
|
// Überprüfe, ob Daten vorhanden sind
|
||||||
|
if (!databaseNodes || databaseNodes.length === 0) {
|
||||||
|
console.warn('Keine Knotendaten zum Verarbeiten vorhanden');
|
||||||
|
return { nodes: [], links: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Knoten mit D3-Kompatiblem Format erstellen
|
||||||
|
const nodes = databaseNodes.map(node => {
|
||||||
|
// Farbgenerierung, falls keine vorhanden
|
||||||
|
const nodeColor = node.color_code ||
|
||||||
|
node.color ||
|
||||||
|
D3Extensions.stringToColor(node.name || 'default');
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: node.id,
|
||||||
|
name: node.name,
|
||||||
|
description: node.description || '',
|
||||||
|
thought_count: node.thought_count || 0,
|
||||||
|
color: nodeColor,
|
||||||
|
// Zusätzliche Attribute
|
||||||
|
category_id: node.category_id,
|
||||||
|
is_public: node.is_public !== undefined ? node.is_public : true,
|
||||||
|
// Position, falls vorhanden
|
||||||
|
x: node.x_position,
|
||||||
|
y: node.y_position,
|
||||||
|
// Größe, falls vorhanden
|
||||||
|
scale: node.scale || 1.0
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verbindungen verarbeiten
|
||||||
|
let processedLinks = [];
|
||||||
|
|
||||||
|
if (links && Array.isArray(links)) {
|
||||||
|
// Verwende übergebene Verbindungen
|
||||||
|
processedLinks = links.map(link => {
|
||||||
|
return {
|
||||||
|
source: link.source,
|
||||||
|
target: link.target,
|
||||||
|
// Zusätzliche Attribute
|
||||||
|
type: link.type || 'default',
|
||||||
|
strength: link.strength || 1
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Extrahiere Verbindungen aus den Knoten
|
||||||
|
databaseNodes.forEach(node => {
|
||||||
|
if (node.connections && Array.isArray(node.connections)) {
|
||||||
|
node.connections.forEach(conn => {
|
||||||
|
processedLinks.push({
|
||||||
|
source: node.id,
|
||||||
|
target: conn.target,
|
||||||
|
type: conn.type || 'default',
|
||||||
|
strength: conn.strength || 1
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { nodes, links: processedLinks };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Globale Verfügbarkeit sicherstellen
|
// Globale Verfügbarkeit sicherstellen
|
||||||
|
|||||||
BIN
static/example.png
Normal file
BIN
static/example.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
35
static/fonts/inter.css
Normal file
35
static/fonts/inter.css
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
/* Inter font - Local version */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
src: url('../fonts/inter-light.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: url('../fonts/inter-regular.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
src: url('../fonts/inter-medium.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
src: url('../fonts/inter-semibold.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
src: url('../fonts/inter-bold.woff2') format('woff2');
|
||||||
|
}
|
||||||
21
static/fonts/jetbrains-mono.css
Normal file
21
static/fonts/jetbrains-mono.css
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
/* JetBrains Mono font - Local version */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: url('../fonts/jetbrainsmono-regular.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
src: url('../fonts/jetbrainsmono-medium.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
src: url('../fonts/jetbrainsmono-bold.woff2') format('woff2');
|
||||||
|
}
|
||||||
11
static/img/default-avatar.svg
Normal file
11
static/img/default-avatar.svg
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<svg width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="100" cy="100" r="98" fill="url(#gradient)" stroke="#7C3AED" stroke-width="4"/>
|
||||||
|
<circle cx="100" cy="80" r="36" fill="white"/>
|
||||||
|
<path d="M100 140C77.9086 140 60 157.909 60 180H140C140 157.909 122.091 140 100 140Z" fill="white"/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="gradient" x1="0" y1="0" x2="200" y2="200" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stop-color="#8B5CF6"/>
|
||||||
|
<stop offset="1" stop-color="#3B82F6"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 583 B |
@@ -1,25 +1,54 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
"""
|
||||||
|
Generate favicon.ico from SVG using cairosvg and PIL
|
||||||
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import io
|
||||||
|
from cairosvg import svg2png
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
import cairosvg
|
|
||||||
|
|
||||||
# Pfad zum SVG-Favicon
|
# Verzeichnis dieses Skripts
|
||||||
svg_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'favicon.svg')
|
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
# Ausgabepfad für das PNG
|
|
||||||
png_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'favicon.png')
|
|
||||||
# Ausgabepfad für das ICO
|
|
||||||
ico_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'favicon.ico')
|
|
||||||
|
|
||||||
# SVG zu PNG konvertieren
|
def svg_to_ico(svg_path, ico_path, sizes=[16, 32, 48, 64, 128, 256]):
|
||||||
cairosvg.svg2png(url=svg_path, write_to=png_path, output_width=512, output_height=512)
|
"""Convert SVG to multi-size ICO file"""
|
||||||
|
img_io = io.BytesIO()
|
||||||
|
|
||||||
|
# Höchste Auflösung für Zwischenspeicherung
|
||||||
|
max_size = max(sizes)
|
||||||
|
|
||||||
|
# SVG in PNG konvertieren
|
||||||
|
with open(svg_path, 'rb') as svg_file:
|
||||||
|
svg_data = svg_file.read()
|
||||||
|
svg2png(bytestring=svg_data, write_to=img_io, output_width=max_size, output_height=max_size)
|
||||||
|
|
||||||
|
# PNG in verschiedene Größen konvertieren
|
||||||
|
img = Image.open(img_io)
|
||||||
|
|
||||||
|
# Alle Größen für das ICO-Format vorbereiten
|
||||||
|
img_list = []
|
||||||
|
for size in sizes:
|
||||||
|
resized_img = img.resize((size, size), Image.LANCZOS)
|
||||||
|
img_list.append(resized_img)
|
||||||
|
|
||||||
|
# ICO-Datei speichern
|
||||||
|
img_list[0].save(
|
||||||
|
ico_path,
|
||||||
|
format='ICO',
|
||||||
|
sizes=[(img.width, img.height) for img in img_list],
|
||||||
|
append_images=img_list[1:]
|
||||||
|
)
|
||||||
|
print(f"Favicon {ico_path} wurde erstellt!")
|
||||||
|
|
||||||
# PNG zu ICO konvertieren
|
# Ursprüngliches Favicon konvertieren
|
||||||
img = Image.open(png_path)
|
svg_to_ico(
|
||||||
img.save(ico_path, sizes=[(16, 16), (32, 32), (48, 48), (64, 64), (128, 128)])
|
os.path.join(CURRENT_DIR, 'favicon.svg'),
|
||||||
|
os.path.join(CURRENT_DIR, 'favicon.ico')
|
||||||
|
)
|
||||||
|
|
||||||
print(f"Favicon erfolgreich erstellt: {ico_path}")
|
# Neues Neuron-Favicon konvertieren
|
||||||
|
svg_to_ico(
|
||||||
# Optional: PNG-Datei löschen, wenn nur ICO benötigt wird
|
os.path.join(CURRENT_DIR, 'neuron-favicon.svg'),
|
||||||
# os.remove(png_path)
|
os.path.join(CURRENT_DIR, 'neuron-favicon.ico')
|
||||||
|
)
|
||||||
29
static/img/neuron-favicon.svg
Normal file
29
static/img/neuron-favicon.svg
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<!-- Hintergrund -->
|
||||||
|
<rect width="32" height="32" rx="8" fill="#6d28d9" />
|
||||||
|
|
||||||
|
<!-- Mindmap-Punkte -->
|
||||||
|
<!-- Zentraler Punkt -->
|
||||||
|
<circle cx="16" cy="16" r="3.5" fill="#a78bfa" />
|
||||||
|
|
||||||
|
<!-- Umgebende Punkte -->
|
||||||
|
<circle cx="8" cy="10" r="2.5" fill="#8b5cf6" />
|
||||||
|
<circle cx="24" cy="10" r="2.5" fill="#8b5cf6" />
|
||||||
|
<circle cx="16" cy="26" r="2.5" fill="#8b5cf6" />
|
||||||
|
|
||||||
|
<!-- Verbindende Linien -->
|
||||||
|
<path d="M16 16 L8 10" stroke="white" stroke-width="1" stroke-linecap="round" />
|
||||||
|
<path d="M16 16 L24 10" stroke="white" stroke-width="1" stroke-linecap="round" />
|
||||||
|
<path d="M16 16 L16 26" stroke="white" stroke-width="1" stroke-linecap="round" />
|
||||||
|
|
||||||
|
<!-- Weitere Verbindungslinien für mehr Komplexität -->
|
||||||
|
<path d="M8 10 L16 26" stroke="#c4b5fd" stroke-width="0.8" stroke-linecap="round" stroke-dasharray="2 1" />
|
||||||
|
<path d="M24 10 L16 26" stroke="#c4b5fd" stroke-width="0.8" stroke-linecap="round" stroke-dasharray="2 1" />
|
||||||
|
<path d="M8 10 L24 10" stroke="#c4b5fd" stroke-width="0.8" stroke-linecap="round" stroke-dasharray="2 1" />
|
||||||
|
|
||||||
|
<!-- Kleine Dekoration-Punkte für Hintergrund-Ähnlichkeit -->
|
||||||
|
<circle cx="5" cy="20" r="0.8" fill="#ddd6fe" opacity="0.7" />
|
||||||
|
<circle cx="27" cy="20" r="0.8" fill="#ddd6fe" opacity="0.7" />
|
||||||
|
<circle cx="20" cy="5" r="0.8" fill="#ddd6fe" opacity="0.7" />
|
||||||
|
<circle cx="12" cy="5" r="0.8" fill="#ddd6fe" opacity="0.7" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
59
static/img/neuron-logo.svg
Normal file
59
static/img/neuron-logo.svg
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<!-- Hintergrund mit Farbverlauf -->
|
||||||
|
<rect width="64" height="64" rx="16" fill="url(#paint0_linear)" />
|
||||||
|
|
||||||
|
<!-- Mindmap-Punkte -->
|
||||||
|
<!-- Zentraler Punkt -->
|
||||||
|
<circle cx="32" cy="32" r="8" fill="url(#glow_gradient)" filter="url(#glow)" />
|
||||||
|
|
||||||
|
<!-- Umgebende Punkte -->
|
||||||
|
<circle cx="16" cy="20" r="6" fill="#8b5cf6" />
|
||||||
|
<circle cx="48" cy="20" r="6" fill="#8b5cf6" />
|
||||||
|
<circle cx="32" cy="52" r="6" fill="#8b5cf6" />
|
||||||
|
<circle cx="16" cy="48" r="4" fill="#a78bfa" />
|
||||||
|
<circle cx="48" cy="48" r="4" fill="#a78bfa" />
|
||||||
|
|
||||||
|
<!-- Verbindende Linien (Hauptpfade) -->
|
||||||
|
<path d="M32 32 L16 20" stroke="white" stroke-width="2" stroke-linecap="round" />
|
||||||
|
<path d="M32 32 L48 20" stroke="white" stroke-width="2" stroke-linecap="round" />
|
||||||
|
<path d="M32 32 L32 52" stroke="white" stroke-width="2" stroke-linecap="round" />
|
||||||
|
<path d="M32 32 L16 48" stroke="white" stroke-width="2" stroke-linecap="round" />
|
||||||
|
<path d="M32 32 L48 48" stroke="white" stroke-width="2" stroke-linecap="round" />
|
||||||
|
|
||||||
|
<!-- Zusätzliche Verbindungslinien -->
|
||||||
|
<path d="M16 20 L16 48" stroke="#c4b5fd" stroke-width="1.5" stroke-linecap="round" stroke-dasharray="3 2" />
|
||||||
|
<path d="M48 20 L48 48" stroke="#c4b5fd" stroke-width="1.5" stroke-linecap="round" stroke-dasharray="3 2" />
|
||||||
|
<path d="M16 20 L48 20" stroke="#c4b5fd" stroke-width="1.5" stroke-linecap="round" stroke-dasharray="3 2" />
|
||||||
|
<path d="M16 48 L32 52" stroke="#c4b5fd" stroke-width="1.5" stroke-linecap="round" stroke-dasharray="3 2" />
|
||||||
|
<path d="M48 48 L32 52" stroke="#c4b5fd" stroke-width="1.5" stroke-linecap="round" stroke-dasharray="3 2" />
|
||||||
|
|
||||||
|
<!-- Kleine Dekoration-Punkte für Hintergrund-Ähnlichkeit -->
|
||||||
|
<circle cx="10" cy="36" r="1.5" fill="#ddd6fe" opacity="0.7" />
|
||||||
|
<circle cx="54" cy="36" r="1.5" fill="#ddd6fe" opacity="0.7" />
|
||||||
|
<circle cx="40" cy="10" r="1.5" fill="#ddd6fe" opacity="0.7" />
|
||||||
|
<circle cx="24" cy="10" r="1.5" fill="#ddd6fe" opacity="0.7" />
|
||||||
|
<circle cx="20" cy="36" r="1.2" fill="#ddd6fe" opacity="0.5" />
|
||||||
|
<circle cx="44" cy="36" r="1.2" fill="#ddd6fe" opacity="0.5" />
|
||||||
|
<circle cx="32" cy="16" r="1.2" fill="#ddd6fe" opacity="0.5" />
|
||||||
|
|
||||||
|
<!-- Definitionen für Farbverläufe und Effekte -->
|
||||||
|
<defs>
|
||||||
|
<!-- Haupthintergrund-Farbverlauf -->
|
||||||
|
<linearGradient id="paint0_linear" x1="0" y1="0" x2="64" y2="64" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#6d28d9" />
|
||||||
|
<stop offset="1" stop-color="#4c1d95" />
|
||||||
|
</linearGradient>
|
||||||
|
|
||||||
|
<!-- Glüheffekt für den zentralen Punkt -->
|
||||||
|
<filter id="glow" x="20" y="20" width="24" height="24" filterUnits="userSpaceOnUse">
|
||||||
|
<feGaussianBlur stdDeviation="2" result="blur" />
|
||||||
|
<feComposite in="SourceGraphic" in2="blur" operator="over" />
|
||||||
|
</filter>
|
||||||
|
|
||||||
|
<!-- Farbverlauf für den zentralen Punkt -->
|
||||||
|
<linearGradient id="glow_gradient" x1="24" y1="24" x2="40" y2="40" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#a78bfa" />
|
||||||
|
<stop offset="1" stop-color="#8b5cf6" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.0 KiB |
5
static/js/alpine.min.js
vendored
Normal file
5
static/js/alpine.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -2,23 +2,11 @@
|
|||||||
* MindMap - Hauptdatei für globale JavaScript-Funktionen
|
* MindMap - Hauptdatei für globale JavaScript-Funktionen
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Import des ChatGPT-Assistenten
|
|
||||||
import ChatGPTAssistant from './modules/chatgpt-assistant.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hauptmodul für die MindMap-Anwendung
|
* Hauptmodul für die MindMap-Anwendung
|
||||||
* Verwaltet die globale Anwendungslogik
|
* Verwaltet die globale Anwendungslogik
|
||||||
*/
|
*/
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
// Initialisiere die Anwendung
|
|
||||||
MindMap.init();
|
|
||||||
|
|
||||||
// Wende Dunkel-/Hellmodus an
|
|
||||||
const isDarkMode = localStorage.getItem('darkMode') === 'dark';
|
|
||||||
document.documentElement.classList.toggle('dark', isDarkMode);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hauptobjekt der MindMap-Anwendung
|
* Hauptobjekt der MindMap-Anwendung
|
||||||
*/
|
*/
|
||||||
@@ -27,7 +15,7 @@ const MindMap = {
|
|||||||
initialized: false,
|
initialized: false,
|
||||||
darkMode: document.documentElement.classList.contains('dark'),
|
darkMode: document.documentElement.classList.contains('dark'),
|
||||||
pageInitializers: {},
|
pageInitializers: {},
|
||||||
currentPage: document.body.dataset.page,
|
currentPage: null,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialisiert die MindMap-Anwendung
|
* Initialisiert die MindMap-Anwendung
|
||||||
@@ -35,13 +23,18 @@ const MindMap = {
|
|||||||
init() {
|
init() {
|
||||||
if (this.initialized) return;
|
if (this.initialized) return;
|
||||||
|
|
||||||
|
// Setze currentPage erst jetzt, wenn DOM garantiert geladen ist
|
||||||
|
this.currentPage = document.body && document.body.dataset ? document.body.dataset.page : null;
|
||||||
|
|
||||||
console.log('MindMap-Anwendung wird initialisiert...');
|
console.log('MindMap-Anwendung wird initialisiert...');
|
||||||
|
|
||||||
// Initialisiere den ChatGPT-Assistenten
|
// Initialisiere den ChatGPT-Assistenten
|
||||||
const assistant = new ChatGPTAssistant();
|
if (typeof ChatGPTAssistant !== 'undefined') {
|
||||||
assistant.init();
|
const assistant = new ChatGPTAssistant();
|
||||||
// Speichere als Teil von MindMap
|
assistant.init();
|
||||||
this.assistant = assistant;
|
// 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]) {
|
||||||
@@ -74,6 +67,12 @@ const MindMap = {
|
|||||||
try {
|
try {
|
||||||
console.log('Initialisiere Mindmap...');
|
console.log('Initialisiere Mindmap...');
|
||||||
|
|
||||||
|
// Prüfe, ob MindMapVisualization geladen ist
|
||||||
|
if (typeof MindMapVisualization === 'undefined') {
|
||||||
|
console.error('MindMapVisualization-Klasse ist nicht definiert!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Initialisiere die Mindmap
|
// Initialisiere die Mindmap
|
||||||
const mindmap = new MindMapVisualization('#mindmap-container', {
|
const mindmap = new MindMapVisualization('#mindmap-container', {
|
||||||
height: mindmapContainer.clientHeight || 600,
|
height: mindmapContainer.clientHeight || 600,
|
||||||
@@ -224,6 +223,13 @@ const MindMap = {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
window.MindMap = MindMap;
|
||||||
|
|
||||||
// Globale Export für andere Module
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
window.MindMap = MindMap;
|
// Initialisiere die Anwendung
|
||||||
|
MindMap.init();
|
||||||
|
|
||||||
|
// Wende Dunkel-/Hellmodus an
|
||||||
|
const isDarkMode = localStorage.getItem('darkMode') === 'dark';
|
||||||
|
document.documentElement.classList.toggle('dark', isDarkMode);
|
||||||
|
});
|
||||||
214
static/js/mindmap-init.js
Normal file
214
static/js/mindmap-init.js
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
/**
|
||||||
|
* Mindmap Initialisierung und Event-Handling
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Warte auf die Cytoscape-Instanz
|
||||||
|
document.addEventListener('mindmap-loaded', function() {
|
||||||
|
const cy = window.cy;
|
||||||
|
if (!cy) return;
|
||||||
|
|
||||||
|
// Event-Listener für Knoten-Klicks
|
||||||
|
cy.on('tap', 'node', function(evt) {
|
||||||
|
const node = evt.target;
|
||||||
|
|
||||||
|
// Alle vorherigen Hervorhebungen zurücksetzen
|
||||||
|
cy.nodes().forEach(n => {
|
||||||
|
n.removeStyle();
|
||||||
|
n.connectedEdges().removeStyle();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Speichere ausgewählten Knoten
|
||||||
|
window.mindmapInstance.selectedNode = node;
|
||||||
|
|
||||||
|
// Aktiviere leuchtenden Effekt statt Umkreisung
|
||||||
|
node.style({
|
||||||
|
'background-opacity': 1,
|
||||||
|
'background-color': node.data('color'),
|
||||||
|
'shadow-color': node.data('color'),
|
||||||
|
'shadow-opacity': 1,
|
||||||
|
'shadow-blur': 15,
|
||||||
|
'shadow-offset-x': 0,
|
||||||
|
'shadow-offset-y': 0
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verbundene Kanten und Knoten hervorheben
|
||||||
|
const connectedEdges = node.connectedEdges();
|
||||||
|
const connectedNodes = node.neighborhood('node');
|
||||||
|
|
||||||
|
connectedEdges.style({
|
||||||
|
'line-color': '#a78bfa',
|
||||||
|
'target-arrow-color': '#a78bfa',
|
||||||
|
'source-arrow-color': '#a78bfa',
|
||||||
|
'line-opacity': 0.8,
|
||||||
|
'width': 2
|
||||||
|
});
|
||||||
|
|
||||||
|
connectedNodes.style({
|
||||||
|
'shadow-opacity': 0.7,
|
||||||
|
'shadow-blur': 10,
|
||||||
|
'shadow-color': '#a78bfa'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Info-Panel aktualisieren
|
||||||
|
updateInfoPanel(node);
|
||||||
|
|
||||||
|
// Seitenleiste aktualisieren
|
||||||
|
updateSidebar(node);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Klick auf Hintergrund - Auswahl zurücksetzen
|
||||||
|
cy.on('tap', function(evt) {
|
||||||
|
if (evt.target === cy) {
|
||||||
|
resetSelection(cy);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Zoom-Controls
|
||||||
|
document.getElementById('zoomIn')?.addEventListener('click', () => {
|
||||||
|
cy.zoom({
|
||||||
|
level: cy.zoom() * 1.2,
|
||||||
|
renderedPosition: { x: cy.width() / 2, y: cy.height() / 2 }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('zoomOut')?.addEventListener('click', () => {
|
||||||
|
cy.zoom({
|
||||||
|
level: cy.zoom() / 1.2,
|
||||||
|
renderedPosition: { x: cy.width() / 2, y: cy.height() / 2 }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('resetView')?.addEventListener('click', () => {
|
||||||
|
cy.fit();
|
||||||
|
resetSelection(cy);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Legend-Toggle
|
||||||
|
document.getElementById('toggleLegend')?.addEventListener('click', () => {
|
||||||
|
const legend = document.getElementById('categoryLegend');
|
||||||
|
if (legend) {
|
||||||
|
isLegendVisible = !isLegendVisible;
|
||||||
|
legend.style.display = isLegendVisible ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keyboard-Controls
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === '+' || e.key === '=') {
|
||||||
|
cy.zoom({
|
||||||
|
level: cy.zoom() * 1.2,
|
||||||
|
renderedPosition: { x: cy.width() / 2, y: cy.height() / 2 }
|
||||||
|
});
|
||||||
|
} else if (e.key === '-' || e.key === '_') {
|
||||||
|
cy.zoom({
|
||||||
|
level: cy.zoom() / 1.2,
|
||||||
|
renderedPosition: { x: cy.width() / 2, y: cy.height() / 2 }
|
||||||
|
});
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
resetSelection(cy);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aktualisiert das Info-Panel mit Knoteninformationen
|
||||||
|
* @param {Object} node - Der ausgewählte Knoten
|
||||||
|
*/
|
||||||
|
function updateInfoPanel(node) {
|
||||||
|
const infoPanel = document.getElementById('infoPanel');
|
||||||
|
if (!infoPanel) return;
|
||||||
|
|
||||||
|
const data = node.data();
|
||||||
|
const connectedNodes = node.neighborhood('node');
|
||||||
|
|
||||||
|
let html = `
|
||||||
|
<h3>${data.label || data.name}</h3>
|
||||||
|
<p class="category">${data.category || 'Keine Kategorie'}</p>
|
||||||
|
${data.description ? `<p class="description">${data.description}</p>` : ''}
|
||||||
|
<div class="connections">
|
||||||
|
<h4>Verbindungen (${connectedNodes.length})</h4>
|
||||||
|
<ul>
|
||||||
|
`;
|
||||||
|
|
||||||
|
connectedNodes.forEach(connectedNode => {
|
||||||
|
const connectedData = connectedNode.data();
|
||||||
|
html += `
|
||||||
|
<li style="color: ${connectedData.color || '#60a5fa'}">
|
||||||
|
${connectedData.label || connectedData.name}
|
||||||
|
</li>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
html += `
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
infoPanel.innerHTML = html;
|
||||||
|
infoPanel.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aktualisiert die Seitenleiste mit Knoteninformationen
|
||||||
|
* @param {Object} node - Der ausgewählte Knoten
|
||||||
|
*/
|
||||||
|
function updateSidebar(node) {
|
||||||
|
const sidebar = document.getElementById('sidebar');
|
||||||
|
if (!sidebar) return;
|
||||||
|
|
||||||
|
const data = node.data();
|
||||||
|
const connectedNodes = node.neighborhood('node');
|
||||||
|
|
||||||
|
let html = `
|
||||||
|
<div class="node-details">
|
||||||
|
<h3>${data.label || data.name}</h3>
|
||||||
|
<p class="category">${data.category || 'Keine Kategorie'}</p>
|
||||||
|
${data.description ? `<p class="description">${data.description}</p>` : ''}
|
||||||
|
<div class="connections">
|
||||||
|
<h4>Verbindungen (${connectedNodes.length})</h4>
|
||||||
|
<ul>
|
||||||
|
`;
|
||||||
|
|
||||||
|
connectedNodes.forEach(connectedNode => {
|
||||||
|
const connectedData = connectedNode.data();
|
||||||
|
html += `
|
||||||
|
<li style="color: ${connectedData.color || '#60a5fa'}">
|
||||||
|
${connectedData.label || connectedData.name}
|
||||||
|
</li>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
html += `
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
sidebar.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setzt die Auswahl zurück
|
||||||
|
* @param {Object} cy - Cytoscape-Instanz
|
||||||
|
*/
|
||||||
|
function resetSelection(cy) {
|
||||||
|
window.mindmapInstance.selectedNode = null;
|
||||||
|
|
||||||
|
// Alle Hervorhebungen zurücksetzen
|
||||||
|
cy.nodes().forEach(node => {
|
||||||
|
node.removeStyle();
|
||||||
|
node.connectedEdges().removeStyle();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Info-Panel ausblenden
|
||||||
|
const infoPanel = document.getElementById('infoPanel');
|
||||||
|
if (infoPanel) {
|
||||||
|
infoPanel.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seitenleiste leeren
|
||||||
|
const sidebar = document.getElementById('sidebar');
|
||||||
|
if (sidebar) {
|
||||||
|
sidebar.innerHTML = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,62 @@ class ChatGPTAssistant {
|
|||||||
this.container = null;
|
this.container = null;
|
||||||
this.chatHistory = null;
|
this.chatHistory = null;
|
||||||
this.inputField = null;
|
this.inputField = null;
|
||||||
|
this.suggestionArea = null;
|
||||||
|
this.maxRetries = 2;
|
||||||
|
this.retryCount = 0;
|
||||||
|
this.markdownParser = null;
|
||||||
|
this.initializeMarkdownParser();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialisiert den Markdown-Parser
|
||||||
|
*/
|
||||||
|
async initializeMarkdownParser() {
|
||||||
|
// Dynamisch marked.js laden, wenn noch nicht vorhanden
|
||||||
|
if (!window.marked) {
|
||||||
|
try {
|
||||||
|
// Prüfen, ob marked.js bereits im Dokument geladen ist
|
||||||
|
if (!document.querySelector('script[src*="marked"]')) {
|
||||||
|
// Falls nicht, Script-Tag erstellen und einfügen
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = 'https://cdn.jsdelivr.net/npm/marked/marked.min.js';
|
||||||
|
script.async = true;
|
||||||
|
|
||||||
|
// Promise erstellen, das resolved wird, wenn das Script geladen wurde
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
script.onload = resolve;
|
||||||
|
script.onerror = reject;
|
||||||
|
document.head.appendChild(script);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Marked.js erfolgreich geladen');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marked konfigurieren
|
||||||
|
this.markdownParser = window.marked;
|
||||||
|
this.markdownParser.setOptions({
|
||||||
|
gfm: true,
|
||||||
|
breaks: true,
|
||||||
|
sanitize: true,
|
||||||
|
smartLists: true,
|
||||||
|
smartypants: true
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Laden von marked.js:', error);
|
||||||
|
// Fallback-Parser, der nur einfache Absätze erkennt
|
||||||
|
this.markdownParser = {
|
||||||
|
parse: (text) => {
|
||||||
|
return text.split('\n').map(line => {
|
||||||
|
if (line.trim() === '') return '<br>';
|
||||||
|
return `<p>${line}</p>`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Marked ist bereits geladen
|
||||||
|
this.markdownParser = window.marked;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -24,7 +80,16 @@ class ChatGPTAssistant {
|
|||||||
this.setupEventListeners();
|
this.setupEventListeners();
|
||||||
|
|
||||||
// Ersten Willkommensnachricht anzeigen
|
// Ersten Willkommensnachricht anzeigen
|
||||||
this.addMessage("assistant", "Frage den KI-Assistenten");
|
this.addMessage("assistant", "Hallo! Ich bin dein KI-Assistent (4o-mini) und habe Zugriff auf die Wissensdatenbank. Wie kann ich dir helfen?\n\nDu kannst mir Fragen über:\n- **Gedanken** in der Datenbank\n- **Kategorien** und Wissenschaftsbereiche\n- **Mindmaps** und Wissensverknüpfungen\n\nstellen.");
|
||||||
|
|
||||||
|
// Vorschläge anzeigen
|
||||||
|
this.showSuggestions([
|
||||||
|
"Zeige mir Gedanken zur künstlichen Intelligenz",
|
||||||
|
"Welche Kategorien gibt es in der Datenbank?",
|
||||||
|
"Suche nach Mindmaps zum Thema Informatik"
|
||||||
|
]);
|
||||||
|
|
||||||
|
console.log('KI-Assistent initialisiert!');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -45,7 +110,7 @@ class ChatGPTAssistant {
|
|||||||
// Chat-Container
|
// Chat-Container
|
||||||
const chatContainer = document.createElement('div');
|
const chatContainer = document.createElement('div');
|
||||||
chatContainer.id = 'assistant-chat';
|
chatContainer.id = 'assistant-chat';
|
||||||
chatContainer.className = 'bg-white dark:bg-dark-800 rounded-lg shadow-xl overflow-hidden transition-all duration-300 w-80 sm:w-96 max-h-0 opacity-0';
|
chatContainer.className = 'bg-white dark:bg-dark-800 rounded-lg shadow-xl overflow-hidden transition-all duration-300 w-80 md:w-96 max-h-0 opacity-0';
|
||||||
|
|
||||||
// Chat-Header
|
// Chat-Header
|
||||||
const header = document.createElement('div');
|
const header = document.createElement('div');
|
||||||
@@ -53,7 +118,7 @@ class ChatGPTAssistant {
|
|||||||
header.innerHTML = `
|
header.innerHTML = `
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<i class="fas fa-robot mr-2"></i>
|
<i class="fas fa-robot mr-2"></i>
|
||||||
<span>KI-Assistent</span>
|
<span>KI-Assistent (4o-mini)</span>
|
||||||
</div>
|
</div>
|
||||||
<button id="assistant-close" class="text-white hover:text-gray-200">
|
<button id="assistant-close" class="text-white hover:text-gray-200">
|
||||||
<i class="fas fa-times"></i>
|
<i class="fas fa-times"></i>
|
||||||
@@ -63,7 +128,12 @@ class ChatGPTAssistant {
|
|||||||
// Chat-Verlauf
|
// Chat-Verlauf
|
||||||
this.chatHistory = document.createElement('div');
|
this.chatHistory = document.createElement('div');
|
||||||
this.chatHistory.id = 'assistant-history';
|
this.chatHistory.id = 'assistant-history';
|
||||||
this.chatHistory.className = 'p-3 overflow-y-auto max-h-80 space-y-3';
|
this.chatHistory.className = 'p-3 overflow-y-auto max-h-96 space-y-3';
|
||||||
|
|
||||||
|
// Vorschlagsbereich
|
||||||
|
this.suggestionArea = document.createElement('div');
|
||||||
|
this.suggestionArea.id = 'assistant-suggestions';
|
||||||
|
this.suggestionArea.className = 'px-3 pb-2 flex flex-wrap gap-2 overflow-x-auto hidden';
|
||||||
|
|
||||||
// Chat-Eingabe
|
// Chat-Eingabe
|
||||||
const inputContainer = document.createElement('div');
|
const inputContainer = document.createElement('div');
|
||||||
@@ -71,7 +141,7 @@ class ChatGPTAssistant {
|
|||||||
|
|
||||||
this.inputField = document.createElement('input');
|
this.inputField = document.createElement('input');
|
||||||
this.inputField.type = 'text';
|
this.inputField.type = 'text';
|
||||||
this.inputField.placeholder = 'Frage den KI-Assistenten';
|
this.inputField.placeholder = 'Stelle eine Frage zur Wissensdatenbank...';
|
||||||
this.inputField.className = 'flex-1 border border-gray-300 dark:border-dark-600 dark:bg-dark-700 dark:text-white rounded-l-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary-500';
|
this.inputField.className = 'flex-1 border border-gray-300 dark:border-dark-600 dark:bg-dark-700 dark:text-white rounded-l-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary-500';
|
||||||
|
|
||||||
const sendButton = document.createElement('button');
|
const sendButton = document.createElement('button');
|
||||||
@@ -85,6 +155,7 @@ class ChatGPTAssistant {
|
|||||||
|
|
||||||
chatContainer.appendChild(header);
|
chatContainer.appendChild(header);
|
||||||
chatContainer.appendChild(this.chatHistory);
|
chatContainer.appendChild(this.chatHistory);
|
||||||
|
chatContainer.appendChild(this.suggestionArea);
|
||||||
chatContainer.appendChild(inputContainer);
|
chatContainer.appendChild(inputContainer);
|
||||||
|
|
||||||
this.container.appendChild(toggleButton);
|
this.container.appendChild(toggleButton);
|
||||||
@@ -100,22 +171,40 @@ class ChatGPTAssistant {
|
|||||||
setupEventListeners() {
|
setupEventListeners() {
|
||||||
// Toggle-Button
|
// Toggle-Button
|
||||||
const toggleButton = document.getElementById('assistant-toggle');
|
const toggleButton = document.getElementById('assistant-toggle');
|
||||||
toggleButton.addEventListener('click', () => this.toggleAssistant());
|
if (toggleButton) {
|
||||||
|
toggleButton.addEventListener('click', () => this.toggleAssistant());
|
||||||
|
}
|
||||||
|
|
||||||
// Schließen-Button
|
// Schließen-Button
|
||||||
const closeButton = document.getElementById('assistant-close');
|
const closeButton = document.getElementById('assistant-close');
|
||||||
closeButton.addEventListener('click', () => this.toggleAssistant(false));
|
if (closeButton) {
|
||||||
|
closeButton.addEventListener('click', () => this.toggleAssistant(false));
|
||||||
|
}
|
||||||
|
|
||||||
// Senden-Button
|
// Senden-Button
|
||||||
const sendButton = document.getElementById('assistant-send');
|
const sendButton = document.getElementById('assistant-send');
|
||||||
sendButton.addEventListener('click', () => this.sendMessage());
|
if (sendButton) {
|
||||||
|
sendButton.addEventListener('click', () => this.sendMessage());
|
||||||
|
}
|
||||||
|
|
||||||
// Enter-Taste im Eingabefeld
|
// Enter-Taste im Eingabefeld
|
||||||
this.inputField.addEventListener('keyup', (e) => {
|
if (this.inputField) {
|
||||||
if (e.key === 'Enter') {
|
this.inputField.addEventListener('keyup', (e) => {
|
||||||
this.sendMessage();
|
if (e.key === 'Enter') {
|
||||||
}
|
this.sendMessage();
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vorschläge klickbar machen
|
||||||
|
if (this.suggestionArea) {
|
||||||
|
this.suggestionArea.addEventListener('click', (e) => {
|
||||||
|
if (e.target.classList.contains('suggestion-pill')) {
|
||||||
|
this.inputField.value = e.target.textContent;
|
||||||
|
this.sendMessage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -124,14 +213,21 @@ class ChatGPTAssistant {
|
|||||||
*/
|
*/
|
||||||
toggleAssistant(state = null) {
|
toggleAssistant(state = null) {
|
||||||
const chatContainer = document.getElementById('assistant-chat');
|
const chatContainer = document.getElementById('assistant-chat');
|
||||||
|
if (!chatContainer) return;
|
||||||
|
|
||||||
this.isOpen = state !== null ? state : !this.isOpen;
|
this.isOpen = state !== null ? state : !this.isOpen;
|
||||||
|
|
||||||
if (this.isOpen) {
|
if (this.isOpen) {
|
||||||
chatContainer.classList.remove('max-h-0', 'opacity-0');
|
chatContainer.classList.remove('max-h-0', 'opacity-0');
|
||||||
chatContainer.classList.add('max-h-96', 'opacity-100');
|
chatContainer.classList.add('max-h-[32rem]', 'opacity-100');
|
||||||
this.inputField.focus();
|
if (this.inputField) this.inputField.focus();
|
||||||
|
|
||||||
|
// Zeige Vorschläge wenn verfügbar
|
||||||
|
if (this.suggestionArea && this.suggestionArea.children.length > 0) {
|
||||||
|
this.suggestionArea.classList.remove('hidden');
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
chatContainer.classList.remove('max-h-96', 'opacity-100');
|
chatContainer.classList.remove('max-h-[32rem]', 'opacity-100');
|
||||||
chatContainer.classList.add('max-h-0', 'opacity-0');
|
chatContainer.classList.add('max-h-0', 'opacity-0');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -151,24 +247,79 @@ class ChatGPTAssistant {
|
|||||||
|
|
||||||
const bubble = document.createElement('div');
|
const bubble = document.createElement('div');
|
||||||
bubble.className = sender === 'user'
|
bubble.className = sender === 'user'
|
||||||
? 'bg-primary-100 dark:bg-primary-900 text-gray-800 dark:text-white rounded-lg py-2 px-3 max-w-[85%]'
|
? 'user-message rounded-lg py-2 px-3 max-w-[85%]'
|
||||||
: 'bg-gray-100 dark:bg-dark-700 text-gray-800 dark:text-white rounded-lg py-2 px-3 max-w-[85%]';
|
: 'assistant-message rounded-lg py-2 px-3 max-w-[85%]';
|
||||||
bubble.textContent = text;
|
|
||||||
|
// Nachrichtentext einfügen, falls Markdown-Parser verfügbar, nutzen
|
||||||
|
if (this.markdownParser) {
|
||||||
|
bubble.innerHTML = this.markdownParser.parse(text);
|
||||||
|
} else {
|
||||||
|
bubble.textContent = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Links in der Nachricht klickbar machen
|
||||||
|
const links = bubble.querySelectorAll('a');
|
||||||
|
links.forEach(link => {
|
||||||
|
link.target = '_blank';
|
||||||
|
link.rel = 'noopener noreferrer';
|
||||||
|
link.className = 'text-primary-600 dark:text-primary-400 underline';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Code-Blöcke stylen
|
||||||
|
const codeBlocks = bubble.querySelectorAll('pre');
|
||||||
|
codeBlocks.forEach(block => {
|
||||||
|
block.className = 'bg-gray-100 dark:bg-dark-900 p-2 rounded my-2 overflow-x-auto';
|
||||||
|
});
|
||||||
|
|
||||||
|
const inlineCode = bubble.querySelectorAll('code:not(pre code)');
|
||||||
|
inlineCode.forEach(code => {
|
||||||
|
code.className = 'bg-gray-100 dark:bg-dark-900 px-1 rounded font-mono text-sm';
|
||||||
|
});
|
||||||
|
|
||||||
messageEl.appendChild(bubble);
|
messageEl.appendChild(bubble);
|
||||||
this.chatHistory.appendChild(messageEl);
|
this.chatHistory.appendChild(messageEl);
|
||||||
|
|
||||||
// Scroll zum Ende des Verlaufs
|
// Scrolle zum Ende des Chat-Verlaufs
|
||||||
this.chatHistory.scrollTop = this.chatHistory.scrollHeight;
|
this.chatHistory.scrollTop = this.chatHistory.scrollHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zeigt Vorschläge für mögliche Fragen an
|
||||||
|
* @param {Array} suggestions - Array von Vorschlägen
|
||||||
|
*/
|
||||||
|
showSuggestions(suggestions) {
|
||||||
|
if (!this.suggestionArea || !suggestions || !suggestions.length) return;
|
||||||
|
|
||||||
|
// Vorherige Vorschläge entfernen
|
||||||
|
this.suggestionArea.innerHTML = '';
|
||||||
|
|
||||||
|
// Neue Vorschläge hinzufügen
|
||||||
|
suggestions.forEach((text, index) => {
|
||||||
|
const pill = document.createElement('button');
|
||||||
|
pill.className = 'suggestion-pill text-sm px-3 py-1.5 rounded-full bg-primary-100 dark:bg-primary-900 text-primary-800 dark:text-primary-200 hover:bg-primary-200 dark:hover:bg-primary-800 transition-all duration-200';
|
||||||
|
pill.style.animationDelay = `${index * 0.1}s`;
|
||||||
|
pill.textContent = text;
|
||||||
|
this.suggestionArea.appendChild(pill);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Vorschlagsbereich anzeigen
|
||||||
|
this.suggestionArea.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sendet die Benutzernachricht an den Server und zeigt die Antwort an
|
* Sendet die Benutzernachricht an den Server und zeigt die Antwort an
|
||||||
*/
|
*/
|
||||||
async sendMessage() {
|
async sendMessage() {
|
||||||
|
if (!this.inputField) return;
|
||||||
|
|
||||||
const userInput = this.inputField.value.trim();
|
const userInput = this.inputField.value.trim();
|
||||||
if (!userInput || this.isLoading) return;
|
if (!userInput || this.isLoading) return;
|
||||||
|
|
||||||
|
// Vorschläge ausblenden
|
||||||
|
if (this.suggestionArea) {
|
||||||
|
this.suggestionArea.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
// Benutzernachricht anzeigen
|
// Benutzernachricht anzeigen
|
||||||
this.addMessage('user', userInput);
|
this.addMessage('user', userInput);
|
||||||
|
|
||||||
@@ -180,6 +331,7 @@ class ChatGPTAssistant {
|
|||||||
this.showLoadingIndicator();
|
this.showLoadingIndicator();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
console.log('Sende Anfrage an KI-Assistent API...');
|
||||||
// Anfrage an den Server senden
|
// Anfrage an den Server senden
|
||||||
const response = await fetch('/api/assistant', {
|
const response = await fetch('/api/assistant', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -189,92 +341,206 @@ class ChatGPTAssistant {
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
messages: this.messages
|
messages: this.messages
|
||||||
}),
|
}),
|
||||||
|
cache: 'no-cache', // Kein Cache verwenden
|
||||||
|
credentials: 'same-origin', // Cookies senden
|
||||||
|
timeout: 60000 // 60 Sekunden Timeout
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Ladeindikator entfernen
|
||||||
|
this.removeLoadingIndicator();
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Netzwerkfehler oder Serverproblem');
|
const errorText = await response.text();
|
||||||
|
let errorMessage;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Versuche, die Fehlermeldung zu parsen
|
||||||
|
const errorData = JSON.parse(errorText);
|
||||||
|
errorMessage = errorData.error || `Serverfehler: ${response.status} ${response.statusText}`;
|
||||||
|
} catch {
|
||||||
|
// Bei Parsing-Fehler verwende Standardfehlermeldung
|
||||||
|
errorMessage = `Serverfehler: ${response.status} ${response.statusText}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
console.log('Antwort erhalten:', data);
|
||||||
// Ladeindikator entfernen
|
|
||||||
this.removeLoadingIndicator();
|
|
||||||
|
|
||||||
// Antwort anzeigen
|
// Antwort anzeigen
|
||||||
this.addMessage('assistant', data.response);
|
if (data.response) {
|
||||||
|
this.addMessage('assistant', data.response);
|
||||||
|
|
||||||
|
// Neue Vorschläge basierend auf dem aktuellen Kontext anzeigen
|
||||||
|
this.generateContextualSuggestions();
|
||||||
|
|
||||||
|
// Erfolgreiche Anfrage zurücksetzen
|
||||||
|
this.retryCount = 0;
|
||||||
|
} else if (data.error) {
|
||||||
|
this.addMessage('assistant', `Fehler: ${data.error}`);
|
||||||
|
} else {
|
||||||
|
throw new Error('Unerwartetes Antwortformat vom Server');
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fehler bei der Kommunikation mit dem Assistenten:', error);
|
console.error('Fehler bei der Kommunikation mit dem Assistenten:', error);
|
||||||
|
|
||||||
// Ladeindikator entfernen
|
// Ladeindikator entfernen, falls noch vorhanden
|
||||||
this.removeLoadingIndicator();
|
this.removeLoadingIndicator();
|
||||||
|
|
||||||
// Fehlermeldung anzeigen
|
// Spezielle Fehlermeldungen für bestimmte Fehlertypen
|
||||||
this.addMessage('assistant', 'Es tut mir leid, aber es gab ein Problem bei der Verarbeitung deiner Anfrage. Bitte versuche es später noch einmal.');
|
const errorMessage = error.message || '';
|
||||||
|
let userFriendlyMessage = 'Es gab ein Problem mit der Anfrage.';
|
||||||
|
|
||||||
|
if (errorMessage.includes('timeout') || errorMessage.includes('Zeitüberschreitung')) {
|
||||||
|
userFriendlyMessage = 'Die Antwort hat zu lange gedauert. Der Server ist möglicherweise überlastet.';
|
||||||
|
} else if (errorMessage.includes('500') || errorMessage.includes('Internal Server Error')) {
|
||||||
|
userFriendlyMessage = 'Ein Serverfehler ist aufgetreten. Wir arbeiten an einer Lösung.';
|
||||||
|
} else if (errorMessage.includes('429') || errorMessage.includes('rate limit')) {
|
||||||
|
userFriendlyMessage = 'Die API-Anfragelimits wurden erreicht. Bitte warte einen Moment.';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fehlermeldung anzeigen oder Wiederholungsversuch starten
|
||||||
|
if (this.retryCount < this.maxRetries) {
|
||||||
|
this.retryCount++;
|
||||||
|
this.addMessage('assistant', `${userFriendlyMessage} Ich versuche es erneut... (Versuch ${this.retryCount}/${this.maxRetries})`);
|
||||||
|
|
||||||
|
// Letzte Benutzernachricht speichern für den Wiederholungsversuch
|
||||||
|
const lastUserMessageIndex = this.messages.findLastIndex(msg => msg.role === 'user');
|
||||||
|
if (lastUserMessageIndex >= 0) {
|
||||||
|
const lastUserMessage = this.messages[lastUserMessageIndex].content;
|
||||||
|
|
||||||
|
// Kurze Verzögerung vor dem erneuten Versuch mit exponentieller Backoff-Strategie
|
||||||
|
const retryDelay = 1500 * Math.pow(2, this.retryCount - 1); // 1.5s, 3s, 6s, ...
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
// Entferne Fehlermeldung aus dem Messages-Array, behalte aber die Benutzernachricht
|
||||||
|
this.messages = this.messages.filter(msg =>
|
||||||
|
!(msg.role === 'assistant' && msg.content.includes('versuche es erneut'))
|
||||||
|
);
|
||||||
|
|
||||||
|
// Erneuter Versand mit gleicher Nachricht
|
||||||
|
this.inputField.value = lastUserMessage;
|
||||||
|
this.sendMessage();
|
||||||
|
}, retryDelay);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Maximale Anzahl an Wiederholungsversuchen erreicht
|
||||||
|
this.addMessage('assistant', 'Es tut mir leid, aber es gab ein Problem bei der Verarbeitung deiner Anfrage. Bitte versuche es später noch einmal oder kontaktiere den Support, falls das Problem weiterhin besteht.');
|
||||||
|
this.retryCount = 0; // Zurücksetzen für die nächste Anfrage
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Zeigt einen Ladeindikator im Chat an
|
* Generiert kontextbasierte Vorschläge basierend auf dem aktuellen Chat-Verlauf
|
||||||
|
*/
|
||||||
|
generateContextualSuggestions() {
|
||||||
|
// Basierend auf letzter Antwort des Assistenten, verschiedene Vorschläge generieren
|
||||||
|
const lastAssistantMessage = this.messages.findLast(msg => msg.role === 'assistant')?.content || '';
|
||||||
|
|
||||||
|
let suggestions = [];
|
||||||
|
|
||||||
|
// Intelligente Vorschläge basierend auf Kontext
|
||||||
|
if (lastAssistantMessage.includes('Künstliche Intelligenz') ||
|
||||||
|
lastAssistantMessage.includes('KI ') ||
|
||||||
|
lastAssistantMessage.includes('AI ')) {
|
||||||
|
suggestions = [
|
||||||
|
"Wie wird KI in der Wissenschaft eingesetzt?",
|
||||||
|
"Zeige mir Gedanken zum maschinellen Lernen",
|
||||||
|
"Was ist der Unterschied zwischen KI und ML?"
|
||||||
|
];
|
||||||
|
} else if (lastAssistantMessage.includes('Kategorie') ||
|
||||||
|
lastAssistantMessage.includes('Kategorien')) {
|
||||||
|
suggestions = [
|
||||||
|
"Zeige mir die Unterkategorien",
|
||||||
|
"Welche Gedanken gehören zu dieser Kategorie?",
|
||||||
|
"Liste alle Wissenschaftskategorien auf"
|
||||||
|
];
|
||||||
|
} else if (lastAssistantMessage.includes('Mindmap') ||
|
||||||
|
lastAssistantMessage.includes('Visualisierung')) {
|
||||||
|
suggestions = [
|
||||||
|
"Wie kann ich eine eigene Mindmap erstellen?",
|
||||||
|
"Zeige mir Beispiele für Mindmaps",
|
||||||
|
"Wie funktionieren die Verbindungen in Mindmaps?"
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
// Standardvorschläge
|
||||||
|
suggestions = [
|
||||||
|
"Erzähle mir mehr dazu",
|
||||||
|
"Gibt es Beispiele dafür?",
|
||||||
|
"Wie kann ich diese Information nutzen?"
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.showSuggestions(suggestions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zeigt eine Ladeanimation an
|
||||||
*/
|
*/
|
||||||
showLoadingIndicator() {
|
showLoadingIndicator() {
|
||||||
|
if (!this.chatHistory) return;
|
||||||
|
|
||||||
|
// Prüfen, ob bereits ein Ladeindikator angezeigt wird
|
||||||
|
if (document.getElementById('assistant-loading-indicator')) return;
|
||||||
|
|
||||||
const loadingEl = document.createElement('div');
|
const loadingEl = document.createElement('div');
|
||||||
loadingEl.id = 'assistant-loading';
|
|
||||||
loadingEl.className = 'flex justify-start';
|
loadingEl.className = 'flex justify-start';
|
||||||
|
loadingEl.id = 'assistant-loading-indicator';
|
||||||
|
|
||||||
const bubble = document.createElement('div');
|
const bubble = document.createElement('div');
|
||||||
bubble.className = 'bg-gray-100 dark:bg-dark-700 text-gray-800 dark:text-white rounded-lg py-2 px-3';
|
bubble.className = 'assistant-message rounded-lg py-3 px-4 max-w-[85%] flex items-center';
|
||||||
bubble.innerHTML = '<div class="typing-indicator"><span></span><span></span><span></span></div>';
|
|
||||||
|
|
||||||
loadingEl.appendChild(bubble);
|
const typingIndicator = document.createElement('div');
|
||||||
this.chatHistory.appendChild(loadingEl);
|
typingIndicator.className = 'typing-indicator';
|
||||||
|
typingIndicator.innerHTML = `
|
||||||
// Scroll zum Ende des Verlaufs
|
<span></span>
|
||||||
this.chatHistory.scrollTop = this.chatHistory.scrollHeight;
|
<span></span>
|
||||||
|
<span></span>
|
||||||
// Stil für den Typing-Indikator
|
|
||||||
const style = document.createElement('style');
|
|
||||||
style.textContent = `
|
|
||||||
.typing-indicator {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.typing-indicator span {
|
|
||||||
height: 8px;
|
|
||||||
width: 8px;
|
|
||||||
background-color: #888;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: inline-block;
|
|
||||||
margin: 0 2px;
|
|
||||||
opacity: 0.4;
|
|
||||||
animation: typing 1.5s infinite ease-in-out;
|
|
||||||
}
|
|
||||||
.typing-indicator span:nth-child(2) {
|
|
||||||
animation-delay: 0.2s;
|
|
||||||
}
|
|
||||||
.typing-indicator span:nth-child(3) {
|
|
||||||
animation-delay: 0.4s;
|
|
||||||
}
|
|
||||||
@keyframes typing {
|
|
||||||
0% { transform: translateY(0); }
|
|
||||||
50% { transform: translateY(-5px); }
|
|
||||||
100% { transform: translateY(0); }
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
document.head.appendChild(style);
|
|
||||||
|
bubble.appendChild(typingIndicator);
|
||||||
|
loadingEl.appendChild(bubble);
|
||||||
|
|
||||||
|
this.chatHistory.appendChild(loadingEl);
|
||||||
|
this.chatHistory.scrollTop = this.chatHistory.scrollHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Entfernt den Ladeindikator aus dem Chat
|
* Entfernt den Ladeindikator aus dem Chat
|
||||||
*/
|
*/
|
||||||
removeLoadingIndicator() {
|
removeLoadingIndicator() {
|
||||||
const loadingEl = document.getElementById('assistant-loading');
|
const loadingIndicator = document.getElementById('assistant-loading-indicator');
|
||||||
if (loadingEl) {
|
if (loadingIndicator) {
|
||||||
loadingEl.remove();
|
loadingIndicator.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Öffnet den Assistenten und sendet eine vorgegebene Frage
|
||||||
|
* @param {string} question - Die zu stellende Frage
|
||||||
|
*/
|
||||||
|
async sendQuestion(question) {
|
||||||
|
if (!question || this.isLoading) return;
|
||||||
|
|
||||||
|
// Assistenten öffnen
|
||||||
|
this.toggleAssistant(true);
|
||||||
|
|
||||||
|
// Kurze Verzögerung, um sicherzustellen, dass der UI vollständig geöffnet ist
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 300));
|
||||||
|
|
||||||
|
// Frage in Eingabefeld setzen
|
||||||
|
if (this.inputField) {
|
||||||
|
this.inputField.value = question;
|
||||||
|
|
||||||
|
// Sende die Frage
|
||||||
|
this.sendMessage();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exportiere die Klasse für die Verwendung in anderen Modulen
|
// Mache die Klasse global verfügbar
|
||||||
export default ChatGPTAssistant;
|
window.ChatGPTAssistant = ChatGPTAssistant;
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
@@ -1,240 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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;
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,777 +0,0 @@
|
|||||||
/**
|
|
||||||
* MindMap D3.js Modul
|
|
||||||
* Visualisiert die Mindmap mit D3.js
|
|
||||||
*/
|
|
||||||
|
|
||||||
class MindMapVisualization {
|
|
||||||
constructor(containerSelector, options = {}) {
|
|
||||||
this.containerSelector = containerSelector;
|
|
||||||
this.container = d3.select(containerSelector);
|
|
||||||
this.width = options.width || this.container.node().clientWidth || 800;
|
|
||||||
this.height = options.height || 600;
|
|
||||||
this.nodeRadius = options.nodeRadius || 14;
|
|
||||||
this.selectedNodeRadius = options.selectedNodeRadius || 20;
|
|
||||||
this.linkDistance = options.linkDistance || 150;
|
|
||||||
this.chargeStrength = options.chargeStrength || -900;
|
|
||||||
this.centerForce = options.centerForce || 0.15;
|
|
||||||
this.onNodeClick = options.onNodeClick || ((node) => console.log('Node clicked:', node));
|
|
||||||
|
|
||||||
this.nodes = [];
|
|
||||||
this.links = [];
|
|
||||||
this.simulation = null;
|
|
||||||
this.svg = null;
|
|
||||||
this.linkElements = null;
|
|
||||||
this.nodeElements = null;
|
|
||||||
this.textElements = null;
|
|
||||||
this.tooltipEnabled = options.tooltipEnabled !== undefined ? options.tooltipEnabled : true;
|
|
||||||
|
|
||||||
this.mouseoverNode = null;
|
|
||||||
this.selectedNode = null;
|
|
||||||
|
|
||||||
this.zoomFactor = 1;
|
|
||||||
this.tooltipDiv = null;
|
|
||||||
this.isLoading = true;
|
|
||||||
|
|
||||||
// Lade die gemerkten Knoten
|
|
||||||
this.bookmarkedNodes = this.loadBookmarkedNodes();
|
|
||||||
|
|
||||||
// Sicherstellen, dass der Container bereit ist
|
|
||||||
if (this.container.node()) {
|
|
||||||
this.init();
|
|
||||||
this.setupDefaultNodes();
|
|
||||||
|
|
||||||
// Sofortige Datenladung
|
|
||||||
window.setTimeout(() => {
|
|
||||||
this.loadData();
|
|
||||||
}, 100);
|
|
||||||
} else {
|
|
||||||
console.error('Mindmap-Container nicht gefunden:', containerSelector);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Standardknoten als Fallback einrichten, falls die API nicht reagiert
|
|
||||||
setupDefaultNodes() {
|
|
||||||
// Basis-Mindmap mit Hauptthemen
|
|
||||||
const defaultNodes = [
|
|
||||||
{ id: "root", name: "Wissen", description: "Zentrale Wissensbasis", thought_count: 0 },
|
|
||||||
{ id: "philosophy", name: "Philosophie", description: "Philosophisches Denken", thought_count: 0 },
|
|
||||||
{ id: "science", name: "Wissenschaft", description: "Wissenschaftliche Erkenntnisse", thought_count: 0 },
|
|
||||||
{ id: "technology", name: "Technologie", description: "Technologische Entwicklungen", thought_count: 0 },
|
|
||||||
{ id: "arts", name: "Künste", description: "Künstlerische Ausdrucksformen", thought_count: 0 }
|
|
||||||
];
|
|
||||||
|
|
||||||
const defaultLinks = [
|
|
||||||
{ source: "root", target: "philosophy" },
|
|
||||||
{ source: "root", target: "science" },
|
|
||||||
{ source: "root", target: "technology" },
|
|
||||||
{ source: "root", target: "arts" }
|
|
||||||
];
|
|
||||||
|
|
||||||
// Als Fallback verwenden, falls die API fehlschlägt
|
|
||||||
this.defaultNodes = defaultNodes;
|
|
||||||
this.defaultLinks = defaultLinks;
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
// SVG erstellen, wenn noch nicht vorhanden
|
|
||||||
if (!this.svg) {
|
|
||||||
// Container zuerst leeren
|
|
||||||
this.container.html('');
|
|
||||||
|
|
||||||
this.svg = this.container
|
|
||||||
.append('svg')
|
|
||||||
.attr('width', '100%')
|
|
||||||
.attr('height', this.height)
|
|
||||||
.attr('viewBox', `0 0 ${this.width} ${this.height}`)
|
|
||||||
.attr('class', 'mindmap-svg')
|
|
||||||
.call(
|
|
||||||
d3.zoom()
|
|
||||||
.scaleExtent([0.1, 5])
|
|
||||||
.on('zoom', (event) => {
|
|
||||||
this.handleZoom(event.transform);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Hauptgruppe für alles, was zoom-transformierbar ist
|
|
||||||
this.g = this.svg.append('g');
|
|
||||||
|
|
||||||
// Tooltip initialisieren
|
|
||||||
if (!d3.select('body').select('.node-tooltip').size()) {
|
|
||||||
this.tooltipDiv = d3.select('body')
|
|
||||||
.append('div')
|
|
||||||
.attr('class', 'node-tooltip')
|
|
||||||
.style('opacity', 0)
|
|
||||||
.style('position', 'absolute')
|
|
||||||
.style('pointer-events', 'none')
|
|
||||||
.style('background', 'rgba(20, 20, 40, 0.9)')
|
|
||||||
.style('color', '#ffffff')
|
|
||||||
.style('border', '1px solid rgba(160, 80, 255, 0.2)')
|
|
||||||
.style('border-radius', '6px')
|
|
||||||
.style('padding', '8px 12px')
|
|
||||||
.style('font-size', '14px')
|
|
||||||
.style('max-width', '250px')
|
|
||||||
.style('box-shadow', '0 10px 25px rgba(0, 0, 0, 0.5), 0 0 10px rgba(160, 80, 255, 0.2)');
|
|
||||||
} else {
|
|
||||||
this.tooltipDiv = d3.select('body').select('.node-tooltip');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Force-Simulation initialisieren
|
|
||||||
this.simulation = d3.forceSimulation()
|
|
||||||
.force('link', d3.forceLink().id(d => d.id).distance(this.linkDistance))
|
|
||||||
.force('charge', d3.forceManyBody().strength(this.chargeStrength))
|
|
||||||
.force('center', d3.forceCenter(this.width / 2, this.height / 2).strength(this.centerForce))
|
|
||||||
.force('collision', d3.forceCollide().radius(this.nodeRadius * 2));
|
|
||||||
|
|
||||||
// Globale Mindmap-Instanz für externe Zugriffe setzen
|
|
||||||
window.mindmapInstance = this;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleZoom(transform) {
|
|
||||||
this.g.attr('transform', transform);
|
|
||||||
this.zoomFactor = transform.k;
|
|
||||||
|
|
||||||
// Knotengröße anpassen, um bei Zoom lesbar zu bleiben
|
|
||||||
if (this.nodeElements) {
|
|
||||||
this.nodeElements
|
|
||||||
.attr('r', d => (d === this.selectedNode ? this.selectedNodeRadius : this.nodeRadius) / Math.sqrt(transform.k));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Textgröße anpassen
|
|
||||||
if (this.textElements) {
|
|
||||||
this.textElements
|
|
||||||
.style('font-size', `${12 / Math.sqrt(transform.k)}px`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadData() {
|
|
||||||
try {
|
|
||||||
// Ladeindikator anzeigen
|
|
||||||
this.showLoading();
|
|
||||||
|
|
||||||
// Verwende sofort die Standarddaten für eine schnelle erste Anzeige
|
|
||||||
this.nodes = [...this.defaultNodes];
|
|
||||||
this.links = [...this.defaultLinks];
|
|
||||||
|
|
||||||
// Visualisierung sofort aktualisieren
|
|
||||||
this.isLoading = false;
|
|
||||||
this.updateVisualization();
|
|
||||||
|
|
||||||
// Status auf bereit setzen - don't wait for API
|
|
||||||
this.container.attr('data-status', 'ready');
|
|
||||||
|
|
||||||
// API-Aufruf mit kürzerem Timeout im Hintergrund durchführen
|
|
||||||
try {
|
|
||||||
const controller = new AbortController();
|
|
||||||
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 Sekunden Timeout - reduced from 10
|
|
||||||
|
|
||||||
const response = await fetch('/api/mindmap', {
|
|
||||||
signal: controller.signal,
|
|
||||||
headers: {
|
|
||||||
'Cache-Control': 'no-cache',
|
|
||||||
'Pragma': 'no-cache'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
console.warn(`HTTP Fehler: ${response.status}, verwende Standarddaten`);
|
|
||||||
return; // Keep using default data
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (!data || !data.nodes || data.nodes.length === 0) {
|
|
||||||
console.warn('Keine Mindmap-Daten vorhanden, verwende weiterhin Standard-Daten.');
|
|
||||||
return; // Keep using default data
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flache Liste von Knoten und Verbindungen erstellen
|
|
||||||
this.nodes = [];
|
|
||||||
this.links = [];
|
|
||||||
this.processHierarchicalData(data.nodes);
|
|
||||||
|
|
||||||
// Visualisierung aktualisieren mit den tatsächlichen Daten
|
|
||||||
this.updateVisualization();
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Fehler beim Laden der Mindmap-Daten, verwende Standarddaten:', error);
|
|
||||||
// Already using default data, no action needed
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Kritischer Fehler bei der Mindmap-Darstellung:', error);
|
|
||||||
this.showError('Fehler beim Laden der Mindmap-Daten. Bitte laden Sie die Seite neu.');
|
|
||||||
this.container.attr('data-status', 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
showLoading() {
|
|
||||||
// Element nur leeren, wenn es noch kein SVG enthält
|
|
||||||
if (!this.container.select('svg').size()) {
|
|
||||||
this.container.html(`
|
|
||||||
<div class="flex justify-center items-center h-full">
|
|
||||||
<div class="text-center">
|
|
||||||
<div class="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-primary-400 mx-auto mb-4"></div>
|
|
||||||
<p class="text-lg text-white">Mindmap wird geladen...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
processHierarchicalData(hierarchicalNodes, parentId = null) {
|
|
||||||
hierarchicalNodes.forEach(node => {
|
|
||||||
// Knoten hinzufügen, wenn noch nicht vorhanden
|
|
||||||
if (!this.nodes.find(n => n.id === node.id)) {
|
|
||||||
this.nodes.push({
|
|
||||||
id: node.id,
|
|
||||||
name: node.name,
|
|
||||||
description: node.description || '',
|
|
||||||
thought_count: node.thought_count || 0,
|
|
||||||
color: this.generateColorFromString(node.name),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verbindung zum Elternknoten hinzufügen
|
|
||||||
if (parentId !== null) {
|
|
||||||
this.links.push({
|
|
||||||
source: parentId,
|
|
||||||
target: node.id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rekursiv für Kindknoten aufrufen
|
|
||||||
if (node.children && node.children.length > 0) {
|
|
||||||
this.processHierarchicalData(node.children, node.id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
generateColorFromString(str) {
|
|
||||||
// Erzeugt eine deterministische Farbe basierend auf dem String
|
|
||||||
let hash = 0;
|
|
||||||
for (let i = 0; i < str.length; i++) {
|
|
||||||
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verwende deterministische Farbe aus unserem Farbschema
|
|
||||||
const colors = [
|
|
||||||
'#4080ff', // primary-400
|
|
||||||
'#a040ff', // secondary-400
|
|
||||||
'#205cf5', // primary-500
|
|
||||||
'#8020f5', // secondary-500
|
|
||||||
'#1040e0', // primary-600
|
|
||||||
'#6010e0', // secondary-600
|
|
||||||
];
|
|
||||||
|
|
||||||
return colors[Math.abs(hash) % colors.length];
|
|
||||||
}
|
|
||||||
|
|
||||||
updateVisualization() {
|
|
||||||
// Starte die Visualisierung nur, wenn nicht mehr im Ladezustand
|
|
||||||
if (this.isLoading) return;
|
|
||||||
|
|
||||||
// Container leeren, wenn Diagramm neu erstellt wird
|
|
||||||
if (!this.svg) {
|
|
||||||
this.container.html('');
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Performance-Optimierung: Deaktiviere Transition während des Datenladens
|
|
||||||
const useTransitions = false;
|
|
||||||
|
|
||||||
// Links (Edges) erstellen
|
|
||||||
this.linkElements = this.g.selectAll('.link')
|
|
||||||
.data(this.links)
|
|
||||||
.join(
|
|
||||||
enter => enter.append('line')
|
|
||||||
.attr('class', 'link')
|
|
||||||
.attr('stroke', '#ffffff30')
|
|
||||||
.attr('stroke-width', 2)
|
|
||||||
.attr('stroke-dasharray', d => d.relation_type === 'contradicts' ? '5,5' : null)
|
|
||||||
.attr('marker-end', d => d.relation_type === 'builds_upon' ? 'url(#arrowhead)' : null),
|
|
||||||
update => update
|
|
||||||
.attr('stroke', '#ffffff30')
|
|
||||||
.attr('stroke-dasharray', d => d.relation_type === 'contradicts' ? '5,5' : null)
|
|
||||||
.attr('marker-end', d => d.relation_type === 'builds_upon' ? 'url(#arrowhead)' : null),
|
|
||||||
exit => exit.remove()
|
|
||||||
);
|
|
||||||
|
|
||||||
// Pfeilspitze für gerichtete Beziehungen hinzufügen (falls noch nicht vorhanden)
|
|
||||||
if (!this.svg.select('defs').node()) {
|
|
||||||
const defs = this.svg.append('defs');
|
|
||||||
defs.append('marker')
|
|
||||||
.attr('id', 'arrowhead')
|
|
||||||
.attr('viewBox', '0 -5 10 10')
|
|
||||||
.attr('refX', 20)
|
|
||||||
.attr('refY', 0)
|
|
||||||
.attr('orient', 'auto')
|
|
||||||
.attr('markerWidth', 6)
|
|
||||||
.attr('markerHeight', 6)
|
|
||||||
.append('path')
|
|
||||||
.attr('d', 'M0,-5L10,0L0,5')
|
|
||||||
.attr('fill', '#ffffff50');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simplified Effekte definieren, falls noch nicht vorhanden
|
|
||||||
if (!this.svg.select('#glow').node()) {
|
|
||||||
const defs = this.svg.select('defs').size() ? this.svg.select('defs') : this.svg.append('defs');
|
|
||||||
|
|
||||||
// Glow-Effekt für Knoten
|
|
||||||
const filter = defs.append('filter')
|
|
||||||
.attr('id', 'glow')
|
|
||||||
.attr('x', '-50%')
|
|
||||||
.attr('y', '-50%')
|
|
||||||
.attr('width', '200%')
|
|
||||||
.attr('height', '200%');
|
|
||||||
|
|
||||||
filter.append('feGaussianBlur')
|
|
||||||
.attr('stdDeviation', '1')
|
|
||||||
.attr('result', 'blur');
|
|
||||||
|
|
||||||
filter.append('feComposite')
|
|
||||||
.attr('in', 'SourceGraphic')
|
|
||||||
.attr('in2', 'blur')
|
|
||||||
.attr('operator', 'over');
|
|
||||||
|
|
||||||
// Blur-Effekt für Schatten
|
|
||||||
const blurFilter = defs.append('filter')
|
|
||||||
.attr('id', 'blur')
|
|
||||||
.attr('x', '-50%')
|
|
||||||
.attr('y', '-50%')
|
|
||||||
.attr('width', '200%')
|
|
||||||
.attr('height', '200%');
|
|
||||||
|
|
||||||
blurFilter.append('feGaussianBlur')
|
|
||||||
.attr('stdDeviation', '1');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Knoten-Gruppe erstellen/aktualisieren
|
|
||||||
const nodeGroups = this.g.selectAll('.node-group')
|
|
||||||
.data(this.nodes)
|
|
||||||
.join(
|
|
||||||
enter => {
|
|
||||||
const group = enter.append('g')
|
|
||||||
.attr('class', 'node-group')
|
|
||||||
.call(d3.drag()
|
|
||||||
.on('start', (event, d) => this.dragStarted(event, d))
|
|
||||||
.on('drag', (event, d) => this.dragged(event, d))
|
|
||||||
.on('end', (event, d) => this.dragEnded(event, d)));
|
|
||||||
|
|
||||||
// Hintergrundschatten für besseren Kontrast
|
|
||||||
group.append('circle')
|
|
||||||
.attr('class', 'node-shadow')
|
|
||||||
.attr('r', d => this.nodeRadius * 1.2)
|
|
||||||
.attr('fill', 'rgba(0, 0, 0, 0.3)')
|
|
||||||
.attr('filter', 'url(#blur)');
|
|
||||||
|
|
||||||
// Kreis für jeden Knoten
|
|
||||||
group.append('circle')
|
|
||||||
.attr('class', d => `node ${this.isNodeBookmarked(d.id) ? 'bookmarked' : ''}`)
|
|
||||||
.attr('r', this.nodeRadius)
|
|
||||||
.attr('fill', d => d.color || this.generateColorFromString(d.name))
|
|
||||||
.attr('stroke', d => this.isNodeBookmarked(d.id) ? '#FFD700' : '#ffffff50')
|
|
||||||
.attr('stroke-width', d => this.isNodeBookmarked(d.id) ? 3 : 2)
|
|
||||||
.attr('filter', 'url(#glow)');
|
|
||||||
|
|
||||||
// Text-Label mit besserem Kontrast
|
|
||||||
group.append('text')
|
|
||||||
.attr('class', 'node-label')
|
|
||||||
.attr('dy', '0.35em')
|
|
||||||
.attr('text-anchor', 'middle')
|
|
||||||
.attr('fill', '#ffffff')
|
|
||||||
.attr('stroke', 'rgba(0, 0, 0, 0.4)')
|
|
||||||
.attr('stroke-width', '0.7px')
|
|
||||||
.attr('paint-order', 'stroke')
|
|
||||||
.style('font-size', '12px')
|
|
||||||
.style('font-weight', '500')
|
|
||||||
.style('pointer-events', 'none')
|
|
||||||
.text(d => d.name.length > 12 ? d.name.slice(0, 10) + '...' : d.name);
|
|
||||||
|
|
||||||
// Interaktivität hinzufügen
|
|
||||||
group
|
|
||||||
.on('mouseover', (event, d) => this.nodeMouseover(event, d))
|
|
||||||
.on('mouseout', (event, d) => this.nodeMouseout(event, d))
|
|
||||||
.on('click', (event, d) => this.nodeClicked(event, d));
|
|
||||||
|
|
||||||
return group;
|
|
||||||
},
|
|
||||||
update => {
|
|
||||||
// Knoten aktualisieren
|
|
||||||
update.select('.node')
|
|
||||||
.attr('class', d => `node ${this.isNodeBookmarked(d.id) ? 'bookmarked' : ''}`)
|
|
||||||
.attr('fill', d => d.color || this.generateColorFromString(d.name))
|
|
||||||
.attr('stroke', d => this.isNodeBookmarked(d.id) ? '#FFD700' : '#ffffff50')
|
|
||||||
.attr('stroke-width', d => this.isNodeBookmarked(d.id) ? 3 : 2);
|
|
||||||
|
|
||||||
// Text aktualisieren
|
|
||||||
update.select('.node-label')
|
|
||||||
.text(d => d.name.length > 12 ? d.name.slice(0, 10) + '...' : d.name);
|
|
||||||
|
|
||||||
return update;
|
|
||||||
},
|
|
||||||
exit => exit.remove()
|
|
||||||
);
|
|
||||||
|
|
||||||
// Einzelne Elemente für direkten Zugriff speichern
|
|
||||||
this.nodeElements = this.g.selectAll('.node');
|
|
||||||
this.textElements = this.g.selectAll('.node-label');
|
|
||||||
|
|
||||||
// Performance-Optimierung: Weniger Simulationsschritte für schnellere Stabilisierung
|
|
||||||
this.simulation
|
|
||||||
.nodes(this.nodes)
|
|
||||||
.on('tick', () => this.ticked())
|
|
||||||
.alpha(0.3) // Reduzierter Wert für schnellere Stabilisierung
|
|
||||||
.alphaDecay(0.05); // Erhöhter Wert für schnellere Stabilisierung
|
|
||||||
|
|
||||||
this.simulation.force('link')
|
|
||||||
.links(this.links);
|
|
||||||
|
|
||||||
// Simulation neu starten
|
|
||||||
this.simulation.restart();
|
|
||||||
|
|
||||||
// Update connection counts
|
|
||||||
this.updateConnectionCounts();
|
|
||||||
}
|
|
||||||
|
|
||||||
ticked() {
|
|
||||||
// Linienpositionen aktualisieren
|
|
||||||
this.linkElements
|
|
||||||
.attr('x1', d => d.source.x)
|
|
||||||
.attr('y1', d => d.source.y)
|
|
||||||
.attr('x2', d => d.target.x)
|
|
||||||
.attr('y2', d => d.target.y);
|
|
||||||
|
|
||||||
// Knotenpositionen aktualisieren
|
|
||||||
this.g.selectAll('.node-group')
|
|
||||||
.attr('transform', d => `translate(${d.x}, ${d.y})`);
|
|
||||||
}
|
|
||||||
|
|
||||||
dragStarted(event, d) {
|
|
||||||
if (!event.active) this.simulation.alphaTarget(0.3).restart();
|
|
||||||
d.fx = d.x;
|
|
||||||
d.fy = d.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
dragged(event, d) {
|
|
||||||
d.fx = event.x;
|
|
||||||
d.fy = event.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
dragEnded(event, d) {
|
|
||||||
if (!event.active) this.simulation.alphaTarget(0);
|
|
||||||
d.fx = null;
|
|
||||||
d.fy = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
nodeMouseover(event, d) {
|
|
||||||
this.mouseoverNode = d;
|
|
||||||
|
|
||||||
// Tooltip anzeigen
|
|
||||||
if (this.tooltipEnabled) {
|
|
||||||
const isBookmarked = this.isNodeBookmarked(d.id);
|
|
||||||
const tooltipContent = `
|
|
||||||
<div class="p-2">
|
|
||||||
<strong>${d.name}</strong>
|
|
||||||
${d.description ? `<p class="text-sm text-gray-200 mt-1">${d.description}</p>` : ''}
|
|
||||||
<div class="text-xs text-gray-300 mt-1">
|
|
||||||
Gedanken: ${d.thought_count}
|
|
||||||
</div>
|
|
||||||
<div class="mt-2">
|
|
||||||
<button id="bookmark-button" class="px-2 py-1 text-xs rounded bg-gray-700 hover:bg-gray-600 text-white"
|
|
||||||
data-nodeid="${d.id}">
|
|
||||||
${isBookmarked ? '<i class="fas fa-bookmark mr-1"></i> Gemerkt' : '<i class="far fa-bookmark mr-1"></i> Merken'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
this.tooltipDiv
|
|
||||||
.html(tooltipContent)
|
|
||||||
.style('left', (event.pageX + 10) + 'px')
|
|
||||||
.style('top', (event.pageY - 10) + 'px')
|
|
||||||
.transition()
|
|
||||||
.duration(200)
|
|
||||||
.style('opacity', 1);
|
|
||||||
|
|
||||||
// Event-Listener für den Bookmark-Button hinzufügen
|
|
||||||
document.getElementById('bookmark-button').addEventListener('click', (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
const nodeId = e.currentTarget.getAttribute('data-nodeid');
|
|
||||||
const isNowBookmarked = this.toggleBookmark(nodeId);
|
|
||||||
|
|
||||||
// Button-Text aktualisieren
|
|
||||||
if (isNowBookmarked) {
|
|
||||||
e.currentTarget.innerHTML = '<i class="fas fa-bookmark mr-1"></i> Gemerkt';
|
|
||||||
} else {
|
|
||||||
e.currentTarget.innerHTML = '<i class="far fa-bookmark mr-1"></i> Merken';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Knoten visuell hervorheben
|
|
||||||
d3.select(event.currentTarget).select('circle')
|
|
||||||
.transition()
|
|
||||||
.duration(200)
|
|
||||||
.attr('r', this.nodeRadius * 1.2)
|
|
||||||
.attr('stroke', this.isNodeBookmarked(d.id) ? '#FFD700' : '#ffffff');
|
|
||||||
}
|
|
||||||
|
|
||||||
nodeMouseout(event, d) {
|
|
||||||
this.mouseoverNode = null;
|
|
||||||
|
|
||||||
// Tooltip ausblenden
|
|
||||||
if (this.tooltipEnabled) {
|
|
||||||
this.tooltipDiv
|
|
||||||
.transition()
|
|
||||||
.duration(200)
|
|
||||||
.style('opacity', 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Knoten-Stil zurücksetzen, wenn nicht ausgewählt
|
|
||||||
const nodeElement = d3.select(event.currentTarget).select('circle');
|
|
||||||
if (d !== this.selectedNode) {
|
|
||||||
const isBookmarked = this.isNodeBookmarked(d.id);
|
|
||||||
nodeElement
|
|
||||||
.transition()
|
|
||||||
.duration(200)
|
|
||||||
.attr('r', this.nodeRadius)
|
|
||||||
.attr('stroke', isBookmarked ? '#FFD700' : '#ffffff50')
|
|
||||||
.attr('stroke-width', isBookmarked ? 3 : 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
nodeClicked(event, d) {
|
|
||||||
// Frühere Auswahl zurücksetzen
|
|
||||||
if (this.selectedNode && this.selectedNode !== d) {
|
|
||||||
this.g.selectAll('.node')
|
|
||||||
.filter(n => n === this.selectedNode)
|
|
||||||
.transition()
|
|
||||||
.duration(200)
|
|
||||||
.attr('r', this.nodeRadius)
|
|
||||||
.attr('stroke', '#ffffff50');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Neue Auswahl hervorheben
|
|
||||||
if (this.selectedNode !== d) {
|
|
||||||
this.selectedNode = d;
|
|
||||||
d3.select(event.currentTarget).select('circle')
|
|
||||||
.transition()
|
|
||||||
.duration(200)
|
|
||||||
.attr('r', this.selectedNodeRadius)
|
|
||||||
.attr('stroke', '#ffffff');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Callback mit Node-Daten aufrufen
|
|
||||||
this.onNodeClick(d);
|
|
||||||
}
|
|
||||||
|
|
||||||
showError(message) {
|
|
||||||
this.container.html(`
|
|
||||||
<div class="w-full text-center p-6">
|
|
||||||
<div class="mb-4 text-red-500">
|
|
||||||
<i class="fas fa-exclamation-triangle text-4xl"></i>
|
|
||||||
</div>
|
|
||||||
<p class="text-lg text-gray-200">${message}</p>
|
|
||||||
</div>
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fokussiert die Ansicht auf einen bestimmten Knoten
|
|
||||||
focusNode(nodeId) {
|
|
||||||
const node = this.nodes.find(n => n.id === nodeId);
|
|
||||||
if (!node) return;
|
|
||||||
|
|
||||||
// Simuliere einen Klick auf den Knoten
|
|
||||||
const nodeElement = this.g.selectAll('.node-group')
|
|
||||||
.filter(d => d.id === nodeId);
|
|
||||||
|
|
||||||
nodeElement.dispatch('click');
|
|
||||||
|
|
||||||
// Zentriere den Knoten in der Ansicht
|
|
||||||
const transform = d3.zoomIdentity
|
|
||||||
.translate(this.width / 2, this.height / 2)
|
|
||||||
.scale(1.2)
|
|
||||||
.translate(-node.x, -node.y);
|
|
||||||
|
|
||||||
this.svg.transition()
|
|
||||||
.duration(750)
|
|
||||||
.call(
|
|
||||||
d3.zoom().transform,
|
|
||||||
transform
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filtert die Mindmap basierend auf einem Suchbegriff
|
|
||||||
filterBySearchTerm(searchTerm) {
|
|
||||||
if (!searchTerm || searchTerm.trim() === '') {
|
|
||||||
// Alle Knoten anzeigen
|
|
||||||
this.g.selectAll('.node-group')
|
|
||||||
.style('opacity', 1)
|
|
||||||
.style('pointer-events', 'all');
|
|
||||||
|
|
||||||
this.g.selectAll('.link')
|
|
||||||
.style('opacity', 1);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const searchLower = searchTerm.toLowerCase();
|
|
||||||
const matchingNodes = this.nodes.filter(node =>
|
|
||||||
node.name.toLowerCase().includes(searchLower) ||
|
|
||||||
(node.description && node.description.toLowerCase().includes(searchLower))
|
|
||||||
);
|
|
||||||
|
|
||||||
const matchingNodeIds = new Set(matchingNodes.map(n => n.id));
|
|
||||||
|
|
||||||
// Passende Knoten hervorheben, andere ausblenden
|
|
||||||
this.g.selectAll('.node-group')
|
|
||||||
.style('opacity', d => matchingNodeIds.has(d.id) ? 1 : 0.2)
|
|
||||||
.style('pointer-events', d => matchingNodeIds.has(d.id) ? 'all' : 'none');
|
|
||||||
|
|
||||||
// Verbindungen zwischen passenden Knoten hervorheben
|
|
||||||
this.g.selectAll('.link')
|
|
||||||
.style('opacity', d =>
|
|
||||||
matchingNodeIds.has(d.source.id) && matchingNodeIds.has(d.target.id) ? 1 : 0.1
|
|
||||||
);
|
|
||||||
|
|
||||||
// Auf den ersten passenden Knoten fokussieren, wenn vorhanden
|
|
||||||
if (matchingNodes.length > 0) {
|
|
||||||
this.focusNode(matchingNodes[0].id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the thought_count property for each node based on existing connections
|
|
||||||
*/
|
|
||||||
updateConnectionCounts() {
|
|
||||||
// Reset all counts first
|
|
||||||
this.nodes.forEach(node => {
|
|
||||||
// Initialize thought_count if it doesn't exist
|
|
||||||
if (typeof node.thought_count !== 'number') {
|
|
||||||
node.thought_count = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Count connections for this node
|
|
||||||
const connectedNodes = this.getConnectedNodes(node);
|
|
||||||
node.thought_count = connectedNodes.length;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update UI to show counts
|
|
||||||
this.updateNodeLabels();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the visual representation of node labels to include connection counts
|
|
||||||
*/
|
|
||||||
updateNodeLabels() {
|
|
||||||
if (!this.textElements) return;
|
|
||||||
|
|
||||||
this.textElements.text(d => {
|
|
||||||
if (d.thought_count > 0) {
|
|
||||||
return `${d.name} (${d.thought_count})`;
|
|
||||||
}
|
|
||||||
return d.name;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a new connection between nodes and updates the counts
|
|
||||||
*/
|
|
||||||
addConnection(sourceNode, targetNode) {
|
|
||||||
if (!sourceNode || !targetNode) return false;
|
|
||||||
|
|
||||||
// Check if connection already exists
|
|
||||||
if (this.isConnected(sourceNode, targetNode)) return false;
|
|
||||||
|
|
||||||
// Add new connection
|
|
||||||
this.links.push({
|
|
||||||
source: sourceNode.id,
|
|
||||||
target: targetNode.id
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update counts
|
|
||||||
this.updateConnectionCounts();
|
|
||||||
|
|
||||||
// Update visualization
|
|
||||||
this.updateVisualization();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lädt gemerkete Knoten aus dem LocalStorage
|
|
||||||
loadBookmarkedNodes() {
|
|
||||||
try {
|
|
||||||
const bookmarked = localStorage.getItem('bookmarkedNodes');
|
|
||||||
return bookmarked ? JSON.parse(bookmarked) : [];
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Fehler beim Laden der gemerkten Knoten:', error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Speichert gemerkete Knoten im LocalStorage
|
|
||||||
saveBookmarkedNodes() {
|
|
||||||
try {
|
|
||||||
localStorage.setItem('bookmarkedNodes', JSON.stringify(this.bookmarkedNodes));
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Fehler beim Speichern der gemerkten Knoten:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prüft, ob ein Knoten gemerkt ist
|
|
||||||
isNodeBookmarked(nodeId) {
|
|
||||||
return this.bookmarkedNodes.includes(nodeId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merkt einen Knoten oder hebt die Markierung auf
|
|
||||||
toggleBookmark(nodeId) {
|
|
||||||
const index = this.bookmarkedNodes.indexOf(nodeId);
|
|
||||||
if (index === -1) {
|
|
||||||
// Node hinzufügen
|
|
||||||
this.bookmarkedNodes.push(nodeId);
|
|
||||||
this.updateNodeAppearance(nodeId, true);
|
|
||||||
} else {
|
|
||||||
// Node entfernen
|
|
||||||
this.bookmarkedNodes.splice(index, 1);
|
|
||||||
this.updateNodeAppearance(nodeId, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Änderungen speichern
|
|
||||||
this.saveBookmarkedNodes();
|
|
||||||
|
|
||||||
// Event auslösen für andere Komponenten
|
|
||||||
const event = new CustomEvent('nodeBookmarkToggled', {
|
|
||||||
detail: {
|
|
||||||
nodeId: nodeId,
|
|
||||||
isBookmarked: index === -1
|
|
||||||
}
|
|
||||||
});
|
|
||||||
document.dispatchEvent(event);
|
|
||||||
|
|
||||||
return index === -1; // true wenn jetzt gemerkt, false wenn Markierung aufgehoben
|
|
||||||
}
|
|
||||||
|
|
||||||
// Aktualisiert das Aussehen eines Knotens basierend auf Bookmark-Status
|
|
||||||
updateNodeAppearance(nodeId, isBookmarked) {
|
|
||||||
this.g.selectAll('.node-group')
|
|
||||||
.filter(d => d.id === nodeId)
|
|
||||||
.select('.node')
|
|
||||||
.classed('bookmarked', isBookmarked)
|
|
||||||
.attr('stroke', isBookmarked ? '#FFD700' : '#ffffff50')
|
|
||||||
.attr('stroke-width', isBookmarked ? 3 : 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Aktualisiert das Aussehen aller gemerkten Knoten
|
|
||||||
updateAllBookmarkedNodes() {
|
|
||||||
this.g.selectAll('.node-group')
|
|
||||||
.each((d) => {
|
|
||||||
const isBookmarked = this.isNodeBookmarked(d.id);
|
|
||||||
this.updateNodeAppearance(d.id, isBookmarked);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exportiere die Klasse für die Verwendung in anderen Modulen
|
|
||||||
window.MindMapVisualization = MindMapVisualization;
|
|
||||||
1114
static/js/update_mindmap.js
Normal file
1114
static/js/update_mindmap.js
Normal file
File diff suppressed because it is too large
Load Diff
1834
static/mindmap.js
1834
static/mindmap.js
File diff suppressed because it is too large
Load Diff
@@ -1,88 +0,0 @@
|
|||||||
// Network Animation Effect
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
// Check if we're on the mindmap page
|
|
||||||
const mindmapContainer = document.getElementById('mindmap-container');
|
|
||||||
if (!mindmapContainer) return;
|
|
||||||
|
|
||||||
// Add enhanced animations for links and nodes
|
|
||||||
setTimeout(function() {
|
|
||||||
// Get all SVG links (connections between nodes)
|
|
||||||
const links = document.querySelectorAll('.link');
|
|
||||||
const nodes = document.querySelectorAll('.node');
|
|
||||||
|
|
||||||
// Add animation to links
|
|
||||||
links.forEach(link => {
|
|
||||||
// Create random animation duration between 15 and 30 seconds
|
|
||||||
const duration = 15 + Math.random() * 15;
|
|
||||||
link.style.animation = `dash ${duration}s linear infinite`;
|
|
||||||
link.style.strokeDasharray = '5, 5';
|
|
||||||
|
|
||||||
// Add pulse effect on hover
|
|
||||||
link.addEventListener('mouseover', function() {
|
|
||||||
this.classList.add('highlighted');
|
|
||||||
this.style.animation = 'dash 5s linear infinite';
|
|
||||||
});
|
|
||||||
|
|
||||||
link.addEventListener('mouseout', function() {
|
|
||||||
this.classList.remove('highlighted');
|
|
||||||
this.style.animation = `dash ${duration}s linear infinite`;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add effects to nodes
|
|
||||||
nodes.forEach(node => {
|
|
||||||
node.addEventListener('mouseover', function() {
|
|
||||||
this.querySelector('circle').style.filter = 'drop-shadow(0 0 15px rgba(179, 143, 255, 0.8))';
|
|
||||||
|
|
||||||
// Highlight connected links
|
|
||||||
const nodeId = this.getAttribute('data-id') || this.id;
|
|
||||||
links.forEach(link => {
|
|
||||||
const source = link.getAttribute('data-source');
|
|
||||||
const target = link.getAttribute('data-target');
|
|
||||||
|
|
||||||
if (source === nodeId || target === nodeId) {
|
|
||||||
link.classList.add('highlighted');
|
|
||||||
link.style.animation = 'dash 5s linear infinite';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
node.addEventListener('mouseout', function() {
|
|
||||||
this.querySelector('circle').style.filter = 'drop-shadow(0 0 8px rgba(179, 143, 255, 0.5))';
|
|
||||||
|
|
||||||
// Remove highlight from connected links
|
|
||||||
const nodeId = this.getAttribute('data-id') || this.id;
|
|
||||||
links.forEach(link => {
|
|
||||||
const source = link.getAttribute('data-source');
|
|
||||||
const target = link.getAttribute('data-target');
|
|
||||||
|
|
||||||
if (source === nodeId || target === nodeId) {
|
|
||||||
link.classList.remove('highlighted');
|
|
||||||
const duration = 15 + Math.random() * 15;
|
|
||||||
link.style.animation = `dash ${duration}s linear infinite`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}, 1000); // Wait for the mindmap to be fully loaded
|
|
||||||
|
|
||||||
// Add network background effect
|
|
||||||
const networkBackground = document.createElement('div');
|
|
||||||
networkBackground.className = 'network-background';
|
|
||||||
networkBackground.style.cssText = `
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: rgba(179, 143, 255, 0.05);
|
|
||||||
background-size: cover;
|
|
||||||
background-position: center;
|
|
||||||
opacity: 0.15;
|
|
||||||
z-index: -1;
|
|
||||||
pointer-events: none;
|
|
||||||
animation: pulse 10s ease-in-out infinite alternate;
|
|
||||||
`;
|
|
||||||
|
|
||||||
mindmapContainer.appendChild(networkBackground);
|
|
||||||
});
|
|
||||||
@@ -1,232 +0,0 @@
|
|||||||
// Animated Network Background
|
|
||||||
let canvas, ctx, networkImage;
|
|
||||||
let isImageLoaded = false;
|
|
||||||
let animationSpeed = 0.0003; // Reduzierte Geschwindigkeit für sanftere Rotation
|
|
||||||
let scaleSpeed = 0.0001; // Reduzierte Geschwindigkeit für sanftere Skalierung
|
|
||||||
let opacitySpeed = 0.0002; // Reduzierte Geschwindigkeit für sanftere Opazitätsänderung
|
|
||||||
let rotation = 0;
|
|
||||||
let scale = 1;
|
|
||||||
let opacity = 0.7; // Höhere Basisopazität für bessere Sichtbarkeit
|
|
||||||
let scaleDirection = 1;
|
|
||||||
let opacityDirection = 1;
|
|
||||||
let animationFrameId = null;
|
|
||||||
let isDarkMode = document.documentElement.classList.contains('dark');
|
|
||||||
let loadAttempts = 0;
|
|
||||||
const MAX_LOAD_ATTEMPTS = 2;
|
|
||||||
|
|
||||||
// Initialize the canvas and load the image
|
|
||||||
function initNetworkBackground() {
|
|
||||||
// Create canvas element if it doesn't exist
|
|
||||||
if (!document.getElementById('network-background')) {
|
|
||||||
canvas = document.createElement('canvas');
|
|
||||||
canvas.id = 'network-background';
|
|
||||||
canvas.style.position = 'fixed';
|
|
||||||
canvas.style.top = '0';
|
|
||||||
canvas.style.left = '0';
|
|
||||||
canvas.style.width = '100%';
|
|
||||||
canvas.style.height = '100%';
|
|
||||||
canvas.style.zIndex = '-5'; // Höher als -10 für den full-page-bg
|
|
||||||
canvas.style.pointerEvents = 'none'; // Stellt sicher, dass der Canvas keine Mausinteraktionen blockiert
|
|
||||||
document.body.appendChild(canvas);
|
|
||||||
} else {
|
|
||||||
canvas = document.getElementById('network-background');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set canvas size to window size with pixel ratio consideration
|
|
||||||
resizeCanvas();
|
|
||||||
|
|
||||||
// Get context with alpha enabled
|
|
||||||
ctx = canvas.getContext('2d', { alpha: true });
|
|
||||||
|
|
||||||
// Load the network image - versuche zuerst die SVG-Version
|
|
||||||
networkImage = new Image();
|
|
||||||
networkImage.crossOrigin = "anonymous"; // Vermeidet CORS-Probleme
|
|
||||||
|
|
||||||
// Keine Bilder laden, direkt Fallback-Hintergrund verwenden
|
|
||||||
console.log("Verwende einfachen Hintergrund ohne Bilddateien");
|
|
||||||
isImageLoaded = true; // Animation ohne Hintergrundbild starten
|
|
||||||
startAnimation();
|
|
||||||
|
|
||||||
// Handle window resize
|
|
||||||
window.addEventListener('resize', debounce(resizeCanvas, 250));
|
|
||||||
|
|
||||||
// Überwache Dark Mode-Änderungen
|
|
||||||
document.addEventListener('darkModeToggled', function(event) {
|
|
||||||
isDarkMode = event.detail.isDark;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hilfsfunktion zur Reduzierung der Resize-Event-Aufrufe
|
|
||||||
function debounce(func, wait) {
|
|
||||||
let timeout;
|
|
||||||
return function() {
|
|
||||||
const context = this;
|
|
||||||
const args = arguments;
|
|
||||||
clearTimeout(timeout);
|
|
||||||
timeout = setTimeout(function() {
|
|
||||||
func.apply(context, args);
|
|
||||||
}, wait);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resize canvas to match window size with proper pixel ratio
|
|
||||||
function resizeCanvas() {
|
|
||||||
if (!canvas) return;
|
|
||||||
|
|
||||||
const pixelRatio = window.devicePixelRatio || 1;
|
|
||||||
const width = window.innerWidth;
|
|
||||||
const height = window.innerHeight;
|
|
||||||
|
|
||||||
// Set display size (css pixels)
|
|
||||||
canvas.style.width = width + 'px';
|
|
||||||
canvas.style.height = height + 'px';
|
|
||||||
|
|
||||||
// Set actual size in memory (scaled for pixel ratio)
|
|
||||||
canvas.width = width * pixelRatio;
|
|
||||||
canvas.height = height * pixelRatio;
|
|
||||||
|
|
||||||
// Scale context to match pixel ratio
|
|
||||||
if (ctx) {
|
|
||||||
ctx.scale(pixelRatio, pixelRatio);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wenn Animation läuft und Bild geladen, zeichne erneut
|
|
||||||
if (isImageLoaded && animationFrameId) {
|
|
||||||
drawNetworkImage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start animation
|
|
||||||
function startAnimation() {
|
|
||||||
if (animationFrameId) {
|
|
||||||
cancelAnimationFrame(animationFrameId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start animation loop
|
|
||||||
animate();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw network image
|
|
||||||
function drawNetworkImage() {
|
|
||||||
if (!ctx) return;
|
|
||||||
|
|
||||||
// Clear canvas with proper clear method
|
|
||||||
ctx.clearRect(0, 0, canvas.width / (window.devicePixelRatio || 1), canvas.height / (window.devicePixelRatio || 1));
|
|
||||||
|
|
||||||
// Save context state
|
|
||||||
ctx.save();
|
|
||||||
|
|
||||||
// Move to center of canvas
|
|
||||||
ctx.translate(canvas.width / (2 * (window.devicePixelRatio || 1)), canvas.height / (2 * (window.devicePixelRatio || 1)));
|
|
||||||
|
|
||||||
// Rotate
|
|
||||||
ctx.rotate(rotation);
|
|
||||||
|
|
||||||
// Scale
|
|
||||||
ctx.scale(scale, scale);
|
|
||||||
|
|
||||||
// Set global opacity, angepasst für Dark Mode
|
|
||||||
ctx.globalAlpha = isDarkMode ? opacity : opacity * 0.8;
|
|
||||||
|
|
||||||
if (isImageLoaded && networkImage.complete) {
|
|
||||||
// Bildgröße berechnen, um den Bildschirm abzudecken
|
|
||||||
const imgAspect = networkImage.width / networkImage.height;
|
|
||||||
const canvasAspect = canvas.width / canvas.height;
|
|
||||||
|
|
||||||
let drawWidth, drawHeight;
|
|
||||||
|
|
||||||
if (canvasAspect > imgAspect) {
|
|
||||||
drawWidth = canvas.width / (window.devicePixelRatio || 1);
|
|
||||||
drawHeight = drawWidth / imgAspect;
|
|
||||||
} else {
|
|
||||||
drawHeight = canvas.height / (window.devicePixelRatio || 1);
|
|
||||||
drawWidth = drawHeight * imgAspect;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw image centered
|
|
||||||
ctx.drawImage(
|
|
||||||
networkImage,
|
|
||||||
-drawWidth / 2,
|
|
||||||
-drawHeight / 2,
|
|
||||||
drawWidth,
|
|
||||||
drawHeight
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// Fallback: Zeichne einen einfachen Hintergrund mit Punkten
|
|
||||||
drawFallbackBackground();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore context state
|
|
||||||
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
|
|
||||||
function animate() {
|
|
||||||
// Update animation parameters
|
|
||||||
rotation += animationSpeed;
|
|
||||||
|
|
||||||
// Update scale with oscillation
|
|
||||||
scale += scaleSpeed * scaleDirection;
|
|
||||||
if (scale > 1.05) { // Kleinerer Skalierungsbereich für weniger starke Größenänderung
|
|
||||||
scaleDirection = -1;
|
|
||||||
} else if (scale < 0.95) {
|
|
||||||
scaleDirection = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update opacity with oscillation
|
|
||||||
opacity += opacitySpeed * opacityDirection;
|
|
||||||
if (opacity > 0.75) { // Kleinerer Opazitätsbereich für subtilere Änderungen
|
|
||||||
opacityDirection = -1;
|
|
||||||
} else if (opacity < 0.65) {
|
|
||||||
opacityDirection = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw the image
|
|
||||||
drawNetworkImage();
|
|
||||||
|
|
||||||
// Request next frame
|
|
||||||
animationFrameId = requestAnimationFrame(animate);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cleanup Funktion für Speicherbereinigung
|
|
||||||
function cleanupNetworkBackground() {
|
|
||||||
if (animationFrameId) {
|
|
||||||
cancelAnimationFrame(animationFrameId);
|
|
||||||
animationFrameId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (canvas && canvas.parentNode) {
|
|
||||||
canvas.parentNode.removeChild(canvas);
|
|
||||||
}
|
|
||||||
|
|
||||||
window.removeEventListener('resize', resizeCanvas);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Führe Initialisierung aus, wenn DOM geladen ist
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', initNetworkBackground);
|
|
||||||
} else {
|
|
||||||
initNetworkBackground();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Führe Cleanup durch, wenn das Fenster geschlossen wird
|
|
||||||
window.addEventListener('beforeunload', cleanupNetworkBackground);
|
|
||||||
1171
static/neural-network-background-full.js
Normal file
1171
static/neural-network-background-full.js
Normal file
File diff suppressed because it is too large
Load Diff
415
static/neural-network-background.js
Normal file
415
static/neural-network-background.js
Normal file
@@ -0,0 +1,415 @@
|
|||||||
|
/**
|
||||||
|
* Vereinfachter Neuronales Netzwerk Hintergrund
|
||||||
|
* Verwendet Canvas 2D anstelle von WebGL für bessere Leistung
|
||||||
|
*/
|
||||||
|
|
||||||
|
class NeuralNetworkBackground {
|
||||||
|
constructor() {
|
||||||
|
// Canvas einrichten
|
||||||
|
this.canvas = document.createElement('canvas');
|
||||||
|
this.canvas.id = 'neural-network-background';
|
||||||
|
this.canvas.style.position = 'fixed';
|
||||||
|
this.canvas.style.top = '0';
|
||||||
|
this.canvas.style.left = '0';
|
||||||
|
this.canvas.style.width = '100%';
|
||||||
|
this.canvas.style.height = '100%';
|
||||||
|
this.canvas.style.zIndex = '-10';
|
||||||
|
this.canvas.style.pointerEvents = 'none';
|
||||||
|
this.canvas.style.opacity = '1';
|
||||||
|
this.canvas.style.transition = 'opacity 3.5s ease-in-out';
|
||||||
|
|
||||||
|
// Falls Canvas bereits existiert, entfernen
|
||||||
|
const existingCanvas = document.getElementById('neural-network-background');
|
||||||
|
if (existingCanvas) {
|
||||||
|
existingCanvas.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// An body anhängen als erstes Kind
|
||||||
|
if (document.body.firstChild) {
|
||||||
|
document.body.insertBefore(this.canvas, document.body.firstChild);
|
||||||
|
} else {
|
||||||
|
document.body.appendChild(this.canvas);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2D Context
|
||||||
|
this.ctx = this.canvas.getContext('2d');
|
||||||
|
|
||||||
|
// Eigenschaften
|
||||||
|
this.nodes = [];
|
||||||
|
this.connections = [];
|
||||||
|
this.activeConnections = new Set();
|
||||||
|
this.animationFrameId = null;
|
||||||
|
this.isDestroying = false;
|
||||||
|
|
||||||
|
// Farben für Dark/Light Mode
|
||||||
|
this.colors = {
|
||||||
|
dark: {
|
||||||
|
background: '#040215',
|
||||||
|
nodeColor: '#6a5498',
|
||||||
|
nodePulse: '#9c7fe0',
|
||||||
|
connectionColor: '#4a3870',
|
||||||
|
flowColor: '#b47fea'
|
||||||
|
},
|
||||||
|
light: {
|
||||||
|
background: '#f8f9fc',
|
||||||
|
nodeColor: '#8c6db5',
|
||||||
|
nodePulse: '#b094dd',
|
||||||
|
connectionColor: '#9882bd',
|
||||||
|
flowColor: '#7d5bb5'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Aktuelle Farbpalette basierend auf Theme
|
||||||
|
this.currentColors = document.documentElement.classList.contains('dark')
|
||||||
|
? this.colors.dark
|
||||||
|
: this.colors.light;
|
||||||
|
|
||||||
|
// Konfiguration
|
||||||
|
this.config = {
|
||||||
|
nodeCount: 80, // Anzahl der Knoten
|
||||||
|
nodeSize: 2.5, // Größe der Knoten
|
||||||
|
connectionDistance: 150, // Maximale Verbindungsdistanz
|
||||||
|
connectionOpacity: 0.5, // Erhöht von 0.3 auf 0.5 - Deckkraft der ständigen Verbindungen
|
||||||
|
animationSpeed: 0.15, // Geschwindigkeit der Animation
|
||||||
|
flowDensity: 2, // Anzahl aktiver Verbindungen
|
||||||
|
maxFlowsPerNode: 2, // Maximale Anzahl aktiver Verbindungen pro Knoten
|
||||||
|
flowDuration: [2000, 5000], // Min/Max Dauer des Flows in ms
|
||||||
|
nodePulseFrequency: 0.01 // Wie oft Knoten pulsieren
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialisieren
|
||||||
|
this.init();
|
||||||
|
|
||||||
|
// Event-Listener
|
||||||
|
window.addEventListener('resize', this.resizeCanvas.bind(this));
|
||||||
|
|
||||||
|
console.log('Vereinfachter Neural Network Background initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.resizeCanvas();
|
||||||
|
this.createNodes();
|
||||||
|
this.createConnections();
|
||||||
|
this.startAnimation();
|
||||||
|
}
|
||||||
|
|
||||||
|
resizeCanvas() {
|
||||||
|
const pixelRatio = window.devicePixelRatio || 1;
|
||||||
|
const width = window.innerWidth;
|
||||||
|
const height = window.innerHeight;
|
||||||
|
|
||||||
|
this.canvas.style.width = width + 'px';
|
||||||
|
this.canvas.style.height = height + 'px';
|
||||||
|
this.canvas.width = width * pixelRatio;
|
||||||
|
this.canvas.height = height * pixelRatio;
|
||||||
|
|
||||||
|
if (this.ctx) {
|
||||||
|
this.ctx.scale(pixelRatio, pixelRatio);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Neuberechnung der Knotenpositionen nach Größenänderung
|
||||||
|
if (this.nodes.length) {
|
||||||
|
this.createNodes();
|
||||||
|
this.createConnections();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createNodes() {
|
||||||
|
this.nodes = [];
|
||||||
|
const width = this.canvas.width / (window.devicePixelRatio || 1);
|
||||||
|
const height = this.canvas.height / (window.devicePixelRatio || 1);
|
||||||
|
|
||||||
|
// Cluster-Zentren für realistisches neuronales Netzwerk
|
||||||
|
const clusterCount = Math.floor(6 + Math.random() * 4);
|
||||||
|
const clusters = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < clusterCount; i++) {
|
||||||
|
clusters.push({
|
||||||
|
x: Math.random() * width,
|
||||||
|
y: Math.random() * height,
|
||||||
|
radius: 100 + Math.random() * 150
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Knoten erstellen
|
||||||
|
for (let i = 0; i < this.config.nodeCount; i++) {
|
||||||
|
// Wähle zufällig ein Cluster
|
||||||
|
const cluster = clusters[Math.floor(Math.random() * clusters.length)];
|
||||||
|
|
||||||
|
// Erstelle einen Knoten innerhalb des Clusters mit zufälligem Offset
|
||||||
|
const angle = Math.random() * Math.PI * 2;
|
||||||
|
const distance = Math.random() * cluster.radius;
|
||||||
|
|
||||||
|
const node = {
|
||||||
|
id: i,
|
||||||
|
x: cluster.x + Math.cos(angle) * distance,
|
||||||
|
y: cluster.y + Math.sin(angle) * distance,
|
||||||
|
size: this.config.nodeSize * (0.8 + Math.random() * 0.4),
|
||||||
|
speed: {
|
||||||
|
x: (Math.random() - 0.5) * 0.2,
|
||||||
|
y: (Math.random() - 0.5) * 0.2
|
||||||
|
},
|
||||||
|
lastPulse: 0,
|
||||||
|
pulseInterval: 5000 + Math.random() * 10000, // Zufälliges Pulsieren
|
||||||
|
connections: []
|
||||||
|
};
|
||||||
|
|
||||||
|
this.nodes.push(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createConnections() {
|
||||||
|
this.connections = [];
|
||||||
|
|
||||||
|
// Verbindungen zwischen Knoten erstellen
|
||||||
|
for (let i = 0; i < this.nodes.length; i++) {
|
||||||
|
const nodeA = this.nodes[i];
|
||||||
|
|
||||||
|
for (let j = i + 1; j < this.nodes.length; j++) {
|
||||||
|
const nodeB = this.nodes[j];
|
||||||
|
|
||||||
|
const dx = nodeA.x - nodeB.x;
|
||||||
|
const dy = nodeA.y - nodeB.y;
|
||||||
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
|
||||||
|
if (distance < this.config.connectionDistance) {
|
||||||
|
const connection = {
|
||||||
|
id: `${i}-${j}`,
|
||||||
|
from: i,
|
||||||
|
to: j,
|
||||||
|
distance: distance,
|
||||||
|
opacity: Math.max(0.05, 1 - (distance / this.config.connectionDistance)),
|
||||||
|
active: false,
|
||||||
|
flowProgress: 0,
|
||||||
|
flowDuration: 0,
|
||||||
|
flowStart: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
this.connections.push(connection);
|
||||||
|
nodeA.connections.push(connection);
|
||||||
|
nodeB.connections.push(connection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startAnimation() {
|
||||||
|
this.animate();
|
||||||
|
}
|
||||||
|
|
||||||
|
animate() {
|
||||||
|
this.animationFrameId = requestAnimationFrame(this.animate.bind(this));
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
this.updateNodes(now);
|
||||||
|
this.updateConnections(now);
|
||||||
|
this.render(now);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateNodes(now) {
|
||||||
|
const width = this.canvas.width / (window.devicePixelRatio || 1);
|
||||||
|
const height = this.canvas.height / (window.devicePixelRatio || 1);
|
||||||
|
|
||||||
|
// Knoten bewegen
|
||||||
|
for (let i = 0; i < this.nodes.length; i++) {
|
||||||
|
const node = this.nodes[i];
|
||||||
|
|
||||||
|
node.x += node.speed.x;
|
||||||
|
node.y += node.speed.y;
|
||||||
|
|
||||||
|
// Begrenzung am Rand
|
||||||
|
if (node.x < 0 || node.x > width) {
|
||||||
|
node.speed.x *= -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.y < 0 || node.y > height) {
|
||||||
|
node.speed.y *= -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zufällig Richtung ändern
|
||||||
|
if (Math.random() < 0.01) {
|
||||||
|
node.speed.x = (Math.random() - 0.5) * 0.2;
|
||||||
|
node.speed.y = (Math.random() - 0.5) * 0.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zufälliges Pulsieren
|
||||||
|
if (Math.random() < this.config.nodePulseFrequency && now - node.lastPulse > node.pulseInterval) {
|
||||||
|
node.lastPulse = now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateConnections(now) {
|
||||||
|
// Update aktive Verbindungen
|
||||||
|
for (const connectionId of this.activeConnections) {
|
||||||
|
const connection = this.connections.find(c => c.id === connectionId);
|
||||||
|
if (!connection) continue;
|
||||||
|
|
||||||
|
// Aktualisiere den Flow-Fortschritt
|
||||||
|
const elapsed = now - connection.flowStart;
|
||||||
|
const progress = elapsed / connection.flowDuration;
|
||||||
|
|
||||||
|
if (progress >= 1) {
|
||||||
|
// Flow beenden
|
||||||
|
connection.active = false;
|
||||||
|
this.activeConnections.delete(connectionId);
|
||||||
|
} else {
|
||||||
|
connection.flowProgress = progress;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Neue Flows starten, wenn unter dem Limit
|
||||||
|
if (this.activeConnections.size < this.config.flowDensity) {
|
||||||
|
// Wähle eine zufällige Verbindung
|
||||||
|
const availableConnections = this.connections.filter(c => !c.active);
|
||||||
|
|
||||||
|
if (availableConnections.length > 0) {
|
||||||
|
const randomIndex = Math.floor(Math.random() * availableConnections.length);
|
||||||
|
const connection = availableConnections[randomIndex];
|
||||||
|
|
||||||
|
// Aktiviere die Verbindung
|
||||||
|
connection.active = true;
|
||||||
|
connection.flowProgress = 0;
|
||||||
|
connection.flowStart = now;
|
||||||
|
connection.flowDuration = this.config.flowDuration[0] +
|
||||||
|
Math.random() * (this.config.flowDuration[1] - this.config.flowDuration[0]);
|
||||||
|
|
||||||
|
this.activeConnections.add(connection.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render(now) {
|
||||||
|
// Aktualisiere Farben basierend auf aktuellem Theme
|
||||||
|
this.currentColors = document.documentElement.classList.contains('dark')
|
||||||
|
? this.colors.dark
|
||||||
|
: this.colors.light;
|
||||||
|
const colors = this.currentColors;
|
||||||
|
const width = this.canvas.width / (window.devicePixelRatio || 1);
|
||||||
|
const height = this.canvas.height / (window.devicePixelRatio || 1);
|
||||||
|
|
||||||
|
// Hintergrund löschen
|
||||||
|
this.ctx.fillStyle = colors.background;
|
||||||
|
this.ctx.fillRect(0, 0, width, height);
|
||||||
|
|
||||||
|
// Verbindungen zeichnen (statisch)
|
||||||
|
this.ctx.strokeStyle = colors.connectionColor;
|
||||||
|
this.ctx.lineWidth = 1.2;
|
||||||
|
|
||||||
|
for (const connection of this.connections) {
|
||||||
|
const fromNode = this.nodes[connection.from];
|
||||||
|
const toNode = this.nodes[connection.to];
|
||||||
|
|
||||||
|
this.ctx.globalAlpha = connection.opacity * 0.5;
|
||||||
|
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.moveTo(fromNode.x, fromNode.y);
|
||||||
|
this.ctx.lineTo(toNode.x, toNode.y);
|
||||||
|
this.ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aktive Verbindungen zeichnen (Flows)
|
||||||
|
this.ctx.strokeStyle = colors.flowColor;
|
||||||
|
this.ctx.lineWidth = 2.5;
|
||||||
|
|
||||||
|
for (const connectionId of this.activeConnections) {
|
||||||
|
const connection = this.connections.find(c => c.id === connectionId);
|
||||||
|
if (!connection) continue;
|
||||||
|
|
||||||
|
const fromNode = this.nodes[connection.from];
|
||||||
|
const toNode = this.nodes[connection.to];
|
||||||
|
|
||||||
|
// Glühen-Effekt
|
||||||
|
this.ctx.globalAlpha = Math.sin(connection.flowProgress * Math.PI) * 0.8;
|
||||||
|
|
||||||
|
// Linie zeichnen
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.moveTo(fromNode.x, fromNode.y);
|
||||||
|
this.ctx.lineTo(toNode.x, toNode.y);
|
||||||
|
this.ctx.stroke();
|
||||||
|
|
||||||
|
// Fließendes Partikel
|
||||||
|
const progress = connection.flowProgress;
|
||||||
|
const x = fromNode.x + (toNode.x - fromNode.x) * progress;
|
||||||
|
const y = fromNode.y + (toNode.y - fromNode.y) * progress;
|
||||||
|
|
||||||
|
this.ctx.globalAlpha = 0.9;
|
||||||
|
this.ctx.fillStyle = colors.flowColor;
|
||||||
|
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.arc(x, y, 2, 0, Math.PI * 2);
|
||||||
|
this.ctx.fill();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Knoten zeichnen
|
||||||
|
for (const node of this.nodes) {
|
||||||
|
// Pulsierende Knoten
|
||||||
|
const timeSinceLastPulse = now - node.lastPulse;
|
||||||
|
const isPulsing = timeSinceLastPulse < 800;
|
||||||
|
const pulseProgress = isPulsing ? timeSinceLastPulse / 800 : 0;
|
||||||
|
|
||||||
|
// Knoten selbst
|
||||||
|
this.ctx.globalAlpha = 1;
|
||||||
|
this.ctx.fillStyle = isPulsing
|
||||||
|
? colors.nodePulse
|
||||||
|
: colors.nodeColor;
|
||||||
|
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.arc(node.x, node.y, node.size + (isPulsing ? 1 * Math.sin(pulseProgress * Math.PI) : 0), 0, Math.PI * 2);
|
||||||
|
this.ctx.fill();
|
||||||
|
|
||||||
|
// Wenn pulsierend, füge einen Glow-Effekt hinzu
|
||||||
|
if (isPulsing) {
|
||||||
|
this.ctx.globalAlpha = 0.5 * (1 - pulseProgress);
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.arc(node.x, node.y, node.size + 5 * pulseProgress, 0, Math.PI * 2);
|
||||||
|
this.ctx.fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ctx.globalAlpha = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
if (this.isDestroying) return;
|
||||||
|
this.isDestroying = true;
|
||||||
|
|
||||||
|
// Animation stoppen
|
||||||
|
if (this.animationFrameId) {
|
||||||
|
cancelAnimationFrame(this.animationFrameId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Canvas ausblenden
|
||||||
|
this.canvas.style.opacity = '0';
|
||||||
|
|
||||||
|
// Nach Übergang entfernen
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.canvas && this.canvas.parentNode) {
|
||||||
|
this.canvas.parentNode.removeChild(this.canvas);
|
||||||
|
}
|
||||||
|
}, 3500);
|
||||||
|
}
|
||||||
|
|
||||||
|
hexToRgb(hex) {
|
||||||
|
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||||
|
return result ? {
|
||||||
|
r: parseInt(result[1], 16),
|
||||||
|
g: parseInt(result[2], 16),
|
||||||
|
b: parseInt(result[3], 16)
|
||||||
|
} : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialisiert den Hintergrund, sobald die Seite geladen ist
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
window.neuralBackground = new NeuralNetworkBackground();
|
||||||
|
|
||||||
|
// Theme-Wechsel-Event-Listener
|
||||||
|
document.addEventListener('theme-changed', () => {
|
||||||
|
if (window.neuralBackground) {
|
||||||
|
window.neuralBackground.currentColors = document.documentElement.classList.contains('dark')
|
||||||
|
? window.neuralBackground.colors.dark
|
||||||
|
: window.neuralBackground.colors.light;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
508
static/style.css
Normal file
508
static/style.css
Normal file
@@ -0,0 +1,508 @@
|
|||||||
|
/* Main Systades Styles - Dark Mystical Theme */
|
||||||
|
|
||||||
|
/* Import Fonts */
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap');
|
||||||
|
|
||||||
|
/* Root Variables */
|
||||||
|
:root {
|
||||||
|
/* Light Theme Colors */
|
||||||
|
--light-bg-primary: #f8fafc;
|
||||||
|
--light-bg-secondary: #f1f5f9;
|
||||||
|
--light-text-primary: #1e293b;
|
||||||
|
--light-text-secondary: #475569;
|
||||||
|
--light-accent-primary: #7c3aed;
|
||||||
|
--light-accent-secondary: #8b5cf6;
|
||||||
|
--light-border: #e2e8f0;
|
||||||
|
|
||||||
|
/* Dark Theme Colors */
|
||||||
|
--dark-bg-primary: #0a0e19;
|
||||||
|
--dark-bg-secondary: #111827;
|
||||||
|
--dark-text-primary: #f9fafb;
|
||||||
|
--dark-text-secondary: #e5e7eb;
|
||||||
|
--dark-accent-primary: #6d28d9;
|
||||||
|
--dark-accent-secondary: #8b5cf6;
|
||||||
|
--dark-border: #1f2937;
|
||||||
|
|
||||||
|
/* Common */
|
||||||
|
--font-sans: 'Inter', system-ui, -apple-system, sans-serif;
|
||||||
|
--font-mono: 'JetBrains Mono', monospace;
|
||||||
|
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||||
|
--shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||||
|
--shadow-md: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||||
|
--shadow-lg: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||||
|
|
||||||
|
/* Transitions */
|
||||||
|
--transition-fast: 150ms ease-in-out;
|
||||||
|
--transition-normal: 300ms ease-in-out;
|
||||||
|
--transition-slow: 500ms ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Base Elements */
|
||||||
|
body {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
transition: background-color var(--transition-normal), color var(--transition-normal);
|
||||||
|
background-color: transparent !important; /* Ensure background is transparent */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* HTML root element should also be transparent */
|
||||||
|
html {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Theme Specific - keep the color but remove background */
|
||||||
|
body {
|
||||||
|
color: var(--light-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark {
|
||||||
|
color: var(--dark-text-primary);
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure proper contrast in both modes */
|
||||||
|
body:not(.dark) {
|
||||||
|
--text-primary: var(--light-text-primary);
|
||||||
|
--text-secondary: var(--light-text-secondary);
|
||||||
|
--bg-primary: var(--light-bg-primary);
|
||||||
|
--bg-secondary: var(--light-bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark {
|
||||||
|
--text-primary: var(--dark-text-primary);
|
||||||
|
--text-secondary: var(--dark-text-secondary);
|
||||||
|
--bg-primary: var(--dark-bg-primary);
|
||||||
|
--bg-secondary: var(--dark-bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Typography */
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradient-text {
|
||||||
|
background-clip: text;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
color: transparent;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
body .gradient-text {
|
||||||
|
background-image: linear-gradient(135deg, var(--light-accent-primary), var(--light-accent-secondary));
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark .gradient-text {
|
||||||
|
background-image: linear-gradient(135deg, var(--dark-accent-primary), var(--dark-accent-secondary));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Subtle glow for dark mode gradient text */
|
||||||
|
body.dark .gradient-text::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
filter: blur(8px);
|
||||||
|
opacity: 0.3;
|
||||||
|
background-image: inherit;
|
||||||
|
z-index: -1;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Containers and Layout */
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
padding-left: 1rem;
|
||||||
|
padding-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.container {
|
||||||
|
max-width: 640px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.container {
|
||||||
|
max-width: 768px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.container {
|
||||||
|
max-width: 1024px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1280px) {
|
||||||
|
.container {
|
||||||
|
max-width: 1280px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Glass Morphism */
|
||||||
|
.glass-morphism {
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
body .glass-navbar-light {
|
||||||
|
background-color: rgba(255, 255, 255, 0.8);
|
||||||
|
border-color: rgba(226, 232, 240, 0.5);
|
||||||
|
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark .glass-navbar-dark {
|
||||||
|
background-color: rgba(10, 14, 25, 0.8);
|
||||||
|
border-color: rgba(31, 41, 55, 0.5);
|
||||||
|
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navigation */
|
||||||
|
.nav-link {
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
transition: all var(--transition-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
body .nav-link {
|
||||||
|
color: var(--light-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark .nav-link {
|
||||||
|
color: var(--dark-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
body .nav-link:hover {
|
||||||
|
color: var(--light-text-primary);
|
||||||
|
background-color: rgba(241, 245, 249, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark .nav-link:hover {
|
||||||
|
color: var(--dark-text-primary);
|
||||||
|
background-color: rgba(31, 41, 55, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
body .nav-link-light-active {
|
||||||
|
color: var(--light-accent-primary);
|
||||||
|
background-color: rgba(139, 92, 246, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark .nav-link-active {
|
||||||
|
color: var(--dark-accent-secondary);
|
||||||
|
background-color: rgba(109, 40, 217, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
.btn {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all var(--transition-normal);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
body .btn-primary {
|
||||||
|
background-color: var(--light-accent-primary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark .btn-primary {
|
||||||
|
background-color: var(--dark-accent-primary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
body .btn-primary:hover {
|
||||||
|
background-color: var(--light-accent-secondary);
|
||||||
|
box-shadow: 0 0 15px rgba(124, 58, 237, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark .btn-primary:hover {
|
||||||
|
background-color: var(--dark-accent-secondary);
|
||||||
|
box-shadow: 0 0 15px rgba(109, 40, 217, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
body .btn-secondary {
|
||||||
|
background-color: transparent;
|
||||||
|
border: 1px solid var(--light-border);
|
||||||
|
color: var(--light-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark .btn-secondary {
|
||||||
|
background-color: transparent;
|
||||||
|
border: 1px solid var(--dark-border);
|
||||||
|
color: var(--dark-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
body .btn-secondary:hover {
|
||||||
|
background-color: var(--light-bg-secondary);
|
||||||
|
border-color: var(--light-accent-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark .btn-secondary:hover {
|
||||||
|
background-color: var(--dark-bg-secondary);
|
||||||
|
border-color: var(--dark-accent-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cards */
|
||||||
|
.card {
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all var(--transition-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
body .card {
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid var(--light-border);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark .card {
|
||||||
|
background-color: var(--dark-bg-secondary);
|
||||||
|
border: 1px solid var(--dark-border);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
body .card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark .card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form Elements */
|
||||||
|
.form-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
transition: all var(--transition-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
body .form-input {
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid var(--light-border);
|
||||||
|
color: var(--light-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark .form-input {
|
||||||
|
background-color: var(--dark-bg-secondary);
|
||||||
|
border: 1px solid var(--dark-border);
|
||||||
|
color: var(--dark-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
body .form-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--light-accent-secondary);
|
||||||
|
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark .form-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--dark-accent-secondary);
|
||||||
|
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-fade-in {
|
||||||
|
animation: fadeIn 0.5s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes float {
|
||||||
|
0%, 100% { transform: translateY(0); }
|
||||||
|
50% { transform: translateY(-10px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-float {
|
||||||
|
animation: float 5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% { opacity: 0.7; }
|
||||||
|
50% { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-pulse {
|
||||||
|
animation: pulse 3s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Utilities */
|
||||||
|
.sr-only {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
padding: 0;
|
||||||
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
white-space: nowrap;
|
||||||
|
border-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shadow-elevation {
|
||||||
|
transition: box-shadow var(--transition-normal), transform var(--transition-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
body .shadow-elevation {
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark .shadow-elevation {
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
body .shadow-elevation:hover {
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark .shadow-elevation:hover {
|
||||||
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.4), 0 4px 6px -2px rgba(0, 0, 0, 0.3);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tooltips */
|
||||||
|
.tooltip {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip:hover::before {
|
||||||
|
content: attr(data-tooltip);
|
||||||
|
position: absolute;
|
||||||
|
bottom: 100%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
z-index: 10;
|
||||||
|
opacity: 0;
|
||||||
|
animation: fadeIn 0.3s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
body .tooltip:hover::before {
|
||||||
|
background-color: var(--light-text-primary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark .tooltip:hover::before {
|
||||||
|
background-color: var(--dark-text-primary);
|
||||||
|
color: var(--dark-bg-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mystical elements */
|
||||||
|
.mystical-border {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystical-border::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
border: 1px solid;
|
||||||
|
border-radius: inherit;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.3;
|
||||||
|
transition: opacity var(--transition-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
body .mystical-border::after {
|
||||||
|
border-color: var(--light-accent-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark .mystical-border::after {
|
||||||
|
border-color: var(--dark-accent-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystical-border:hover::after {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Design Helpers */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.container {
|
||||||
|
padding-left: 1rem;
|
||||||
|
padding-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-heading {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-heading {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Accessibility */
|
||||||
|
:focus-visible {
|
||||||
|
outline: 2px solid;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body :focus-visible {
|
||||||
|
outline-color: var(--light-accent-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark :focus-visible {
|
||||||
|
outline-color: var(--dark-accent-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar styling */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 0.5rem;
|
||||||
|
height: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
body ::-webkit-scrollbar-track {
|
||||||
|
background: var(--light-bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark ::-webkit-scrollbar-track {
|
||||||
|
background: var(--dark-bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
body ::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--light-accent-primary);
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark ::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--dark-accent-primary);
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
body ::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--light-accent-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark ::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--dark-accent-secondary);
|
||||||
|
}
|
||||||
6
static/three.min.js
vendored
6
static/three.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -1,100 +0,0 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
|
||||||
module.exports = {
|
|
||||||
darkMode: 'class',
|
|
||||||
content: [
|
|
||||||
"./templates/**/*.{html,jinja,jinja2}",
|
|
||||||
"./static/**/*.js"
|
|
||||||
],
|
|
||||||
theme: {
|
|
||||||
extend: {
|
|
||||||
colors: {
|
|
||||||
primary: {
|
|
||||||
50: '#eef5ff',
|
|
||||||
100: '#d9e7ff',
|
|
||||||
200: '#bcd4ff',
|
|
||||||
300: '#8eb8ff',
|
|
||||||
400: '#5a93ff',
|
|
||||||
500: '#2970ff',
|
|
||||||
600: '#1654f6',
|
|
||||||
700: '#1142e2',
|
|
||||||
800: '#1336b7',
|
|
||||||
900: '#153390',
|
|
||||||
},
|
|
||||||
secondary: {
|
|
||||||
50: '#f5f2ff',
|
|
||||||
100: '#ece8ff',
|
|
||||||
200: '#ddd5ff',
|
|
||||||
300: '#c4b3ff',
|
|
||||||
400: '#a685ff',
|
|
||||||
500: '#8b55ff',
|
|
||||||
600: '#7833f8',
|
|
||||||
700: '#6924e2',
|
|
||||||
800: '#5720b8',
|
|
||||||
900: '#481c96',
|
|
||||||
},
|
|
||||||
dark: {
|
|
||||||
50: '#f8f8f9',
|
|
||||||
100: '#e7e7ea',
|
|
||||||
200: '#d1d1d8',
|
|
||||||
300: '#aeaeba',
|
|
||||||
400: '#8a8a99',
|
|
||||||
500: '#6f6f7e',
|
|
||||||
600: '#5b5b69',
|
|
||||||
700: '#49494f',
|
|
||||||
800: '#2c2c33',
|
|
||||||
900: '#18181c',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
fontFamily: {
|
|
||||||
'sans': ['Inter', 'ui-sans-serif', 'system-ui', '-apple-system', 'BlinkMacSystemFont', 'sans-serif'],
|
|
||||||
'mono': ['JetBrains Mono', 'ui-monospace', 'SFMono-Regular', 'monospace']
|
|
||||||
},
|
|
||||||
backgroundImage: {
|
|
||||||
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
|
|
||||||
'gradient-conic': 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
|
|
||||||
'gradient-tech': 'linear-gradient(to right, var(--tw-gradient-stops))',
|
|
||||||
},
|
|
||||||
animation: {
|
|
||||||
'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
|
||||||
'float': 'float 6s ease-in-out infinite',
|
|
||||||
},
|
|
||||||
keyframes: {
|
|
||||||
float: {
|
|
||||||
'0%, 100%': { transform: 'translateY(0)' },
|
|
||||||
'50%': { transform: 'translateY(-10px)' },
|
|
||||||
}
|
|
||||||
},
|
|
||||||
typography: {
|
|
||||||
DEFAULT: {
|
|
||||||
css: {
|
|
||||||
color: 'rgb(31, 41, 55)',
|
|
||||||
a: {
|
|
||||||
color: 'rgb(41, 112, 255)',
|
|
||||||
'&:hover': {
|
|
||||||
color: 'rgb(22, 84, 246)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dark: {
|
|
||||||
css: {
|
|
||||||
color: 'rgb(229, 231, 235)',
|
|
||||||
a: {
|
|
||||||
color: 'rgb(90, 147, 255)',
|
|
||||||
'&:hover': {
|
|
||||||
color: 'rgb(142, 184, 255)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
boxShadow: {
|
|
||||||
'soft': '0 4px 15px rgba(0, 0, 0, 0.05)',
|
|
||||||
'glow': '0 0 15px rgba(32, 92, 245, 0.3)'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
// Typography and forms plugins removed, we'll implement their basic functionality in CSS
|
|
||||||
],
|
|
||||||
}
|
|
||||||
46
templates/admin/update_database.html
Normal file
46
templates/admin/update_database.html
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Datenbank aktualisieren{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mx-auto px-4 py-10">
|
||||||
|
<div class="bg-gray-800 bg-opacity-70 rounded-lg p-6 mb-6">
|
||||||
|
<h1 class="text-2xl font-bold text-purple-400 mb-4">Datenbank aktualisieren</h1>
|
||||||
|
|
||||||
|
{% if message %}
|
||||||
|
<div class="mb-6 p-4 rounded-lg {{ 'bg-green-800 bg-opacity-50' if success else 'bg-red-800 bg-opacity-50' }}">
|
||||||
|
<p class="text-white">{{ message }}</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<p class="text-gray-300 mb-4">
|
||||||
|
Diese Funktion aktualisiert die Datenbankstruktur, um mit dem aktuellen Datenmodell kompatibel zu sein.
|
||||||
|
Dabei werden folgende Änderungen vorgenommen:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul class="list-disc pl-6 text-gray-300 mb-6">
|
||||||
|
<li>Hinzufügen von <code>bio</code>, <code>location</code>, <code>website</code>, <code>avatar</code> und <code>last_login</code> zur Benutzer-Tabelle</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="bg-yellow-800 bg-opacity-30 p-4 rounded-lg mb-6">
|
||||||
|
<p class="text-yellow-200">
|
||||||
|
<i class="fas fa-exclamation-triangle mr-2"></i>
|
||||||
|
<strong>Warnung:</strong> Bitte stelle sicher, dass du ein Backup der Datenbank erstellt hast, bevor du fortfährst.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="POST" action="{{ url_for('admin_update_database') }}">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<a href="{{ url_for('index') }}" class="px-4 py-2 bg-gray-700 text-white rounded-lg hover:bg-gray-600">
|
||||||
|
Zurück zur Startseite
|
||||||
|
</a>
|
||||||
|
<button type="submit" class="px-4 py-2 bg-purple-700 text-white rounded-lg hover:bg-purple-600">
|
||||||
|
Datenbank aktualisieren
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -6,17 +6,18 @@
|
|||||||
<title>Systades - {% block title %}{% endblock %}</title>
|
<title>Systades - {% block title %}{% endblock %}</title>
|
||||||
|
|
||||||
<!-- Favicon -->
|
<!-- 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/neuron-favicon.svg') }}" type="image/svg+xml">
|
||||||
<link rel="icon" href="{{ url_for('static', filename='img/favicon.ico') }}" sizes="any">
|
|
||||||
|
|
||||||
<!-- Meta Tags -->
|
<!-- Meta Tags -->
|
||||||
<meta name="description" content="Eine interaktive Plattform zum Visualisieren, Erforschen und Teilen von Wissen">
|
<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="keywords" content="systades, wissen, visualisierung, lernen, gedanken, theorie">
|
||||||
<meta name="author" content="Systades-Team">
|
<meta name="author" content="Systades-Team">
|
||||||
|
|
||||||
<!-- Tailwind CSS über CDN -->
|
<!-- Tailwind CSS - CDN für Entwicklung und Produktion (laut Vorgabe) -->
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<!-- Alternative lokale Version, falls die CDN-Version blockiert wird -->
|
||||||
<script>
|
<script>
|
||||||
|
tailwind = window.tailwind || {};
|
||||||
tailwind.config = {
|
tailwind.config = {
|
||||||
darkMode: 'class',
|
darkMode: 'class',
|
||||||
theme: {
|
theme: {
|
||||||
@@ -57,23 +58,35 @@
|
|||||||
800: '#0e1220',
|
800: '#0e1220',
|
||||||
900: '#0a0e19'
|
900: '#0a0e19'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
keyframes: {
|
||||||
|
float: {
|
||||||
|
'0%, 100%': { transform: 'translateY(0)' },
|
||||||
|
'50%': { transform: 'translateY(-5px)' }
|
||||||
|
},
|
||||||
|
'bounce-slow': {
|
||||||
|
'0%, 100%': { transform: 'translateY(0)' },
|
||||||
|
'50%': { transform: 'translateY(-8px)' }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
float: 'float 3s ease-in-out infinite',
|
||||||
|
'bounce-slow': 'bounce-slow 2s ease-in-out infinite'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Fonts -->
|
<!-- Local Font Files -->
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link href="{{ url_for('static', filename='fonts/inter.css') }}" rel="stylesheet">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link href="{{ url_for('static', filename='fonts/jetbrains-mono.css') }}" rel="stylesheet">
|
||||||
<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 -->
|
<!-- Font Awesome vom CDN -->
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" rel="stylesheet">
|
||||||
|
|
||||||
<!-- Assistent CSS -->
|
<!-- Assistent CSS -->
|
||||||
<link href="{{ url_for('static', filename='css/assistant.css') }}" rel="stylesheet">
|
<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 -->
|
<!-- Basis-Stylesheet -->
|
||||||
<link href="{{ url_for('static', filename='style.css') }}" rel="stylesheet">
|
<link href="{{ url_for('static', filename='style.css') }}" rel="stylesheet">
|
||||||
@@ -81,101 +94,297 @@
|
|||||||
<!-- Base-Styles ausgelagert in eigene Datei -->
|
<!-- Base-Styles ausgelagert in eigene Datei -->
|
||||||
<link href="{{ url_for('static', filename='css/base-styles.css') }}" rel="stylesheet">
|
<link href="{{ url_for('static', filename='css/base-styles.css') }}" rel="stylesheet">
|
||||||
|
|
||||||
<!-- Alpine.js -->
|
<!-- Alpine.js - CDN Version -->
|
||||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.12.3/dist/cdn.min.js"></script>
|
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.12.3/dist/cdn.min.js"></script>
|
||||||
|
|
||||||
<!-- Network Background Script -->
|
<!-- Neural Network Background CSS -->
|
||||||
<script src="{{ url_for('static', filename='network-background.js') }}"></script>
|
<link href="{{ url_for('static', filename='css/neural-network-background.css') }}" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- Mindmap CSS -->
|
||||||
|
<link href="{{ url_for('static', filename='css/mindmap.css') }}" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- D3.js für Visualisierungen -->
|
||||||
|
<script src="https://d3js.org/d3.v7.min.js"></script>
|
||||||
|
|
||||||
<!-- Hauptmodul laden (als ES6 Modul) -->
|
<!-- Marked.js für Markdown-Parsing -->
|
||||||
<script type="module">
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||||
import MindMap from "{{ url_for('static', filename='js/main.js') }}";
|
|
||||||
// Alpine.js-Integration
|
<!-- ChatGPT Assistant -->
|
||||||
document.addEventListener('alpine:init', () => {
|
<script src="{{ url_for('static', filename='js/modules/chatgpt-assistant.js') }}"></script>
|
||||||
Alpine.data('layout', () => ({
|
|
||||||
darkMode: false,
|
<!-- Neural Network Background Script -->
|
||||||
mobileMenuOpen: false,
|
<script src="{{ url_for('static', filename='neural-network-background.js') }}"></script>
|
||||||
userMenuOpen: false,
|
|
||||||
showSettingsModal: false,
|
<!-- Hauptmodul laden (als traditionelles Skript) -->
|
||||||
|
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
|
||||||
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 -->
|
<!-- Seitenspezifische Styles -->
|
||||||
{% block extra_css %}{% endblock %}
|
{% block extra_css %}{% endblock %}
|
||||||
|
|
||||||
<!-- Cybertechnisches Netzwerk-Hintergrund -->
|
<!-- Custom dark/light mode styles -->
|
||||||
<script type="module" src="{{ url_for('static', filename='js/modules/cyber-network-init.js') }}"></script>
|
<!-- ► ► Farb‑Token strikt getrennt ◄ ◄ -->
|
||||||
|
<style>
|
||||||
|
/* Light‑Mode */
|
||||||
|
:root {
|
||||||
|
--bg-primary:#f8fafc;
|
||||||
|
--bg-secondary:#f1f5f9;
|
||||||
|
--text-primary:#232837;
|
||||||
|
--text-secondary:#475569;
|
||||||
|
--accent-primary:#7c3aed;
|
||||||
|
--accent-secondary:#8b5cf6;
|
||||||
|
--glow-effect:0 0 8px rgba(139,92,246,.08);
|
||||||
|
background-image: linear-gradient(to bottom right, rgba(248, 250, 252, 0.8), rgba(241, 245, 249, 0.8));
|
||||||
|
background-attachment: fixed;
|
||||||
|
}
|
||||||
|
/* Dark‑Mode */
|
||||||
|
.dark {
|
||||||
|
--bg-primary:#181c24;
|
||||||
|
--bg-secondary:#232837;
|
||||||
|
--text-primary:#f9fafb;
|
||||||
|
--text-secondary:#e5e7eb;
|
||||||
|
--accent-primary:#6d28d9;
|
||||||
|
--accent-secondary:#8b5cf6;
|
||||||
|
--glow-effect:0 0 8px rgba(124,58,237,.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
@apply min-h-screen bg-[color:var(--bg-primary)] text-[color:var(--text-primary)];
|
||||||
|
transition: background-color 0.5s ease-in-out, color 0.3s ease-in-out, background-image 0.5s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Utilities */
|
||||||
|
.mystical-glow { text-shadow: var(--glow-effect); }
|
||||||
|
.gradient-text {
|
||||||
|
background:linear-gradient(135deg,var(--accent-primary),var(--accent-secondary));
|
||||||
|
-webkit-background-clip:text; background-clip:text; color:transparent; text-shadow:none;
|
||||||
|
}
|
||||||
|
.glass-morphism { backdrop-filter: blur(10px); }
|
||||||
|
.glass-navbar { @apply glass-morphism border backdrop-blur-xl; }
|
||||||
|
.light .glass-navbar { background-color:rgba(255,255,255,.8); border-color:rgba(0,0,0,.05); }
|
||||||
|
.dark .glass-navbar { background-color:rgba(10,14,25,.8); border-color:rgba(255,255,255,.05); }
|
||||||
|
|
||||||
|
/* Light-Mode spezifische Stile */
|
||||||
|
body:not(.dark) {
|
||||||
|
background-color: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link-light {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link-light:hover {
|
||||||
|
color: var(--text-primary);
|
||||||
|
background-color: rgba(126, 34, 206, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link-light-active {
|
||||||
|
color: var(--accent-primary);
|
||||||
|
background-color: rgba(126, 34, 206, 0.15);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Kartendesign im Light-Mode */
|
||||||
|
body:not(.dark) .card {
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .card:hover {
|
||||||
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: var(--light-primary-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light Mode Buttons */
|
||||||
|
body:not(.dark) .btn,
|
||||||
|
body:not(.dark) button:not(.toggle) {
|
||||||
|
background: linear-gradient(135deg, #7c3aed, #6d28d9);
|
||||||
|
color: white !important;
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 2px 4px rgba(124, 58, 237, 0.25);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.625rem 1.25rem;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .btn:hover,
|
||||||
|
body:not(.dark) button:not(.toggle):hover {
|
||||||
|
background: linear-gradient(135deg, #8b5cf6, #7c3aed);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(124, 58, 237, 0.3);
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* KI-Chat Button im Light-Mode */
|
||||||
|
body:not(.dark) [onclick*="MindMap.assistant.toggleAssistant"] {
|
||||||
|
background: linear-gradient(135deg, #7c3aed, #4f46e5);
|
||||||
|
color: white !important;
|
||||||
|
font-weight: 500;
|
||||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) [onclick*="MindMap.assistant.toggleAssistant"]:hover {
|
||||||
|
background: linear-gradient(135deg, #8b5cf6, #6366f1);
|
||||||
|
box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style improvements for the theme toggle button */
|
||||||
|
.theme-toggle {
|
||||||
|
position: relative;
|
||||||
|
width: 48px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 24px;
|
||||||
|
padding: 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark .theme-toggle {
|
||||||
|
background: linear-gradient(to right, #7c3aed, #3b82f6);
|
||||||
|
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.3), 0 0 10px rgba(124, 58, 237, 0.3);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .theme-toggle {
|
||||||
|
background: linear-gradient(to right, #8b5cf6, #60a5fa);
|
||||||
|
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1), 0 0 10px rgba(124, 58, 237, 0.15);
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
top: 2px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark .theme-toggle::after {
|
||||||
|
background: #f1f5f9 url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%237c3aed' width='14' height='14'%3E%3Cpath d='M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z'%3E%3C/path%3E%3C/svg%3E") no-repeat center center;
|
||||||
|
transform: translateX(24px);
|
||||||
|
box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .theme-toggle::after {
|
||||||
|
background: #fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23f59e0b' width='14' height='14'%3E%3Cpath d='M12 2.25a.75.75 0 01.75.75v2.25a.75.75 0 01-1.5 0V3a.75.75 0 01.75-.75zM7.5 12a4.5 4.5 0 119 0 4.5 4.5 0 01-9 0zM18.894 6.166a.75.75 0 00-1.06-1.06l-1.591 1.59a.75.75 0 101.06 1.061l1.591-1.59zM21.75 12a.75.75 0 01-.75.75h-2.25a.75.75 0 010-1.5H21a.75.75 0 01.75.75zM17.834 18.894a.75.75 0 001.06-1.06l-1.59-1.591a.75.75 0 10-1.061 1.06l1.59 1.591zM12 18a.75.75 0 01.75.75V21a.75.75 0 01-1.5 0v-2.25A.75.75 0 0112 18zM7.758 17.303a.75.75 0 00-1.061-1.06l-1.591 1.59a.75.75 0 001.06 1.061l1.591-1.59zM6 12a.75.75 0 01-.75.75H3a.75.75 0 010-1.5h2.25A.75.75 0 016 12zM6.697 7.757a.75.75 0 001.06-1.06l-1.59-1.591a.75.75 0 00-1.061 1.06l1.59 1.591z'%3E%3C/path%3E%3C/svg%3E") no-repeat center center;
|
||||||
|
transform: translateX(2px);
|
||||||
|
box-shadow: 0 0 8px rgba(124, 58, 237, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle:hover::after {
|
||||||
|
box-shadow: 0 0 12px rgba(124, 58, 237, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fixes for light mode button text colors */
|
||||||
|
body:not(.dark) .btn-primary {
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix for KI-Chat container */
|
||||||
|
#chatgpt-assistant {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 1.5rem;
|
||||||
|
right: 1.5rem;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-assistant {
|
||||||
|
max-height: 80vh !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-assistant .chat-messages {
|
||||||
|
max-height: calc(80vh - 160px) !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body data-page="{{ request.endpoint }}" class="relative overflow-x-hidden">
|
<body data-page="{{ request.endpoint }}" class="relative overflow-x-hidden dark bg-gray-900 text-white" x-data="{
|
||||||
<!-- Cybertechnisches Netzwerk-Hintergrund Container (wird via JavaScript befüllt) -->
|
darkMode: true,
|
||||||
<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>
|
mobileMenuOpen: false,
|
||||||
|
userMenuOpen: false,
|
||||||
|
showSettingsModal: false,
|
||||||
|
|
||||||
<!-- Globaler Hintergrund -->
|
init() {
|
||||||
<div class="full-page-bg"></div>
|
this.initDarkMode();
|
||||||
<!-- Statischer Fallback-Hintergrund (wird nur angezeigt, wenn JavaScript deaktiviert ist) -->
|
},
|
||||||
<div class="fixed inset-0 z-[-9] bg-cover bg-center opacity-50"></div>
|
|
||||||
|
|
||||||
|
initDarkMode() {
|
||||||
|
// Lade zuerst den Wert aus dem localStorage (client-seitig)
|
||||||
|
const storedMode = localStorage.getItem('colorMode');
|
||||||
|
if (storedMode) {
|
||||||
|
this.darkMode = storedMode === 'dark';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dann hole die Server-Einstellung, die Vorrang hat
|
||||||
|
this.fetchDarkModeFromSession();
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchDarkModeFromSession() {
|
||||||
|
fetch('/api/get_dark_mode')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
this.darkMode = data.darkMode === 'true';
|
||||||
|
this.applyDarkMode();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Fehler beim Laden der Dark Mode-Einstellung:', error);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
applyDarkMode() {
|
||||||
|
document.querySelector('html').classList.toggle('dark', this.darkMode);
|
||||||
|
document.querySelector('body').classList.toggle('dark', this.darkMode);
|
||||||
|
localStorage.setItem('colorMode', this.darkMode ? 'dark' : 'light');
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleDarkMode() {
|
||||||
|
this.darkMode = !this.darkMode;
|
||||||
|
this.applyDarkMode();
|
||||||
|
|
||||||
|
// Server über Änderung informieren
|
||||||
|
fetch('/api/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) {
|
||||||
|
// Event auslösen für andere Komponenten
|
||||||
|
document.dispatchEvent(new CustomEvent('darkModeToggled', {
|
||||||
|
detail: { isDark: this.darkMode }
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Fehler beim Speichern der Dark Mode-Einstellung:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}">
|
||||||
<!-- App-Container -->
|
<!-- App-Container -->
|
||||||
<div id="app-container" class="flex flex-col min-h-screen" x-data="layout">
|
<div id="app-container" class="flex flex-col min-h-screen">
|
||||||
<!-- Hauptnavigation -->
|
<!-- Hauptnavigation -->
|
||||||
<nav class="sticky top-0 left-0 right-0 z-50 transition-all duration-300 py-4 px-5 border-b glass-morphism"
|
<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'">
|
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">
|
<div class="container mx-auto flex justify-between items-center">
|
||||||
<!-- Logo -->
|
<!-- Logo -->
|
||||||
<a href="{{ url_for('index') }}" class="flex items-center group">
|
<a href="{{ url_for('index') }}" class="flex items-center group">
|
||||||
|
<img src="{{ url_for('static', filename='img/neuron-logo.svg') }}" alt="Systades Logo" class="w-8 h-8 mr-2 transform transition-transform group-hover:scale-110">
|
||||||
<span class="text-2xl font-bold gradient-text transform transition-transform group-hover:scale-105">Systades</span>
|
<span class="text-2xl font-bold gradient-text transform transition-transform group-hover:scale-105">Systades</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
@@ -206,8 +415,8 @@
|
|||||||
<button onclick="window.MindMap && window.MindMap.assistant && window.MindMap.assistant.toggleAssistant(true)"
|
<button onclick="window.MindMap && window.MindMap.assistant && window.MindMap.assistant.toggleAssistant(true)"
|
||||||
class="nav-link flex items-center"
|
class="nav-link flex items-center"
|
||||||
x-bind:class="darkMode
|
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-900/90 to-indigo-800/90 text-white font-medium px-4 py-2 rounded-xl hover:shadow-lg hover:shadow-purple-800/30 transition-all duration-300'
|
||||||
: 'bg-gradient-to-r from-purple-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'">
|
: 'bg-gradient-to-r from-purple-600 to-indigo-500 text-white font-medium px-4 py-2 rounded-xl hover:shadow-md transition-all duration-300'">
|
||||||
<i class="fa-solid fa-robot mr-2"></i>KI-Chat
|
<i class="fa-solid fa-robot mr-2"></i>KI-Chat
|
||||||
</button>
|
</button>
|
||||||
{% if current_user.is_authenticated %}
|
{% if current_user.is_authenticated %}
|
||||||
@@ -223,25 +432,14 @@
|
|||||||
|
|
||||||
<!-- Rechte Seite -->
|
<!-- Rechte Seite -->
|
||||||
<div class="flex items-center space-x-4">
|
<div class="flex items-center space-x-4">
|
||||||
<!-- Dark Mode Toggle Switch -->
|
<!-- Dark/Light Mode Schalter -->
|
||||||
<div class="flex items-center cursor-pointer" @click="toggleDarkMode">
|
<button
|
||||||
<div class="relative w-12 h-6">
|
@click="toggleDarkMode()"
|
||||||
<input type="checkbox" id="darkModeToggle" class="sr-only" x-model="darkMode">
|
class="theme-toggle relative w-12 h-6 rounded-full transition-all duration-300 flex items-center overflow-hidden"
|
||||||
<div class="block w-12 h-6 rounded-full transition-colors duration-300"
|
aria-label="Dark Mode umschalten"
|
||||||
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"
|
<span class="sr-only" x-text="darkMode ? 'Zum Light Mode wechseln' : 'Zum Dark Mode wechseln'"></span>
|
||||||
x-bind:class="darkMode ? 'bg-blue-500 transform translate-x-6' : 'bg-white'"></div>
|
</button>
|
||||||
</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 -->
|
<!-- Profil-Link oder Login -->
|
||||||
{% if current_user.is_authenticated %}
|
{% if current_user.is_authenticated %}
|
||||||
<div class="relative" x-data="{ open: false }">
|
<div class="relative" x-data="{ open: false }">
|
||||||
@@ -255,12 +453,21 @@
|
|||||||
{% if current_user.avatar %}
|
{% if current_user.avatar %}
|
||||||
<img src="{{ current_user.avatar }}" alt="{{ current_user.username }}" class="w-full h-full object-cover">
|
<img src="{{ current_user.avatar }}" alt="{{ current_user.username }}" class="w-full h-full object-cover">
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ current_user.username[0].upper() }}
|
<svg width="100%" height="100%" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="100" cy="100" r="98" fill="url(#user-gradient)" stroke="#7C3AED" stroke-width="4"/>
|
||||||
|
<circle cx="100" cy="80" r="36" fill="white"/>
|
||||||
|
<path d="M100 140C77.9086 140 60 157.909 60 180H140C140 157.909 122.091 140 100 140Z" fill="white"/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="user-gradient" x1="0" y1="0" x2="200" y2="200" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stop-color="#8B5CF6"/>
|
||||||
|
<stop offset="1" stop-color="#3B82F6"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<span class="text-sm hidden lg:block">{{ current_user.username }}</span>
|
<span class="hidden md:block">{{ current_user.username }}</span>
|
||||||
<i class="fa-solid fa-chevron-down text-xs hidden lg:block transition-transform duration-200"
|
<i class="fas fa-chevron-down text-xs opacity-60 ml-1.5"></i>
|
||||||
x-bind:class="open ? 'transform rotate-180' : ''"></i>
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Dropdown-Menü -->
|
<!-- Dropdown-Menü -->
|
||||||
@@ -308,13 +515,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{{ url_for('login') }}"
|
<div class="flex items-center space-x-2">
|
||||||
class="flex items-center px-4 py-2.5 rounded-xl font-medium transition-all duration-300"
|
<a href="{{ url_for('login') }}"
|
||||||
x-bind:class="darkMode
|
class="py-2 px-4 rounded-lg transition-all duration-300"
|
||||||
? 'bg-gray-800/80 text-white hover:bg-gray-700/80 shadow-md hover:shadow-lg hover:-translate-y-0.5'
|
x-bind:class="darkMode
|
||||||
: 'bg-gray-200/80 text-gray-800 hover:bg-gray-300/80 shadow-sm hover:shadow-md hover:-translate-y-0.5'">
|
? 'text-white/90 hover:bg-dark-700/80'
|
||||||
<i class="fa-solid fa-user mr-2"></i>Mein Konto
|
: 'text-gray-700 hover:bg-gray-100/80'">
|
||||||
</a>
|
<i class="fa-solid fa-sign-in-alt mr-2"></i>Login
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('register') }}"
|
||||||
|
class="py-2 px-4 rounded-lg transition-all duration-300 font-medium"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-purple-800/80 text-white hover:bg-purple-700/80'
|
||||||
|
: 'bg-purple-600/20 text-gray-700 hover:bg-purple-600/30'">
|
||||||
|
Registrieren
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Mobilmenü-Button -->
|
<!-- Mobilmenü-Button -->
|
||||||
@@ -331,6 +547,7 @@
|
|||||||
|
|
||||||
<!-- Mobile Menü -->
|
<!-- Mobile Menü -->
|
||||||
<div x-show="mobileMenuOpen"
|
<div x-show="mobileMenuOpen"
|
||||||
|
x-cloak
|
||||||
x-transition:enter="transition ease-out duration-200"
|
x-transition:enter="transition ease-out duration-200"
|
||||||
x-transition:enter-start="opacity-0 -translate-y-4"
|
x-transition:enter-start="opacity-0 -translate-y-4"
|
||||||
x-transition:enter-end="opacity-100 translate-y-0"
|
x-transition:enter-end="opacity-100 translate-y-0"
|
||||||
@@ -368,7 +585,7 @@
|
|||||||
class="block w-full text-left py-3.5 px-4 rounded-xl transition-all duration-200 flex items-center"
|
class="block w-full text-left py-3.5 px-4 rounded-xl transition-all duration-200 flex items-center"
|
||||||
x-bind:class="darkMode
|
x-bind:class="darkMode
|
||||||
? 'bg-gradient-to-r from-purple-600/30 to-blue-500/30 text-white hover:from-purple-600/40 hover:to-blue-500/40'
|
? 'bg-gradient-to-r from-purple-600/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'">
|
: 'bg-gradient-to-r from-purple-600 to-blue-500 text-white hover:from-purple-600/90 hover:to-blue-500/90'">
|
||||||
<i class="fa-solid fa-robot w-5 mr-3"></i>KI-Chat
|
<i class="fa-solid fa-robot w-5 mr-3"></i>KI-Chat
|
||||||
</button>
|
</button>
|
||||||
{% if current_user.is_authenticated %}
|
{% if current_user.is_authenticated %}
|
||||||
@@ -434,6 +651,10 @@
|
|||||||
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
||||||
Mindmap
|
Mindmap
|
||||||
</a>
|
</a>
|
||||||
|
<a href="{{ url_for('search_thoughts_page') }}" class="text-sm transition-all duration-200"
|
||||||
|
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
||||||
|
Suche
|
||||||
|
</a>
|
||||||
{% if current_user.is_authenticated %}
|
{% if current_user.is_authenticated %}
|
||||||
<a href="{{ url_for('profile') }}" class="text-sm transition-all duration-200"
|
<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'">
|
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
||||||
@@ -462,6 +683,10 @@
|
|||||||
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
||||||
Impressum
|
Impressum
|
||||||
</a>
|
</a>
|
||||||
|
<a href="{{ url_for('ueber_uns') }}" class="text-sm transition-all duration-200"
|
||||||
|
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
||||||
|
Über uns
|
||||||
|
</a>
|
||||||
<a href="{{ url_for('datenschutz') }}" class="text-sm transition-all duration-200"
|
<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'">
|
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
||||||
Datenschutz
|
Datenschutz
|
||||||
@@ -509,27 +734,286 @@
|
|||||||
|
|
||||||
<!-- Hilfsscripts -->
|
<!-- Hilfsscripts -->
|
||||||
{% block scripts %}{% endblock %}
|
{% block scripts %}{% endblock %}
|
||||||
|
{% block extra_js %}{% endblock %}
|
||||||
|
|
||||||
<!-- KI-Chat Initialisierung -->
|
<!-- ChatGPT Initialisierung -->
|
||||||
<script type="module">
|
<script>
|
||||||
// Importiere und initialisiere den ChatGPT-Assistenten direkt, um sicherzustellen,
|
// Prüfe, ob ChatGPTAssistant bereits existiert
|
||||||
// dass er auf jeder Seite verfügbar ist, selbst wenn MindMap nicht geladen ist
|
if (typeof ChatGPTAssistant === 'undefined') {
|
||||||
import ChatGPTAssistant from "{{ url_for('static', filename='js/modules/chatgpt-assistant.js') }}";
|
class ChatGPTAssistant {
|
||||||
|
constructor() {
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
this.chatContainer = null;
|
||||||
// Prüfen, ob der Assistent bereits durch MindMap initialisiert wurde
|
this.messages = [];
|
||||||
if (!window.MindMap || !window.MindMap.assistant) {
|
this.isOpen = false;
|
||||||
console.log('KI-Assistent wird direkt initialisiert...');
|
}
|
||||||
const assistant = new ChatGPTAssistant();
|
|
||||||
assistant.init();
|
init() {
|
||||||
|
// Chat-Container erstellen, falls noch nicht vorhanden
|
||||||
// Speichere in window.MindMap, falls es existiert, oder erstelle es
|
if (!document.getElementById('chat-assistant-container')) {
|
||||||
if (!window.MindMap) {
|
this.createChatInterface();
|
||||||
window.MindMap = {};
|
}
|
||||||
|
|
||||||
|
// Event-Listener für Chat-Button
|
||||||
|
const chatButton = document.getElementById('chat-assistant-button');
|
||||||
|
if (chatButton) {
|
||||||
|
chatButton.addEventListener('click', () => this.toggleChat());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event-Listener für Senden-Button
|
||||||
|
const sendButton = document.getElementById('chat-send-button');
|
||||||
|
if (sendButton) {
|
||||||
|
sendButton.addEventListener('click', () => this.sendMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event-Listener für Eingabefeld (Enter-Taste)
|
||||||
|
const inputField = document.getElementById('chat-input');
|
||||||
|
if (inputField) {
|
||||||
|
inputField.addEventListener('keypress', (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
this.sendMessage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('KI-Assistent erfolgreich initialisiert');
|
||||||
|
}
|
||||||
|
|
||||||
|
createChatInterface() {
|
||||||
|
// Chat-Button erstellen
|
||||||
|
const chatButton = document.createElement('button');
|
||||||
|
chatButton.id = 'chat-assistant-button';
|
||||||
|
chatButton.className = 'fixed bottom-6 right-6 bg-primary-600 text-white rounded-full p-4 shadow-lg z-50 hover:bg-primary-700 transition-all';
|
||||||
|
chatButton.innerHTML = '<i class="fas fa-robot text-xl"></i>';
|
||||||
|
document.body.appendChild(chatButton);
|
||||||
|
|
||||||
|
// Chat-Container erstellen
|
||||||
|
const chatContainer = document.createElement('div');
|
||||||
|
chatContainer.id = 'chat-assistant-container';
|
||||||
|
chatContainer.className = 'fixed bottom-24 right-6 w-80 md:w-96 bg-white dark:bg-gray-800 rounded-xl shadow-xl z-50 flex flex-col transition-all duration-300 transform scale-0 origin-bottom-right';
|
||||||
|
chatContainer.style.height = '500px';
|
||||||
|
chatContainer.style.maxHeight = '70vh';
|
||||||
|
|
||||||
|
// Chat-Header
|
||||||
|
chatContainer.innerHTML = `
|
||||||
|
<div class="p-4 border-b dark:border-gray-700 flex justify-between items-center">
|
||||||
|
<h3 class="font-bold text-gray-800 dark:text-white">Systades Assistent</h3>
|
||||||
|
<button id="chat-close-button" class="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="chat-messages" class="flex-1 overflow-y-auto p-4 space-y-4"></div>
|
||||||
|
<div class="p-4 border-t dark:border-gray-700">
|
||||||
|
<div class="flex space-x-2">
|
||||||
|
<input id="chat-input" type="text" placeholder="Frage stellen..." class="flex-1 px-4 py-2 rounded-lg border dark:border-gray-700 dark:bg-gray-700 dark:text-white focus:outline-none focus:ring-2 focus:ring-primary-500">
|
||||||
|
<button id="chat-send-button" class="bg-primary-600 text-white px-4 py-2 rounded-lg hover:bg-primary-700 transition-all">
|
||||||
|
<i class="fas fa-paper-plane"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(chatContainer);
|
||||||
|
this.chatContainer = chatContainer;
|
||||||
|
|
||||||
|
// Event-Listener für Schließen-Button
|
||||||
|
const closeButton = document.getElementById('chat-close-button');
|
||||||
|
if (closeButton) {
|
||||||
|
closeButton.addEventListener('click', () => this.toggleChat());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleChat() {
|
||||||
|
this.isOpen = !this.isOpen;
|
||||||
|
if (this.isOpen) {
|
||||||
|
this.chatContainer.classList.remove('scale-0');
|
||||||
|
this.chatContainer.classList.add('scale-100');
|
||||||
|
} else {
|
||||||
|
this.chatContainer.classList.remove('scale-100');
|
||||||
|
this.chatContainer.classList.add('scale-0');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendMessage() {
|
||||||
|
const inputField = document.getElementById('chat-input');
|
||||||
|
const messageText = inputField.value.trim();
|
||||||
|
|
||||||
|
if (!messageText) return;
|
||||||
|
|
||||||
|
// Benutzer-Nachricht anzeigen
|
||||||
|
this.addMessage('user', messageText);
|
||||||
|
inputField.value = '';
|
||||||
|
|
||||||
|
// Lade-Indikator anzeigen
|
||||||
|
this.addMessage('assistant', '...', 'loading-message');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// API-Anfrage senden
|
||||||
|
const response = await fetch('/api/assistant', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
messages: this.messages.map(msg => ({
|
||||||
|
role: msg.role,
|
||||||
|
content: msg.content
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Lade-Nachricht entfernen
|
||||||
|
const loadingMessage = document.getElementById('loading-message');
|
||||||
|
if (loadingMessage) {
|
||||||
|
loadingMessage.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
this.addMessage('assistant', 'Entschuldigung, es ist ein Fehler aufgetreten: ' + data.error);
|
||||||
|
} else {
|
||||||
|
this.addMessage('assistant', data.response);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler bei der API-Anfrage:', error);
|
||||||
|
|
||||||
|
// Lade-Nachricht entfernen
|
||||||
|
const loadingMessage = document.getElementById('loading-message');
|
||||||
|
if (loadingMessage) {
|
||||||
|
loadingMessage.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.addMessage('assistant', 'Entschuldigung, es ist ein Fehler aufgetreten. Bitte versuche es später erneut.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addMessage(role, content, id = null) {
|
||||||
|
const messagesContainer = document.getElementById('chat-messages');
|
||||||
|
|
||||||
|
// Nachricht zum Array hinzufügen (außer Lade-Nachrichten)
|
||||||
|
if (id !== 'loading-message') {
|
||||||
|
this.messages.push({ role, content });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nachricht zum DOM hinzufügen
|
||||||
|
const messageElement = document.createElement('div');
|
||||||
|
messageElement.className = `p-3 rounded-lg ${role === 'user' ? 'bg-primary-100 dark:bg-primary-900/30 ml-6' : 'bg-gray-100 dark:bg-gray-700 mr-6'}`;
|
||||||
|
if (id) {
|
||||||
|
messageElement.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
messageElement.innerHTML = `
|
||||||
|
<div class="flex items-start">
|
||||||
|
<div class="w-8 h-8 rounded-full flex items-center justify-center ${role === 'user' ? 'bg-primary-600' : 'bg-gray-600'} text-white mr-2">
|
||||||
|
<i class="fas ${role === 'user' ? 'fa-user' : 'fa-robot'} text-xs"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 text-sm ${role === 'user' ? 'text-gray-800 dark:text-gray-200' : 'text-gray-700 dark:text-gray-300'}">
|
||||||
|
${content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
messagesContainer.appendChild(messageElement);
|
||||||
|
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||||
}
|
}
|
||||||
window.MindMap.assistant = assistant;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialisiere den ChatGPT-Assistenten direkt
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Prüfen, ob der Assistent bereits durch MindMap initialisiert wurde
|
||||||
|
if (!window.MindMap || !window.MindMap.assistant) {
|
||||||
|
console.log('KI-Assistent wird direkt initialisiert...');
|
||||||
|
const assistant = new ChatGPTAssistant();
|
||||||
|
assistant.init();
|
||||||
|
|
||||||
|
// Speichere in window.MindMap, falls es existiert, oder erstelle es
|
||||||
|
if (!window.MindMap) {
|
||||||
|
window.MindMap = {};
|
||||||
|
}
|
||||||
|
window.MindMap.assistant = assistant;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Dark/Light-Mode vereinheitlicht -->
|
||||||
|
<script>
|
||||||
|
// Globaler Zugriff für externe Skripte
|
||||||
|
window.MindMap = window.MindMap || {};
|
||||||
|
|
||||||
|
// Funktion zum Anwenden des Dark Mode, strikt getrennt
|
||||||
|
function applyDarkModeClasses(isDarkMode) {
|
||||||
|
if (isDarkMode) {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
document.body.classList.add('dark');
|
||||||
|
localStorage.setItem('colorMode', 'dark');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('dark');
|
||||||
|
document.body.classList.remove('dark');
|
||||||
|
localStorage.setItem('colorMode', 'light');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alpine.js darkMode-Variable aktualisieren, falls zutreffend
|
||||||
|
const appEl = document.querySelector('body');
|
||||||
|
if (appEl && appEl.__x) {
|
||||||
|
appEl.__x.$data.darkMode = isDarkMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event für andere Komponenten auslösen
|
||||||
|
document.dispatchEvent(new CustomEvent('darkModeToggled', {
|
||||||
|
detail: { isDark: isDarkMode }
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
window.MindMap.toggleDarkMode = function() {
|
||||||
|
const isDark = document.documentElement.classList.contains('dark');
|
||||||
|
const newIsDark = !isDark;
|
||||||
|
|
||||||
|
// DOM aktualisieren
|
||||||
|
applyDarkModeClasses(newIsDark);
|
||||||
|
|
||||||
|
// Server aktualisieren
|
||||||
|
fetch('/api/set_dark_mode', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ darkMode: newIsDark })
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialisierung beim Laden
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Reihenfolge der Prüfungen: Serverseitige Einstellung > Lokale Einstellung > Browser-Präferenz
|
||||||
|
|
||||||
|
// 1. Zuerst lokale Einstellung prüfen
|
||||||
|
const storedMode = localStorage.getItem('colorMode');
|
||||||
|
if (storedMode) {
|
||||||
|
applyDarkModeClasses(storedMode === 'dark');
|
||||||
|
} else {
|
||||||
|
// 2. Fallback auf Browser-Präferenz
|
||||||
|
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
applyDarkModeClasses(prefersDark);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Serverseitige Einstellung abrufen und anwenden
|
||||||
|
fetch('/api/get_dark_mode')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
const serverDarkMode = data.darkMode === true || data.darkMode === 'true';
|
||||||
|
applyDarkModeClasses(serverDarkMode);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => console.error('Fehler beim Abrufen des Dark Mode Status:', error));
|
||||||
|
|
||||||
|
// Listener für Änderungen der Browser-Präferenz
|
||||||
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
|
||||||
|
if (localStorage.getItem('colorMode') === null) {
|
||||||
|
applyDarkModeClasses(e.matches);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
192
templates/community/category.html
Normal file
192
templates/community/category.html
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}{{ category.title }} - Forum{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<style>
|
||||||
|
.thread-item {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
.thread-item:hover {
|
||||||
|
transform: translateX(2px);
|
||||||
|
}
|
||||||
|
.thread-pinned {
|
||||||
|
border-left-width: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mx-auto px-4 py-8">
|
||||||
|
<!-- Breadcrumb Navigation -->
|
||||||
|
<div class="mb-6 flex items-center text-sm">
|
||||||
|
<a href="{{ url_for('community') }}" class="opacity-75 hover:opacity-100 transition-opacity">
|
||||||
|
<i class="fas fa-home mr-1"></i> Forum
|
||||||
|
</a>
|
||||||
|
<span class="mx-2 opacity-50">/</span>
|
||||||
|
<span class="font-medium">{{ category.title }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Kategorie-Header -->
|
||||||
|
<div class="mb-8 flex flex-wrap items-center justify-between gap-4">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<!-- Kategorie-Icon -->
|
||||||
|
<div class="w-12 h-12 rounded-xl mr-4 flex items-center justify-center text-white"
|
||||||
|
style="background-color: {{ node.color_code or '#6d28d9' }}">
|
||||||
|
<i class="fas {{ node.icon or 'fa-folder' }} text-2xl"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Kategorie-Info -->
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-bold">{{ category.title }}</h1>
|
||||||
|
<p class="opacity-75">{{ category.description }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Neues Thema erstellen -->
|
||||||
|
<a href="{{ url_for('new_post', category_id=category.id) }}"
|
||||||
|
class="px-5 py-2.5 rounded-lg transition-all duration-300 flex items-center"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-indigo-700 hover:bg-indigo-600 text-white'
|
||||||
|
: 'bg-indigo-500 hover:bg-indigo-600 text-white'">
|
||||||
|
<i class="fas fa-plus-circle mr-2"></i>
|
||||||
|
Neues Thema
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Threads anzeigen -->
|
||||||
|
<div class="mb-8 rounded-xl overflow-hidden"
|
||||||
|
x-bind:class="darkMode ? 'bg-gray-800/60 border border-white/10' : 'bg-white border border-gray-200'">
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="p-4 border-b" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
|
||||||
|
<div class="grid grid-cols-12 gap-4">
|
||||||
|
<div class="col-span-7 font-medium">Thema</div>
|
||||||
|
<div class="col-span-1 text-center font-medium hidden md:block">Antworten</div>
|
||||||
|
<div class="col-span-2 text-center font-medium hidden md:block">Autor</div>
|
||||||
|
<div class="col-span-2 text-center font-medium hidden md:block">Letzte Antwort</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Thread-Liste -->
|
||||||
|
{% if threads_data %}
|
||||||
|
{% for thread_data in threads_data %}
|
||||||
|
{% set thread = thread_data.thread %}
|
||||||
|
<div class="thread-item p-4 border-b last:border-b-0 {{ 'thread-pinned' if thread.is_pinned }}"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'border-white/10 hover:bg-gray-700/50 {{ 'border-l-yellow-500' if thread.is_pinned }}'
|
||||||
|
: 'border-gray-200 hover:bg-gray-50 {{ 'border-l-yellow-500' if thread.is_pinned }}'">
|
||||||
|
<a href="{{ url_for('forum_post', post_id=thread.id) }}" class="block">
|
||||||
|
<div class="grid grid-cols-12 gap-4">
|
||||||
|
<!-- Thema -->
|
||||||
|
<div class="col-span-12 md:col-span-7">
|
||||||
|
<div class="flex items-start">
|
||||||
|
<!-- Status-Icons -->
|
||||||
|
<div class="flex flex-col items-center mr-3 pt-1">
|
||||||
|
{% if thread.is_pinned %}
|
||||||
|
<i class="fas fa-thumbtack text-yellow-500" title="Angepinnt"></i>
|
||||||
|
{% endif %}
|
||||||
|
{% if thread.is_locked %}
|
||||||
|
<i class="fas fa-lock text-red-500 mt-1" title="Gesperrt"></i>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Themen-Info -->
|
||||||
|
<div>
|
||||||
|
<h3 class="font-medium leading-snug mb-1 {% if thread.is_locked %}opacity-70{% endif %}">
|
||||||
|
{{ thread.title }}
|
||||||
|
</h3>
|
||||||
|
<div class="flex items-center text-xs opacity-70 mt-1">
|
||||||
|
<span><i class="fas fa-eye mr-1"></i> {{ thread.view_count }}</span>
|
||||||
|
<span class="mx-2 block md:hidden">•</span>
|
||||||
|
<span class="block md:hidden"><i class="fas fa-reply mr-1"></i> {{ thread_data.reply_count }}</span>
|
||||||
|
<span class="mx-2">•</span>
|
||||||
|
<span><i class="fas fa-clock mr-1"></i> {{ thread.created_at.strftime('%d.%m.%Y, %H:%M') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Antworten -->
|
||||||
|
<div class="col-span-1 text-center hidden md:flex items-center justify-center">
|
||||||
|
<span class="px-2.5 py-1 rounded-full text-sm font-medium"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-indigo-900/40 text-indigo-300'
|
||||||
|
: 'bg-indigo-100 text-indigo-800'">
|
||||||
|
{{ thread_data.reply_count }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Autor -->
|
||||||
|
<div class="col-span-2 text-center hidden md:flex items-center justify-center">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="w-7 h-7 rounded-full flex items-center justify-center text-white text-xs font-medium overflow-hidden mr-2"
|
||||||
|
style="background: linear-gradient(135deg, #8b5cf6, #6366f1);">
|
||||||
|
{% if thread.author.avatar %}
|
||||||
|
<img src="{{ thread.author.avatar }}" alt="{{ thread.author.username }}" class="w-full h-full object-cover">
|
||||||
|
{% else %}
|
||||||
|
{{ thread.author.username[0].upper() }}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<span class="text-sm truncate max-w-[80px]">{{ thread.author.username }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Letzte Antwort -->
|
||||||
|
<div class="col-span-2 text-center hidden md:block text-sm">
|
||||||
|
{% if thread_data.latest_reply %}
|
||||||
|
<div>{{ thread_data.latest_reply.created_at.strftime('%d.%m.%Y') }}</div>
|
||||||
|
<div class="opacity-75 text-xs">{{ thread_data.latest_reply.created_at.strftime('%H:%M') }} Uhr</div>
|
||||||
|
{% else %}
|
||||||
|
<span class="opacity-60">Keine Antworten</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<div class="p-8 text-center">
|
||||||
|
<div class="text-3xl mb-3 opacity-30"><i class="fas fa-comments"></i></div>
|
||||||
|
<h3 class="text-xl font-semibold mb-2">Keine Themen vorhanden</h3>
|
||||||
|
<p class="opacity-75 mb-4">In dieser Kategorie wurden noch keine Themen erstellt.</p>
|
||||||
|
<a href="{{ url_for('new_post', category_id=category.id) }}"
|
||||||
|
class="inline-block px-5 py-2.5 rounded-lg transition-all duration-300"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-indigo-700 hover:bg-indigo-600 text-white'
|
||||||
|
: 'bg-indigo-500 hover:bg-indigo-600 text-white'">
|
||||||
|
<i class="fas fa-plus-circle mr-2"></i>
|
||||||
|
Erstes Thema erstellen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Link zur Mindmap -->
|
||||||
|
<div class="rounded-xl p-5 mb-4 flex items-center"
|
||||||
|
x-bind:class="darkMode ? 'bg-purple-900/20 border border-purple-800/30' : 'bg-purple-50 border border-purple-100'">
|
||||||
|
<div class="text-3xl mr-4 opacity-80">
|
||||||
|
<i class="fas fa-diagram-project" style="color: {{ node.color_code }}"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-medium mb-1">Mindmap-Knotenpunkt: {{ node.name }}</h3>
|
||||||
|
<p class="text-sm opacity-75">In der Mindmap findest du weitere Informationen zu diesem Themenbereich.</p>
|
||||||
|
</div>
|
||||||
|
<div class="ml-auto">
|
||||||
|
<a href="{{ url_for('mindmap') }}"
|
||||||
|
class="px-4 py-2 rounded-lg inline-block text-sm transition-all"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-purple-800/60 hover:bg-purple-700/60 text-white'
|
||||||
|
: 'bg-white hover:bg-purple-100 text-purple-800 border border-purple-200'">
|
||||||
|
Zur Mindmap
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
// Hier können bei Bedarf kategoriespezifische Scripts eingefügt werden
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
344
templates/community/edit_post.html
Normal file
344
templates/community/edit_post.html
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Beitrag bearbeiten{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<style>
|
||||||
|
.markdown-preview {
|
||||||
|
min-height: 200px;
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.markdown-preview p {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.markdown-preview h1, .markdown-preview h2, .markdown-preview h3,
|
||||||
|
.markdown-preview h4, .markdown-preview h5, .markdown-preview h6 {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.markdown-preview h1 { font-size: 1.8rem; }
|
||||||
|
.markdown-preview h2 { font-size: 1.5rem; }
|
||||||
|
.markdown-preview h3 { font-size: 1.3rem; }
|
||||||
|
.markdown-preview h4 { font-size: 1.1rem; }
|
||||||
|
.markdown-preview ul, .markdown-preview ol {
|
||||||
|
margin-left: 1.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.markdown-preview ul { list-style-type: disc; }
|
||||||
|
.markdown-preview ol { list-style-type: decimal; }
|
||||||
|
.markdown-preview pre {
|
||||||
|
background-color: rgba(0,0,0,0.05);
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
.markdown-preview code {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 0.9em;
|
||||||
|
padding: 0.1em 0.3em;
|
||||||
|
border-radius: 0.3em;
|
||||||
|
background-color: rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
.markdown-preview pre code {
|
||||||
|
padding: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
.markdown-preview blockquote {
|
||||||
|
border-left: 4px solid;
|
||||||
|
padding-left: 1rem;
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
.dark .markdown-preview code {
|
||||||
|
background-color: rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
.dark .markdown-preview blockquote {
|
||||||
|
border-color: rgba(255,255,255,0.2);
|
||||||
|
}
|
||||||
|
.node-mention {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
background-color: rgba(109, 40, 217, 0.1);
|
||||||
|
color: #6d28d9;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 1px 6px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
margin: 0 2px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.dark .node-mention {
|
||||||
|
background-color: rgba(167, 139, 250, 0.2);
|
||||||
|
color: #a78bfa;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mx-auto px-4 py-8">
|
||||||
|
<!-- Breadcrumb Navigation -->
|
||||||
|
<div class="mb-6 flex items-center text-sm">
|
||||||
|
<a href="{{ url_for('community') }}" class="opacity-75 hover:opacity-100 transition-opacity">
|
||||||
|
<i class="fas fa-home mr-1"></i> Forum
|
||||||
|
</a>
|
||||||
|
<span class="mx-2 opacity-50">/</span>
|
||||||
|
<a href="{{ url_for('forum_category', category_id=post.category_id) }}" class="opacity-75 hover:opacity-100 transition-opacity">
|
||||||
|
{{ post.category.title }}
|
||||||
|
</a>
|
||||||
|
<span class="mx-2 opacity-50">/</span>
|
||||||
|
{% if post.parent_id %}
|
||||||
|
<a href="{{ url_for('forum_post', post_id=post.parent_id) }}" class="opacity-75 hover:opacity-100 transition-opacity truncate max-w-[200px]">
|
||||||
|
{{ post.parent.title }}
|
||||||
|
</a>
|
||||||
|
<span class="mx-2 opacity-50">/</span>
|
||||||
|
{% endif %}
|
||||||
|
<span class="font-medium">Beitrag bearbeiten</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Formular-Header -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<h1 class="text-2xl font-bold mb-2">Beitrag bearbeiten</h1>
|
||||||
|
<p class="opacity-75">
|
||||||
|
{% if post.parent_id %}
|
||||||
|
Antwort auf <span class="font-medium">{{ post.parent.title }}</span>
|
||||||
|
{% else %}
|
||||||
|
in der Kategorie <span class="font-medium">{{ post.category.title }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Formular -->
|
||||||
|
<div class="mb-8 rounded-xl overflow-hidden"
|
||||||
|
x-bind:class="darkMode ? 'bg-gray-800/60 border border-white/10' : 'bg-white border border-gray-200'">
|
||||||
|
<div class="p-4 border-b font-medium" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
|
||||||
|
<i class="fas fa-edit mr-2"></i>
|
||||||
|
Beitrag bearbeiten
|
||||||
|
</div>
|
||||||
|
<div class="p-6">
|
||||||
|
<form action="{{ url_for('edit_post', post_id=post.id) }}" method="POST" x-data="{
|
||||||
|
title: '{{ post.title|safe }}',
|
||||||
|
content: '{{ post.content|replace('\n', '\\n')|replace('\'', '\\\'')|safe }}',
|
||||||
|
showPreview: false,
|
||||||
|
previewHtml: '',
|
||||||
|
|
||||||
|
updatePreview() {
|
||||||
|
// Verarbeite den Inhalt
|
||||||
|
if (this.content.trim() === '') {
|
||||||
|
this.previewHtml = '<div class=\'opacity-50 italic\'>Die Vorschau wird hier angezeigt...</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verarbeite Markdown
|
||||||
|
let html = marked.parse(this.content);
|
||||||
|
|
||||||
|
// Ersetze @Knotenname mit entsprechenden Links
|
||||||
|
html = html.replace(/@([a-zA-Z0-9äöüÄÖÜß_-]+)/g, '<span class=\'node-mention\'><i class=\'fas fa-diagram-project fa-xs mr-1\'></i>$1</span>');
|
||||||
|
|
||||||
|
this.previewHtml = html;
|
||||||
|
}
|
||||||
|
}">
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="title" class="block mb-2 font-medium">Titel</label>
|
||||||
|
<div class="rounded-lg overflow-hidden"
|
||||||
|
x-bind:class="darkMode ? 'border border-white/20' : 'border border-gray-300'">
|
||||||
|
<input type="text" id="title" name="title"
|
||||||
|
class="w-full px-4 py-3"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-gray-700/50 text-white placeholder-gray-400 focus:border-indigo-500'
|
||||||
|
: 'bg-white text-gray-700 placeholder-gray-400 focus:border-indigo-500'"
|
||||||
|
x-model="title"
|
||||||
|
required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<div class="flex justify-between items-center mb-2">
|
||||||
|
<label for="content" class="font-medium">Inhalt</label>
|
||||||
|
<div class="flex space-x-2">
|
||||||
|
<button type="button"
|
||||||
|
class="px-3 py-1 rounded text-sm flex items-center"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-gray-700 hover:bg-gray-600 text-white'
|
||||||
|
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'"
|
||||||
|
@click="showPreview = false"
|
||||||
|
x-bind:disabled="!showPreview"
|
||||||
|
x-bind:class="{'opacity-50': !showPreview}">
|
||||||
|
<i class="fas fa-edit mr-1"></i> Bearbeiten
|
||||||
|
</button>
|
||||||
|
<button type="button"
|
||||||
|
class="px-3 py-1 rounded text-sm flex items-center"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-gray-700 hover:bg-gray-600 text-white'
|
||||||
|
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'"
|
||||||
|
@click="updatePreview(); showPreview = true"
|
||||||
|
x-bind:disabled="showPreview"
|
||||||
|
x-bind:class="{'opacity-50': showPreview}">
|
||||||
|
<i class="fas fa-eye mr-1"></i> Vorschau
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Editor -->
|
||||||
|
<div class="rounded-lg overflow-hidden mb-2"
|
||||||
|
x-bind:class="darkMode ? 'border border-white/20' : 'border border-gray-300'"
|
||||||
|
x-show="!showPreview">
|
||||||
|
<textarea id="content" name="content" rows="12"
|
||||||
|
class="w-full p-3 resize-y"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-gray-700/50 text-white placeholder-gray-400 focus:border-indigo-500'
|
||||||
|
: 'bg-white text-gray-700 placeholder-gray-400 focus:border-indigo-500'"
|
||||||
|
x-model="content"
|
||||||
|
required></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Preview -->
|
||||||
|
<div class="rounded-lg overflow-hidden mb-2 p-4 markdown-preview"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'border border-white/20 bg-gray-700/30'
|
||||||
|
: 'border border-gray-300 bg-gray-50'"
|
||||||
|
x-show="showPreview"
|
||||||
|
x-html="previewHtml">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Markdown-Hilfsmittel -->
|
||||||
|
<div class="mb-4" x-show="!showPreview">
|
||||||
|
<div class="text-xs opacity-70">
|
||||||
|
<p>Du kannst Knotenpunkte der Mindmap durch <code>@Knotenname</code> verlinken.</p>
|
||||||
|
<p>Dieser Editor unterstützt Markdown-Formatierung:</p>
|
||||||
|
<div class="flex flex-wrap gap-2 mt-1">
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="**" data-before="" data-after="" title="Fett">
|
||||||
|
<i class="fas fa-bold"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="*" data-before="" data-after="" title="Kursiv">
|
||||||
|
<i class="fas fa-italic"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="`" data-before="" data-after="" title="Code">
|
||||||
|
<i class="fas fa-code"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="[Link-Text](URL)" data-before="" data-after="" title="Link">
|
||||||
|
<i class="fas fa-link"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="\n```\nCode-Block\n```" data-before="" data-after="" title="Code-Block">
|
||||||
|
<i class="fas fa-file-code"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format=">" data-before="" data-after="" title="Zitat">
|
||||||
|
<i class="fas fa-quote-right"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="- " data-before="" data-after="" title="Liste">
|
||||||
|
<i class="fas fa-list-ul"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="1. " data-before="" data-after="" title="Nummerierte Liste">
|
||||||
|
<i class="fas fa-list-ol"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="# " data-before="" data-after="" title="Überschrift">
|
||||||
|
<i class="fas fa-heading"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<a href="{{ url_for('forum_post', post_id=post.parent_id or post.id) }}"
|
||||||
|
class="px-5 py-2.5 rounded-lg transition-all duration-300 flex items-center"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-gray-700 hover:bg-gray-600 text-white'
|
||||||
|
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'">
|
||||||
|
Abbrechen
|
||||||
|
</a>
|
||||||
|
<button type="submit"
|
||||||
|
class="px-5 py-2.5 rounded-lg transition-all duration-300 flex items-center"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-indigo-700 hover:bg-indigo-600 text-white'
|
||||||
|
: 'bg-indigo-500 hover:bg-indigo-600 text-white'">
|
||||||
|
<i class="fas fa-save mr-2"></i>
|
||||||
|
Änderungen speichern
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Markdown-Buttons für den Beitragseditor
|
||||||
|
document.querySelectorAll('.markdown-button').forEach(button => {
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
const textarea = document.getElementById('content');
|
||||||
|
const format = this.dataset.format;
|
||||||
|
const before = this.dataset.before || '';
|
||||||
|
const after = this.dataset.after || '';
|
||||||
|
|
||||||
|
// Hole die aktuelle Auswahl
|
||||||
|
const start = textarea.selectionStart;
|
||||||
|
const end = textarea.selectionEnd;
|
||||||
|
const selection = textarea.value.substring(start, end);
|
||||||
|
|
||||||
|
// Wende die Formatierung an
|
||||||
|
let formattedText;
|
||||||
|
if (format.includes('\n')) {
|
||||||
|
// Für Formate mit Zeilenumbrüchen (z.B. Code-Blöcke)
|
||||||
|
formattedText = format.replace('Code-Block', selection || 'Code-Block');
|
||||||
|
} else if (format.includes('[Link-Text](URL)')) {
|
||||||
|
formattedText = format.replace('Link-Text', selection || 'Link-Text');
|
||||||
|
} else if (format === '- ' || format === '1. ' || format === '# ' || format === '> ') {
|
||||||
|
// Für Listen und Überschriften: am Anfang der Zeile einfügen
|
||||||
|
const beforeSelection = textarea.value.substring(0, start);
|
||||||
|
const afterSelection = textarea.value.substring(end);
|
||||||
|
|
||||||
|
// Finde den Anfang der aktuellen Zeile
|
||||||
|
const lastNewline = beforeSelection.lastIndexOf('\n');
|
||||||
|
const lineStart = lastNewline === -1 ? 0 : lastNewline + 1;
|
||||||
|
|
||||||
|
// Füge das Format am Zeilenanfang ein
|
||||||
|
formattedText = beforeSelection.substring(0, lineStart) +
|
||||||
|
format +
|
||||||
|
beforeSelection.substring(lineStart) +
|
||||||
|
selection +
|
||||||
|
afterSelection;
|
||||||
|
|
||||||
|
// Setze die neue Cursor-Position
|
||||||
|
const newCursorPos = end + format.length;
|
||||||
|
textarea.value = formattedText;
|
||||||
|
textarea.setSelectionRange(newCursorPos, newCursorPos);
|
||||||
|
|
||||||
|
// Alpine.js Model aktualisieren
|
||||||
|
textarea.dispatchEvent(new Event('input'));
|
||||||
|
return; // Früher zurückkehren, da wir die Formatierung bereits angewendet haben
|
||||||
|
} else {
|
||||||
|
// Für einfache Formatierungen wie fett, kursiv, Code
|
||||||
|
formattedText = before + format + selection + format + after;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ersetze den Text
|
||||||
|
textarea.value = textarea.value.substring(0, start) + formattedText + textarea.value.substring(end);
|
||||||
|
|
||||||
|
// Setze den Fokus zurück auf das Textarea
|
||||||
|
textarea.focus();
|
||||||
|
|
||||||
|
// Alpine.js Model aktualisieren
|
||||||
|
textarea.dispatchEvent(new Event('input'));
|
||||||
|
|
||||||
|
// Setze die Auswahl neu, wenn es eine Auswahl gab
|
||||||
|
if (selection) {
|
||||||
|
const newStart = start + before.length + format.length;
|
||||||
|
const newEnd = newStart + selection.length;
|
||||||
|
textarea.setSelectionRange(newStart, newEnd);
|
||||||
|
} else {
|
||||||
|
// Setze den Cursor in die Mitte von **|** oder `|`
|
||||||
|
const newCursorPos = start + before.length + format.length;
|
||||||
|
textarea.setSelectionRange(newCursorPos, newCursorPos);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
125
templates/community/index.html
Normal file
125
templates/community/index.html
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Community Forum{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<style>
|
||||||
|
.forum-category {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
.forum-category:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
.category-icon {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
width: 2.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mx-auto px-4 py-8">
|
||||||
|
<!-- Seitenüberschrift -->
|
||||||
|
<div class="mb-8 text-center">
|
||||||
|
<h1 class="text-3xl font-bold mb-2 gradient-text">Community Forum</h1>
|
||||||
|
<p class="text-lg opacity-75">Diskutiere mit anderen Nutzern über die Hauptthemenbereiche der Mindmap</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Forumskategorien -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
||||||
|
{% if categories_data %}
|
||||||
|
{% for cat_data in categories_data %}
|
||||||
|
<a href="{{ url_for('forum_category', category_id=cat_data.category.id) }}" class="forum-category block">
|
||||||
|
<div class="rounded-xl p-5 h-full"
|
||||||
|
x-bind:class="darkMode ? 'bg-gray-800/60 hover:bg-gray-800/80 border border-white/10' : 'bg-white hover:bg-gray-50 border border-gray-200 shadow-md'">
|
||||||
|
<div class="flex items-start">
|
||||||
|
<!-- Kategorie-Icon -->
|
||||||
|
<div class="category-icon mr-4 text-white"
|
||||||
|
style="background-color: {{ cat_data.category.node.color_code or '#6d28d9' }}">
|
||||||
|
<i class="fas {{ cat_data.category.node.icon or 'fa-folder' }}"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Kategorie-Info -->
|
||||||
|
<div class="flex-grow">
|
||||||
|
<h3 class="text-xl font-semibold mb-2">{{ cat_data.category.title }}</h3>
|
||||||
|
<p class="opacity-75 text-sm mb-3">{{ cat_data.category.description }}</p>
|
||||||
|
|
||||||
|
<!-- Statistik -->
|
||||||
|
<div class="flex flex-wrap gap-4 text-sm opacity-80">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fas fa-comment-alt mr-2"></i>
|
||||||
|
<span>{{ cat_data.total_posts }} Themen</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fas fa-reply mr-2"></i>
|
||||||
|
<span>{{ cat_data.total_replies }} Antworten</span>
|
||||||
|
</div>
|
||||||
|
{% if cat_data.latest_post %}
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fas fa-clock mr-2"></i>
|
||||||
|
<span>Neuster Beitrag: {{ cat_data.latest_post.created_at.strftime('%d.%m.%Y, %H:%M') }}</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pfeil-Icon -->
|
||||||
|
<div class="ml-2">
|
||||||
|
<i class="fas fa-chevron-right opacity-50"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<div class="col-span-2 text-center py-8">
|
||||||
|
<div class="text-3xl mb-3 opacity-30"><i class="fas fa-exclamation-circle"></i></div>
|
||||||
|
<h3 class="text-xl font-semibold mb-2">Keine Forum-Kategorien gefunden</h3>
|
||||||
|
<p class="opacity-75">Es sind derzeit keine Kategorien für Diskussionen verfügbar.</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Hinweis zur Nutzung -->
|
||||||
|
<div class="rounded-xl p-6 text-center mb-8"
|
||||||
|
x-bind:class="darkMode ? 'bg-indigo-900/30 border border-indigo-700/30' : 'bg-indigo-50 border border-indigo-100'">
|
||||||
|
<h3 class="text-xl font-semibold mb-3">
|
||||||
|
<i class="fas fa-lightbulb mr-2 text-yellow-500"></i>
|
||||||
|
So funktioniert das Forum
|
||||||
|
</h3>
|
||||||
|
<p class="mb-4">Das Community-Forum ist nach den Hauptknotenpunkten der Systades-Mindmap strukturiert.
|
||||||
|
In deinen Beiträgen kannst du Knotenpunkte mit <code>@Knotenname</code> verlinken.</p>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mt-6">
|
||||||
|
<div class="p-4 rounded-lg"
|
||||||
|
x-bind:class="darkMode ? 'bg-indigo-800/40' : 'bg-white border border-indigo-100'">
|
||||||
|
<div class="text-2xl mb-2"><i class="fas fa-users text-indigo-400"></i></div>
|
||||||
|
<h4 class="font-medium mb-1">Fachliche Diskussionen</h4>
|
||||||
|
<p class="text-sm opacity-75">Tausche dich mit anderen zu spezifischen Themen aus</p>
|
||||||
|
</div>
|
||||||
|
<div class="p-4 rounded-lg"
|
||||||
|
x-bind:class="darkMode ? 'bg-indigo-800/40' : 'bg-white border border-indigo-100'">
|
||||||
|
<div class="text-2xl mb-2"><i class="fas fa-link text-indigo-400"></i></div>
|
||||||
|
<h4 class="font-medium mb-1">Wissensvernetzung</h4>
|
||||||
|
<p class="text-sm opacity-75">Verknüpfe Inhalte durch Knotenreferenzen</p>
|
||||||
|
</div>
|
||||||
|
<div class="p-4 rounded-lg"
|
||||||
|
x-bind:class="darkMode ? 'bg-indigo-800/40' : 'bg-white border border-indigo-100'">
|
||||||
|
<div class="text-2xl mb-2"><i class="fas fa-markdown text-indigo-400"></i></div>
|
||||||
|
<h4 class="font-medium mb-1">Markdown Support</h4>
|
||||||
|
<p class="text-sm opacity-75">Formatiere deine Beiträge mit Markdown</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
// Hier können bei Bedarf forumspezifische Scripts eingefügt werden
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
355
templates/community/new_post.html
Normal file
355
templates/community/new_post.html
Normal file
@@ -0,0 +1,355 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Neues Thema - {{ category.title }}{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<style>
|
||||||
|
.markdown-preview {
|
||||||
|
min-height: 200px;
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.markdown-preview p {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.markdown-preview h1, .markdown-preview h2, .markdown-preview h3,
|
||||||
|
.markdown-preview h4, .markdown-preview h5, .markdown-preview h6 {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.markdown-preview h1 { font-size: 1.8rem; }
|
||||||
|
.markdown-preview h2 { font-size: 1.5rem; }
|
||||||
|
.markdown-preview h3 { font-size: 1.3rem; }
|
||||||
|
.markdown-preview h4 { font-size: 1.1rem; }
|
||||||
|
.markdown-preview ul, .markdown-preview ol {
|
||||||
|
margin-left: 1.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.markdown-preview ul { list-style-type: disc; }
|
||||||
|
.markdown-preview ol { list-style-type: decimal; }
|
||||||
|
.markdown-preview pre {
|
||||||
|
background-color: rgba(0,0,0,0.05);
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
.markdown-preview code {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 0.9em;
|
||||||
|
padding: 0.1em 0.3em;
|
||||||
|
border-radius: 0.3em;
|
||||||
|
background-color: rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
.markdown-preview pre code {
|
||||||
|
padding: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
.markdown-preview blockquote {
|
||||||
|
border-left: 4px solid;
|
||||||
|
padding-left: 1rem;
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
.dark .markdown-preview code {
|
||||||
|
background-color: rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
.dark .markdown-preview blockquote {
|
||||||
|
border-color: rgba(255,255,255,0.2);
|
||||||
|
}
|
||||||
|
.node-mention {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
background-color: rgba(109, 40, 217, 0.1);
|
||||||
|
color: #6d28d9;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 1px 6px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
margin: 0 2px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.dark .node-mention {
|
||||||
|
background-color: rgba(167, 139, 250, 0.2);
|
||||||
|
color: #a78bfa;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mx-auto px-4 py-8">
|
||||||
|
<!-- Breadcrumb Navigation -->
|
||||||
|
<div class="mb-6 flex items-center text-sm">
|
||||||
|
<a href="{{ url_for('community') }}" class="opacity-75 hover:opacity-100 transition-opacity">
|
||||||
|
<i class="fas fa-home mr-1"></i> Forum
|
||||||
|
</a>
|
||||||
|
<span class="mx-2 opacity-50">/</span>
|
||||||
|
<a href="{{ url_for('forum_category', category_id=category.id) }}" class="opacity-75 hover:opacity-100 transition-opacity">
|
||||||
|
{{ category.title }}
|
||||||
|
</a>
|
||||||
|
<span class="mx-2 opacity-50">/</span>
|
||||||
|
<span class="font-medium">Neues Thema</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Formular-Header -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<h1 class="text-2xl font-bold mb-2">Neues Thema erstellen</h1>
|
||||||
|
<p class="opacity-75">in der Kategorie <span class="font-medium">{{ category.title }}</span></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Formular -->
|
||||||
|
<div class="mb-8 rounded-xl overflow-hidden"
|
||||||
|
x-bind:class="darkMode ? 'bg-gray-800/60 border border-white/10' : 'bg-white border border-gray-200'">
|
||||||
|
<div class="p-4 border-b font-medium" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
|
||||||
|
<i class="fas fa-plus-circle mr-2"></i>
|
||||||
|
Neues Thema
|
||||||
|
</div>
|
||||||
|
<div class="p-6">
|
||||||
|
<form action="{{ url_for('new_post', category_id=category.id) }}" method="POST" x-data="{
|
||||||
|
title: '',
|
||||||
|
content: '',
|
||||||
|
showPreview: false,
|
||||||
|
previewHtml: '',
|
||||||
|
|
||||||
|
updatePreview() {
|
||||||
|
// Verarbeite den Inhalt
|
||||||
|
if (this.content.trim() === '') {
|
||||||
|
this.previewHtml = '<div class=\'opacity-50 italic\'>Die Vorschau wird hier angezeigt...</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verarbeite Markdown
|
||||||
|
let html = marked.parse(this.content);
|
||||||
|
|
||||||
|
// Ersetze @Knotenname mit entsprechenden Links
|
||||||
|
html = html.replace(/@([a-zA-Z0-9äöüÄÖÜß_-]+)/g, '<span class=\'node-mention\'><i class=\'fas fa-diagram-project fa-xs mr-1\'></i>$1</span>');
|
||||||
|
|
||||||
|
this.previewHtml = html;
|
||||||
|
}
|
||||||
|
}">
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="title" class="block mb-2 font-medium">Titel des Themas</label>
|
||||||
|
<div class="rounded-lg overflow-hidden"
|
||||||
|
x-bind:class="darkMode ? 'border border-white/20' : 'border border-gray-300'">
|
||||||
|
<input type="text" id="title" name="title"
|
||||||
|
class="w-full px-4 py-3"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-gray-700/50 text-white placeholder-gray-400 focus:border-indigo-500'
|
||||||
|
: 'bg-white text-gray-700 placeholder-gray-400 focus:border-indigo-500'"
|
||||||
|
placeholder="Ein prägnanter Titel für dein Thema"
|
||||||
|
x-model="title"
|
||||||
|
required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<div class="flex justify-between items-center mb-2">
|
||||||
|
<label for="content" class="font-medium">Inhalt</label>
|
||||||
|
<div class="flex space-x-2">
|
||||||
|
<button type="button"
|
||||||
|
class="px-3 py-1 rounded text-sm flex items-center"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-gray-700 hover:bg-gray-600 text-white'
|
||||||
|
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'"
|
||||||
|
@click="showPreview = false"
|
||||||
|
x-bind:disabled="!showPreview"
|
||||||
|
x-bind:class="{'opacity-50': !showPreview}">
|
||||||
|
<i class="fas fa-edit mr-1"></i> Bearbeiten
|
||||||
|
</button>
|
||||||
|
<button type="button"
|
||||||
|
class="px-3 py-1 rounded text-sm flex items-center"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-gray-700 hover:bg-gray-600 text-white'
|
||||||
|
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'"
|
||||||
|
@click="updatePreview(); showPreview = true"
|
||||||
|
x-bind:disabled="showPreview"
|
||||||
|
x-bind:class="{'opacity-50': showPreview}">
|
||||||
|
<i class="fas fa-eye mr-1"></i> Vorschau
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Editor -->
|
||||||
|
<div class="rounded-lg overflow-hidden mb-2"
|
||||||
|
x-bind:class="darkMode ? 'border border-white/20' : 'border border-gray-300'"
|
||||||
|
x-show="!showPreview">
|
||||||
|
<textarea id="content" name="content" rows="12"
|
||||||
|
class="w-full p-3 resize-y"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-gray-700/50 text-white placeholder-gray-400 focus:border-indigo-500'
|
||||||
|
: 'bg-white text-gray-700 placeholder-gray-400 focus:border-indigo-500'"
|
||||||
|
placeholder="Schreibe deinen Beitrag hier (unterstützt Markdown und @Knotenname-Erwähnungen)..."
|
||||||
|
x-model="content"
|
||||||
|
required></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Preview -->
|
||||||
|
<div class="rounded-lg overflow-hidden mb-2 p-4 markdown-preview"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'border border-white/20 bg-gray-700/30'
|
||||||
|
: 'border border-gray-300 bg-gray-50'"
|
||||||
|
x-show="showPreview"
|
||||||
|
x-html="previewHtml">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Markdown-Hilfsmittel -->
|
||||||
|
<div class="mb-4" x-show="!showPreview">
|
||||||
|
<div class="text-xs opacity-70">
|
||||||
|
<p>Du kannst Knotenpunkte der Mindmap durch <code>@Knotenname</code> verlinken.</p>
|
||||||
|
<p>Dieser Editor unterstützt Markdown-Formatierung:</p>
|
||||||
|
<div class="flex flex-wrap gap-2 mt-1">
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="**" data-before="" data-after="" title="Fett">
|
||||||
|
<i class="fas fa-bold"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="*" data-before="" data-after="" title="Kursiv">
|
||||||
|
<i class="fas fa-italic"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="`" data-before="" data-after="" title="Code">
|
||||||
|
<i class="fas fa-code"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="[Link-Text](URL)" data-before="" data-after="" title="Link">
|
||||||
|
<i class="fas fa-link"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="\n```\nCode-Block\n```" data-before="" data-after="" title="Code-Block">
|
||||||
|
<i class="fas fa-file-code"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format=">" data-before="" data-after="" title="Zitat">
|
||||||
|
<i class="fas fa-quote-right"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="- " data-before="" data-after="" title="Liste">
|
||||||
|
<i class="fas fa-list-ul"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="1. " data-before="" data-after="" title="Nummerierte Liste">
|
||||||
|
<i class="fas fa-list-ol"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="# " data-before="" data-after="" title="Überschrift">
|
||||||
|
<i class="fas fa-heading"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<a href="{{ url_for('forum_category', category_id=category.id) }}"
|
||||||
|
class="px-5 py-2.5 rounded-lg transition-all duration-300 flex items-center"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-gray-700 hover:bg-gray-600 text-white'
|
||||||
|
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'">
|
||||||
|
Abbrechen
|
||||||
|
</a>
|
||||||
|
<button type="submit"
|
||||||
|
class="px-5 py-2.5 rounded-lg transition-all duration-300 flex items-center"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-indigo-700 hover:bg-indigo-600 text-white'
|
||||||
|
: 'bg-indigo-500 hover:bg-indigo-600 text-white'">
|
||||||
|
<i class="fas fa-paper-plane mr-2"></i>
|
||||||
|
Thema erstellen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Link zur Mindmap -->
|
||||||
|
<div class="rounded-xl p-5 mb-4 flex items-center"
|
||||||
|
x-bind:class="darkMode ? 'bg-purple-900/20 border border-purple-800/30' : 'bg-purple-50 border border-purple-100'">
|
||||||
|
<div class="text-3xl mr-4 opacity-80">
|
||||||
|
<i class="fas fa-diagram-project" style="color: {{ category.node.color_code }}"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-medium mb-1">Mindmap-Knotenpunkt: {{ category.node.name }}</h3>
|
||||||
|
<p class="text-sm opacity-75">Dieser Diskussionsbereich ist mit dem Mindmap-Knotenpunkt "{{ category.node.name }}" verknüpft.</p>
|
||||||
|
</div>
|
||||||
|
<div class="ml-auto">
|
||||||
|
<a href="{{ url_for('mindmap') }}"
|
||||||
|
class="px-4 py-2 rounded-lg inline-block text-sm transition-all"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-purple-800/60 hover:bg-purple-700/60 text-white'
|
||||||
|
: 'bg-white hover:bg-purple-100 text-purple-800 border border-purple-200'">
|
||||||
|
Zur Mindmap
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Markdown-Buttons für den Beitragseditor
|
||||||
|
document.querySelectorAll('.markdown-button').forEach(button => {
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
const textarea = document.getElementById('content');
|
||||||
|
const format = this.dataset.format;
|
||||||
|
const before = this.dataset.before || '';
|
||||||
|
const after = this.dataset.after || '';
|
||||||
|
|
||||||
|
// Hole die aktuelle Auswahl
|
||||||
|
const start = textarea.selectionStart;
|
||||||
|
const end = textarea.selectionEnd;
|
||||||
|
const selection = textarea.value.substring(start, end);
|
||||||
|
|
||||||
|
// Wende die Formatierung an
|
||||||
|
let formattedText;
|
||||||
|
if (format.includes('\n')) {
|
||||||
|
// Für Formate mit Zeilenumbrüchen (z.B. Code-Blöcke)
|
||||||
|
formattedText = format.replace('Code-Block', selection || 'Code-Block');
|
||||||
|
} else if (format.includes('[Link-Text](URL)')) {
|
||||||
|
formattedText = format.replace('Link-Text', selection || 'Link-Text');
|
||||||
|
} else if (format === '- ' || format === '1. ' || format === '# ' || format === '> ') {
|
||||||
|
// Für Listen und Überschriften: am Anfang der Zeile einfügen
|
||||||
|
const beforeSelection = textarea.value.substring(0, start);
|
||||||
|
const afterSelection = textarea.value.substring(end);
|
||||||
|
|
||||||
|
// Finde den Anfang der aktuellen Zeile
|
||||||
|
const lastNewline = beforeSelection.lastIndexOf('\n');
|
||||||
|
const lineStart = lastNewline === -1 ? 0 : lastNewline + 1;
|
||||||
|
|
||||||
|
// Füge das Format am Zeilenanfang ein
|
||||||
|
formattedText = beforeSelection.substring(0, lineStart) +
|
||||||
|
format +
|
||||||
|
beforeSelection.substring(lineStart) +
|
||||||
|
selection +
|
||||||
|
afterSelection;
|
||||||
|
|
||||||
|
// Setze die neue Cursor-Position
|
||||||
|
const newCursorPos = end + format.length;
|
||||||
|
textarea.value = formattedText;
|
||||||
|
textarea.setSelectionRange(newCursorPos, newCursorPos);
|
||||||
|
|
||||||
|
// Alpine.js Model aktualisieren
|
||||||
|
textarea.dispatchEvent(new Event('input'));
|
||||||
|
return; // Früher zurückkehren, da wir die Formatierung bereits angewendet haben
|
||||||
|
} else {
|
||||||
|
// Für einfache Formatierungen wie fett, kursiv, Code
|
||||||
|
formattedText = before + format + selection + format + after;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ersetze den Text
|
||||||
|
textarea.value = textarea.value.substring(0, start) + formattedText + textarea.value.substring(end);
|
||||||
|
|
||||||
|
// Setze den Fokus zurück auf das Textarea
|
||||||
|
textarea.focus();
|
||||||
|
|
||||||
|
// Alpine.js Model aktualisieren
|
||||||
|
textarea.dispatchEvent(new Event('input'));
|
||||||
|
|
||||||
|
// Setze die Auswahl neu, wenn es eine Auswahl gab
|
||||||
|
if (selection) {
|
||||||
|
const newStart = start + before.length + format.length;
|
||||||
|
const newEnd = newStart + selection.length;
|
||||||
|
textarea.setSelectionRange(newStart, newEnd);
|
||||||
|
} else {
|
||||||
|
// Setze den Cursor in die Mitte von **|** oder `|`
|
||||||
|
const newCursorPos = start + before.length + format.length;
|
||||||
|
textarea.setSelectionRange(newCursorPos, newCursorPos);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
511
templates/community/post.html
Normal file
511
templates/community/post.html
Normal file
@@ -0,0 +1,511 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}{{ post.title }} - Forum{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<style>
|
||||||
|
.post-content {
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
.post-content p {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.post-content h1, .post-content h2, .post-content h3,
|
||||||
|
.post-content h4, .post-content h5, .post-content h6 {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.post-content h1 { font-size: 1.8rem; }
|
||||||
|
.post-content h2 { font-size: 1.5rem; }
|
||||||
|
.post-content h3 { font-size: 1.3rem; }
|
||||||
|
.post-content h4 { font-size: 1.1rem; }
|
||||||
|
.post-content ul, .post-content ol {
|
||||||
|
margin-left: 1.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.post-content ul { list-style-type: disc; }
|
||||||
|
.post-content ol { list-style-type: decimal; }
|
||||||
|
.post-content pre {
|
||||||
|
background-color: rgba(0,0,0,0.05);
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
.post-content code {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 0.9em;
|
||||||
|
padding: 0.1em 0.3em;
|
||||||
|
border-radius: 0.3em;
|
||||||
|
background-color: rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
.post-content pre code {
|
||||||
|
padding: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
.post-content blockquote {
|
||||||
|
border-left: 4px solid;
|
||||||
|
padding-left: 1rem;
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
.post-content img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
.post-content table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
.post-content th, .post-content td {
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 1px solid;
|
||||||
|
border-color: rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
.post-content th {
|
||||||
|
background-color: rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
.post-content a {
|
||||||
|
color: #6d28d9;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.post-content a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.dark .post-content code {
|
||||||
|
background-color: rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
.dark .post-content th, .dark .post-content td {
|
||||||
|
border-color: rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
.dark .post-content th {
|
||||||
|
background-color: rgba(255,255,255,0.05);
|
||||||
|
}
|
||||||
|
.dark .post-content a {
|
||||||
|
color: #a78bfa;
|
||||||
|
}
|
||||||
|
.node-mention {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
background-color: rgba(109, 40, 217, 0.1);
|
||||||
|
color: #6d28d9;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 1px 6px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
margin: 0 2px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.dark .node-mention {
|
||||||
|
background-color: rgba(167, 139, 250, 0.2);
|
||||||
|
color: #a78bfa;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mx-auto px-4 py-8">
|
||||||
|
<!-- Breadcrumb Navigation -->
|
||||||
|
<div class="mb-6 flex items-center text-sm">
|
||||||
|
<a href="{{ url_for('community') }}" class="opacity-75 hover:opacity-100 transition-opacity">
|
||||||
|
<i class="fas fa-home mr-1"></i> Forum
|
||||||
|
</a>
|
||||||
|
<span class="mx-2 opacity-50">/</span>
|
||||||
|
<a href="{{ url_for('forum_category', category_id=category.id) }}" class="opacity-75 hover:opacity-100 transition-opacity">
|
||||||
|
{{ category.title }}
|
||||||
|
</a>
|
||||||
|
<span class="mx-2 opacity-50">/</span>
|
||||||
|
<span class="font-medium truncate max-w-[300px]">{{ post.title }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Beitrags-Header -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<h1 class="text-2xl font-bold mb-2">{{ post.title }}</h1>
|
||||||
|
<div class="flex flex-wrap items-center gap-3 text-sm opacity-75">
|
||||||
|
<span><i class="fas fa-calendar-alt mr-1"></i> {{ post.created_at.strftime('%d.%m.%Y, %H:%M') }}</span>
|
||||||
|
<span><i class="fas fa-eye mr-1"></i> {{ post.view_count }} Aufrufe</span>
|
||||||
|
<span><i class="fas fa-reply mr-1"></i> {{ replies|length }} Antworten</span>
|
||||||
|
|
||||||
|
{% if post.is_pinned or post.is_locked %}
|
||||||
|
<div class="flex gap-2 ml-2">
|
||||||
|
{% if post.is_pinned %}
|
||||||
|
<span class="px-2 py-0.5 text-xs rounded-full"
|
||||||
|
x-bind:class="darkMode ? 'bg-yellow-700/50 text-yellow-300' : 'bg-yellow-100 text-yellow-800'">
|
||||||
|
<i class="fas fa-thumbtack mr-1"></i> Angepinnt
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if post.is_locked %}
|
||||||
|
<span class="px-2 py-0.5 text-xs rounded-full"
|
||||||
|
x-bind:class="darkMode ? 'bg-red-700/50 text-red-300' : 'bg-red-100 text-red-800'">
|
||||||
|
<i class="fas fa-lock mr-1"></i> Gesperrt
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Hauptbeitrag -->
|
||||||
|
<div class="mb-8 rounded-xl overflow-hidden"
|
||||||
|
x-bind:class="darkMode ? 'bg-gray-800/60 border border-white/10' : 'bg-white border border-gray-200 shadow-sm'">
|
||||||
|
<!-- Beitrags-Header -->
|
||||||
|
<div class="p-4 border-b" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<!-- Autor-Avatar -->
|
||||||
|
<div class="w-10 h-10 rounded-full flex items-center justify-center text-white font-medium text-sm overflow-hidden mr-3"
|
||||||
|
style="background: linear-gradient(135deg, #8b5cf6, #6366f1);">
|
||||||
|
{% if post.author.avatar %}
|
||||||
|
<img src="{{ post.author.avatar }}" alt="{{ post.author.username }}" class="w-full h-full object-cover">
|
||||||
|
{% else %}
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="100" cy="100" r="98" fill="url(#post-avatar-gradient)" stroke="#7C3AED" stroke-width="4"/>
|
||||||
|
<circle cx="100" cy="80" r="36" fill="white"/>
|
||||||
|
<path d="M100 140C77.9086 140 60 157.909 60 180H140C140 157.909 122.091 140 100 140Z" fill="white"/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="post-avatar-gradient" x1="0" y1="0" x2="200" y2="200" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stop-color="#8B5CF6"/>
|
||||||
|
<stop offset="1" stop-color="#3B82F6"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Autor-Info -->
|
||||||
|
<div>
|
||||||
|
<div class="font-medium">{{ post.author.username }}</div>
|
||||||
|
<div class="text-xs opacity-70">Erstellt am {{ post.created_at.strftime('%d.%m.%Y, %H:%M') }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Aktionen -->
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
{% if current_user.id == post.user_id or current_user.role == 'admin' %}
|
||||||
|
<a href="{{ url_for('edit_post', post_id=post.id) }}"
|
||||||
|
class="p-2 rounded transition-colors"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'hover:bg-gray-700/50 text-gray-300'
|
||||||
|
: 'hover:bg-gray-100 text-gray-600'">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</a>
|
||||||
|
<form action="{{ url_for('delete_post', post_id=post.id) }}" method="POST" class="inline" onsubmit="return confirm('Möchtest du diesen Beitrag wirklich löschen?');">
|
||||||
|
<button type="submit"
|
||||||
|
class="p-2 rounded transition-colors"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'hover:bg-red-800/50 text-red-300'
|
||||||
|
: 'hover:bg-red-100 text-red-600'">
|
||||||
|
<i class="fas fa-trash-alt"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Moderation-Optionen -->
|
||||||
|
{% if current_user.role in ['admin', 'moderator'] %}
|
||||||
|
<div class="ml-2 border-l" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'"></div>
|
||||||
|
<form action="{{ url_for('toggle_pin_post', post_id=post.id) }}" method="POST" class="inline">
|
||||||
|
<button type="submit"
|
||||||
|
class="p-2 rounded transition-colors"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'hover:bg-yellow-800/50 text-yellow-300'
|
||||||
|
: 'hover:bg-yellow-100 text-yellow-600'"
|
||||||
|
title="{% if post.is_pinned %}Nicht mehr anpinnen{% else %}Anpinnen{% endif %}">
|
||||||
|
<i class="fas fa-thumbtack"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<form action="{{ url_for('toggle_lock_post', post_id=post.id) }}" method="POST" class="inline">
|
||||||
|
<button type="submit"
|
||||||
|
class="p-2 rounded transition-colors"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'hover:bg-blue-800/50 text-blue-300'
|
||||||
|
: 'hover:bg-blue-100 text-blue-600'"
|
||||||
|
title="{% if post.is_locked %}Entsperren{% else %}Sperren{% endif %}">
|
||||||
|
<i class="fas {% if post.is_locked %}fa-unlock{% else %}fa-lock{% endif %}"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Beitrags-Inhalt -->
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="post-content markdown-content" id="main-post-content">
|
||||||
|
{{ post.content|safe }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if post.updated_at and post.updated_at != post.created_at %}
|
||||||
|
<div class="mt-6 pt-4 text-xs opacity-60 border-t" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
|
||||||
|
<i class="fas fa-edit mr-1"></i> Zuletzt bearbeitet: {{ post.updated_at.strftime('%d.%m.%Y, %H:%M') }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Antworten-Bereich -->
|
||||||
|
<div class="mb-8">
|
||||||
|
<h2 class="text-xl font-semibold mb-4">
|
||||||
|
<i class="fas fa-reply mr-2 opacity-60"></i>
|
||||||
|
{{ replies|length }} Antworten
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<!-- Antworten-Liste -->
|
||||||
|
{% if replies %}
|
||||||
|
{% for reply in replies %}
|
||||||
|
<div class="mb-5 rounded-xl overflow-hidden"
|
||||||
|
x-bind:class="darkMode ? 'bg-gray-800/40 border border-white/10' : 'bg-white border border-gray-200'">
|
||||||
|
<!-- Antwort-Header -->
|
||||||
|
<div class="p-3 border-b" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<!-- Autor-Avatar -->
|
||||||
|
<div class="w-8 h-8 rounded-full flex items-center justify-center text-white font-medium text-xs overflow-hidden mr-3"
|
||||||
|
style="background: linear-gradient(135deg, #8b5cf6, #6366f1);">
|
||||||
|
{% if reply.author.avatar %}
|
||||||
|
<img src="{{ reply.author.avatar }}" alt="{{ reply.author.username }}" class="w-full h-full object-cover">
|
||||||
|
{% else %}
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="100" cy="100" r="98" fill="url(#reply-avatar-gradient)" stroke="#7C3AED" stroke-width="4"/>
|
||||||
|
<circle cx="100" cy="80" r="36" fill="white"/>
|
||||||
|
<path d="M100 140C77.9086 140 60 157.909 60 180H140C140 157.909 122.091 140 100 140Z" fill="white"/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="reply-avatar-gradient" x1="0" y1="0" x2="200" y2="200" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stop-color="#8B5CF6"/>
|
||||||
|
<stop offset="1" stop-color="#3B82F6"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Autor-Info -->
|
||||||
|
<div>
|
||||||
|
<div class="font-medium text-sm">{{ reply.author.username }}</div>
|
||||||
|
<div class="text-xs opacity-70">{{ reply.created_at.strftime('%d.%m.%Y, %H:%M') }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Aktionen -->
|
||||||
|
<div class="flex items-center space-x-1">
|
||||||
|
{% if current_user.id == reply.user_id or current_user.role == 'admin' %}
|
||||||
|
<a href="{{ url_for('edit_post', post_id=reply.id) }}"
|
||||||
|
class="p-1.5 rounded text-sm transition-colors"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'hover:bg-gray-700/50 text-gray-300'
|
||||||
|
: 'hover:bg-gray-100 text-gray-600'">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</a>
|
||||||
|
<form action="{{ url_for('delete_post', post_id=reply.id) }}" method="POST" class="inline" onsubmit="return confirm('Möchtest du diese Antwort wirklich löschen?');">
|
||||||
|
<button type="submit"
|
||||||
|
class="p-1.5 rounded text-sm transition-colors"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'hover:bg-red-800/50 text-red-300'
|
||||||
|
: 'hover:bg-red-100 text-red-600'">
|
||||||
|
<i class="fas fa-trash-alt"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Antwort-Inhalt -->
|
||||||
|
<div class="p-5">
|
||||||
|
<div class="post-content markdown-content reply-content" id="reply-content-{{ reply.id }}">
|
||||||
|
{{ reply.content|safe }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if reply.updated_at and reply.updated_at != reply.created_at %}
|
||||||
|
<div class="mt-4 pt-3 text-xs opacity-60 border-t" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
|
||||||
|
<i class="fas fa-edit mr-1"></i> Zuletzt bearbeitet: {{ reply.updated_at.strftime('%d.%m.%Y, %H:%M') }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<div class="rounded-xl p-6 text-center"
|
||||||
|
x-bind:class="darkMode ? 'bg-gray-800/40 border border-white/10' : 'bg-white border border-gray-200'">
|
||||||
|
<div class="text-3xl mb-3 opacity-30"><i class="fas fa-comments"></i></div>
|
||||||
|
<h3 class="text-lg font-semibold mb-2">Noch keine Antworten</h3>
|
||||||
|
<p class="opacity-75">Sei der Erste, der auf diesen Beitrag antwortet!</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Antwort-Formular -->
|
||||||
|
{% if not post.is_locked %}
|
||||||
|
<div class="mb-8 rounded-xl overflow-hidden"
|
||||||
|
x-bind:class="darkMode ? 'bg-gray-800/60 border border-white/10' : 'bg-white border border-gray-200'">
|
||||||
|
<div class="p-4 border-b font-medium" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
|
||||||
|
<i class="fas fa-reply mr-2"></i>
|
||||||
|
Antworten
|
||||||
|
</div>
|
||||||
|
<div class="p-6">
|
||||||
|
<form action="{{ url_for('reply_to_post', post_id=post.id) }}" method="POST">
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="content" class="block mb-2 font-medium">Deine Antwort</label>
|
||||||
|
<div class="mb-2 rounded-lg overflow-hidden"
|
||||||
|
x-bind:class="darkMode ? 'border border-white/20' : 'border border-gray-300'">
|
||||||
|
<textarea id="content" name="content" rows="6"
|
||||||
|
class="w-full p-3 resize-y"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-gray-700/50 text-white placeholder-gray-400 focus:border-indigo-500'
|
||||||
|
: 'bg-white text-gray-700 placeholder-gray-400 focus:border-indigo-500'"
|
||||||
|
placeholder="Schreibe deine Antwort hier (unterstützt Markdown und @Knotenname-Erwähnungen)..."
|
||||||
|
required></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs opacity-70">
|
||||||
|
<p>Du kannst Knotenpunkte der Mindmap durch <code>@Knotenname</code> verlinken.</p>
|
||||||
|
<p>Dieser Editor unterstützt Markdown-Formatierung:</p>
|
||||||
|
<div class="flex flex-wrap gap-2 mt-1">
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="**" data-before="" data-after="" title="Fett">
|
||||||
|
<i class="fas fa-bold"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="*" data-before="" data-after="" title="Kursiv">
|
||||||
|
<i class="fas fa-italic"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="`" data-before="" data-after="" title="Code">
|
||||||
|
<i class="fas fa-code"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="[Link-Text](URL)" data-before="" data-after="" title="Link">
|
||||||
|
<i class="fas fa-link"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="\n```\nCode-Block\n```" data-before="" data-after="" title="Code-Block">
|
||||||
|
<i class="fas fa-file-code"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format=">" data-before="" data-after="" title="Zitat">
|
||||||
|
<i class="fas fa-quote-right"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="- " data-before="" data-after="" title="Liste">
|
||||||
|
<i class="fas fa-list-ul"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="1. " data-before="" data-after="" title="Nummerierte Liste">
|
||||||
|
<i class="fas fa-list-ol"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="# " data-before="" data-after="" title="Überschrift">
|
||||||
|
<i class="fas fa-heading"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button type="submit"
|
||||||
|
class="px-5 py-2.5 rounded-lg transition-all duration-300 flex items-center"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-indigo-700 hover:bg-indigo-600 text-white'
|
||||||
|
: 'bg-indigo-500 hover:bg-indigo-600 text-white'">
|
||||||
|
<i class="fas fa-paper-plane mr-2"></i>
|
||||||
|
Antwort senden
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="rounded-xl p-5 text-center mb-6"
|
||||||
|
x-bind:class="darkMode ? 'bg-red-900/20 border border-red-800/30' : 'bg-red-50 border border-red-100'">
|
||||||
|
<i class="fas fa-lock mr-2 text-red-500"></i>
|
||||||
|
<span>Dieser Beitrag ist geschlossen. Es können keine neuen Antworten mehr verfasst werden.</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Markdown und Knotenerwähnungen verarbeiten
|
||||||
|
const processContent = (content) => {
|
||||||
|
// Verarbeite Markdown mit marked.js
|
||||||
|
let html = marked.parse(content);
|
||||||
|
|
||||||
|
// Ersetze @Knotenname mit entsprechenden Links
|
||||||
|
html = html.replace(/@([a-zA-Z0-9äöüÄÖÜß_-]+)/g, '<span class="node-mention"><i class="fas fa-diagram-project fa-xs mr-1"></i>$1</span>');
|
||||||
|
|
||||||
|
return html;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Markdown-Inhalt für Hauptbeitrag rendern
|
||||||
|
const mainPostContent = document.getElementById('main-post-content');
|
||||||
|
if (mainPostContent) {
|
||||||
|
mainPostContent.innerHTML = processContent(mainPostContent.textContent.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Markdown-Inhalt für Antworten rendern
|
||||||
|
document.querySelectorAll('.reply-content').forEach(reply => {
|
||||||
|
reply.innerHTML = processContent(reply.textContent.trim());
|
||||||
|
});
|
||||||
|
|
||||||
|
// Markdown-Buttons für das Antwortformular
|
||||||
|
document.querySelectorAll('.markdown-button').forEach(button => {
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
const textarea = document.getElementById('content');
|
||||||
|
const format = this.dataset.format;
|
||||||
|
const before = this.dataset.before || '';
|
||||||
|
const after = this.dataset.after || '';
|
||||||
|
|
||||||
|
// Hole die aktuelle Auswahl
|
||||||
|
const start = textarea.selectionStart;
|
||||||
|
const end = textarea.selectionEnd;
|
||||||
|
const selection = textarea.value.substring(start, end);
|
||||||
|
|
||||||
|
// Wende die Formatierung an
|
||||||
|
let formattedText;
|
||||||
|
if (format.includes('\n')) {
|
||||||
|
// Für Formate mit Zeilenumbrüchen (z.B. Code-Blöcke)
|
||||||
|
formattedText = format.replace('Code-Block', selection || 'Code-Block');
|
||||||
|
} else if (format.includes('[Link-Text](URL)')) {
|
||||||
|
formattedText = format.replace('Link-Text', selection || 'Link-Text');
|
||||||
|
} else if (format === '- ' || format === '1. ' || format === '# ' || format === '> ') {
|
||||||
|
// Für Listen und Überschriften: am Anfang der Zeile einfügen
|
||||||
|
const beforeSelection = textarea.value.substring(0, start);
|
||||||
|
const afterSelection = textarea.value.substring(end);
|
||||||
|
|
||||||
|
// Finde den Anfang der aktuellen Zeile
|
||||||
|
const lastNewline = beforeSelection.lastIndexOf('\n');
|
||||||
|
const lineStart = lastNewline === -1 ? 0 : lastNewline + 1;
|
||||||
|
|
||||||
|
// Füge das Format am Zeilenanfang ein
|
||||||
|
formattedText = beforeSelection.substring(0, lineStart) +
|
||||||
|
format +
|
||||||
|
beforeSelection.substring(lineStart) +
|
||||||
|
selection +
|
||||||
|
afterSelection;
|
||||||
|
|
||||||
|
// Setze die neue Cursor-Position
|
||||||
|
const newCursorPos = end + format.length;
|
||||||
|
textarea.value = formattedText;
|
||||||
|
textarea.setSelectionRange(newCursorPos, newCursorPos);
|
||||||
|
return; // Früher zurückkehren, da wir die Formatierung bereits angewendet haben
|
||||||
|
} else {
|
||||||
|
// Für einfache Formatierungen wie fett, kursiv, Code
|
||||||
|
formattedText = before + format + selection + format + after;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ersetze den Text
|
||||||
|
textarea.value = textarea.value.substring(0, start) + formattedText + textarea.value.substring(end);
|
||||||
|
|
||||||
|
// Setze den Fokus zurück auf das Textarea
|
||||||
|
textarea.focus();
|
||||||
|
|
||||||
|
// Setze die Auswahl neu, wenn es eine Auswahl gab
|
||||||
|
if (selection) {
|
||||||
|
const newStart = start + before.length + format.length;
|
||||||
|
const newEnd = newStart + selection.length;
|
||||||
|
textarea.setSelectionRange(newStart, newEnd);
|
||||||
|
} else {
|
||||||
|
// Setze den Cursor in die Mitte von **|** oder `|`
|
||||||
|
const newCursorPos = start + before.length + format.length;
|
||||||
|
textarea.setSelectionRange(newCursorPos, newCursorPos);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
137
templates/community/preview.html
Normal file
137
templates/community/preview.html
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Community Forum Vorschau{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<style>
|
||||||
|
.forum-category {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
.forum-category:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
.category-icon {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
width: 2.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mx-auto px-4 py-8">
|
||||||
|
<!-- Seitenüberschrift -->
|
||||||
|
<div class="mb-8 text-center">
|
||||||
|
<h1 class="text-3xl font-bold mb-2 gradient-text">Community Forum</h1>
|
||||||
|
<p class="text-lg opacity-75">Diskutiere mit anderen Nutzern über die Hauptthemenbereiche der Mindmap</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Login-Aufforderung -->
|
||||||
|
<div class="rounded-xl p-6 text-center mb-8 bg-indigo-50 dark:bg-indigo-900/30 border border-indigo-100 dark:border-indigo-700/30">
|
||||||
|
<h3 class="text-xl font-semibold mb-3">
|
||||||
|
<i class="fas fa-lock mr-2 text-indigo-500"></i>
|
||||||
|
Anmeldung erforderlich
|
||||||
|
</h3>
|
||||||
|
<p class="mb-4">Um am Community-Forum teilzunehmen und alle Funktionen nutzen zu können, musst du dich anmelden oder registrieren.</p>
|
||||||
|
<div class="flex justify-center gap-4 mt-4">
|
||||||
|
<a href="{{ url_for('login', next=url_for('forum')) }}" class="px-4 py-2 bg-indigo-500 hover:bg-indigo-600 text-white rounded-lg transition-colors">
|
||||||
|
<i class="fas fa-sign-in-alt mr-2"></i>Anmelden
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('register') }}" class="px-4 py-2 bg-purple-500 hover:bg-purple-600 text-white rounded-lg transition-colors">
|
||||||
|
<i class="fas fa-user-plus mr-2"></i>Registrieren
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Forumskategorien Vorschau -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
||||||
|
{% if categories_data %}
|
||||||
|
{% for cat_data in categories_data %}
|
||||||
|
<div class="forum-category block">
|
||||||
|
<div class="rounded-xl p-5 h-full"
|
||||||
|
x-bind:class="darkMode ? 'bg-gray-800/60 border border-white/10' : 'bg-white border border-gray-200 shadow-md'">
|
||||||
|
<div class="flex items-start">
|
||||||
|
<!-- Kategorie-Icon -->
|
||||||
|
<div class="category-icon mr-4 text-white"
|
||||||
|
style="background-color: {{ cat_data.category.node.color_code or '#6d28d9' }}">
|
||||||
|
<i class="fas {{ cat_data.category.node.icon or 'fa-folder' }}"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Kategorie-Info -->
|
||||||
|
<div class="flex-grow">
|
||||||
|
<h3 class="text-xl font-semibold mb-2">{{ cat_data.category.title }}</h3>
|
||||||
|
<p class="opacity-75 text-sm mb-3">{{ cat_data.category.description }}</p>
|
||||||
|
|
||||||
|
<!-- Statistik -->
|
||||||
|
<div class="flex flex-wrap gap-4 text-sm opacity-80">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fas fa-comment-alt mr-2"></i>
|
||||||
|
<span>{{ cat_data.total_posts }} Themen</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fas fa-reply mr-2"></i>
|
||||||
|
<span>{{ cat_data.total_replies }} Antworten</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pfeil-Icon -->
|
||||||
|
<div class="ml-2">
|
||||||
|
<i class="fas fa-lock opacity-50"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<div class="col-span-2 text-center py-8">
|
||||||
|
<div class="text-3xl mb-3 opacity-30"><i class="fas fa-exclamation-circle"></i></div>
|
||||||
|
<h3 class="text-xl font-semibold mb-2">Keine Forum-Kategorien gefunden</h3>
|
||||||
|
<p class="opacity-75">Es sind derzeit keine Kategorien für Diskussionen verfügbar.</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Hinweis zur Nutzung -->
|
||||||
|
<div class="rounded-xl p-6 text-center mb-8"
|
||||||
|
x-bind:class="darkMode ? 'bg-indigo-900/30 border border-indigo-700/30' : 'bg-indigo-50 border border-indigo-100'">
|
||||||
|
<h3 class="text-xl font-semibold mb-3">
|
||||||
|
<i class="fas fa-lightbulb mr-2 text-yellow-500"></i>
|
||||||
|
So funktioniert das Forum
|
||||||
|
</h3>
|
||||||
|
<p class="mb-4">Das Community-Forum ist nach den Hauptknotenpunkten der Systades-Mindmap strukturiert.
|
||||||
|
In deinen Beiträgen kannst du Knotenpunkte mit <code>@Knotenname</code> verlinken.</p>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mt-6">
|
||||||
|
<div class="p-4 rounded-lg"
|
||||||
|
x-bind:class="darkMode ? 'bg-indigo-800/40' : 'bg-white border border-indigo-100'">
|
||||||
|
<div class="text-2xl mb-2"><i class="fas fa-users text-indigo-400"></i></div>
|
||||||
|
<h4 class="font-medium mb-1">Fachliche Diskussionen</h4>
|
||||||
|
<p class="text-sm opacity-75">Tausche dich mit anderen zu spezifischen Themen aus</p>
|
||||||
|
</div>
|
||||||
|
<div class="p-4 rounded-lg"
|
||||||
|
x-bind:class="darkMode ? 'bg-indigo-800/40' : 'bg-white border border-indigo-100'">
|
||||||
|
<div class="text-2xl mb-2"><i class="fas fa-link text-indigo-400"></i></div>
|
||||||
|
<h4 class="font-medium mb-1">Wissensvernetzung</h4>
|
||||||
|
<p class="text-sm opacity-75">Verknüpfe Inhalte durch Knotenreferenzen</p>
|
||||||
|
</div>
|
||||||
|
<div class="p-4 rounded-lg"
|
||||||
|
x-bind:class="darkMode ? 'bg-indigo-800/40' : 'bg-white border border-indigo-100'">
|
||||||
|
<div class="text-2xl mb-2"><i class="fas fa-markdown text-indigo-400"></i></div>
|
||||||
|
<h4 class="font-medium mb-1">Markdown Support</h4>
|
||||||
|
<p class="text-sm opacity-75">Formatiere deine Beiträge mit Markdown</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
// Hier können bei Bedarf forumspezifische Scripts eingefügt werden
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
365
templates/create_mindmap.html
Normal file
365
templates/create_mindmap.html
Normal file
@@ -0,0 +1,365 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Mindmap erstellen{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<style>
|
||||||
|
/* Spezifische Stile für die Mindmap-Erstellungsseite */
|
||||||
|
.form-container {
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark .form-container {
|
||||||
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.2);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .form-container {
|
||||||
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-header {
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .form-header {
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-body {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input,
|
||||||
|
.form-textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark .form-input,
|
||||||
|
body.dark .form-textarea {
|
||||||
|
background-color: rgba(15, 23, 42, 0.6);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
color: #f1f5f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .form-input,
|
||||||
|
body:not(.dark) .form-textarea {
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
color: #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark .form-input:focus,
|
||||||
|
body.dark .form-textarea:focus {
|
||||||
|
border-color: #7c3aed;
|
||||||
|
box-shadow: 0 0 0 2px rgba(124, 58, 237, 0.2);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .form-input:focus,
|
||||||
|
body:not(.dark) .form-textarea:focus {
|
||||||
|
border-color: #7c3aed;
|
||||||
|
box-shadow: 0 0 0 2px rgba(124, 58, 237, 0.2);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-textarea {
|
||||||
|
min-height: 120px;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-switch {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-switch input[type="checkbox"] {
|
||||||
|
height: 0;
|
||||||
|
width: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-switch label {
|
||||||
|
cursor: pointer;
|
||||||
|
width: 50px;
|
||||||
|
height: 25px;
|
||||||
|
background: rgba(100, 116, 139, 0.3);
|
||||||
|
display: block;
|
||||||
|
border-radius: 25px;
|
||||||
|
position: relative;
|
||||||
|
margin-right: 10px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-switch label:after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 3px;
|
||||||
|
left: 3px;
|
||||||
|
width: 19px;
|
||||||
|
height: 19px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 19px;
|
||||||
|
transition: 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-switch input:checked + label {
|
||||||
|
background: #7c3aed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-switch input:checked + label:after {
|
||||||
|
left: calc(100% - 3px);
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-submit {
|
||||||
|
background-color: #7c3aed;
|
||||||
|
color: white;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-submit:hover {
|
||||||
|
background-color: #6d28d9;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(109, 40, 217, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel {
|
||||||
|
background-color: transparent;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
border: 1px solid;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark .btn-cancel {
|
||||||
|
color: #e2e8f0;
|
||||||
|
border-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .btn-cancel {
|
||||||
|
color: #475569;
|
||||||
|
border-color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark .btn-cancel:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .btn-cancel:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animation für den Seiteneintritt */
|
||||||
|
@keyframes slideInUp {
|
||||||
|
from {
|
||||||
|
transform: translateY(20px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-container {
|
||||||
|
animation: slideInUp 0.5s ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animation für Hover-Effekte */
|
||||||
|
.input-animation {
|
||||||
|
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-animation:focus {
|
||||||
|
transform: scale(1.01);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mx-auto px-4 py-8 animate-fadeIn">
|
||||||
|
<div class="max-w-3xl mx-auto">
|
||||||
|
<!-- Titel mit Animation -->
|
||||||
|
<div class="text-center mb-8 animate-pulse">
|
||||||
|
<h1 class="text-3xl font-bold mb-2 mystical-glow gradient-text">
|
||||||
|
Neue Mindmap erstellen
|
||||||
|
</h1>
|
||||||
|
<p class="opacity-80">Erstelle deine eigene Wissenslandkarte und organisiere deine Gedanken</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-container">
|
||||||
|
<div class="form-header">
|
||||||
|
<h2 class="text-xl font-semibold">Mindmap-Details</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-body">
|
||||||
|
<form action="{{ url_for('create_mindmap') }}" method="POST">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="name" class="form-label">Name der Mindmap</label>
|
||||||
|
<input type="text" id="name" name="name" class="form-input input-animation" required placeholder="z.B. Meine Philosophie-Mindmap">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="description" class="form-label">Beschreibung</label>
|
||||||
|
<textarea id="description" name="description" class="form-textarea input-animation" placeholder="Worum geht es in dieser Mindmap?"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="form-switch">
|
||||||
|
<input type="checkbox" id="is_private" name="is_private" checked>
|
||||||
|
<label for="is_private"></label>
|
||||||
|
<span>Private Mindmap (nur für dich sichtbar)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-between mt-6">
|
||||||
|
<a href="{{ url_for('profile') }}" class="btn-cancel">
|
||||||
|
<i class="fas fa-arrow-left"></i>
|
||||||
|
Zurück
|
||||||
|
</a>
|
||||||
|
<button type="submit" class="btn-submit">
|
||||||
|
<i class="fas fa-save"></i>
|
||||||
|
Mindmap erstellen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Mindmap-Vorschau -->
|
||||||
|
<div class="mt-8">
|
||||||
|
<h3 class="text-xl font-semibold mb-4">Vorschau</h3>
|
||||||
|
<div class="mindmap-container">
|
||||||
|
<div id="cy" class="w-full h-[400px] rounded-xl border"
|
||||||
|
x-bind:class="darkMode ? 'border-gray-700' : 'border-gray-200'">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tipps-Sektion -->
|
||||||
|
<div class="mt-8 p-5 rounded-lg border animate-fadeIn"
|
||||||
|
x-bind:class="darkMode ? 'bg-slate-800/40 border-slate-700/50' : 'bg-white border-slate-200'">
|
||||||
|
<h3 class="text-xl font-semibold mb-3"
|
||||||
|
x-bind:class="darkMode ? 'text-white' : 'text-gray-800'">
|
||||||
|
<i class="fa-solid fa-lightbulb text-yellow-400 mr-2"></i>Tipps zum Erstellen einer Mindmap
|
||||||
|
</h3>
|
||||||
|
<div x-bind:class="darkMode ? 'text-gray-300' : 'text-gray-600'">
|
||||||
|
<ul class="list-disc pl-5 space-y-2">
|
||||||
|
<li>Wähle einen prägnanten, aber aussagekräftigen Namen für deine Mindmap</li>
|
||||||
|
<li>Beginne mit einem zentralen Konzept und arbeite dich nach außen vor</li>
|
||||||
|
<li>Verwende verschiedene Farben für unterschiedliche Kategorien oder Themenbereiche</li>
|
||||||
|
<li>Füge Notizen zu Knoten hinzu, um komplexere Ideen zu erklären</li>
|
||||||
|
<li>Verknüpfe verwandte Konzepte, um Beziehungen zu visualisieren</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.26.0/cytoscape.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape-cose-bilkent/4.1.0/cytoscape-cose-bilkent.min.js"></script>
|
||||||
|
<script nonce="{{ csp_nonce }}">
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Einfache Animationen für die Eingabefelder
|
||||||
|
const inputs = document.querySelectorAll('.input-animation');
|
||||||
|
|
||||||
|
inputs.forEach(input => {
|
||||||
|
// Subtile Skalierung bei Fokus
|
||||||
|
input.addEventListener('focus', function() {
|
||||||
|
this.style.transform = 'scale(1.01)';
|
||||||
|
this.style.boxShadow = '0 4px 12px rgba(124, 58, 237, 0.15)';
|
||||||
|
});
|
||||||
|
|
||||||
|
input.addEventListener('blur', function() {
|
||||||
|
this.style.transform = 'scale(1)';
|
||||||
|
this.style.boxShadow = 'none';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Formular-Absenden-Animation
|
||||||
|
const form = document.querySelector('form');
|
||||||
|
form.addEventListener('submit', function(e) {
|
||||||
|
const submitBtn = this.querySelector('.btn-submit');
|
||||||
|
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Wird erstellt...';
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mindmap-Vorschau initialisieren
|
||||||
|
const mindmap = new MindMap.Visualization('cy', {
|
||||||
|
enableEditing: true,
|
||||||
|
onNodeClick: function(nodeData) {
|
||||||
|
console.log("Knoten ausgewählt:", nodeData);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Formularfelder mit Mindmap verbinden
|
||||||
|
const nameInput = document.getElementById('name');
|
||||||
|
const descriptionInput = document.getElementById('description');
|
||||||
|
|
||||||
|
// Aktualisiere Mindmap wenn sich die Eingaben ändern
|
||||||
|
nameInput.addEventListener('input', function() {
|
||||||
|
if (mindmap.cy) {
|
||||||
|
const rootNode = mindmap.cy.$('#root');
|
||||||
|
if (rootNode.length > 0) {
|
||||||
|
rootNode.data('name', this.value || 'Neue Mindmap');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialisiere die Mindmap
|
||||||
|
mindmap.initialize().then(() => {
|
||||||
|
console.log("Mindmap-Vorschau initialisiert");
|
||||||
|
|
||||||
|
// Setze initiale Werte
|
||||||
|
if (nameInput.value) {
|
||||||
|
const rootNode = mindmap.cy.$('#root');
|
||||||
|
if (rootNode.length > 0) {
|
||||||
|
rootNode.data('name', nameInput.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
console.error("Fehler bei der Initialisierung der Mindmap:", error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
525
templates/edit_mindmap.html
Normal file
525
templates/edit_mindmap.html
Normal file
@@ -0,0 +1,525 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Mindmap bearbeiten{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<style>
|
||||||
|
/* Spezifische Stile für die Mindmap-Bearbeitungsseite */
|
||||||
|
.form-container {
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark .form-container {
|
||||||
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.2);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .form-container {
|
||||||
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-header {
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .form-header {
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-body {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input,
|
||||||
|
.form-textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark .form-input,
|
||||||
|
body.dark .form-textarea {
|
||||||
|
background-color: rgba(15, 23, 42, 0.6);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
color: #f1f5f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .form-input,
|
||||||
|
body:not(.dark) .form-textarea {
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
color: #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark .form-input:focus,
|
||||||
|
body.dark .form-textarea:focus {
|
||||||
|
border-color: #7c3aed;
|
||||||
|
box-shadow: 0 0 0 2px rgba(124, 58, 237, 0.2);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .form-input:focus,
|
||||||
|
body:not(.dark) .form-textarea:focus {
|
||||||
|
border-color: #7c3aed;
|
||||||
|
box-shadow: 0 0 0 2px rgba(124, 58, 237, 0.2);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-textarea {
|
||||||
|
min-height: 120px;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-switch {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-switch input[type="checkbox"] {
|
||||||
|
height: 0;
|
||||||
|
width: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-switch label {
|
||||||
|
cursor: pointer;
|
||||||
|
width: 50px;
|
||||||
|
height: 25px;
|
||||||
|
background: rgba(100, 116, 139, 0.3);
|
||||||
|
display: block;
|
||||||
|
border-radius: 25px;
|
||||||
|
position: relative;
|
||||||
|
margin-right: 10px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-switch label:after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 3px;
|
||||||
|
left: 3px;
|
||||||
|
width: 19px;
|
||||||
|
height: 19px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 19px;
|
||||||
|
transition: 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-switch input:checked + label {
|
||||||
|
background: #7c3aed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-switch input:checked + label:after {
|
||||||
|
left: calc(100% - 3px);
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-submit {
|
||||||
|
background-color: #7c3aed;
|
||||||
|
color: white;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-submit:hover {
|
||||||
|
background-color: #6d28d9;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(109, 40, 217, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel {
|
||||||
|
background-color: transparent;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
border: 1px solid;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark .btn-cancel {
|
||||||
|
color: #e2e8f0;
|
||||||
|
border-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .btn-cancel {
|
||||||
|
color: #475569;
|
||||||
|
border-color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark .btn-cancel:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .btn-cancel:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animation für den Seiteneintritt */
|
||||||
|
@keyframes slideInUp {
|
||||||
|
from {
|
||||||
|
transform: translateY(20px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-container {
|
||||||
|
animation: slideInUp 0.5s ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animation für Hover-Effekte */
|
||||||
|
.input-animation {
|
||||||
|
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-animation:focus {
|
||||||
|
transform: scale(1.01);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mx-auto px-4 py-8 animate-fadeIn">
|
||||||
|
<div class="max-w-3xl mx-auto">
|
||||||
|
<!-- Titel mit Animation -->
|
||||||
|
<div class="text-center mb-8 animate-pulse">
|
||||||
|
<h1 class="text-3xl font-bold mb-2 mystical-glow gradient-text">
|
||||||
|
Mindmap bearbeiten
|
||||||
|
</h1>
|
||||||
|
<p class="opacity-80">Aktualisiere die Details deiner Mindmap</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-container">
|
||||||
|
<div class="form-header">
|
||||||
|
<h2 class="text-xl font-semibold">Mindmap-Details</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-body">
|
||||||
|
<form id="edit-mindmap-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="name" class="form-label">Name der Mindmap</label>
|
||||||
|
<input type="text" id="name" name="name" class="form-input input-animation" required
|
||||||
|
placeholder="z.B. Meine Philosophie-Mindmap" value="{{ mindmap.name }}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="description" class="form-label">Beschreibung</label>
|
||||||
|
<textarea id="description" name="description" class="form-textarea input-animation"
|
||||||
|
placeholder="Worum geht es in dieser Mindmap?">{{ mindmap.description }}</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="form-switch">
|
||||||
|
<input type="checkbox" id="is_private" name="is_private" {% if mindmap.is_private %}checked{% endif %}>
|
||||||
|
<label for="is_private"></label>
|
||||||
|
<span>Private Mindmap (nur für dich sichtbar)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-between mt-6">
|
||||||
|
<a href="{{ url_for('my_account') }}" class="btn-cancel"> {# Zurück zur Kontoübersicht geändert #}
|
||||||
|
<i class="fas fa-arrow-left"></i>
|
||||||
|
Zurück
|
||||||
|
</a>
|
||||||
|
<button type="button" id="save-mindmap-details-btn" class="btn-submit"> {# type="button" und ID hinzugefügt #}
|
||||||
|
<i class="fas fa-save"></i>
|
||||||
|
Änderungen speichern
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Mindmap-Editor -->
|
||||||
|
<div class="mt-8">
|
||||||
|
<h3 class="text-xl font-semibold mb-4">Mindmap bearbeiten</h3>
|
||||||
|
<div class="mindmap-container">
|
||||||
|
<div id="cy" class="w-full h-[600px] rounded-xl border"
|
||||||
|
x-bind:class="darkMode ? 'border-gray-700' : 'border-gray-200'">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Bearbeitungshinweise -->
|
||||||
|
<div class="mt-4 text-sm opacity-80">
|
||||||
|
<p><i class="fas fa-info-circle mr-2"></i>Klicke auf Knoten zum Bearbeiten, ziehe sie zum Neuanordnen oder nutze die Toolbar für weitere Funktionen.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tipps-Sektion -->
|
||||||
|
<div class="mt-8 p-5 rounded-lg border animate-fadeIn"
|
||||||
|
x-bind:class="darkMode ? 'bg-slate-800/40 border-slate-700/50' : 'bg-white border-slate-200'">
|
||||||
|
<h3 class="text-xl font-semibold mb-3"
|
||||||
|
x-bind:class="darkMode ? 'text-white' : 'text-gray-800'">
|
||||||
|
<i class="fa-solid fa-lightbulb text-yellow-400 mr-2"></i>Tipps zum Bearbeiten einer Mindmap
|
||||||
|
</h3>
|
||||||
|
<div x-bind:class="darkMode ? 'text-gray-300' : 'text-gray-600'">
|
||||||
|
<ul class="list-disc pl-5 space-y-2">
|
||||||
|
<li>Überprüfe, ob der Name noch zum aktuellen Inhalt passt</li>
|
||||||
|
<li>Aktualisiere die Beschreibung, um neue Aspekte zu berücksichtigen</li>
|
||||||
|
<li>Entscheide, ob die Sichtbarkeitseinstellungen noch passend sind</li>
|
||||||
|
<li>Nutze aussagekräftige Namen für bessere Auffindbarkeit</li>
|
||||||
|
<li>Behalte die Konsistenz mit verknüpften Konzepten im Auge</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.26.0/cytoscape.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape-cose-bilkent/4.1.0/cytoscape-cose-bilkent.min.js"></script>
|
||||||
|
<script nonce="{{ csp_nonce }}">
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Einfache Animationen für die Eingabefelder
|
||||||
|
const inputs = document.querySelectorAll('.input-animation');
|
||||||
|
|
||||||
|
inputs.forEach(input => {
|
||||||
|
// Subtile Skalierung bei Fokus
|
||||||
|
input.addEventListener('focus', function() {
|
||||||
|
this.style.transform = 'scale(1.01)';
|
||||||
|
this.style.boxShadow = '0 4px 12px rgba(124, 58, 237, 0.15)';
|
||||||
|
});
|
||||||
|
|
||||||
|
input.addEventListener('blur', function() {
|
||||||
|
this.style.transform = 'scale(1)';
|
||||||
|
this.style.boxShadow = 'none';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Formular-Absenden-Logik für Metadaten
|
||||||
|
const editMindmapForm = document.getElementById('edit-mindmap-form');
|
||||||
|
const saveDetailsBtn = document.getElementById('save-mindmap-details-btn');
|
||||||
|
|
||||||
|
if (saveDetailsBtn && editMindmapForm) {
|
||||||
|
saveDetailsBtn.addEventListener('click', async function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const nameInput = document.getElementById('name');
|
||||||
|
const descriptionInput = document.getElementById('description');
|
||||||
|
const isPrivateInput = document.getElementById('is_private');
|
||||||
|
|
||||||
|
const mindmapId = "{{ mindmap.id }}"; // Sicherstellen, dass mindmap.id hier verfügbar ist
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
name: nameInput.value,
|
||||||
|
description: descriptionInput.value,
|
||||||
|
is_private: isPrivateInput.checked
|
||||||
|
// Die 'data' (Knoten/Kanten) wird separat vom Cytoscape-Editor gehandhabt
|
||||||
|
};
|
||||||
|
|
||||||
|
saveDetailsBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Wird gespeichert...';
|
||||||
|
saveDetailsBtn.disabled = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/mindmaps/${mindmapId}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const result = await response.json();
|
||||||
|
showStatus('Metadaten erfolgreich gespeichert!', false);
|
||||||
|
// Optional: Weiterleitung oder Aktualisierung der Seiteninhalte
|
||||||
|
// window.location.href = "{{ url_for('my_account') }}";
|
||||||
|
} else {
|
||||||
|
const errorData = await response.json();
|
||||||
|
console.error('Fehler beim Speichern der Metadaten:', errorData);
|
||||||
|
showStatus(`Fehler: ${errorData.error || response.statusText}`, true);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Netzwerkfehler oder anderer Fehler:', error);
|
||||||
|
showStatus('Speichern fehlgeschlagen. Netzwerkproblem?', true);
|
||||||
|
} finally {
|
||||||
|
saveDetailsBtn.innerHTML = '<i class="fas fa-save"></i> Änderungen speichern';
|
||||||
|
saveDetailsBtn.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mindmap initialisieren
|
||||||
|
const mindmap = new MindMap.Visualization('cy', {
|
||||||
|
enableEditing: true,
|
||||||
|
apiEndpoint: '/api/mindmap/{{ mindmap.id }}',
|
||||||
|
onNodeClick: function(nodeData) {
|
||||||
|
console.log("Knoten ausgewählt:", nodeData);
|
||||||
|
},
|
||||||
|
onChange: function(dataFromCytoscape) {
|
||||||
|
// Automatisches Speichern bei Änderungen der Mindmap-Struktur
|
||||||
|
// Die Metadaten (Name, Beschreibung, is_private) werden separat über das Formular oben gespeichert.
|
||||||
|
// Diese onChange Funktion kümmert sich nur um die Strukturdaten (Knoten/Kanten).
|
||||||
|
const mindmapId = "{{ mindmap.id }}";
|
||||||
|
|
||||||
|
// Debounce-Funktion, um API-Aufrufe zu limitieren
|
||||||
|
let debounceTimer;
|
||||||
|
const debounceSaveStructure = (currentMindmapData) => {
|
||||||
|
clearTimeout(debounceTimer);
|
||||||
|
debounceTimer = setTimeout(() => {
|
||||||
|
// Der Backend-Endpunkt PUT /api/mindmaps/<id> erwartet ein Objekt,
|
||||||
|
// das die zu aktualisierenden Felder enthält. Für die Struktur ist das 'data'.
|
||||||
|
const payload = {
|
||||||
|
data: currentMindmapData // Dies sind die von Cytoscape gelieferten Strukturdaten
|
||||||
|
};
|
||||||
|
|
||||||
|
// showStatus('Speichere Struktur...', false); // Status wird jetzt über Event gehandhabt
|
||||||
|
fetch(`/api/mindmaps/${mindmapId}`, { // Endpunkt angepasst
|
||||||
|
method: 'PUT', // Methode zu PUT geändert
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload) // Sende die Mindmap-Daten als { data: ... }
|
||||||
|
}).then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
response.json().then(err => {
|
||||||
|
console.error('Fehler beim Speichern der Struktur:', err);
|
||||||
|
document.dispatchEvent(new CustomEvent('mindmapError', { detail: { message: `Struktur: ${err.message || err.error || 'Speicherfehler'}` } }));
|
||||||
|
}).catch(() => {
|
||||||
|
console.error('Fehler beim Speichern der Struktur, Status:', response.statusText);
|
||||||
|
document.dispatchEvent(new CustomEvent('mindmapError', { detail: { message: `Struktur: ${response.statusText}` } }));
|
||||||
|
});
|
||||||
|
// throw new Error('Netzwerkfehler beim Speichern der Struktur'); // Wird schon behandelt
|
||||||
|
return; // Verhindere weitere Verarbeitung bei Fehler
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
}).then(responseData => {
|
||||||
|
if (responseData) { // Nur wenn response.ok war
|
||||||
|
console.log('Mindmap-Struktur erfolgreich gespeichert:', responseData);
|
||||||
|
// Die responseData von einem PUT könnte die aktualisierte Mindmap oder nur eine Erfolgsmeldung sein.
|
||||||
|
// Annahme: { message: "Mindmap updated successfully", mindmap: { ... } } oder ähnlich
|
||||||
|
document.dispatchEvent(new CustomEvent('mindmapSaved', { detail: { message: 'Struktur aktualisiert!' }}));
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('Netzwerkfehler oder anderer Fehler beim Speichern der Struktur:', error);
|
||||||
|
// Vermeide doppelte Fehlermeldung, falls schon durch !response.ok behandelt
|
||||||
|
if (!document.querySelector('.bg-red-500')) { // Prüft, ob schon eine Fehlermeldung angezeigt wird
|
||||||
|
document.dispatchEvent(new CustomEvent('mindmapError', { detail: { message: 'Struktur: Netzwerkfehler' } }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 1500); // Speichern 1.5 Sekunden nach der letzten Änderung
|
||||||
|
};
|
||||||
|
|
||||||
|
debounceSaveStructure(dataFromCytoscape); // Aufruf der Debounce-Funktion mit Cytoscape-Daten
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Die Verknüpfung der Formularfelder (Name, Beschreibung) mit dem Cytoscape Root-Knoten wird entfernt,
|
||||||
|
// da die Metadaten nun über das separate Formular oben gespeichert werden und nicht mehr direkt
|
||||||
|
// die Cytoscape-Daten manipulieren sollen. Die Logik für mindmap.saveToServer() wurde entfernt,
|
||||||
|
// da das Speichern jetzt über den onChange Handler mit PUT /api/mindmaps/<id> erfolgt.
|
||||||
|
// const nameInput = document.getElementById('name'); // Bereits oben deklariert für Metadaten
|
||||||
|
// nameInput.removeEventListener('input', ...); // Event Listener muss hier nicht entfernt werden, da er nicht neu hinzugefügt wird.
|
||||||
|
|
||||||
|
// Initialisiere die Mindmap mit existierenden Daten
|
||||||
|
mindmap.initialize().then(() => {
|
||||||
|
console.log("Mindmap-Editor initialisiert");
|
||||||
|
const mindmapId = "{{ mindmap.id }}";
|
||||||
|
|
||||||
|
// Lade existierende Daten für die Mindmap-Struktur
|
||||||
|
fetch(`/api/mindmaps/${mindmapId}`, { // Endpunkt für GET angepasst
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
response.json().then(err => {
|
||||||
|
showStatus(`Fehler beim Laden: ${err.message || err.error || response.statusText}`, true);
|
||||||
|
}).catch(() => {
|
||||||
|
showStatus(`Fehler beim Laden: ${response.statusText}`, true);
|
||||||
|
});
|
||||||
|
throw new Error(`Netzwerkantwort war nicht ok: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(mindmapDataFromServer => {
|
||||||
|
// Die API GET /api/mindmaps/<id> gibt ein Objekt zurück, das { id, name, description, is_private, data, ... } enthält.
|
||||||
|
// Wir brauchen nur den 'data'-Teil (Struktur) für Cytoscape.
|
||||||
|
// Die Metadaten (name, description, is_private) werden bereits serverseitig in die Formularfelder gerendert.
|
||||||
|
if (mindmapDataFromServer && mindmapDataFromServer.data) {
|
||||||
|
mindmap.loadData(mindmapDataFromServer.data); // Lade nur die Strukturdaten
|
||||||
|
console.log("Mindmap-Strukturdaten geladen:", mindmapDataFromServer.data);
|
||||||
|
showStatus("Mindmap geladen.", false);
|
||||||
|
} else {
|
||||||
|
console.error("Fehler: Mindmap-Daten (Struktur) nicht im erwarteten Format:", mindmapDataFromServer);
|
||||||
|
showStatus("Fehler: Mindmap-Struktur konnte nicht geladen werden (Formatfehler).", true);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error("Fehler beim Laden der Mindmap-Strukturdaten:", error);
|
||||||
|
if (!document.querySelector('.bg-red-500')) { // Prüft, ob schon eine Fehlermeldung angezeigt wird
|
||||||
|
showStatus("Laden der Struktur fehlgeschlagen.", true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).catch(error => {
|
||||||
|
console.error("Fehler bei der Initialisierung des Editors:", error);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Autosave-Status Anzeige
|
||||||
|
const statusIndicator = document.createElement('div');
|
||||||
|
statusIndicator.className = 'fixed bottom-4 right-4 px-4 py-2 rounded-full text-sm transition-all duration-300';
|
||||||
|
document.body.appendChild(statusIndicator);
|
||||||
|
|
||||||
|
// Zeige Speicherstatus
|
||||||
|
function showStatus(message, isError = false) {
|
||||||
|
statusIndicator.textContent = message;
|
||||||
|
statusIndicator.className = `fixed bottom-4 right-4 px-4 py-2 rounded-full text-sm transition-all duration-300 ${
|
||||||
|
isError
|
||||||
|
? 'bg-red-500 text-white'
|
||||||
|
: 'bg-green-500 text-white'
|
||||||
|
}`;
|
||||||
|
setTimeout(() => {
|
||||||
|
statusIndicator.className = 'fixed bottom-4 right-4 px-4 py-2 rounded-full text-sm transition-all duration-300 opacity-0';
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event-Listener für Speicherstatus
|
||||||
|
document.addEventListener('mindmapSaved', (event) => {
|
||||||
|
const message = event.detail && event.detail.message ? event.detail.message : 'Erfolgreich gespeichert!';
|
||||||
|
showStatus(message, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('mindmapError', (event) => {
|
||||||
|
showStatus(event.detail.message, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
48
templates/errors/400.html
Normal file
48
templates/errors/400.html
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}400 - Ungültige Anfrage{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mx-auto max-w-4xl px-4 py-8">
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl p-8 border border-gray-200 dark:border-gray-700">
|
||||||
|
<div class="text-center mb-8">
|
||||||
|
<div class="text-6xl font-bold text-red-500 mb-4">400</div>
|
||||||
|
<h1 class="text-3xl font-bold mb-2">Ungültige Anfrage</h1>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400">Die Anfrage konnte nicht verarbeitet werden.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-8 p-4 border border-red-200 dark:border-red-900 bg-red-50 dark:bg-red-900/20 rounded-lg">
|
||||||
|
<div class="flex items-start">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<svg class="h-5 w-5 text-red-500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="ml-3">
|
||||||
|
<h3 class="text-sm font-medium text-red-800 dark:text-red-400">Fehlerbeschreibung</h3>
|
||||||
|
<div class="mt-2 text-sm text-red-700 dark:text-red-300">
|
||||||
|
{% if error %}
|
||||||
|
<p>{{ error }}</p>
|
||||||
|
{% else %}
|
||||||
|
<p>Die Anfrage enthält ungültige oder fehlerhafte Daten und konnte nicht verarbeitet werden.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<p class="mb-4 text-gray-600 dark:text-gray-400">Hier sind einige Dinge, die Sie versuchen können:</p>
|
||||||
|
<ul class="list-disc list-inside text-left max-w-md mx-auto mb-6 text-gray-600 dark:text-gray-400">
|
||||||
|
<li>Überprüfen Sie Ihre Eingaben auf Fehler.</li>
|
||||||
|
<li>Stellen Sie sicher, dass Sie die richtigen Daten übermittelt haben.</li>
|
||||||
|
<li>Versuchen Sie, die Seite neu zu laden.</li>
|
||||||
|
<li>Kehren Sie zur Startseite zurück und versuchen Sie es erneut.</li>
|
||||||
|
</ul>
|
||||||
|
<a href="{{ url_for('index') }}" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||||
|
Zurück zur Startseite
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -3,21 +3,31 @@
|
|||||||
{% block title %}403 - Zugriff verweigert{% endblock %}
|
{% block title %}403 - Zugriff verweigert{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="min-h-[65vh] flex flex-col items-center justify-center px-4 py-12">
|
<div class="min-h-[75vh] flex flex-col items-center justify-center px-4 py-12 bg-gradient-to-b from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800">
|
||||||
<div class="glass-effect max-w-2xl w-full p-8 rounded-lg border border-gray-300/20 dark:border-gray-700/30 shadow-lg">
|
<div class="glass-effect max-w-2xl w-full p-6 md:p-10 rounded-xl border border-gray-300/20 dark:border-gray-700/30 shadow-xl transform transition-all duration-300 hover:shadow-2xl">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<h1 class="text-6xl font-bold text-primary-600 dark:text-primary-400 mb-4">403</h1>
|
<div class="flex justify-center mb-6">
|
||||||
<h2 class="text-2xl font-semibold mb-4">Zugriff verweigert</h2>
|
<div class="relative">
|
||||||
<p class="text-gray-600 dark:text-gray-300 mb-8">Sie haben nicht die erforderlichen Berechtigungen, um auf diese Seite zuzugreifen. Bitte melden Sie sich an oder nutzen Sie ein Konto mit entsprechenden Rechten.</p>
|
<h1 class="text-7xl md:text-8xl font-extrabold text-primary-600 dark:text-primary-400 opacity-90">403</h1>
|
||||||
|
<div class="absolute -top-4 -right-4 w-12 h-12 bg-red-500 rounded-full flex items-center justify-center animate-pulse">
|
||||||
|
<i class="fa-solid fa-lock text-white text-xl"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h2 class="text-2xl md:text-3xl font-semibold mb-4 text-gray-800 dark:text-gray-200">Zugriff verweigert</h2>
|
||||||
|
<p class="text-gray-600 dark:text-gray-300 mb-8 max-w-lg mx-auto text-base md:text-lg">Sie haben nicht die erforderlichen Berechtigungen, um auf diese Seite zuzugreifen. Bitte melden Sie sich an oder nutzen Sie ein Konto mit entsprechenden Rechten.</p>
|
||||||
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||||
<a href="{{ url_for('index') }}" class="btn-primary">
|
<a href="{{ url_for('index') }}" class="btn-primary transform transition-transform duration-300 hover:scale-105 px-6 py-3 rounded-lg">
|
||||||
<i class="fa-solid fa-home mr-2"></i>Zur Startseite
|
<i class="fa-solid fa-home mr-2"></i>Zur Startseite
|
||||||
</a>
|
</a>
|
||||||
<a href="javascript:history.back()" class="btn-secondary">
|
<a href="javascript:history.back()" class="btn-secondary transform transition-transform duration-300 hover:scale-105 px-6 py-3 rounded-lg">
|
||||||
<i class="fa-solid fa-arrow-left mr-2"></i>Zurück
|
<i class="fa-solid fa-arrow-left mr-2"></i>Zurück
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mt-8 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
<p>Benötigen Sie Hilfe? <a href="#" class="text-primary-600 dark:text-primary-400 hover:underline">Kontaktieren Sie uns</a></p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -3,21 +3,31 @@
|
|||||||
{% block title %}404 - Seite nicht gefunden{% endblock %}
|
{% block title %}404 - Seite nicht gefunden{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="min-h-[65vh] flex flex-col items-center justify-center px-4 py-12">
|
<div class="min-h-[75vh] flex flex-col items-center justify-center px-4 py-12 bg-gradient-to-b from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800">
|
||||||
<div class="glass-effect max-w-2xl w-full p-8 rounded-lg border border-gray-300/20 dark:border-gray-700/30 shadow-lg">
|
<div class="glass-effect max-w-2xl w-full p-6 md:p-10 rounded-xl border border-gray-300/20 dark:border-gray-700/30 shadow-xl transform transition-all duration-300 hover:shadow-2xl">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<h1 class="text-6xl font-bold text-primary-600 dark:text-primary-400 mb-4">404</h1>
|
<div class="flex justify-center mb-6">
|
||||||
<h2 class="text-2xl font-semibold mb-4">Seite nicht gefunden</h2>
|
<div class="relative">
|
||||||
<p class="text-gray-600 dark:text-gray-300 mb-8">Die gesuchte Seite existiert nicht oder wurde verschoben. Bitte prüfen Sie die URL oder nutzen Sie die Navigation.</p>
|
<h1 class="text-7xl md:text-8xl font-extrabold text-primary-600 dark:text-primary-400 opacity-90">404</h1>
|
||||||
|
<div class="absolute -top-4 -right-4 w-12 h-12 bg-yellow-500 rounded-full flex items-center justify-center animate-pulse">
|
||||||
|
<i class="fa-solid fa-question text-white text-xl"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h2 class="text-2xl md:text-3xl font-semibold mb-4 text-gray-800 dark:text-gray-200">Seite nicht gefunden</h2>
|
||||||
|
<p class="text-gray-600 dark:text-gray-300 mb-8 max-w-lg mx-auto text-base md:text-lg">Die gesuchte Seite existiert nicht oder wurde verschoben. Bitte prüfen Sie die URL oder nutzen Sie die Navigation.</p>
|
||||||
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||||
<a href="{{ url_for('index') }}" class="btn-primary">
|
<a href="{{ url_for('index') }}" class="btn-primary transform transition-transform duration-300 hover:scale-105 px-6 py-3 rounded-lg">
|
||||||
<i class="fa-solid fa-home mr-2"></i>Zur Startseite
|
<i class="fa-solid fa-home mr-2"></i>Zur Startseite
|
||||||
</a>
|
</a>
|
||||||
<a href="javascript:history.back()" class="btn-secondary">
|
<a href="javascript:history.back()" class="btn-secondary transform transition-transform duration-300 hover:scale-105 px-6 py-3 rounded-lg">
|
||||||
<i class="fa-solid fa-arrow-left mr-2"></i>Zurück
|
<i class="fa-solid fa-arrow-left mr-2"></i>Zurück
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mt-8 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
<p>Benötigen Sie Hilfe? <a href="#" class="text-primary-600 dark:text-primary-400 hover:underline">Kontaktieren Sie uns</a></p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -3,21 +3,31 @@
|
|||||||
{% block title %}429 - Zu viele Anfragen{% endblock %}
|
{% block title %}429 - Zu viele Anfragen{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="min-h-[65vh] flex flex-col items-center justify-center px-4 py-12">
|
<div class="min-h-[75vh] flex flex-col items-center justify-center px-4 py-12 bg-gradient-to-b from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800">
|
||||||
<div class="glass-effect max-w-2xl w-full p-8 rounded-lg border border-gray-300/20 dark:border-gray-700/30 shadow-lg">
|
<div class="glass-effect max-w-2xl w-full p-6 md:p-10 rounded-xl border border-gray-300/20 dark:border-gray-700/30 shadow-xl transform transition-all duration-300 hover:shadow-2xl">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<h1 class="text-6xl font-bold text-primary-600 dark:text-primary-400 mb-4">429</h1>
|
<div class="flex justify-center mb-6">
|
||||||
<h2 class="text-2xl font-semibold mb-4">Zu viele Anfragen</h2>
|
<div class="relative">
|
||||||
<p class="text-gray-600 dark:text-gray-300 mb-8">Sie haben zu viele Anfragen in kurzer Zeit gestellt. Bitte warten Sie einen Moment und versuchen Sie es dann erneut.</p>
|
<h1 class="text-7xl md:text-8xl font-extrabold text-primary-600 dark:text-primary-400 opacity-90">429</h1>
|
||||||
|
<div class="absolute -top-4 -right-4 w-12 h-12 bg-orange-500 rounded-full flex items-center justify-center animate-pulse">
|
||||||
|
<i class="fa-solid fa-hourglass-half text-white text-xl"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h2 class="text-2xl md:text-3xl font-semibold mb-4 text-gray-800 dark:text-gray-200">Zu viele Anfragen</h2>
|
||||||
|
<p class="text-gray-600 dark:text-gray-300 mb-8 max-w-lg mx-auto text-base md:text-lg">Sie haben zu viele Anfragen in kurzer Zeit gestellt. Bitte warten Sie einen Moment und versuchen Sie es dann erneut.</p>
|
||||||
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||||
<a href="{{ url_for('index') }}" class="btn-primary">
|
<a href="{{ url_for('index') }}" class="btn-primary transform transition-transform duration-300 hover:scale-105 px-6 py-3 rounded-lg">
|
||||||
<i class="fa-solid fa-home mr-2"></i>Zur Startseite
|
<i class="fa-solid fa-home mr-2"></i>Zur Startseite
|
||||||
</a>
|
</a>
|
||||||
<a href="javascript:history.back()" class="btn-secondary">
|
<a href="javascript:history.back()" class="btn-secondary transform transition-transform duration-300 hover:scale-105 px-6 py-3 rounded-lg">
|
||||||
<i class="fa-solid fa-arrow-left mr-2"></i>Zurück
|
<i class="fa-solid fa-arrow-left mr-2"></i>Zurück
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mt-8 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
<p>Benötigen Sie Hilfe? <a href="#" class="text-primary-600 dark:text-primary-400 hover:underline">Kontaktieren Sie uns</a></p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user