Compare commits
230 Commits
till-v2
...
37c457ca3f
| Author | SHA1 | Date | |
|---|---|---|---|
| 37c457ca3f | |||
| 936d983cb3 | |||
| 9ed9adfeaf | |||
| 9c1475844c | |||
| 310d0af0d1 | |||
| cab8d28aeb | |||
| 9dc44f94f6 | |||
| 5b9ae85453 | |||
| 302d5213ef | |||
| bb3211ab3d | |||
| 2a246ee063 | |||
| fc8861c73c | |||
| 8c49e7396e | |||
| 8e3c81fd06 | |||
| f18d23cfea | |||
| d3405a7031 | |||
| 5793902e47 | |||
| e73ccd7e80 | |||
| e6784b712d | |||
| 35b5f321d4 | |||
| b68f65cc76 | |||
| 3a2f721f63 | |||
| 5933195196 | |||
| beccfa25a6 | |||
| bc5cef3ba8 | |||
| b867af9c8b | |||
| ee04432a49 | |||
| bbcee7f610 | |||
| 1eb47fc230 | |||
| 2921c5a824 | |||
| c98e238841 | |||
| af30a208ca | |||
| 2e2f35ccc1 | |||
| fd293e53e1 | |||
| 2b19cb000b | |||
| 3aefe6c5e6 | |||
| c7b87dc643 | |||
| dc96252013 | |||
| ab56f44ae9 | |||
| 61124f5266 | |||
| fab8d10f03 | |||
| dec30e4681 | |||
| a1bd999c6a | |||
| b1d33ce643 | |||
| 293f877017 | |||
| e86d0b0f90 | |||
| 059fd167d6 | |||
| 256d38e140 | |||
| 4b75489631 | |||
| cb95c78276 | |||
| 00cb100467 | |||
| 8c66461dc8 | |||
| 566f84fc0c | |||
| 07eae42ba3 | |||
| 0a1bebd862 | |||
| 59b79b3466 | |||
| 6f5526b648 | |||
| 21148f0c0e | |||
| ba6cac32a9 | |||
| 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
|
||||
|
||||
# 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_KEY=sk-dein-openai-api-schluessel-hier
|
||||
OPENAI_API_KEY=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
|
||||
|
||||
# Datenbank
|
||||
# Bei Bedarf kann hier eine andere Datenbank-URL angegeben werden
|
||||
# Der Pfad wird relativ zum Projektverzeichnis angegeben
|
||||
# SQLALCHEMY_DATABASE_URI=sqlite:////absoluter/pfad/zu/database/systades.db OPENAI_API_KEY=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
|
||||
# SQLALCHEMY_DATABASE_URI=sqlite:////absoluter/pfad/zu/database/systades.db
|
||||
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
logs/app.log
|
||||
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
|
||||
- **Backend**: Python/Flask
|
||||
- **Frontend**:
|
||||
- Tailwind CSS für moderne UI
|
||||
- Tailwind CSS (via CDN) für moderne UI
|
||||
- SVG-Bibliotheken für Visualisierungen (D3.js)
|
||||
- JavaScript/Alpine.js für interaktive Komponenten
|
||||
- WebGL für animierte Hintergrundeffekte
|
||||
- **Datenbank**: SQLite mit SQLAlchemy
|
||||
- **KI-Integration**: OpenAI API für intelligente Assistenz
|
||||
|
||||
@@ -61,16 +62,20 @@ Für detaillierte Hilfe: `python TOOLS.py -h`
|
||||
- [x] Favicon erstellen
|
||||
- [x] Setup-Skript für einfache Installation
|
||||
|
||||
### Phase 2: Design-Überarbeitung 🔄
|
||||
### Phase 2: Design-Überarbeitung ✅
|
||||
- [x] Implementierung des Dark Mode
|
||||
- [x] Erstellung eines modernen, minimalistischen UI mit Tech-Ästhetik
|
||||
- [x] Responsive Design für alle Geräte
|
||||
- [ ] Gestaltung der Landing Page mit großer Typografie
|
||||
- [x] Gestaltung der Landing Page mit großer Typografie
|
||||
- [x] Animierter Neurales Netzwerk-Hintergrund mit WebGL
|
||||
|
||||
### Phase 3: Mindmap-Funktionalitäten 🔄
|
||||
- [x] Verbesserte Visualisierung mit SVG und D3.js
|
||||
- [x] Implementierung der Mouseover-Funktion
|
||||
- [x] Entwicklung der Suchfunktion für Knoten
|
||||
- [x] Clustertopologie für neuronale Netzwerkdarstellung
|
||||
- [x] Fehlerbehandlung für robuste Datenverarbeitung und Knotenverweise
|
||||
- [x] Verbesserte Verbindungserkennung zwischen Knoten
|
||||
- [ ] Tagging-System für Inhalte
|
||||
- [ ] Quellenmanagement und -verlinkung
|
||||
- [ ] Upload-Funktionalität an Knotenpunkten
|
||||
@@ -115,8 +120,8 @@ Für detaillierte Hilfe: `python TOOLS.py -h`
|
||||
|
||||
## Aktueller Status
|
||||
- **Phase 1**: ✅ Abgeschlossen
|
||||
- **Phase 2**: 🔄 In Bearbeitung (75% abgeschlossen)
|
||||
- **Phase 3**: 🔄 In Bearbeitung (50% abgeschlossen)
|
||||
- **Phase 2**: ✅ Abgeschlossen
|
||||
- **Phase 3**: 🔄 In Bearbeitung (75% abgeschlossen)
|
||||
|
||||
## Aktuelle Fortschritte
|
||||
- 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
|
||||
- Mindmap-Visualisierung komplett überarbeitet mit D3.js für eine interaktivere Erfahrung
|
||||
- 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
|
||||
- Fertigstellung der Landing Page
|
||||
- Erstellung der "Wer sind wir?"-Seite
|
||||
- Implementierung des Tagging-Systems für Gedanken
|
||||
- Fertigstellung des Tagging-Systems für Gedanken
|
||||
- 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*
|
||||
96
ROADMAP.md
96
ROADMAP.md
@@ -2,7 +2,7 @@
|
||||
|
||||
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] 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] 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
|
||||
- [ ] Implementierung von AJAX-Anfragen zum Laden der Mindmap-Daten
|
||||
- [ ] Dynamisches Rendering der Knoten, Verbindungen und Labels
|
||||
- [ ] Drag-and-Drop-Funktionalität für die Bewegung von Knoten
|
||||
- [ ] Zoom- und Pan-Funktionalität mit Persistenz der Ansicht
|
||||
- [x] Anpassung des Frontend-Codes zur Verwendung der DB-Daten anstelle des SVG
|
||||
- [x] Implementierung von AJAX-Anfragen zum Laden der Mindmap-Daten
|
||||
- [x] Dynamisches Rendering der Knoten, Verbindungen und Labels
|
||||
- [x] Drag-and-Drop-Funktionalität für die Bewegung von Knoten
|
||||
- [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
|
||||
- [ ] Speichern der Knotenpositionen und Ansichtseinstellungen
|
||||
- [ ] Benutzerspezifische Visualisierungseinstellungen
|
||||
- [ ] 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
|
||||
- [ ] Visuelle Anzeige von Notizen in der Mindmap
|
||||
- [ ] Texteditor mit Markdown-Unterstützung für Notizen
|
||||
- [ ] Kategorisierung und Farbkodierung von Notizen
|
||||
- [ ] Suchfunktion für Notizen
|
||||
|
||||
## Phase 5: Integrationen und Erweiterungen
|
||||
## 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)
|
||||
- [ ] 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)
|
||||
- [ ] 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
|
||||
- [ ] Automatische Kategorisierung von Inhalten
|
||||
@@ -50,7 +73,7 @@ Diese Roadmap beschreibt die geplante Entwicklung der dynamischen, benutzerorien
|
||||
- [ ] Mindmap-Statistiken und Analysen
|
||||
- [ ] KI-basierte Zusammenfassung von Teilbereichen der Mindmap
|
||||
|
||||
## Phase 7: Optimierung und Skalierung
|
||||
## Phase 9: Optimierung und Skalierung
|
||||
|
||||
- [ ] Performance-Optimierung für große Mindmaps
|
||||
- [ ] 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
|
||||
- [ ] 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
|
||||
@@ -85,6 +122,7 @@ Das Datenbankschema umfasst folgende Hauptentitäten:
|
||||
### Frontend-Technologien
|
||||
|
||||
- D3.js für die Visualisierung der Mindmap
|
||||
- WebGL für den neuronalen Netzwerk-Hintergrund
|
||||
- AJAX für dynamisches Laden von Daten
|
||||
- Interaktive Bedienelemente mit JavaScript
|
||||
- Responsive Design mit Tailwind CSS
|
||||
@@ -100,3 +138,35 @@ Die implementierten API-Endpunkte umfassen:
|
||||
- `/api/mindmap/<id>/update_node_position` - Aktualisierung von Knotenpositionen
|
||||
- `/api/mindmap/<id>/notes` - Verwaltung von Notizen
|
||||
- `/api/nodes/<id>/thoughts` - Abrufen und Hinzufügen von Gedanken zu Knoten
|
||||
- `/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.
2526
app.py.bak
Normal file
2526
app.py.bak
Normal file
File diff suppressed because it is too large
Load Diff
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.
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
|
||||
|
||||
# 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_KEY=sk-dein-openai-api-schluessel-hier
|
||||
OPENAI_API_KEY=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
|
||||
|
||||
# Datenbank
|
||||
# Bei Bedarf kann hier eine andere Datenbank-URL angegeben werden
|
||||
# Der Pfad wird relativ zum Projektverzeichnis angegeben
|
||||
# SQLALCHEMY_DATABASE_URI=sqlite:////absoluter/pfad/zu/database/systades.db
|
||||
SQLALCHEMY_DATABASE_URI=sqlite:///database/systades.db
|
||||
550
init_db.py
Executable file → Normal file
550
init_db.py
Executable file → Normal file
@@ -1,256 +1,320 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from app import app, initialize_database, db_path
|
||||
import os
|
||||
import sqlite3
|
||||
from flask import Flask
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from datetime import datetime
|
||||
|
||||
# Pfad zur Datenbank
|
||||
basedir = os.path.abspath(os.path.dirname(__file__))
|
||||
db_path = os.path.join(basedir, 'database', 'systades.db')
|
||||
|
||||
# Stelle sicher, dass das Verzeichnis existiert
|
||||
db_dir = os.path.dirname(db_path)
|
||||
os.makedirs(db_dir, exist_ok=True)
|
||||
|
||||
# Erstelle eine temporäre Flask-App, um die Datenbank zu initialisieren
|
||||
app = Flask(__name__)
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{db_path}'
|
||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||
|
||||
# Importiere die Modelle nach der App-Initialisierung
|
||||
from models import db, User, Thought, Comment, MindMapNode, ThoughtRelation, ThoughtRating, RelationType
|
||||
from models import Category, UserMindmap, UserMindmapNode, MindmapNote
|
||||
import os
|
||||
|
||||
def init_database():
|
||||
"""Initialisiert die Datenbank mit Beispieldaten."""
|
||||
with app.app_context():
|
||||
# Datenbank löschen und neu erstellen
|
||||
if os.path.exists(db_path):
|
||||
os.remove(db_path)
|
||||
|
||||
# Stellen Sie sicher, dass das Verzeichnis existiert
|
||||
os.makedirs(os.path.dirname(db_path), exist_ok=True)
|
||||
|
||||
db.create_all()
|
||||
|
||||
# Admin-Benutzer erstellen
|
||||
admin = User(username='admin', email='admin@example.com', is_admin=True)
|
||||
admin.set_password('admin')
|
||||
db.session.add(admin)
|
||||
|
||||
# Beispiel-Benutzer erstellen
|
||||
user = User(username='user', email='user@example.com')
|
||||
user.set_password('user')
|
||||
db.session.add(user)
|
||||
|
||||
# Commit, um IDs zu generieren
|
||||
db.session.commit()
|
||||
|
||||
# Wissenschaftliche Kategorien erstellen
|
||||
science = Category(name='Wissenschaft', description='Wissenschaftliche Erkenntnisse',
|
||||
color_code='#4CAF50', icon='flask')
|
||||
db.session.add(science)
|
||||
|
||||
philosophy = Category(name='Philosophie', description='Philosophische Theorien und Gedanken',
|
||||
color_code='#9C27B0', icon='lightbulb')
|
||||
db.session.add(philosophy)
|
||||
|
||||
technology = Category(name='Technologie', description='Technologische Entwicklungen',
|
||||
color_code='#FF9800', icon='microchip')
|
||||
db.session.add(technology)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
# Wissenschaftliche Unterkategorien
|
||||
physics = Category(name='Physik', description='Studium der Materie und Energie',
|
||||
color_code='#81C784', icon='atom', parent_id=science.id)
|
||||
biology = Category(name='Biologie', description='Studium lebender Organismen',
|
||||
color_code='#66BB6A', icon='leaf', parent_id=science.id)
|
||||
chemistry = Category(name='Chemie', description='Studium der Stoffe und ihrer Reaktionen',
|
||||
color_code='#A5D6A7', icon='vial', parent_id=science.id)
|
||||
|
||||
db.session.add_all([physics, biology, chemistry])
|
||||
|
||||
# Technologie-Unterkategorien
|
||||
informatics = Category(name='Informatik', description='Studium der Informationsverarbeitung',
|
||||
color_code='#FFB74D', icon='laptop-code', parent_id=technology.id)
|
||||
ai = Category(name='Künstliche Intelligenz', description='Entwicklung intelligenter Systeme',
|
||||
color_code='#FFA726', icon='robot', parent_id=technology.id)
|
||||
|
||||
db.session.add_all([informatics, ai])
|
||||
|
||||
# Philosophie-Unterkategorien
|
||||
ethics = Category(name='Ethik', description='Moralphilosophie und Wertesysteme',
|
||||
color_code='#BA68C8', icon='balance-scale', parent_id=philosophy.id)
|
||||
logic = Category(name='Logik', description='Studie der gültigen Schlussfolgerungen',
|
||||
color_code='#AB47BC', icon='project-diagram', parent_id=philosophy.id)
|
||||
|
||||
db.session.add_all([ethics, logic])
|
||||
|
||||
db.session.commit()
|
||||
|
||||
# Knoten für die öffentliche Mindmap erstellen
|
||||
nodes = {
|
||||
'quantenmechanik': MindMapNode(
|
||||
name='Quantenmechanik',
|
||||
description='Physikalische Theorie zur Beschreibung der Materie auf atomarer Ebene',
|
||||
color_code='#81C784',
|
||||
category_id=physics.id,
|
||||
created_by_id=admin.id
|
||||
),
|
||||
'relativitaetstheorie': MindMapNode(
|
||||
name='Relativitätstheorie',
|
||||
description='Einsteins Theorien zur Raumzeit und Gravitation',
|
||||
color_code='#81C784',
|
||||
category_id=physics.id,
|
||||
created_by_id=admin.id
|
||||
),
|
||||
'genetik': MindMapNode(
|
||||
name='Genetik',
|
||||
description='Wissenschaft der Gene und Vererbung',
|
||||
color_code='#66BB6A',
|
||||
category_id=biology.id,
|
||||
created_by_id=admin.id
|
||||
),
|
||||
'machine_learning': MindMapNode(
|
||||
name='Machine Learning',
|
||||
description='Algorithmen, die aus Daten lernen können',
|
||||
color_code='#FFA726',
|
||||
category_id=ai.id,
|
||||
created_by_id=admin.id
|
||||
),
|
||||
'ki_ethik': MindMapNode(
|
||||
name='KI-Ethik',
|
||||
description='Moralische Implikationen künstlicher Intelligenz',
|
||||
color_code='#BA68C8',
|
||||
category_id=ethics.id,
|
||||
created_by_id=user.id
|
||||
)
|
||||
}
|
||||
|
||||
for node in nodes.values():
|
||||
db.session.add(node)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
# Verknüpfungen zwischen Knoten herstellen (Hierarchie)
|
||||
nodes['machine_learning'].parents.append(nodes['ki_ethik'])
|
||||
db.session.commit()
|
||||
|
||||
# Gedanken erstellen
|
||||
thoughts = [
|
||||
{
|
||||
'title': 'Künstliche Intelligenz und Bewusstsein',
|
||||
'content': 'Die Frage nach maschinellem Bewusstsein ist fundamental für die KI-Ethik. Aktuelle KI-Systeme haben kein Bewusstsein, aber fortschrittliche KI könnte in Zukunft Eigenschaften entwickeln, die diesem nahekommen.',
|
||||
'abstract': 'Eine Untersuchung der philosophischen Implikationen von KI-Bewusstsein.',
|
||||
'keywords': 'KI, Bewusstsein, Ethik, Philosophie',
|
||||
'branch': 'Philosophie',
|
||||
'color_code': '#BA68C8',
|
||||
'source_type': 'Markdown',
|
||||
'user_id': user.id,
|
||||
'node': nodes['ki_ethik']
|
||||
},
|
||||
{
|
||||
'title': 'Quantenmechanik und Realität',
|
||||
'content': 'Die Kopenhagener Deutung und ihre Auswirkungen auf unser Verständnis der Realität. Quantenmechanik stellt grundlegende Annahmen über Determinismus und Lokalität in Frage.',
|
||||
'abstract': 'Eine Analyse verschiedener Interpretationen der Quantenmechanik.',
|
||||
'keywords': 'Quantenmechanik, Physik, Realität',
|
||||
'branch': 'Physik',
|
||||
'color_code': '#81C784',
|
||||
'source_type': 'PDF',
|
||||
'user_id': admin.id,
|
||||
'node': nodes['quantenmechanik']
|
||||
},
|
||||
{
|
||||
'title': 'Deep Learning Fortschritte',
|
||||
'content': 'Die neuesten Fortschritte im Deep Learning haben zu beeindruckenden Ergebnissen in Bereichen wie Computer Vision, Natural Language Processing und Reinforcement Learning geführt.',
|
||||
'abstract': 'Überblick über aktuelle Deep Learning-Techniken und ihre Anwendungen.',
|
||||
'keywords': 'Deep Learning, Neural Networks, AI',
|
||||
'branch': 'Technologie',
|
||||
'color_code': '#FFA726',
|
||||
'source_type': 'Webpage',
|
||||
'user_id': admin.id,
|
||||
'node': nodes['machine_learning']
|
||||
}
|
||||
]
|
||||
|
||||
thought_objects = []
|
||||
for t_data in thoughts:
|
||||
node = t_data.pop('node')
|
||||
thought = Thought(**t_data)
|
||||
node.thoughts.append(thought)
|
||||
thought_objects.append(thought)
|
||||
db.session.add(thought)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
# Beziehungen zwischen Gedanken
|
||||
relation = ThoughtRelation(
|
||||
source_id=thought_objects[0].id,
|
||||
target_id=thought_objects[2].id,
|
||||
relation_type=RelationType.INSPIRES,
|
||||
created_by_id=user.id
|
||||
)
|
||||
db.session.add(relation)
|
||||
|
||||
# Bewertungen erstellen
|
||||
rating1 = ThoughtRating(
|
||||
thought_id=thought_objects[0].id,
|
||||
user_id=admin.id,
|
||||
relevance_score=5
|
||||
)
|
||||
rating2 = ThoughtRating(
|
||||
thought_id=thought_objects[2].id,
|
||||
user_id=user.id,
|
||||
relevance_score=4
|
||||
)
|
||||
db.session.add_all([rating1, rating2])
|
||||
|
||||
# Kommentare erstellen
|
||||
for thought in thought_objects:
|
||||
comment = Comment(
|
||||
content=f'Interessante Perspektive zu {thought.title}!',
|
||||
thought_id=thought.id,
|
||||
user_id=admin.id if thought.user_id != admin.id else user.id
|
||||
)
|
||||
db.session.add(comment)
|
||||
|
||||
# Benutzer-Mindmaps erstellen
|
||||
user_mindmap = UserMindmap(
|
||||
name='Meine KI-Forschung',
|
||||
description='Meine persönliche Sammlung zu KI und Ethik',
|
||||
user_id=user.id
|
||||
)
|
||||
db.session.add(user_mindmap)
|
||||
db.session.commit()
|
||||
|
||||
# Knoten zur Benutzer-Mindmap hinzufügen
|
||||
user_mindmap_nodes = [
|
||||
UserMindmapNode(
|
||||
user_mindmap_id=user_mindmap.id,
|
||||
node_id=nodes['machine_learning'].id,
|
||||
x_position=200,
|
||||
y_position=300
|
||||
),
|
||||
UserMindmapNode(
|
||||
user_mindmap_id=user_mindmap.id,
|
||||
node_id=nodes['ki_ethik'].id,
|
||||
x_position=500,
|
||||
y_position=200
|
||||
)
|
||||
]
|
||||
db.session.add_all(user_mindmap_nodes)
|
||||
|
||||
# Private Notizen
|
||||
note = MindmapNote(
|
||||
user_id=user.id,
|
||||
mindmap_id=user_mindmap.id,
|
||||
node_id=nodes['ki_ethik'].id,
|
||||
content="Recherchiere mehr über aktuelle ethische Richtlinien für KI-Entwicklung!",
|
||||
color_code="#FFF59D"
|
||||
)
|
||||
db.session.add(note)
|
||||
|
||||
# Gedanken zu Bookmarks hinzufügen
|
||||
user.bookmarked_thoughts.append(thought_objects[0])
|
||||
admin.bookmarked_thoughts.append(thought_objects[1])
|
||||
|
||||
# Finaler Commit
|
||||
db.session.commit()
|
||||
|
||||
print("Datenbank wurde erfolgreich initialisiert!")
|
||||
db.init_app(app)
|
||||
|
||||
def init_db():
|
||||
"""Alias für Kompatibilität mit älteren Scripts."""
|
||||
init_database()
|
||||
with app.app_context():
|
||||
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"""
|
||||
# Hauptkategorien
|
||||
main_categories = [
|
||||
{
|
||||
"name": "Philosophie",
|
||||
"description": "Philosophisches Denken und Konzepte",
|
||||
"color_code": "#9F7AEA",
|
||||
"icon": "fa-brain"
|
||||
},
|
||||
{
|
||||
"name": "Wissenschaft",
|
||||
"description": "Wissenschaftliche Disziplinen und Erkenntnisse",
|
||||
"color_code": "#60A5FA",
|
||||
"icon": "fa-flask"
|
||||
},
|
||||
{
|
||||
"name": "Technologie",
|
||||
"description": "Technologische Entwicklungen und Anwendungen",
|
||||
"color_code": "#10B981",
|
||||
"icon": "fa-microchip"
|
||||
},
|
||||
{
|
||||
"name": "Künste",
|
||||
"description": "Künstlerische Ausdrucksformen und Werke",
|
||||
"color_code": "#F59E0B",
|
||||
"icon": "fa-palette"
|
||||
},
|
||||
{
|
||||
"name": "Psychologie",
|
||||
"description": "Mentale Prozesse und Verhaltensweisen",
|
||||
"color_code": "#EF4444",
|
||||
"icon": "fa-brain"
|
||||
}
|
||||
]
|
||||
|
||||
# Hauptkategorien erstellen
|
||||
category_map = {}
|
||||
for cat_data in main_categories:
|
||||
category = Category(**cat_data)
|
||||
db.session.add(category)
|
||||
db.session.flush() # ID generieren
|
||||
category_map[cat_data["name"]] = category
|
||||
|
||||
# Unterkategorien für Philosophie
|
||||
philosophy_subcategories = [
|
||||
{"name": "Ethik", "description": "Moralische Grundsätze", "icon": "fa-balance-scale", "color_code": "#8B5CF6"},
|
||||
{"name": "Logik", "description": "Gesetze des Denkens", "icon": "fa-project-diagram", "color_code": "#8B5CF6"},
|
||||
{"name": "Erkenntnistheorie", "description": "Natur des Wissens", "icon": "fa-lightbulb", "color_code": "#8B5CF6"}
|
||||
]
|
||||
|
||||
# Unterkategorien für Wissenschaft
|
||||
science_subcategories = [
|
||||
{"name": "Physik", "description": "Studie der Materie und Energie", "icon": "fa-atom", "color_code": "#3B82F6"},
|
||||
{"name": "Biologie", "description": "Studie des Lebens", "icon": "fa-dna", "color_code": "#3B82F6"},
|
||||
{"name": "Mathematik", "description": "Studie der Zahlen und Strukturen", "icon": "fa-square-root-alt", "color_code": "#3B82F6"}
|
||||
]
|
||||
|
||||
# Unterkategorien für Technologie
|
||||
tech_subcategories = [
|
||||
{"name": "Software", "description": "Computerprogramme und Anwendungen", "icon": "fa-code", "color_code": "#059669"},
|
||||
{"name": "Hardware", "description": "Physische Komponenten der Technik", "icon": "fa-microchip", "color_code": "#059669"},
|
||||
{"name": "Internet", "description": "Globales Netzwerk und Web", "icon": "fa-globe", "color_code": "#059669"}
|
||||
]
|
||||
|
||||
# Unterkategorien für Künste
|
||||
arts_subcategories = [
|
||||
{"name": "Musik", "description": "Klangkunst", "icon": "fa-music", "color_code": "#D97706"},
|
||||
{"name": "Literatur", "description": "Geschriebene Kunst", "icon": "fa-book", "color_code": "#D97706"},
|
||||
{"name": "Bildende Kunst", "description": "Visuelle Kunst", "icon": "fa-paint-brush", "color_code": "#D97706"}
|
||||
]
|
||||
|
||||
# Unterkategorien für Psychologie
|
||||
psychology_subcategories = [
|
||||
{"name": "Kognition", "description": "Gedächtnisprozesse und Denken", "icon": "fa-brain", "color_code": "#DC2626"},
|
||||
{"name": "Emotionen", "description": "Gefühle und emotionale Prozesse", "icon": "fa-heart", "color_code": "#DC2626"},
|
||||
{"name": "Verhalten", "description": "Beobachtbares Verhalten und Reaktionen", "icon": "fa-user", "color_code": "#DC2626"}
|
||||
]
|
||||
|
||||
# Alle Unterkategorien zu ihren Hauptkategorien hinzufügen
|
||||
for subcat_data in philosophy_subcategories:
|
||||
subcat = Category(**subcat_data)
|
||||
subcat.parent_id = category_map["Philosophie"].id
|
||||
db.session.add(subcat)
|
||||
|
||||
for subcat_data in science_subcategories:
|
||||
subcat = Category(**subcat_data)
|
||||
subcat.parent_id = category_map["Wissenschaft"].id
|
||||
db.session.add(subcat)
|
||||
|
||||
for subcat_data in tech_subcategories:
|
||||
subcat = Category(**subcat_data)
|
||||
subcat.parent_id = category_map["Technologie"].id
|
||||
db.session.add(subcat)
|
||||
|
||||
for subcat_data in arts_subcategories:
|
||||
subcat = Category(**subcat_data)
|
||||
subcat.parent_id = category_map["Künste"].id
|
||||
db.session.add(subcat)
|
||||
|
||||
for subcat_data in psychology_subcategories:
|
||||
subcat = Category(**subcat_data)
|
||||
subcat.parent_id = category_map["Psychologie"].id
|
||||
db.session.add(subcat)
|
||||
|
||||
db.session.commit()
|
||||
print(f"{len(main_categories)} Hauptkategorien und {len(philosophy_subcategories + science_subcategories + tech_subcategories + arts_subcategories + psychology_subcategories)} Unterkategorien 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__':
|
||||
init_database()
|
||||
init_db()
|
||||
print("Datenbank wurde erfolgreich initialisiert!")
|
||||
print("Sie können die Anwendung jetzt mit 'python app.py' starten")
|
||||
print("Anmelden mit:")
|
||||
|
||||
1219
logs/app.log
Normal file
1219
logs/app.log
Normal file
File diff suppressed because it is too large
Load Diff
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 ###
|
||||
187
models.py
Executable file → Normal file
187
models.py
Executable file → Normal file
@@ -6,6 +6,8 @@ from flask_login import UserMixin
|
||||
from datetime import datetime
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
from enum import Enum
|
||||
import uuid as uuid_pkg
|
||||
import os
|
||||
|
||||
db = SQLAlchemy()
|
||||
|
||||
@@ -43,30 +45,45 @@ user_thought_bookmark = db.Table('user_thought_bookmark',
|
||||
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)
|
||||
username = db.Column(db.String(80), unique=True, nullable=False)
|
||||
email = db.Column(db.String(120), unique=True, nullable=False)
|
||||
password_hash = db.Column(db.String(128))
|
||||
is_admin = db.Column(db.Boolean, default=False)
|
||||
password = db.Column(db.String(512), nullable=False)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
last_login = db.Column(db.DateTime)
|
||||
avatar = db.Column(db.String(200))
|
||||
bio = db.Column(db.Text)
|
||||
is_active = db.Column(db.Boolean, default=True)
|
||||
role = db.Column(db.String(20), default="user") # 'user', 'admin', 'moderator'
|
||||
bio = db.Column(db.Text, nullable=True) # Profil-Bio
|
||||
location = db.Column(db.String(100), nullable=True) # Standort
|
||||
website = db.Column(db.String(200), nullable=True) # Website
|
||||
avatar = db.Column(db.String(200), nullable=True) # Profilbild-URL
|
||||
last_login = db.Column(db.DateTime, nullable=True) # Letzter Login
|
||||
|
||||
# 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)
|
||||
comments = db.relationship('Comment', backref='author', lazy=True)
|
||||
user_mindmaps = db.relationship('UserMindmap', backref='user', lazy=True)
|
||||
mindmap_notes = db.relationship('MindmapNote', backref='user', lazy=True)
|
||||
bookmarked_thoughts = db.relationship('Thought', secondary=user_thought_bookmark,
|
||||
backref=db.backref('bookmarked_by', lazy='dynamic'))
|
||||
lazy='dynamic', backref=db.backref('bookmarked_by', lazy='dynamic'))
|
||||
|
||||
def __repr__(self):
|
||||
return f'<User {self.username}>'
|
||||
|
||||
def set_password(self, password):
|
||||
self.password_hash = generate_password_hash(password)
|
||||
self.password = generate_password_hash(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):
|
||||
"""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]))
|
||||
nodes = db.relationship('MindMapNode', backref='category', lazy=True)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Category {self.name}>'
|
||||
|
||||
class MindMapNode(db.Model):
|
||||
"""Öffentliche Mindmap-Knoten, die für alle Benutzer sichtbar sind"""
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
@@ -93,6 +113,8 @@ class MindMapNode(db.Model):
|
||||
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)
|
||||
|
||||
__table_args__ = {'extend_existing': True}
|
||||
|
||||
# Beziehungen für Baumstruktur (mehrere Eltern möglich)
|
||||
parents = db.relationship(
|
||||
'MindMapNode',
|
||||
@@ -111,6 +133,20 @@ class MindMapNode(db.Model):
|
||||
# Beziehung zum Ersteller
|
||||
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):
|
||||
"""Benutzerspezifische Mindmap, die vom Benutzer personalisierbar ist"""
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
@@ -228,3 +264,128 @@ class Comment(db.Model):
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
last_modified = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
# 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
|
||||
werkzeug==2.2.3
|
||||
flask-sqlalchemy==3.0.5
|
||||
openai==1.3.0
|
||||
openai
|
||||
requests==2.31.0
|
||||
flask-cors==4.0.0
|
||||
gunicorn==21.2.0
|
||||
#pillow==10.0.1
|
||||
pytest==7.4.0
|
||||
pytest-flask==1.2.0
|
||||
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 {
|
||||
font-family: 'Inter', sans-serif;
|
||||
bottom: 5.5rem;
|
||||
z-index: 100;
|
||||
max-height: 85vh;
|
||||
}
|
||||
|
||||
#assistant-chat {
|
||||
transition: max-height 0.3s ease, opacity 0.3s ease;
|
||||
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.2);
|
||||
transition: max-height 0.4s cubic-bezier(0.25, 0.1, 0.25, 1),
|
||||
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;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#assistant-toggle {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
#assistant-toggle:hover {
|
||||
transform: scale(1.1);
|
||||
max-width: calc(100vw - 2rem);
|
||||
max-height: 80vh !important;
|
||||
}
|
||||
|
||||
#assistant-history {
|
||||
max-height: calc(80vh - 150px);
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(156, 163, 175, 0.5) transparent;
|
||||
padding-bottom: 2rem; /* Zusätzlicher Abstand unten */
|
||||
}
|
||||
|
||||
#assistant-toggle {
|
||||
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 {
|
||||
@@ -40,27 +51,74 @@
|
||||
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 */
|
||||
.notification-area {
|
||||
bottom: 5rem;
|
||||
}
|
||||
|
||||
/* Verbesserter Glassmorphism-Effekt */
|
||||
/* Verbesserte Glassmorphism-Effekt */
|
||||
.glass-morphism {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
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 {
|
||||
background: rgba(15, 23, 42, 0.3);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
background: rgba(15, 23, 42, 0.35);
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
/* Dunkleres Dark Theme */
|
||||
/* Verbesserte Farbpalette für Dark Theme */
|
||||
.dark {
|
||||
--tw-bg-opacity: 1;
|
||||
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;
|
||||
}
|
||||
|
||||
/* 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 */
|
||||
html, body {
|
||||
height: 100%;
|
||||
@@ -102,3 +216,37 @@ main {
|
||||
footer {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Verbesserte Farbkontraste für Nachrichtenblasen */
|
||||
.user-message {
|
||||
background-color: rgba(124, 58, 237, 0.1) !important;
|
||||
color: #4B5563 !important;
|
||||
}
|
||||
|
||||
body.dark .user-message {
|
||||
background-color: rgba(124, 58, 237, 0.2) !important;
|
||||
color: #F9FAFB !important;
|
||||
}
|
||||
|
||||
.assistant-message {
|
||||
background-color: #F3F4F6 !important;
|
||||
color: #1F2937 !important;
|
||||
border-left: 3px solid #8B5CF6;
|
||||
}
|
||||
|
||||
body.dark .assistant-message {
|
||||
background-color: rgba(31, 41, 55, 0.5) !important;
|
||||
color: #F9FAFB !important;
|
||||
border-left: 3px solid #8B5CF6;
|
||||
}
|
||||
|
||||
/* Chat-Assistent-Position im Footer-Bereich anpassen */
|
||||
.chat-assistant {
|
||||
max-height: 75vh;
|
||||
bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.chat-assistant .chat-messages {
|
||||
max-height: calc(75vh - 180px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
437
static/css/mindmap.css
Normal file
437
static/css/mindmap.css
Normal file
@@ -0,0 +1,437 @@
|
||||
/* Mindmap Container Styles */
|
||||
.mindmap-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 600px;
|
||||
background: var(--bg-primary);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Cytoscape Container für die Hauptmindmap */
|
||||
#cy {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: transparent;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Subpage Styles - Identisches Design wie Hauptmindmap */
|
||||
.mindmap-subpage {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--bg-primary);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 10;
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Subpage Header */
|
||||
.subpage-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
background: var(--bg-secondary);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.dark .subpage-header {
|
||||
background: rgba(30, 41, 59, 0.8);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* Zurück-Button */
|
||||
.back-button {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: none;
|
||||
color: white;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.back-button:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Subpage Titel */
|
||||
.subpage-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
margin: 0;
|
||||
background: linear-gradient(90deg, #60a5fa, #8b5cf6);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
/* Subpage Cytoscape Container */
|
||||
.subpage-cy-container {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
height: calc(100% - 72px);
|
||||
overflow: hidden;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Toolbar für Zoom-Kontrollen */
|
||||
.mindmap-toolbar {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
background: rgba(30, 41, 59, 0.8);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
|
||||
z-index: 20;
|
||||
backdrop-filter: blur(8px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.mindmap-toolbar button {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: none;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: white;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.mindmap-toolbar button:hover {
|
||||
background: rgba(139, 92, 246, 0.5);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.mindmap-toolbar button i {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* Mindmap Header */
|
||||
.mindmap-header {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 1.5rem;
|
||||
background: rgba(15, 23, 42, 0.8);
|
||||
backdrop-filter: blur(10px);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Dark Mode spezifische Stile */
|
||||
.dark .mindmap-subpage {
|
||||
background: linear-gradient(135deg, #0f172a 0%, #0c1221 100%);
|
||||
}
|
||||
|
||||
/* Fix für Zoom-Buttons */
|
||||
body.dark .mindmap-toolbar button {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
body:not(.dark) .mindmap-toolbar button {
|
||||
background: rgba(30, 41, 59, 0.2);
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
/* Kontext-Menü-Anpassungen */
|
||||
.context-menu {
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
/* 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);
|
||||
}
|
||||
|
||||
/* Kategorien-Panel */
|
||||
.categories-panel {
|
||||
position: absolute;
|
||||
top: 80px;
|
||||
left: 20px;
|
||||
width: 300px;
|
||||
max-height: calc(100vh - 120px);
|
||||
background: rgba(15, 23, 42, 0.95);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
z-index: 1000;
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
transform: translateX(-320px);
|
||||
transition: transform 0.3s ease-in-out;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.categories-panel.visible {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.categories-panel h3 {
|
||||
color: white;
|
||||
font-size: 1.2rem;
|
||||
margin: 0 0 16px 0;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.category-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.category-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
margin: 4px 0;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.category-item:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.category-color {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
margin-right: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.category-name {
|
||||
flex-grow: 1;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.category-count {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.8rem;
|
||||
margin-left: 8px;
|
||||
}
|
||||
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;
|
||||
background: linear-gradient(135deg, var(--background-start), var(--background-end));
|
||||
background-attachment: fixed;
|
||||
scroll-behavior: smooth;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Sticky navbar */
|
||||
.navbar.sticky-top {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 50;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
/* Importiere das Cyber-Network CSS */
|
||||
@import url('/static/css/src/cybernetwork-bg.css');
|
||||
/* Light Mode Optimierungen für wichtige UI-Komponenten */
|
||||
|
||||
/* 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
|
||||
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
|
||||
|
||||
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
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Generate favicon.ico from SVG using cairosvg and PIL
|
||||
"""
|
||||
|
||||
import os
|
||||
import io
|
||||
from cairosvg import svg2png
|
||||
from PIL import Image
|
||||
import cairosvg
|
||||
|
||||
# Pfad zum SVG-Favicon
|
||||
svg_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'favicon.svg')
|
||||
# Ausgabepfad für das PNG
|
||||
png_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'favicon.png')
|
||||
# Ausgabepfad für das ICO
|
||||
ico_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'favicon.ico')
|
||||
# Verzeichnis dieses Skripts
|
||||
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
# SVG zu PNG konvertieren
|
||||
cairosvg.svg2png(url=svg_path, write_to=png_path, output_width=512, output_height=512)
|
||||
def svg_to_ico(svg_path, ico_path, sizes=[16, 32, 48, 64, 128, 256]):
|
||||
"""Convert SVG to multi-size ICO file"""
|
||||
img_io = io.BytesIO()
|
||||
|
||||
# PNG zu ICO konvertieren
|
||||
img = Image.open(png_path)
|
||||
img.save(ico_path, sizes=[(16, 16), (32, 32), (48, 48), (64, 64), (128, 128)])
|
||||
# Höchste Auflösung für Zwischenspeicherung
|
||||
max_size = max(sizes)
|
||||
|
||||
print(f"Favicon erfolgreich erstellt: {ico_path}")
|
||||
# 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)
|
||||
|
||||
# Optional: PNG-Datei löschen, wenn nur ICO benötigt wird
|
||||
# os.remove(png_path)
|
||||
# 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!")
|
||||
|
||||
# Ursprüngliches Favicon konvertieren
|
||||
svg_to_ico(
|
||||
os.path.join(CURRENT_DIR, 'favicon.svg'),
|
||||
os.path.join(CURRENT_DIR, 'favicon.ico')
|
||||
)
|
||||
|
||||
# Neues Neuron-Favicon konvertieren
|
||||
svg_to_ico(
|
||||
os.path.join(CURRENT_DIR, 'neuron-favicon.svg'),
|
||||
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
|
||||
*/
|
||||
|
||||
// Import des ChatGPT-Assistenten
|
||||
import ChatGPTAssistant from './modules/chatgpt-assistant.js';
|
||||
|
||||
/**
|
||||
* Hauptmodul für die MindMap-Anwendung
|
||||
* 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
|
||||
*/
|
||||
@@ -27,7 +15,7 @@ const MindMap = {
|
||||
initialized: false,
|
||||
darkMode: document.documentElement.classList.contains('dark'),
|
||||
pageInitializers: {},
|
||||
currentPage: document.body.dataset.page,
|
||||
currentPage: null,
|
||||
|
||||
/**
|
||||
* Initialisiert die MindMap-Anwendung
|
||||
@@ -35,13 +23,18 @@ const MindMap = {
|
||||
init() {
|
||||
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...');
|
||||
|
||||
// Initialisiere den ChatGPT-Assistenten
|
||||
const assistant = new ChatGPTAssistant();
|
||||
assistant.init();
|
||||
// Speichere als Teil von MindMap
|
||||
this.assistant = assistant;
|
||||
if (typeof ChatGPTAssistant !== 'undefined') {
|
||||
const assistant = new ChatGPTAssistant();
|
||||
assistant.init();
|
||||
// Speichere als Teil von MindMap
|
||||
this.assistant = assistant;
|
||||
}
|
||||
|
||||
// Seiten-spezifische Initialisierer aufrufen
|
||||
if (this.currentPage && this.pageInitializers[this.currentPage]) {
|
||||
@@ -74,6 +67,12 @@ const MindMap = {
|
||||
try {
|
||||
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
|
||||
const mindmap = new MindMapVisualization('#mindmap-container', {
|
||||
height: mindmapContainer.clientHeight || 600,
|
||||
@@ -224,6 +223,13 @@ const MindMap = {
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Globale Export für andere Module
|
||||
window.MindMap = MindMap;
|
||||
|
||||
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);
|
||||
});
|
||||
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.chatHistory = 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();
|
||||
|
||||
// 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
|
||||
const chatContainer = document.createElement('div');
|
||||
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
|
||||
const header = document.createElement('div');
|
||||
@@ -53,7 +118,7 @@ class ChatGPTAssistant {
|
||||
header.innerHTML = `
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-robot mr-2"></i>
|
||||
<span>KI-Assistent</span>
|
||||
<span>KI-Assistent (4o-mini)</span>
|
||||
</div>
|
||||
<button id="assistant-close" class="text-white hover:text-gray-200">
|
||||
<i class="fas fa-times"></i>
|
||||
@@ -63,7 +128,12 @@ class ChatGPTAssistant {
|
||||
// Chat-Verlauf
|
||||
this.chatHistory = document.createElement('div');
|
||||
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
|
||||
const inputContainer = document.createElement('div');
|
||||
@@ -71,7 +141,7 @@ class ChatGPTAssistant {
|
||||
|
||||
this.inputField = document.createElement('input');
|
||||
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';
|
||||
|
||||
const sendButton = document.createElement('button');
|
||||
@@ -85,6 +155,7 @@ class ChatGPTAssistant {
|
||||
|
||||
chatContainer.appendChild(header);
|
||||
chatContainer.appendChild(this.chatHistory);
|
||||
chatContainer.appendChild(this.suggestionArea);
|
||||
chatContainer.appendChild(inputContainer);
|
||||
|
||||
this.container.appendChild(toggleButton);
|
||||
@@ -100,22 +171,40 @@ class ChatGPTAssistant {
|
||||
setupEventListeners() {
|
||||
// Toggle-Button
|
||||
const toggleButton = document.getElementById('assistant-toggle');
|
||||
toggleButton.addEventListener('click', () => this.toggleAssistant());
|
||||
if (toggleButton) {
|
||||
toggleButton.addEventListener('click', () => this.toggleAssistant());
|
||||
}
|
||||
|
||||
// Schließen-Button
|
||||
const closeButton = document.getElementById('assistant-close');
|
||||
closeButton.addEventListener('click', () => this.toggleAssistant(false));
|
||||
if (closeButton) {
|
||||
closeButton.addEventListener('click', () => this.toggleAssistant(false));
|
||||
}
|
||||
|
||||
// Senden-Button
|
||||
const sendButton = document.getElementById('assistant-send');
|
||||
sendButton.addEventListener('click', () => this.sendMessage());
|
||||
if (sendButton) {
|
||||
sendButton.addEventListener('click', () => this.sendMessage());
|
||||
}
|
||||
|
||||
// Enter-Taste im Eingabefeld
|
||||
this.inputField.addEventListener('keyup', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
this.sendMessage();
|
||||
}
|
||||
});
|
||||
if (this.inputField) {
|
||||
this.inputField.addEventListener('keyup', (e) => {
|
||||
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) {
|
||||
const chatContainer = document.getElementById('assistant-chat');
|
||||
if (!chatContainer) return;
|
||||
|
||||
this.isOpen = state !== null ? state : !this.isOpen;
|
||||
|
||||
if (this.isOpen) {
|
||||
chatContainer.classList.remove('max-h-0', 'opacity-0');
|
||||
chatContainer.classList.add('max-h-96', 'opacity-100');
|
||||
this.inputField.focus();
|
||||
chatContainer.classList.add('max-h-[32rem]', 'opacity-100');
|
||||
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 {
|
||||
chatContainer.classList.remove('max-h-96', 'opacity-100');
|
||||
chatContainer.classList.remove('max-h-[32rem]', 'opacity-100');
|
||||
chatContainer.classList.add('max-h-0', 'opacity-0');
|
||||
}
|
||||
}
|
||||
@@ -151,24 +247,79 @@ class ChatGPTAssistant {
|
||||
|
||||
const bubble = document.createElement('div');
|
||||
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%]'
|
||||
: 'bg-gray-100 dark:bg-dark-700 text-gray-800 dark:text-white rounded-lg py-2 px-3 max-w-[85%]';
|
||||
bubble.textContent = text;
|
||||
? 'user-message rounded-lg py-2 px-3 max-w-[85%]'
|
||||
: 'assistant-message rounded-lg py-2 px-3 max-w-[85%]';
|
||||
|
||||
// Nachrichtentext einfügen, falls Markdown-Parser verfügbar, nutzen
|
||||
if (this.markdownParser) {
|
||||
bubble.innerHTML = this.markdownParser.parse(text);
|
||||
} 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);
|
||||
this.chatHistory.appendChild(messageEl);
|
||||
|
||||
// Scroll zum Ende des Verlaufs
|
||||
// Scrolle zum Ende des Chat-Verlaufs
|
||||
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
|
||||
*/
|
||||
async sendMessage() {
|
||||
if (!this.inputField) return;
|
||||
|
||||
const userInput = this.inputField.value.trim();
|
||||
if (!userInput || this.isLoading) return;
|
||||
|
||||
// Vorschläge ausblenden
|
||||
if (this.suggestionArea) {
|
||||
this.suggestionArea.classList.add('hidden');
|
||||
}
|
||||
|
||||
// Benutzernachricht anzeigen
|
||||
this.addMessage('user', userInput);
|
||||
|
||||
@@ -180,6 +331,7 @@ class ChatGPTAssistant {
|
||||
this.showLoadingIndicator();
|
||||
|
||||
try {
|
||||
console.log('Sende Anfrage an KI-Assistent API...');
|
||||
// Anfrage an den Server senden
|
||||
const response = await fetch('/api/assistant', {
|
||||
method: 'POST',
|
||||
@@ -189,92 +341,206 @@ class ChatGPTAssistant {
|
||||
body: JSON.stringify({
|
||||
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) {
|
||||
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();
|
||||
|
||||
// Ladeindikator entfernen
|
||||
this.removeLoadingIndicator();
|
||||
console.log('Antwort erhalten:', data);
|
||||
|
||||
// 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) {
|
||||
console.error('Fehler bei der Kommunikation mit dem Assistenten:', error);
|
||||
|
||||
// Ladeindikator entfernen
|
||||
// Ladeindikator entfernen, falls noch vorhanden
|
||||
this.removeLoadingIndicator();
|
||||
|
||||
// Fehlermeldung anzeigen
|
||||
this.addMessage('assistant', 'Es tut mir leid, aber es gab ein Problem bei der Verarbeitung deiner Anfrage. Bitte versuche es später noch einmal.');
|
||||
// Spezielle Fehlermeldungen für bestimmte Fehlertypen
|
||||
const errorMessage = error.message || '';
|
||||
let userFriendlyMessage = 'Es gab ein Problem mit der Anfrage.';
|
||||
|
||||
if (errorMessage.includes('timeout') || errorMessage.includes('Zeitüberschreitung')) {
|
||||
userFriendlyMessage = 'Die Antwort hat zu lange gedauert. Der Server ist möglicherweise überlastet.';
|
||||
} else if (errorMessage.includes('500') || errorMessage.includes('Internal Server Error')) {
|
||||
userFriendlyMessage = 'Ein Serverfehler ist aufgetreten. Wir arbeiten an einer Lösung.';
|
||||
} else if (errorMessage.includes('429') || errorMessage.includes('rate limit')) {
|
||||
userFriendlyMessage = 'Die API-Anfragelimits wurden erreicht. Bitte warte einen Moment.';
|
||||
}
|
||||
|
||||
// Fehlermeldung anzeigen oder Wiederholungsversuch starten
|
||||
if (this.retryCount < this.maxRetries) {
|
||||
this.retryCount++;
|
||||
this.addMessage('assistant', `${userFriendlyMessage} Ich versuche es erneut... (Versuch ${this.retryCount}/${this.maxRetries})`);
|
||||
|
||||
// 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 {
|
||||
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() {
|
||||
if (!this.chatHistory) return;
|
||||
|
||||
// Prüfen, ob bereits ein Ladeindikator angezeigt wird
|
||||
if (document.getElementById('assistant-loading-indicator')) return;
|
||||
|
||||
const loadingEl = document.createElement('div');
|
||||
loadingEl.id = 'assistant-loading';
|
||||
loadingEl.className = 'flex justify-start';
|
||||
loadingEl.id = 'assistant-loading-indicator';
|
||||
|
||||
const bubble = document.createElement('div');
|
||||
bubble.className = 'bg-gray-100 dark:bg-dark-700 text-gray-800 dark:text-white rounded-lg py-2 px-3';
|
||||
bubble.innerHTML = '<div class="typing-indicator"><span></span><span></span><span></span></div>';
|
||||
bubble.className = 'assistant-message rounded-lg py-3 px-4 max-w-[85%] flex items-center';
|
||||
|
||||
loadingEl.appendChild(bubble);
|
||||
this.chatHistory.appendChild(loadingEl);
|
||||
|
||||
// Scroll zum Ende des Verlaufs
|
||||
this.chatHistory.scrollTop = this.chatHistory.scrollHeight;
|
||||
|
||||
// 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); }
|
||||
}
|
||||
const typingIndicator = document.createElement('div');
|
||||
typingIndicator.className = 'typing-indicator';
|
||||
typingIndicator.innerHTML = `
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
`;
|
||||
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
|
||||
*/
|
||||
removeLoadingIndicator() {
|
||||
const loadingEl = document.getElementById('assistant-loading');
|
||||
if (loadingEl) {
|
||||
loadingEl.remove();
|
||||
const loadingIndicator = document.getElementById('assistant-loading-indicator');
|
||||
if (loadingIndicator) {
|
||||
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
|
||||
export default ChatGPTAssistant;
|
||||
// Mache die Klasse global verfügbar
|
||||
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;
|
||||
2724
static/js/update_mindmap.js
Normal file
2724
static/js/update_mindmap.js
Normal file
File diff suppressed because it is too large
Load Diff
1078
static/js/update_mindmap.js.bak
Normal file
1078
static/js/update_mindmap.js.bak
Normal file
File diff suppressed because it is too large
Load Diff
BIN
static/js/update_mindmap.js.new
Normal file
BIN
static/js/update_mindmap.js.new
Normal file
Binary file not shown.
1078
static/js/update_mindmap.js.original
Normal file
1078
static/js/update_mindmap.js.original
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>
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" href="{{ url_for('static', filename='img/favicon.svg') }}" type="image/svg+xml">
|
||||
<link rel="icon" href="{{ url_for('static', filename='img/favicon.ico') }}" sizes="any">
|
||||
<link rel="icon" href="{{ url_for('static', filename='img/neuron-favicon.svg') }}" type="image/svg+xml">
|
||||
|
||||
<!-- Meta Tags -->
|
||||
<meta name="description" content="Eine interaktive Plattform zum Visualisieren, Erforschen und Teilen von Wissen">
|
||||
<meta name="keywords" content="systades, wissen, visualisierung, lernen, gedanken, theorie">
|
||||
<meta name="author" content="Systades-Team">
|
||||
|
||||
<!-- Tailwind CSS über CDN -->
|
||||
<!-- Tailwind CSS - CDN für Entwicklung und Produktion (laut Vorgabe) -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<!-- Alternative lokale Version, falls die CDN-Version blockiert wird -->
|
||||
<script>
|
||||
tailwind = window.tailwind || {};
|
||||
tailwind.config = {
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
@@ -57,23 +58,35 @@
|
||||
800: '#0e1220',
|
||||
900: '#0a0e19'
|
||||
}
|
||||
},
|
||||
keyframes: {
|
||||
float: {
|
||||
'0%, 100%': { transform: 'translateY(0)' },
|
||||
'50%': { transform: 'translateY(-5px)' }
|
||||
},
|
||||
'bounce-slow': {
|
||||
'0%, 100%': { transform: 'translateY(0)' },
|
||||
'50%': { transform: 'translateY(-8px)' }
|
||||
}
|
||||
},
|
||||
animation: {
|
||||
float: 'float 3s ease-in-out infinite',
|
||||
'bounce-slow': 'bounce-slow 2s ease-in-out infinite'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
|
||||
<!-- Local Font Files -->
|
||||
<link href="{{ url_for('static', filename='fonts/inter.css') }}" rel="stylesheet">
|
||||
<link href="{{ url_for('static', filename='fonts/jetbrains-mono.css') }}" rel="stylesheet">
|
||||
|
||||
<!-- Icons -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<!-- Font Awesome vom CDN -->
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" rel="stylesheet">
|
||||
|
||||
<!-- Assistent CSS -->
|
||||
<link href="{{ url_for('static', filename='css/assistant.css') }}" rel="stylesheet">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/src/cybernetwork-bg.css') }}">
|
||||
|
||||
<!-- Basis-Stylesheet -->
|
||||
<link href="{{ url_for('static', filename='style.css') }}" rel="stylesheet">
|
||||
@@ -81,101 +94,297 @@
|
||||
<!-- Base-Styles ausgelagert in eigene Datei -->
|
||||
<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>
|
||||
|
||||
<!-- Network Background Script -->
|
||||
<script src="{{ url_for('static', filename='network-background.js') }}"></script>
|
||||
<!-- Neural Network Background CSS -->
|
||||
<link href="{{ url_for('static', filename='css/neural-network-background.css') }}" rel="stylesheet">
|
||||
|
||||
<!-- Hauptmodul laden (als ES6 Modul) -->
|
||||
<script type="module">
|
||||
import MindMap from "{{ url_for('static', filename='js/main.js') }}";
|
||||
// Alpine.js-Integration
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.data('layout', () => ({
|
||||
darkMode: false,
|
||||
mobileMenuOpen: false,
|
||||
userMenuOpen: false,
|
||||
showSettingsModal: false,
|
||||
<!-- Mindmap CSS -->
|
||||
<link href="{{ url_for('static', filename='css/mindmap.css', v='1.0.1') }}" rel="stylesheet">
|
||||
|
||||
init() {
|
||||
this.fetchDarkModeFromSession();
|
||||
},
|
||||
<!-- D3.js für Visualisierungen -->
|
||||
<script src="https://d3js.org/d3.v7.min.js"></script>
|
||||
|
||||
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);
|
||||
});
|
||||
},
|
||||
<!-- Marked.js für Markdown-Parsing -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
|
||||
toggleDarkMode() {
|
||||
this.darkMode = !this.darkMode;
|
||||
document.querySelector('html').classList.toggle('dark', this.darkMode);
|
||||
<!-- ChatGPT Assistant -->
|
||||
<script src="{{ url_for('static', filename='js/modules/chatgpt-assistant.js') }}"></script>
|
||||
|
||||
// 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);
|
||||
});
|
||||
}
|
||||
}));
|
||||
});
|
||||
<!-- Neural Network Background Script -->
|
||||
<script src="{{ url_for('static', filename='neural-network-background.js') }}"></script>
|
||||
|
||||
// MindMap global verfügbar machen (für Alpine.js und andere nicht-Module Skripte)
|
||||
window.MindMap = MindMap;
|
||||
</script>
|
||||
<!-- Hauptmodul laden (als traditionelles Skript) -->
|
||||
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
|
||||
|
||||
<!-- Seitenspezifische Styles -->
|
||||
{% block extra_css %}{% endblock %}
|
||||
|
||||
<!-- Cybertechnisches Netzwerk-Hintergrund -->
|
||||
<script type="module" src="{{ url_for('static', filename='js/modules/cyber-network-init.js') }}"></script>
|
||||
<!-- Custom dark/light mode styles -->
|
||||
<!-- ► ► 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>
|
||||
<body data-page="{{ request.endpoint }}" class="relative overflow-x-hidden">
|
||||
<!-- Cybertechnisches Netzwerk-Hintergrund Container (wird via JavaScript befüllt) -->
|
||||
<div id="cyber-background-container" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: -1; pointer-events: none; overflow: hidden;"></div>
|
||||
<body data-page="{{ request.endpoint }}" class="relative overflow-x-hidden dark bg-gray-900 text-white" x-data="{
|
||||
darkMode: true,
|
||||
mobileMenuOpen: false,
|
||||
userMenuOpen: false,
|
||||
showSettingsModal: false,
|
||||
|
||||
<!-- Globaler Hintergrund -->
|
||||
<div class="full-page-bg"></div>
|
||||
<!-- Statischer Fallback-Hintergrund (wird nur angezeigt, wenn JavaScript deaktiviert ist) -->
|
||||
<div class="fixed inset-0 z-[-9] bg-cover bg-center opacity-50"></div>
|
||||
init() {
|
||||
this.initDarkMode();
|
||||
},
|
||||
|
||||
initDarkMode() {
|
||||
// Lade zuerst den Wert aus dem localStorage (client-seitig)
|
||||
const storedMode = localStorage.getItem('colorMode');
|
||||
if (storedMode) {
|
||||
this.darkMode = storedMode === 'dark';
|
||||
}
|
||||
|
||||
// Dann hole die Server-Einstellung, die Vorrang hat
|
||||
this.fetchDarkModeFromSession();
|
||||
},
|
||||
|
||||
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 -->
|
||||
<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 -->
|
||||
<nav class="sticky top-0 left-0 right-0 z-50 transition-all duration-300 py-4 px-5 border-b glass-morphism"
|
||||
x-bind:class="darkMode ? 'glass-navbar-dark border-white/10' : 'glass-navbar-light border-gray-200/50'">
|
||||
<div class="container mx-auto flex justify-between items-center">
|
||||
<!-- Logo -->
|
||||
<a href="{{ url_for('index') }}" class="flex items-center group">
|
||||
<img src="{{ url_for('static', filename='img/neuron-logo.svg') }}" alt="Systades Logo" class="w-8 h-8 mr-2 transform transition-transform group-hover:scale-110">
|
||||
<span class="text-2xl font-bold gradient-text transform transition-transform group-hover:scale-105">Systades</span>
|
||||
</a>
|
||||
|
||||
@@ -206,8 +415,8 @@
|
||||
<button onclick="window.MindMap && window.MindMap.assistant && window.MindMap.assistant.toggleAssistant(true)"
|
||||
class="nav-link flex items-center"
|
||||
x-bind:class="darkMode
|
||||
? 'bg-gradient-to-r from-purple-600/80 to-blue-500/80 text-white font-medium px-4 py-2 rounded-xl hover:shadow-lg transition-all duration-300 hover:-translate-y-0.5'
|
||||
: 'bg-gradient-to-r from-purple-500/20 to-blue-400/20 text-gray-800 font-medium px-4 py-2 rounded-xl hover:shadow-md transition-all duration-300 hover:-translate-y-0.5'">
|
||||
? 'bg-gradient-to-r from-purple-900/90 to-indigo-800/90 text-white font-medium px-4 py-2 rounded-xl hover:shadow-lg hover:shadow-purple-800/30 transition-all duration-300'
|
||||
: 'bg-gradient-to-r from-purple-600 to-indigo-500 text-white font-medium px-4 py-2 rounded-xl hover:shadow-md transition-all duration-300'">
|
||||
<i class="fa-solid fa-robot mr-2"></i>KI-Chat
|
||||
</button>
|
||||
{% if current_user.is_authenticated %}
|
||||
@@ -223,25 +432,14 @@
|
||||
|
||||
<!-- Rechte Seite -->
|
||||
<div class="flex items-center space-x-4">
|
||||
<!-- Dark Mode Toggle Switch -->
|
||||
<div class="flex items-center cursor-pointer" @click="toggleDarkMode">
|
||||
<div class="relative w-12 h-6">
|
||||
<input type="checkbox" id="darkModeToggle" class="sr-only" x-model="darkMode">
|
||||
<div class="block w-12 h-6 rounded-full transition-colors duration-300"
|
||||
x-bind:class="darkMode ? 'bg-blue-400/50' : 'bg-gray-400/50'"></div>
|
||||
<div class="dot absolute left-1 top-1 w-4 h-4 rounded-full transition-transform duration-300 shadow-md"
|
||||
x-bind:class="darkMode ? 'bg-blue-500 transform translate-x-6' : 'bg-white'"></div>
|
||||
</div>
|
||||
<div class="ml-3 hidden sm:block"
|
||||
x-bind:class="darkMode ? 'text-white/90' : 'text-gray-700'">
|
||||
<span x-text="darkMode ? 'Dunkel' : 'Hell'"></span>
|
||||
</div>
|
||||
<div class="ml-2 sm:hidden"
|
||||
x-bind:class="darkMode ? 'text-white/90' : 'text-gray-700'">
|
||||
<i class="fa-solid" :class="darkMode ? 'fa-sun' : 'fa-moon'"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dark/Light Mode Schalter -->
|
||||
<button
|
||||
@click="toggleDarkMode()"
|
||||
class="theme-toggle relative w-12 h-6 rounded-full transition-all duration-300 flex items-center overflow-hidden"
|
||||
aria-label="Dark Mode umschalten"
|
||||
>
|
||||
<span class="sr-only" x-text="darkMode ? 'Zum Light Mode wechseln' : 'Zum Dark Mode wechseln'"></span>
|
||||
</button>
|
||||
<!-- Profil-Link oder Login -->
|
||||
{% if current_user.is_authenticated %}
|
||||
<div class="relative" x-data="{ open: false }">
|
||||
@@ -255,12 +453,21 @@
|
||||
{% if current_user.avatar %}
|
||||
<img src="{{ current_user.avatar }}" alt="{{ current_user.username }}" class="w-full h-full object-cover">
|
||||
{% else %}
|
||||
{{ current_user.username[0].upper() }}
|
||||
<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 %}
|
||||
</div>
|
||||
<span class="text-sm hidden lg:block">{{ current_user.username }}</span>
|
||||
<i class="fa-solid fa-chevron-down text-xs hidden lg:block transition-transform duration-200"
|
||||
x-bind:class="open ? 'transform rotate-180' : ''"></i>
|
||||
<span class="hidden md:block">{{ current_user.username }}</span>
|
||||
<i class="fas fa-chevron-down text-xs opacity-60 ml-1.5"></i>
|
||||
</button>
|
||||
|
||||
<!-- Dropdown-Menü -->
|
||||
@@ -308,13 +515,22 @@
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<a href="{{ url_for('login') }}"
|
||||
class="flex items-center px-4 py-2.5 rounded-xl font-medium transition-all duration-300"
|
||||
x-bind:class="darkMode
|
||||
? 'bg-gray-800/80 text-white hover:bg-gray-700/80 shadow-md hover:shadow-lg hover:-translate-y-0.5'
|
||||
: 'bg-gray-200/80 text-gray-800 hover:bg-gray-300/80 shadow-sm hover:shadow-md hover:-translate-y-0.5'">
|
||||
<i class="fa-solid fa-user mr-2"></i>Mein Konto
|
||||
</a>
|
||||
<div class="flex items-center space-x-2">
|
||||
<a href="{{ url_for('login') }}"
|
||||
class="py-2 px-4 rounded-lg transition-all duration-300"
|
||||
x-bind:class="darkMode
|
||||
? 'text-white/90 hover:bg-dark-700/80'
|
||||
: 'text-gray-700 hover:bg-gray-100/80'">
|
||||
<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 %}
|
||||
|
||||
<!-- Mobilmenü-Button -->
|
||||
@@ -331,6 +547,7 @@
|
||||
|
||||
<!-- Mobile Menü -->
|
||||
<div x-show="mobileMenuOpen"
|
||||
x-cloak
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 -translate-y-4"
|
||||
x-transition:enter-end="opacity-100 translate-y-0"
|
||||
@@ -368,7 +585,7 @@
|
||||
class="block w-full text-left py-3.5 px-4 rounded-xl transition-all duration-200 flex items-center"
|
||||
x-bind:class="darkMode
|
||||
? 'bg-gradient-to-r from-purple-600/30 to-blue-500/30 text-white hover:from-purple-600/40 hover:to-blue-500/40'
|
||||
: 'bg-gradient-to-r from-purple-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
|
||||
</button>
|
||||
{% if current_user.is_authenticated %}
|
||||
@@ -434,6 +651,10 @@
|
||||
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
||||
Mindmap
|
||||
</a>
|
||||
<a href="{{ url_for('search_thoughts_page') }}" class="text-sm transition-all duration-200"
|
||||
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
||||
Suche
|
||||
</a>
|
||||
{% if current_user.is_authenticated %}
|
||||
<a href="{{ url_for('profile') }}" class="text-sm transition-all duration-200"
|
||||
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
||||
@@ -462,6 +683,10 @@
|
||||
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
||||
Impressum
|
||||
</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"
|
||||
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
||||
Datenschutz
|
||||
@@ -509,27 +734,286 @@
|
||||
|
||||
<!-- Hilfsscripts -->
|
||||
{% block scripts %}{% endblock %}
|
||||
{% block extra_js %}{% endblock %}
|
||||
|
||||
<!-- KI-Chat Initialisierung -->
|
||||
<script type="module">
|
||||
// Importiere und initialisiere den ChatGPT-Assistenten direkt, um sicherzustellen,
|
||||
// dass er auf jeder Seite verfügbar ist, selbst wenn MindMap nicht geladen ist
|
||||
import ChatGPTAssistant from "{{ url_for('static', filename='js/modules/chatgpt-assistant.js') }}";
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Prüfen, ob der Assistent bereits durch MindMap initialisiert wurde
|
||||
if (!window.MindMap || !window.MindMap.assistant) {
|
||||
console.log('KI-Assistent wird direkt initialisiert...');
|
||||
const assistant = new ChatGPTAssistant();
|
||||
assistant.init();
|
||||
|
||||
// Speichere in window.MindMap, falls es existiert, oder erstelle es
|
||||
if (!window.MindMap) {
|
||||
window.MindMap = {};
|
||||
<!-- ChatGPT Initialisierung -->
|
||||
<script>
|
||||
// Prüfe, ob ChatGPTAssistant bereits existiert
|
||||
if (typeof ChatGPTAssistant === 'undefined') {
|
||||
class ChatGPTAssistant {
|
||||
constructor() {
|
||||
this.chatContainer = null;
|
||||
this.messages = [];
|
||||
this.isOpen = false;
|
||||
}
|
||||
|
||||
init() {
|
||||
// Chat-Container erstellen, falls noch nicht vorhanden
|
||||
if (!document.getElementById('chat-assistant-container')) {
|
||||
this.createChatInterface();
|
||||
}
|
||||
|
||||
// Event-Listener für Chat-Button
|
||||
const chatButton = document.getElementById('chat-assistant-button');
|
||||
if (chatButton) {
|
||||
chatButton.addEventListener('click', () => this.toggleChat());
|
||||
}
|
||||
|
||||
// Event-Listener für Senden-Button
|
||||
const sendButton = document.getElementById('chat-send-button');
|
||||
if (sendButton) {
|
||||
sendButton.addEventListener('click', () => this.sendMessage());
|
||||
}
|
||||
|
||||
// Event-Listener für Eingabefeld (Enter-Taste)
|
||||
const inputField = document.getElementById('chat-input');
|
||||
if (inputField) {
|
||||
inputField.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
this.sendMessage();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log('KI-Assistent erfolgreich initialisiert');
|
||||
}
|
||||
|
||||
createChatInterface() {
|
||||
// Chat-Button erstellen
|
||||
const chatButton = document.createElement('button');
|
||||
chatButton.id = 'chat-assistant-button';
|
||||
chatButton.className = 'fixed bottom-6 right-6 bg-primary-600 text-white rounded-full p-4 shadow-lg z-50 hover:bg-primary-700 transition-all';
|
||||
chatButton.innerHTML = '<i class="fas fa-robot text-xl"></i>';
|
||||
document.body.appendChild(chatButton);
|
||||
|
||||
// Chat-Container erstellen
|
||||
const chatContainer = document.createElement('div');
|
||||
chatContainer.id = 'chat-assistant-container';
|
||||
chatContainer.className = 'fixed bottom-24 right-6 w-80 md:w-96 bg-white dark:bg-gray-800 rounded-xl shadow-xl z-50 flex flex-col transition-all duration-300 transform scale-0 origin-bottom-right';
|
||||
chatContainer.style.height = '500px';
|
||||
chatContainer.style.maxHeight = '70vh';
|
||||
|
||||
// Chat-Header
|
||||
chatContainer.innerHTML = `
|
||||
<div class="p-4 border-b dark:border-gray-700 flex justify-between items-center">
|
||||
<h3 class="font-bold text-gray-800 dark:text-white">Systades Assistent</h3>
|
||||
<button id="chat-close-button" class="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div id="chat-messages" class="flex-1 overflow-y-auto p-4 space-y-4"></div>
|
||||
<div class="p-4 border-t dark:border-gray-700">
|
||||
<div class="flex space-x-2">
|
||||
<input id="chat-input" type="text" placeholder="Frage stellen..." class="flex-1 px-4 py-2 rounded-lg border dark:border-gray-700 dark:bg-gray-700 dark:text-white focus:outline-none focus:ring-2 focus:ring-primary-500">
|
||||
<button id="chat-send-button" class="bg-primary-600 text-white px-4 py-2 rounded-lg hover:bg-primary-700 transition-all">
|
||||
<i class="fas fa-paper-plane"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(chatContainer);
|
||||
this.chatContainer = chatContainer;
|
||||
|
||||
// Event-Listener für Schließen-Button
|
||||
const closeButton = document.getElementById('chat-close-button');
|
||||
if (closeButton) {
|
||||
closeButton.addEventListener('click', () => this.toggleChat());
|
||||
}
|
||||
}
|
||||
|
||||
toggleChat() {
|
||||
this.isOpen = !this.isOpen;
|
||||
if (this.isOpen) {
|
||||
this.chatContainer.classList.remove('scale-0');
|
||||
this.chatContainer.classList.add('scale-100');
|
||||
} else {
|
||||
this.chatContainer.classList.remove('scale-100');
|
||||
this.chatContainer.classList.add('scale-0');
|
||||
}
|
||||
}
|
||||
|
||||
async sendMessage() {
|
||||
const inputField = document.getElementById('chat-input');
|
||||
const messageText = inputField.value.trim();
|
||||
|
||||
if (!messageText) return;
|
||||
|
||||
// Benutzer-Nachricht anzeigen
|
||||
this.addMessage('user', messageText);
|
||||
inputField.value = '';
|
||||
|
||||
// Lade-Indikator anzeigen
|
||||
this.addMessage('assistant', '...', 'loading-message');
|
||||
|
||||
try {
|
||||
// API-Anfrage senden
|
||||
const response = await fetch('/api/assistant', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
messages: this.messages.map(msg => ({
|
||||
role: msg.role,
|
||||
content: msg.content
|
||||
}))
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Lade-Nachricht entfernen
|
||||
const loadingMessage = document.getElementById('loading-message');
|
||||
if (loadingMessage) {
|
||||
loadingMessage.remove();
|
||||
}
|
||||
|
||||
if (data.error) {
|
||||
this.addMessage('assistant', 'Entschuldigung, es ist ein Fehler aufgetreten: ' + data.error);
|
||||
} else {
|
||||
this.addMessage('assistant', data.response);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler bei der API-Anfrage:', error);
|
||||
|
||||
// Lade-Nachricht entfernen
|
||||
const loadingMessage = document.getElementById('loading-message');
|
||||
if (loadingMessage) {
|
||||
loadingMessage.remove();
|
||||
}
|
||||
|
||||
this.addMessage('assistant', 'Entschuldigung, es ist ein Fehler aufgetreten. Bitte versuche es später erneut.');
|
||||
}
|
||||
}
|
||||
|
||||
addMessage(role, content, id = null) {
|
||||
const messagesContainer = document.getElementById('chat-messages');
|
||||
|
||||
// Nachricht zum Array hinzufügen (außer Lade-Nachrichten)
|
||||
if (id !== 'loading-message') {
|
||||
this.messages.push({ role, content });
|
||||
}
|
||||
|
||||
// Nachricht zum DOM hinzufügen
|
||||
const messageElement = document.createElement('div');
|
||||
messageElement.className = `p-3 rounded-lg ${role === 'user' ? 'bg-primary-100 dark:bg-primary-900/30 ml-6' : 'bg-gray-100 dark:bg-gray-700 mr-6'}`;
|
||||
if (id) {
|
||||
messageElement.id = id;
|
||||
}
|
||||
|
||||
messageElement.innerHTML = `
|
||||
<div class="flex items-start">
|
||||
<div class="w-8 h-8 rounded-full flex items-center justify-center ${role === 'user' ? 'bg-primary-600' : 'bg-gray-600'} text-white mr-2">
|
||||
<i class="fas ${role === 'user' ? 'fa-user' : 'fa-robot'} text-xs"></i>
|
||||
</div>
|
||||
<div class="flex-1 text-sm ${role === 'user' ? 'text-gray-800 dark:text-gray-200' : 'text-gray-700 dark:text-gray-300'}">
|
||||
${content}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
messagesContainer.appendChild(messageElement);
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
}
|
||||
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>
|
||||
</body>
|
||||
</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 %}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user