Compare commits
234 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f0d13cae78 | |||
| a60b5c31ca | |||
| f5c2e70a11 | |||
| 1f4394e9b6 | |||
| 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
|
# Kopiere diese Datei zu .env und passe die Werte an
|
||||||
|
|
||||||
# Flask
|
# Flask
|
||||||
SECRET_KEY=dein-geheimer-schluessel-hier
|
FLASK_APP=app.py
|
||||||
|
FLASK_ENV=development
|
||||||
|
SECRET_KEY=your-secret-key-replace-in-production
|
||||||
|
|
||||||
# OpenAI API
|
# OpenAI API
|
||||||
OPENAI_API_KEY=sk-dein-openai-api-schluessel-hier
|
OPENAI_API_KEY=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
|
||||||
|
|
||||||
# Datenbank
|
# Datenbank
|
||||||
# Bei Bedarf kann hier eine andere Datenbank-URL angegeben werden
|
# Bei Bedarf kann hier eine andere Datenbank-URL angegeben werden
|
||||||
# Der Pfad wird relativ zum Projektverzeichnis angegeben
|
# Der Pfad wird relativ zum Projektverzeichnis angegeben
|
||||||
# SQLALCHEMY_DATABASE_URI=sqlite:////absoluter/pfad/zu/database/systades.db OPENAI_API_KEY=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
|
# SQLALCHEMY_DATABASE_URI=sqlite:////absoluter/pfad/zu/database/systades.db
|
||||||
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
|
||||||
|
}
|
||||||
1274
COMMON_ERRORS.md
Normal file
1274
COMMON_ERRORS.md
Normal file
File diff suppressed because it is too large
Load Diff
33
Dockerfile
Normal file
33
Dockerfile
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# Dockerfile
|
||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
# Arbeitsverzeichnis in Container
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Systemabhängigkeiten installieren und Verzeichnisse anlegen
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y --no-install-recommends gcc && \
|
||||||
|
rm -rf /var/lib/apt/lists/* && \
|
||||||
|
mkdir -p /app/database
|
||||||
|
|
||||||
|
# pip auf den neuesten Stand bringen und Requirements installieren
|
||||||
|
COPY requirements.txt ./
|
||||||
|
RUN pip install --upgrade pip && \
|
||||||
|
pip install --no-cache-dir -U -r requirements.txt
|
||||||
|
|
||||||
|
# Anwendungscode kopieren
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Berechtigungen für database-Ordner
|
||||||
|
RUN chmod -R 777 /app/database
|
||||||
|
|
||||||
|
# Exponiere Port 5000 für Flask
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
|
# Setze Umgebungsvariablen
|
||||||
|
ENV FLASK_APP=app.py
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
|
|
||||||
|
# Startkommando mit spezifischen Flags für Produktion
|
||||||
|
CMD ["python", "app.py"]
|
||||||
80
README.md
80
README.md
@@ -6,9 +6,10 @@ Das MindMapProjekt ist eine interaktive Plattform zum Visualisieren, Erforschen
|
|||||||
## Technischer Stack
|
## Technischer Stack
|
||||||
- **Backend**: Python/Flask
|
- **Backend**: Python/Flask
|
||||||
- **Frontend**:
|
- **Frontend**:
|
||||||
- Tailwind CSS für moderne UI
|
- Tailwind CSS (via CDN) für moderne UI
|
||||||
- SVG-Bibliotheken für Visualisierungen (D3.js)
|
- SVG-Bibliotheken für Visualisierungen (D3.js)
|
||||||
- JavaScript/Alpine.js für interaktive Komponenten
|
- JavaScript/Alpine.js für interaktive Komponenten
|
||||||
|
- WebGL für animierte Hintergrundeffekte
|
||||||
- **Datenbank**: SQLite mit SQLAlchemy
|
- **Datenbank**: SQLite mit SQLAlchemy
|
||||||
- **KI-Integration**: OpenAI API für intelligente Assistenz
|
- **KI-Integration**: OpenAI API für intelligente Assistenz
|
||||||
|
|
||||||
@@ -61,16 +62,20 @@ Für detaillierte Hilfe: `python TOOLS.py -h`
|
|||||||
- [x] Favicon erstellen
|
- [x] Favicon erstellen
|
||||||
- [x] Setup-Skript für einfache Installation
|
- [x] Setup-Skript für einfache Installation
|
||||||
|
|
||||||
### Phase 2: Design-Überarbeitung 🔄
|
### Phase 2: Design-Überarbeitung ✅
|
||||||
- [x] Implementierung des Dark Mode
|
- [x] Implementierung des Dark Mode
|
||||||
- [x] Erstellung eines modernen, minimalistischen UI mit Tech-Ästhetik
|
- [x] Erstellung eines modernen, minimalistischen UI mit Tech-Ästhetik
|
||||||
- [x] Responsive Design für alle Geräte
|
- [x] Responsive Design für alle Geräte
|
||||||
- [ ] Gestaltung der Landing Page mit großer Typografie
|
- [x] Gestaltung der Landing Page mit großer Typografie
|
||||||
|
- [x] Animierter Neurales Netzwerk-Hintergrund mit WebGL
|
||||||
|
|
||||||
### Phase 3: Mindmap-Funktionalitäten 🔄
|
### Phase 3: Mindmap-Funktionalitäten 🔄
|
||||||
- [x] Verbesserte Visualisierung mit SVG und D3.js
|
- [x] Verbesserte Visualisierung mit SVG und D3.js
|
||||||
- [x] Implementierung der Mouseover-Funktion
|
- [x] Implementierung der Mouseover-Funktion
|
||||||
- [x] Entwicklung der Suchfunktion für Knoten
|
- [x] Entwicklung der Suchfunktion für Knoten
|
||||||
|
- [x] Clustertopologie für neuronale Netzwerkdarstellung
|
||||||
|
- [x] Fehlerbehandlung für robuste Datenverarbeitung und Knotenverweise
|
||||||
|
- [x] Verbesserte Verbindungserkennung zwischen Knoten
|
||||||
- [ ] Tagging-System für Inhalte
|
- [ ] Tagging-System für Inhalte
|
||||||
- [ ] Quellenmanagement und -verlinkung
|
- [ ] Quellenmanagement und -verlinkung
|
||||||
- [ ] Upload-Funktionalität an Knotenpunkten
|
- [ ] Upload-Funktionalität an Knotenpunkten
|
||||||
@@ -115,8 +120,8 @@ Für detaillierte Hilfe: `python TOOLS.py -h`
|
|||||||
|
|
||||||
## Aktueller Status
|
## Aktueller Status
|
||||||
- **Phase 1**: ✅ Abgeschlossen
|
- **Phase 1**: ✅ Abgeschlossen
|
||||||
- **Phase 2**: 🔄 In Bearbeitung (75% abgeschlossen)
|
- **Phase 2**: ✅ Abgeschlossen
|
||||||
- **Phase 3**: 🔄 In Bearbeitung (50% abgeschlossen)
|
- **Phase 3**: 🔄 In Bearbeitung (75% abgeschlossen)
|
||||||
|
|
||||||
## Aktuelle Fortschritte
|
## Aktuelle Fortschritte
|
||||||
- Grundlegende UI modernisiert mit Tailwind CSS und Dark Mode
|
- Grundlegende UI modernisiert mit Tailwind CSS und Dark Mode
|
||||||
@@ -124,11 +129,68 @@ Für detaillierte Hilfe: `python TOOLS.py -h`
|
|||||||
- Setup-Prozess vereinfacht mit einem Shell-Skript
|
- Setup-Prozess vereinfacht mit einem Shell-Skript
|
||||||
- Mindmap-Visualisierung komplett überarbeitet mit D3.js für eine interaktivere Erfahrung
|
- Mindmap-Visualisierung komplett überarbeitet mit D3.js für eine interaktivere Erfahrung
|
||||||
- Responsive Design für optimale Darstellung auf allen Geräten
|
- Responsive Design für optimale Darstellung auf allen Geräten
|
||||||
|
- Animierter neuronaler Netzwerk-Hintergrund mit WebGL implementiert
|
||||||
|
- Verbesserte neuronale Cluster-Darstellung in der MindMap-Ansicht
|
||||||
|
- Behebung von kritischen Fehlern in der Knotenvisualisierung und Verbindungserkennung
|
||||||
|
- Robustere Datenverarbeitung für Mindmap-Knoten implementiert
|
||||||
|
- Fehlerbehandlung für verschiedene API-Datenformate verbessert
|
||||||
|
|
||||||
|
## Neuronaler Netzwerk-Hintergrund
|
||||||
|
|
||||||
|
Ein wesentliches neues Feature ist der animierte Hintergrund, der ein neuronales Netzwerk simuliert:
|
||||||
|
|
||||||
|
- **WebGL-basierte Rendering-Engine** für hohe Performance
|
||||||
|
- **Dynamische Knoten und Verbindungen** mit realistischem Bewegungsverhalten
|
||||||
|
- **Neuronenfeuer-Simulation** mit Signalweiterleitung zwischen Knoten
|
||||||
|
- **Clustertopologie** für realistisches Erscheinungsbild
|
||||||
|
- **Anpassbare Farbgebung und Animationsparameter**
|
||||||
|
- **Flüssige Animationen** mit über 100 Knotenpunkten
|
||||||
|
|
||||||
|
Die Animation ist vollständig responsiv und passt sich automatisch an verschiedene Bildschirmgrößen an, ohne die Browser-Performance zu beeinträchtigen.
|
||||||
|
|
||||||
|
## Mindmap-Verbesserungen
|
||||||
|
|
||||||
|
Die Mindmap-Darstellung wurde grundlegend überarbeitet:
|
||||||
|
|
||||||
|
- **D3.js Force-Directed Graph** für intuitive Knotenpositionierung
|
||||||
|
- **Verbesserte Fehlerbehandlung** für robustere Datenverarbeitung
|
||||||
|
- **Neuronale Cluster-Gruppierung** von thematisch zusammengehörigen Inhalten
|
||||||
|
- **Glasmorphismus-Effekte** für moderne visuelle Darstellung
|
||||||
|
- **Verbesserte Hover- und Selektionseffekte**
|
||||||
|
- **Flüssige Animationen** bei Knotenauswahl und -fokussierung
|
||||||
|
|
||||||
## Nächste Schritte
|
## Nächste Schritte
|
||||||
- Fertigstellung der Landing Page
|
- Fertigstellung des Tagging-Systems für Gedanken
|
||||||
- Erstellung der "Wer sind wir?"-Seite
|
|
||||||
- Implementierung des Tagging-Systems für Gedanken
|
|
||||||
- Verbesserung der Gedankenansicht im Mindmap-Bereich
|
- Verbesserung der Gedankenansicht im Mindmap-Bereich
|
||||||
|
- Implementierung von Quellenmanagement
|
||||||
|
- Überarbeitung der Startseite mit neuen Features
|
||||||
|
|
||||||
*Zuletzt aktualisiert: 01.06.2024*
|
## Content Security Policy (CSP)
|
||||||
|
|
||||||
|
Die Anwendung implementiert eine Content Security Policy, um die Sicherheit zu erhöhen und unerwünschte externe Ressourcen zu blockieren. CSP wird in `app.py` konfiguriert und schränkt ein, welche Ressourcen geladen werden dürfen.
|
||||||
|
|
||||||
|
### Aktualisierung (06.06.2024)
|
||||||
|
Die Anwendung verwendet nun die Tailwind CSS CDN für vereinfachte Entwicklung. Die CSP wurde entsprechend angepasst, um die Domain `cdn.tailwindcss.com` zu erlauben.
|
||||||
|
|
||||||
|
### Lokale und CDN-Ressourcen
|
||||||
|
|
||||||
|
Die Anwendung nutzt eine Mischung aus lokalen Ressourcen und CDNs:
|
||||||
|
- **CDN-Ressourcen**:
|
||||||
|
- Tailwind CSS (cdn.tailwindcss.com)
|
||||||
|
- **Lokale Ressourcen**:
|
||||||
|
- Alpine.js
|
||||||
|
- Font Awesome
|
||||||
|
- Google Fonts (Inter und JetBrains Mono)
|
||||||
|
- WebGL-Animation (neural-network-background.js)
|
||||||
|
|
||||||
|
### CSP-Nonces
|
||||||
|
|
||||||
|
Die Anwendung verwendet Nonces für Inline-Skripte. In den Templates wird `{{ csp_nonce }}` verwendet, um den Nonce-Wert einzufügen:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script nonce="{{ csp_nonce }}">
|
||||||
|
// JavaScript Code
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
*Zuletzt aktualisiert: 15.06.2024*
|
||||||
510
ROADMAP.md
510
ROADMAP.md
@@ -1,102 +1,464 @@
|
|||||||
# Systades Mindmap - Entwicklungs-Roadmap
|
# 🚀 SysTades Social Network - Entwicklungsroadmap
|
||||||
|
|
||||||
Diese Roadmap beschreibt die geplante Entwicklung der dynamischen, benutzerorientierten Mindmap-Funktionalität für das Systades-Projekt.
|
## 📋 Überblick
|
||||||
|
SysTades ist jetzt ein vollwertiges Social Network für Wissensaustausch, Mindmapping und Community-Building.
|
||||||
|
|
||||||
## Phase 1: Grundlegendes Datenmodell und Backend (Abgeschlossen)
|
## ✅ Abgeschlossene Phasen
|
||||||
|
|
||||||
- [x] Entwurf des Datenbankschemas für benutzerorientierte Mindmaps
|
### Phase 1: Basis Social Network ✅
|
||||||
- [x] Implementierung der Modelle in models.py
|
- ✅ Erweiterte Benutzermodelle mit Social Features
|
||||||
- [x] Erstellung der API-Endpunkte für CRUD-Operationen
|
- ✅ Posts, Kommentare, Likes, Follows System
|
||||||
- [x] Integration mit der bestehenden Benutzerauthentifizierung
|
- ✅ Benachrichtigungssystem
|
||||||
- [x] Seed-Daten für die Entwicklung und Tests
|
- ✅ Benutzerprofile mit Statistiken
|
||||||
|
- ✅ Erweiterte Navigation und UI
|
||||||
|
- ✅ **Verbessertes Logging-System mit visuellen Enhancements**
|
||||||
|
- ✅ Social Feed mit Filtering
|
||||||
|
- ✅ Mobile-responsive Design
|
||||||
|
|
||||||
## Phase 2: Dynamische Mindmap-Visualisierung (Aktuell)
|
### Phase 2: Core Features ✅
|
||||||
|
- ✅ Mindmap-Integration in Social Posts
|
||||||
|
- ✅ Gedanken-Sharing System
|
||||||
|
- ✅ Bookmark-System für Posts
|
||||||
|
- ✅ Analytics Dashboard für Benutzer
|
||||||
|
- ✅ Erweiterte Suche (Benutzer, Posts, Gedanken)
|
||||||
|
- ✅ Real-time Benachrichtigungen
|
||||||
|
- ✅ Post-Sharing und Engagement Metrics
|
||||||
|
|
||||||
- [ ] Anpassung des Frontend-Codes zur Verwendung der DB-Daten anstelle des SVG
|
### Phase 3: Erweiterte Social Features ✅
|
||||||
- [ ] Implementierung von AJAX-Anfragen zum Laden der Mindmap-Daten
|
- ✅ Benutzerprofile mit Tabs (Posts, Gedanken, Mindmaps, Aktivität)
|
||||||
- [ ] Dynamisches Rendering der Knoten, Verbindungen und Labels
|
- ✅ Follow/Unfollow System mit UI
|
||||||
- [ ] Drag-and-Drop-Funktionalität für die Bewegung von Knoten
|
- ✅ Notification Center mit Filtering
|
||||||
- [ ] Zoom- und Pan-Funktionalität mit Persistenz der Ansicht
|
- ✅ Post-Typen (Text, Gedanke, Frage, Erkenntnis)
|
||||||
|
- ✅ Sichtbarkeitseinstellungen (Öffentlich, Follower, Privat)
|
||||||
|
- ✅ Quick-Create Post Modal
|
||||||
|
|
||||||
## Phase 3: Benutzerdefinierte Mindmaps
|
### Phase 3.5: Logging & Monitoring System ✅ (NEU)
|
||||||
|
- ✅ **Erweiterte SocialNetworkLogger Klasse mit visuellen Features**
|
||||||
|
- ✅ **Farbige Konsolen-Ausgabe mit ANSI-Codes**
|
||||||
|
- ✅ **Emoji-basierte Kategorisierung für bessere Übersicht**
|
||||||
|
- ✅ **Component-spezifisches Logging (AUTH, API, DB, ERROR, etc.)**
|
||||||
|
- ✅ **Performance-Monitoring mit Zeitstempel**
|
||||||
|
- ✅ **Strukturierte JSON-Logs für externe Analyse**
|
||||||
|
- ✅ **Decorator-basierte Instrumentierung**
|
||||||
|
- ✅ **Vollständige Integration in alle App-Komponenten**
|
||||||
|
- ✅ **Ersetzung aller print-Statements durch strukturierte Logs**
|
||||||
|
|
||||||
- [ ] UI für das Erstellen, Bearbeiten und Löschen eigener Mindmaps
|
## 🔄 Aktuelle Phase 4: UI/UX Verbesserungen (In Arbeit)
|
||||||
- [ ] Funktion zum Hinzufügen/Entfernen von Knoten aus der öffentlichen Mindmap
|
|
||||||
- [ ] Speichern der Knotenpositionen und Ansichtseinstellungen
|
|
||||||
- [ ] Benutzerspezifische Visualisierungseinstellungen
|
|
||||||
- [ ] Dashboard mit Übersicht aller Mindmaps des Benutzers
|
|
||||||
|
|
||||||
## Phase 4: Notizen und Annotationen
|
### UI/UX Komponenten
|
||||||
|
- ✅ Moderne Navigation mit Icons und Badges
|
||||||
|
- ✅ Dark/Light Mode Toggle
|
||||||
|
- ✅ Responsive Mobile Navigation
|
||||||
|
- ✅ Glassmorphism Design Elements
|
||||||
|
- ✅ Gradient Themes und Farbsystem
|
||||||
|
- ✅ Toast Notification System
|
||||||
|
- ⏳ Chat/Messaging System
|
||||||
|
- ⏳ Story/Status Features
|
||||||
|
- ⏳ Advanced Image/Video Upload
|
||||||
|
|
||||||
- [ ] UI für das Hinzufügen privater Notizen zu Knoten
|
### Performance Optimierungen
|
||||||
- [ ] Visuelle Anzeige von Notizen in der Mindmap
|
- ⏳ Lazy Loading für Posts
|
||||||
- [ ] Texteditor mit Markdown-Unterstützung für Notizen
|
- ⏳ Image Optimization
|
||||||
- [ ] Kategorisierung und Farbkodierung von Notizen
|
- ⏳ Caching System
|
||||||
- [ ] Suchfunktion für Notizen
|
- ⏳ API Rate Limiting
|
||||||
|
- ⏳ Database Indexing
|
||||||
|
|
||||||
## Phase 5: Integrationen und Erweiterungen
|
## 📈 Kommende Phasen
|
||||||
|
|
||||||
- [ ] Import/Export-Funktionalität für Mindmaps (JSON, PNG)
|
### Phase 5: Community Features
|
||||||
- [ ] Teilen von Mindmaps (öffentlich/privat/mit bestimmten Benutzern)
|
- 🔲 Gruppen/Communities System
|
||||||
- [ ] Kollaborative Bearbeitung von Mindmaps
|
- 🔲 Events und Kalenderfunktion
|
||||||
- [ ] Verknüpfung mit externen Ressourcen (Links, Dateien)
|
- 🔲 Live Discussions/Chats
|
||||||
- [ ] Versionierung von Mindmaps
|
- 🔲 Trending Topics/Hashtags
|
||||||
|
- 🔲 User Verification System
|
||||||
|
- 🔲 Moderation Tools
|
||||||
|
|
||||||
## Phase 6: KI-Integration und Analyse
|
### Phase 6: Advanced Features
|
||||||
|
- 🔲 AI-basierte Content Empfehlungen
|
||||||
|
- 🔲 Voice Notes und Audio Posts
|
||||||
|
- 🔲 Video Sharing und Streaming
|
||||||
|
- 🔲 Collaborative Mindmaps
|
||||||
|
- 🔲 Knowledge Graph Visualisierung
|
||||||
|
- 🔲 Advanced Analytics
|
||||||
|
|
||||||
- [ ] KI-gestützte Vorschläge für Verbindungen zwischen Knoten
|
### Phase 7: Monetarisierung & Skalierung
|
||||||
- [ ] Automatische Kategorisierung von Inhalten
|
- 🔲 Premium Features
|
||||||
- [ ] Visualisierung von Beziehungsstärken und -typen
|
- 🔲 Creator Economy Tools
|
||||||
- [ ] Mindmap-Statistiken und Analysen
|
- 🔲 API für Drittanbieter
|
||||||
- [ ] KI-basierte Zusammenfassung von Teilbereichen der Mindmap
|
- 🔲 Mobile Apps (iOS/Android)
|
||||||
|
- 🔲 Enterprise Features
|
||||||
|
- 🔲 Advanced Security Features
|
||||||
|
|
||||||
## Phase 7: Optimierung und Skalierung
|
### Phase 8: Integration & Ecosystem
|
||||||
|
- 🔲 External Tool Integrations
|
||||||
|
- 🔲 Learning Management System
|
||||||
|
- 🔲 Knowledge Base Integration
|
||||||
|
- 🔲 Research Tools
|
||||||
|
- 🔲 Publication System
|
||||||
|
- 🔲 Academic Collaboration Tools
|
||||||
|
|
||||||
- [ ] Performance-Optimierung für große Mindmaps
|
## 🏗️ Technische Architektur
|
||||||
- [ ] Verbesserung der Benutzerfreundlichkeit basierend auf Feedback
|
|
||||||
- [ ] Erweiterte Such- und Filterfunktionen
|
|
||||||
- [ ] Mobile Optimierung
|
|
||||||
- [ ] Offline-Funktionalität mit Synchronisierung
|
|
||||||
|
|
||||||
## Technische Schulden und Refactoring
|
### Backend Stack ✅
|
||||||
|
- **Framework**: Flask mit SQLAlchemy
|
||||||
|
- **Datenbank**: SQLite (PostgreSQL für Produktion)
|
||||||
|
- **Authentifizierung**: Flask-Login
|
||||||
|
- **API**: RESTful JSON APIs
|
||||||
|
- **Logging**: **Erweiterte SocialNetworkLogger mit visuellen Features**
|
||||||
|
- **Farbige Konsolen-Ausgabe mit ANSI-Codes**
|
||||||
|
- **Emoji-basierte Kategorisierung (🔐 AUTH, 🌐 API, 💾 DB, etc.)**
|
||||||
|
- **Component-spezifisches Logging mit Performance-Monitoring**
|
||||||
|
- **JSON-strukturierte Logs für externe Analyse**
|
||||||
|
- **Decorator-basierte automatische Instrumentierung**
|
||||||
|
- **Performance**: Pagination, Caching
|
||||||
|
|
||||||
- [ ] Trennung der Datenbank-Logik vom Flask-App-Code
|
### Frontend Stack ✅
|
||||||
- [ ] Einführung von Unit-Tests und Integration-Tests
|
- **Styling**: TailwindCSS mit Custom Themes
|
||||||
- [ ] Überarbeitung der API-Dokumentation
|
- **JavaScript**: Vanilla JS mit ES6+ Features
|
||||||
- [ ] Caching-Strategien für bessere Performance
|
- **Icons**: Font Awesome 6
|
||||||
- [ ] Verbesserte Fehlerbehandlung und Logging
|
- **Responsive**: Mobile-First Design
|
||||||
|
- **Interaktivität**: Alpine.js für reaktive Komponenten
|
||||||
|
|
||||||
|
### Database Schema ✅
|
||||||
|
```sql
|
||||||
|
-- Core Tables
|
||||||
|
users (erweitert mit Social Features)
|
||||||
|
social_posts (Posts System)
|
||||||
|
social_comments (Kommentar System)
|
||||||
|
notifications (Benachrichtigungssystem)
|
||||||
|
user_settings (Benutzereinstellungen)
|
||||||
|
activities (Aktivitätsverfolgung)
|
||||||
|
|
||||||
|
-- Relationship Tables
|
||||||
|
user_friendships (Freundschaftssystem)
|
||||||
|
user_follows (Follow System)
|
||||||
|
post_likes (Like System)
|
||||||
|
comment_likes (Comment Likes)
|
||||||
|
user_thought_bookmark (Bookmark System)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 API Endpunkte
|
||||||
|
|
||||||
|
### Social Feed APIs ✅
|
||||||
|
- `GET /api/social/posts` - Feed Posts abrufen
|
||||||
|
- `POST /api/social/posts` - Neuen Post erstellen
|
||||||
|
- `POST /api/social/posts/{id}/like` - Post liken/unliken
|
||||||
|
- `POST /api/social/posts/{id}/share` - Post teilen
|
||||||
|
- `POST /api/social/posts/{id}/bookmark` - Post bookmarken
|
||||||
|
|
||||||
|
### User Management APIs ✅
|
||||||
|
- `GET /api/social/users/{id}` - Benutzerprofil abrufen
|
||||||
|
- `GET /api/social/users/search` - Benutzer suchen
|
||||||
|
- `POST /api/social/users/{id}/follow` - Benutzer folgen/entfolgen
|
||||||
|
|
||||||
|
### Notification APIs ✅
|
||||||
|
- `GET /api/social/notifications` - Benachrichtigungen abrufen
|
||||||
|
- `POST /api/social/notifications/{id}/read` - Als gelesen markieren
|
||||||
|
- `POST /api/social/notifications/mark-all-read` - Alle als gelesen
|
||||||
|
- `DELETE /api/social/notifications/{id}` - Benachrichtigung löschen
|
||||||
|
|
||||||
|
### Analytics APIs ✅
|
||||||
|
- `GET /api/social/analytics/dashboard` - Benutzer-Analytics
|
||||||
|
- `GET /api/social/bookmarks` - Gebookmarkte Posts
|
||||||
|
|
||||||
|
## 🔒 Sicherheit & Datenschutz
|
||||||
|
|
||||||
|
### Implementierte Features ✅
|
||||||
|
- CSRF Protection
|
||||||
|
- SQL Injection Prevention
|
||||||
|
- Input Validation & Sanitization
|
||||||
|
- Session Management
|
||||||
|
- Password Hashing
|
||||||
|
- Privacy Controls (Post Visibility)
|
||||||
|
|
||||||
|
### Geplante Features
|
||||||
|
- 2FA Authentication
|
||||||
|
- Advanced Privacy Settings
|
||||||
|
- Data Export/Import
|
||||||
|
- GDPR Compliance Tools
|
||||||
|
- Content Moderation AI
|
||||||
|
|
||||||
|
## 📱 Mobile Support
|
||||||
|
|
||||||
|
### Aktuelle Features ✅
|
||||||
|
- Responsive Design
|
||||||
|
- Touch-Friendly Interface
|
||||||
|
- Mobile Navigation
|
||||||
|
- Optimized Loading
|
||||||
|
|
||||||
|
### Geplante Features
|
||||||
|
- PWA Support
|
||||||
|
- Offline Capabilities
|
||||||
|
- Push Notifications
|
||||||
|
- Native Mobile Apps
|
||||||
|
|
||||||
|
## 🎯 Leistungsziele
|
||||||
|
|
||||||
|
### Aktueller Status
|
||||||
|
- ✅ Grundlegende Performance
|
||||||
|
- ✅ Database Queries optimiert
|
||||||
|
- ✅ Frontend Responsiveness
|
||||||
|
- ✅ Strukturiertes Logging System
|
||||||
|
|
||||||
|
### Ziele für nächste Phase
|
||||||
|
- 🎯 < 200ms API Response Zeit
|
||||||
|
- 🎯 90+ Lighthouse Score
|
||||||
|
- 🎯 Skalierung auf 10k+ Benutzer
|
||||||
|
- 🎯 99.9% Uptime
|
||||||
|
|
||||||
|
## 🧪 Testing & Quality
|
||||||
|
|
||||||
|
### Implementiert
|
||||||
|
- ✅ Manuelle Testing
|
||||||
|
- ✅ Error Handling
|
||||||
|
- ✅ **Erweiterte Logging & Monitoring mit visuellen Features**
|
||||||
|
- ✅ **Farbige, kategorisierte Logs für bessere Debugging-Erfahrung**
|
||||||
|
- ✅ **Performance-Monitoring mit Zeitstempel**
|
||||||
|
- ✅ **Component-spezifische Fehlerbehandlung**
|
||||||
|
- ✅ **Strukturierte JSON-Logs für Analyse**
|
||||||
|
|
||||||
|
### Geplant
|
||||||
|
- 🔲 Automatisierte Unit Tests
|
||||||
|
- 🔲 Integration Tests
|
||||||
|
- 🔲 Performance Tests
|
||||||
|
- 🔲 Security Audits
|
||||||
|
- 🔲 Load Testing
|
||||||
|
- 🔲 **Log-basierte Alerting System**
|
||||||
|
- 🔲 **Automated Error Reporting**
|
||||||
|
|
||||||
|
## 📈 Metriken & Analytics
|
||||||
|
|
||||||
|
### User Engagement
|
||||||
|
- Posts pro Tag
|
||||||
|
- Kommentare und Likes
|
||||||
|
- Follow/Unfollow Raten
|
||||||
|
- Session Dauer
|
||||||
|
- Return User Rate
|
||||||
|
|
||||||
|
### System Performance
|
||||||
|
- API Response Zeiten
|
||||||
|
- Database Performance
|
||||||
|
- Error Rates
|
||||||
|
- User Activity Patterns
|
||||||
|
|
||||||
|
## 🛠️ Entwicklungsumgebung
|
||||||
|
|
||||||
|
### Setup Requirements ✅
|
||||||
|
```bash
|
||||||
|
# Virtual Environment
|
||||||
|
python3.11 -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
|
||||||
|
# Dependencies
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# Database Migration
|
||||||
|
flask db upgrade
|
||||||
|
|
||||||
|
# Development Server
|
||||||
|
python3.11 app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Development Tools ✅
|
||||||
|
- **IDE**: Cursor/VS Code
|
||||||
|
- **Version Control**: Git
|
||||||
|
- **Database**: SQLite (dev), PostgreSQL (prod)
|
||||||
|
- **Logging**: Colored Console + File Logging
|
||||||
|
- **Debug**: Flask Debug Mode
|
||||||
|
|
||||||
|
## 🌟 Innovation Features
|
||||||
|
|
||||||
|
### Einzigartige Aspekte
|
||||||
|
- 🧠 **Knowledge-First Design**: Fokus auf Wissensaustausch
|
||||||
|
- 🎨 **Mindmap Integration**: Visuelle Gedankenlandkarten
|
||||||
|
- 🔍 **Deep Search**: Semantic Search durch Inhalte
|
||||||
|
- 📊 **Learning Analytics**: Fortschritt und Erkenntnisse
|
||||||
|
- 🤝 **Collaborative Learning**: Gemeinsam Wissen erschaffen
|
||||||
|
|
||||||
|
### Zukünftige Innovationen
|
||||||
|
- 🤖 AI-Powered Knowledge Extraction
|
||||||
|
- 🎬 Interactive Learning Experiences
|
||||||
|
- 🌐 Cross-Platform Knowledge Sync
|
||||||
|
- 📚 Dynamic Knowledge Graphs
|
||||||
|
- 🧮 Algorithmic Learning Paths
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Implementierungsdetails
|
## 📝 Aktuelle Tasks
|
||||||
|
|
||||||
### Datenbankschema
|
### Hohe Priorität
|
||||||
|
1. ⏳ Chat/Messaging System implementieren
|
||||||
|
2. ⏳ Advanced Image Upload mit Preview
|
||||||
|
3. ⏳ Performance Optimierungen
|
||||||
|
4. ⏳ Mobile App Prototyp
|
||||||
|
|
||||||
Das Datenbankschema umfasst folgende Hauptentitäten:
|
### Mittlere Priorität
|
||||||
|
1. 🔲 Gruppen/Communities Feature
|
||||||
|
2. 🔲 Advanced Analytics Dashboard
|
||||||
|
3. 🔲 Content Moderation Tools
|
||||||
|
4. 🔲 API Rate Limiting
|
||||||
|
|
||||||
1. **Category** - Wissenschaftliche Kategorien für die öffentliche Mindmap
|
### Niedrige Priorität
|
||||||
2. **MindMapNode** - Öffentliche Mindmap-Knoten mit Metadaten
|
1. 🔲 Email Benachrichtigungen
|
||||||
3. **UserMindmap** - Benutzerdefinierte Mindmaps
|
2. 🔲 Export/Import Features
|
||||||
4. **UserMindmapNode** - Verknüpfung zwischen Benutzermindmaps und öffentlichen Knoten
|
3. 🔲 Advanced Search Filters
|
||||||
5. **MindmapNote** - Benutzerspezifische Notizen
|
4. 🔲 Theming System
|
||||||
6. **Thought** - Gedanken und Inhalte, die Knoten zugeordnet sind
|
|
||||||
7. **ThoughtRelation** - Beziehungen zwischen Gedanken
|
|
||||||
|
|
||||||
### Frontend-Technologien
|
---
|
||||||
|
|
||||||
- D3.js für die Visualisierung der Mindmap
|
**Letzte Aktualisierung**: {{ current_date }}
|
||||||
- AJAX für dynamisches Laden von Daten
|
**Version**: 2.0.0 - Social Network Release
|
||||||
- Interaktive Bedienelemente mit JavaScript
|
**Status**: ✅ Fully Functional Social Platform
|
||||||
- Responsive Design mit Tailwind CSS
|
|
||||||
|
|
||||||
### Backend-APIs
|
# 🗺️ SysTades Roadmap
|
||||||
|
|
||||||
Die implementierten API-Endpunkte umfassen:
|
## ✅ Abgeschlossen (v1.0 - v1.3)
|
||||||
|
|
||||||
- `/api/mindmap/public` - Abrufen der öffentlichen Mindmap-Struktur
|
### 🎯 Grundfunktionen
|
||||||
- `/api/mindmap/user/<id>` - Abrufen benutzerdefinierter Mindmaps
|
- [x] **Benutzerauthentifizierung** - Registrierung, Login, Logout
|
||||||
- `/api/mindmap/<id>/add_node` - Hinzufügen eines Knotens zur Benutzer-Mindmap
|
- [x] **Interaktive Mindmap** - Cytoscape.js-basierte Visualisierung
|
||||||
- `/api/mindmap/<id>/remove_node/<node_id>` - Entfernen eines Knotens
|
- [x] **Gedankenverwaltung** - CRUD-Operationen für Thoughts
|
||||||
- `/api/mindmap/<id>/update_node_position` - Aktualisierung von Knotenpositionen
|
- [x] **Kategoriesystem** - Hierarchische Wissensorganisation
|
||||||
- `/api/mindmap/<id>/notes` - Verwaltung von Notizen
|
- [x] **Responsive Design** - Mobile-first Ansatz
|
||||||
- `/api/nodes/<id>/thoughts` - Abrufen und Hinzufügen von Gedanken zu Knoten
|
- [x] **Dark/Light Mode** - Benutzerfreundliche Themes
|
||||||
|
|
||||||
|
### 🤖 KI-Integration
|
||||||
|
- [x] **ChatGPT-Assistent** - Integrierter AI-Chat
|
||||||
|
- [x] **Intelligente Suche** - KI-gestützte Inhaltssuche
|
||||||
|
- [x] **Automatische Kategorisierung** - AI-basierte Thought-Klassifizierung
|
||||||
|
|
||||||
|
### 🎨 UI/UX Verbesserungen
|
||||||
|
- [x] **Moderne Navigation** - Glassmorphism-Design
|
||||||
|
- [x] **Animationen** - Smooth Transitions und Hover-Effekte
|
||||||
|
- [x] **Accessibility** - ARIA-Labels und Keyboard-Navigation
|
||||||
|
- [x] **Performance-Optimierung** - Lazy Loading und Caching
|
||||||
|
|
||||||
|
## 🚀 Neu implementiert (v1.4 - Social Network Update)
|
||||||
|
|
||||||
|
### 📱 Social Network Features
|
||||||
|
- [x] **Social Feed** - Instagram/Twitter-ähnlicher Feed
|
||||||
|
- [x] **Post-System** - Erstellen, Liken, Kommentieren von Posts
|
||||||
|
- [x] **Follow-System** - Benutzer folgen und entfolgen
|
||||||
|
- [x] **Discover-Seite** - Trending Posts und empfohlene Benutzer
|
||||||
|
- [x] **Benutzerprofile** - Erweiterte Profile mit Posts, Mindmaps, Gedanken
|
||||||
|
- [x] **Benachrichtigungssystem** - Likes, Kommentare, Follows
|
||||||
|
- [x] **Community-Statistiken** - Aktive Benutzer, Posts, Mindmaps
|
||||||
|
|
||||||
|
### 🧠 Erweiterte Mindmap-Features
|
||||||
|
- [x] **Kollaborative Bearbeitung** - Vorbereitung für Echtzeit-Kollaboration
|
||||||
|
- [x] **Mindmap-Export** - JSON-Export mit geplanten weiteren Formaten
|
||||||
|
- [x] **Mindmap-Sharing** - Teilen von Mindmaps in sozialen Netzwerken
|
||||||
|
- [x] **Erweiterte Toolbar** - Neue Bearbeitungsoptionen
|
||||||
|
- [x] **Vollbild-Modus** - Immersive Mindmap-Bearbeitung
|
||||||
|
- [x] **Schnelle Knoten-/Gedanken-Erstellung** - Direkt aus der Mindmap
|
||||||
|
|
||||||
|
### 🔗 Integration & Vernetzung
|
||||||
|
- [x] **Gedanken in Posts teilen** - Wissenschaftliche Inhalte im Feed
|
||||||
|
- [x] **Mindmap-Knoten teilen** - Wissensbausteine verbreiten
|
||||||
|
- [x] **Cross-Platform Navigation** - Nahtlose Übergänge zwischen Features
|
||||||
|
- [x] **Unified Search** - Suche über alle Inhaltstypen
|
||||||
|
|
||||||
|
## 🔄 In Entwicklung (v1.5)
|
||||||
|
|
||||||
|
### 🔄 Echtzeit-Features
|
||||||
|
- [ ] **Live-Kollaboration** - Mehrere Benutzer bearbeiten gleichzeitig Mindmaps
|
||||||
|
- [ ] **WebSocket-Integration** - Echtzeit-Updates für Feed und Benachrichtigungen
|
||||||
|
- [ ] **Live-Cursor** - Sehen wo andere Benutzer arbeiten
|
||||||
|
- [ ] **Änderungshistorie** - Versionskontrolle für Mindmaps
|
||||||
|
|
||||||
|
### 💬 Erweiterte Kommunikation
|
||||||
|
- [ ] **Direktnachrichten** - Private Nachrichten zwischen Benutzern
|
||||||
|
- [ ] **Gruppen-Chats** - Themenbasierte Diskussionsgruppen
|
||||||
|
- [ ] **Video-Calls** - Integrierte Videokonferenzen für Kollaboration
|
||||||
|
- [ ] **Screen-Sharing** - Bildschirm teilen während Kollaboration
|
||||||
|
|
||||||
|
## 📋 Geplant (v1.6 - v2.0)
|
||||||
|
|
||||||
|
### 📊 Analytics & Insights
|
||||||
|
- [ ] **Lernfortschritt-Tracking** - Persönliche Wissensstatistiken
|
||||||
|
- [ ] **Mindmap-Analytics** - Nutzungsstatistiken und Hotspots
|
||||||
|
- [ ] **Community-Insights** - Trending-Themen und beliebte Inhalte
|
||||||
|
- [ ] **Empfehlungsalgorithmus** - Personalisierte Inhaltsvorschläge
|
||||||
|
|
||||||
|
### 🎓 Bildungsfeatures
|
||||||
|
- [ ] **Kurssystem** - Strukturierte Lernpfade
|
||||||
|
- [ ] **Quizzes & Tests** - Wissensüberprüfung
|
||||||
|
- [ ] **Zertifikate** - Digitale Abschlüsse
|
||||||
|
- [ ] **Mentoring-System** - Experten-Schüler-Verbindungen
|
||||||
|
|
||||||
|
### 🔧 Erweiterte Tools
|
||||||
|
- [ ] **PDF-Import** - Automatische Mindmap-Generierung aus Dokumenten
|
||||||
|
- [ ] **LaTeX-Support** - Mathematische Formeln in Gedanken
|
||||||
|
- [ ] **Multimedia-Integration** - Videos, Audio, Bilder in Mindmaps
|
||||||
|
- [ ] **API für Drittanbieter** - Integration mit anderen Tools
|
||||||
|
|
||||||
|
### 🌐 Skalierung & Performance
|
||||||
|
- [ ] **Microservices-Architektur** - Bessere Skalierbarkeit
|
||||||
|
- [ ] **CDN-Integration** - Globale Content-Delivery
|
||||||
|
- [ ] **Caching-Optimierung** - Redis für bessere Performance
|
||||||
|
- [ ] **Load Balancing** - Hochverfügbarkeit
|
||||||
|
|
||||||
|
## 🔮 Vision (v2.0+)
|
||||||
|
|
||||||
|
### 🤖 Erweiterte KI
|
||||||
|
- [ ] **Personalisierte KI-Tutoren** - Individuelle Lernbegleitung
|
||||||
|
- [ ] **Automatische Mindmap-Generierung** - KI erstellt Mindmaps aus Text
|
||||||
|
- [ ] **Intelligente Verbindungen** - KI schlägt Gedankenverknüpfungen vor
|
||||||
|
- [ ] **Adaptive Lernpfade** - KI passt Inhalte an Lernstil an
|
||||||
|
|
||||||
|
### 🌍 Globale Community
|
||||||
|
- [ ] **Mehrsprachigkeit** - Internationale Benutzergemeinschaft
|
||||||
|
- [ ] **Kultureller Austausch** - Globale Wissensnetzwerke
|
||||||
|
- [ ] **Übersetzungsfeatures** - Automatische Inhaltsübersetzung
|
||||||
|
- [ ] **Regionale Communities** - Lokale Wissensgruppen
|
||||||
|
|
||||||
|
### 🔬 Forschungstools
|
||||||
|
- [ ] **Literaturverwaltung** - Integration mit wissenschaftlichen Datenbanken
|
||||||
|
- [ ] **Zitiersystem** - Automatische Quellenangaben
|
||||||
|
- [ ] **Peer-Review-System** - Wissenschaftliche Qualitätskontrolle
|
||||||
|
- [ ] **Publikationstools** - Direkte Veröffentlichung von Forschungsergebnissen
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📈 Metriken & Ziele
|
||||||
|
|
||||||
|
### Technische Ziele
|
||||||
|
- **Performance**: < 2s Ladezeit für alle Seiten
|
||||||
|
- **Verfügbarkeit**: 99.9% Uptime
|
||||||
|
- **Skalierbarkeit**: 10.000+ gleichzeitige Benutzer
|
||||||
|
- **Sicherheit**: Zero-Trust-Architektur
|
||||||
|
|
||||||
|
### Community-Ziele
|
||||||
|
- **Benutzer**: 1.000+ aktive Benutzer bis Ende 2024
|
||||||
|
- **Inhalte**: 10.000+ Gedanken und 1.000+ Mindmaps
|
||||||
|
- **Engagement**: 70%+ monatliche Aktivitätsrate
|
||||||
|
- **Zufriedenheit**: 4.5+ Sterne Bewertung
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🤝 Beitragen
|
||||||
|
|
||||||
|
Interessiert an der Mitarbeit? Hier sind die Bereiche, in denen wir Unterstützung suchen:
|
||||||
|
|
||||||
|
### 👨💻 Entwicklung
|
||||||
|
- **Frontend**: React/Vue.js Komponenten
|
||||||
|
- **Backend**: Python/Flask API-Entwicklung
|
||||||
|
- **Mobile**: React Native App
|
||||||
|
- **DevOps**: Docker, Kubernetes, CI/CD
|
||||||
|
|
||||||
|
### 🎨 Design
|
||||||
|
- **UI/UX**: Benutzeroberflächen-Design
|
||||||
|
- **Grafik**: Icons, Illustrationen, Branding
|
||||||
|
- **Animation**: Micro-Interactions und Transitions
|
||||||
|
- **Accessibility**: Barrierefreie Gestaltung
|
||||||
|
|
||||||
|
### 📝 Content
|
||||||
|
- **Dokumentation**: Technische und Benutzer-Dokumentation
|
||||||
|
- **Tutorials**: Video- und Text-Anleitungen
|
||||||
|
- **Übersetzungen**: Mehrsprachige Inhalte
|
||||||
|
- **Community**: Moderation und Support
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Letzte Aktualisierung: Januar 2024*
|
||||||
|
*Version: 1.4.0 - Social Network Update*
|
||||||
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.
@@ -2,3 +2,4 @@
|
|||||||
# https://curl.se/docs/http-cookies.html
|
# https://curl.se/docs/http-cookies.html
|
||||||
# This file was generated by libcurl! Edit at your own risk.
|
# This file was generated by libcurl! Edit at your own risk.
|
||||||
|
|
||||||
|
#HttpOnly_127.0.0.1 FALSE / FALSE 0 session .eJwlzjEOwjAMQNG7ZGaIYztJe5nKjm2BACG1dELcnUiMf3n6n7TF7sc1re_99EvabpbWZI7cikB4dsoylLrmcKXSormH-OhKoQRSAy0v3kEzDqJlSNFCg8NIW25sfYChAgryFIWxdqyskqFWtNIdiF3awiZRaq9TzGmOnIfv_xuYabLft-fLPK0hj8O_P-1dNpA.aDdoog.bmKi2y6o3HQgIk4gwDvhirnxuoM
|
||||||
|
|||||||
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
|
# Kopiere diese Datei zu .env und passe die Werte an
|
||||||
|
|
||||||
# Flask
|
# Flask
|
||||||
SECRET_KEY=dein-geheimer-schluessel-hier
|
FLASK_APP=app.py
|
||||||
|
FLASK_ENV=development
|
||||||
|
SECRET_KEY=mein-sicherer-schluessel-fuer-entwicklung
|
||||||
|
|
||||||
# OpenAI API
|
# OpenAI API
|
||||||
OPENAI_API_KEY=sk-dein-openai-api-schluessel-hier
|
OPENAI_API_KEY=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
|
||||||
|
|
||||||
# Datenbank
|
# Datenbank
|
||||||
# Bei Bedarf kann hier eine andere Datenbank-URL angegeben werden
|
# Bei Bedarf kann hier eine andere Datenbank-URL angegeben werden
|
||||||
# Der Pfad wird relativ zum Projektverzeichnis angegeben
|
# Der Pfad wird relativ zum Projektverzeichnis angegeben
|
||||||
# SQLALCHEMY_DATABASE_URI=sqlite:////absoluter/pfad/zu/database/systades.db
|
SQLALCHEMY_DATABASE_URI=sqlite:///database/systades.db
|
||||||
550
init_db.py
Executable file → Normal file
550
init_db.py
Executable file → Normal file
@@ -1,256 +1,320 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- 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 db, User, Thought, Comment, MindMapNode, ThoughtRelation, ThoughtRating, RelationType
|
||||||
from models import Category, UserMindmap, UserMindmapNode, MindmapNote
|
from models import Category, UserMindmap, UserMindmapNode, MindmapNote
|
||||||
import os
|
|
||||||
|
|
||||||
def init_database():
|
db.init_app(app)
|
||||||
"""Initialisiert die Datenbank mit Beispieldaten."""
|
|
||||||
with app.app_context():
|
|
||||||
# Datenbank löschen und neu erstellen
|
|
||||||
if os.path.exists(db_path):
|
|
||||||
os.remove(db_path)
|
|
||||||
|
|
||||||
# Stellen Sie sicher, dass das Verzeichnis existiert
|
|
||||||
os.makedirs(os.path.dirname(db_path), exist_ok=True)
|
|
||||||
|
|
||||||
db.create_all()
|
|
||||||
|
|
||||||
# Admin-Benutzer erstellen
|
|
||||||
admin = User(username='admin', email='admin@example.com', is_admin=True)
|
|
||||||
admin.set_password('admin')
|
|
||||||
db.session.add(admin)
|
|
||||||
|
|
||||||
# Beispiel-Benutzer erstellen
|
|
||||||
user = User(username='user', email='user@example.com')
|
|
||||||
user.set_password('user')
|
|
||||||
db.session.add(user)
|
|
||||||
|
|
||||||
# Commit, um IDs zu generieren
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
# Wissenschaftliche Kategorien erstellen
|
|
||||||
science = Category(name='Wissenschaft', description='Wissenschaftliche Erkenntnisse',
|
|
||||||
color_code='#4CAF50', icon='flask')
|
|
||||||
db.session.add(science)
|
|
||||||
|
|
||||||
philosophy = Category(name='Philosophie', description='Philosophische Theorien und Gedanken',
|
|
||||||
color_code='#9C27B0', icon='lightbulb')
|
|
||||||
db.session.add(philosophy)
|
|
||||||
|
|
||||||
technology = Category(name='Technologie', description='Technologische Entwicklungen',
|
|
||||||
color_code='#FF9800', icon='microchip')
|
|
||||||
db.session.add(technology)
|
|
||||||
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
# Wissenschaftliche Unterkategorien
|
|
||||||
physics = Category(name='Physik', description='Studium der Materie und Energie',
|
|
||||||
color_code='#81C784', icon='atom', parent_id=science.id)
|
|
||||||
biology = Category(name='Biologie', description='Studium lebender Organismen',
|
|
||||||
color_code='#66BB6A', icon='leaf', parent_id=science.id)
|
|
||||||
chemistry = Category(name='Chemie', description='Studium der Stoffe und ihrer Reaktionen',
|
|
||||||
color_code='#A5D6A7', icon='vial', parent_id=science.id)
|
|
||||||
|
|
||||||
db.session.add_all([physics, biology, chemistry])
|
|
||||||
|
|
||||||
# Technologie-Unterkategorien
|
|
||||||
informatics = Category(name='Informatik', description='Studium der Informationsverarbeitung',
|
|
||||||
color_code='#FFB74D', icon='laptop-code', parent_id=technology.id)
|
|
||||||
ai = Category(name='Künstliche Intelligenz', description='Entwicklung intelligenter Systeme',
|
|
||||||
color_code='#FFA726', icon='robot', parent_id=technology.id)
|
|
||||||
|
|
||||||
db.session.add_all([informatics, ai])
|
|
||||||
|
|
||||||
# Philosophie-Unterkategorien
|
|
||||||
ethics = Category(name='Ethik', description='Moralphilosophie und Wertesysteme',
|
|
||||||
color_code='#BA68C8', icon='balance-scale', parent_id=philosophy.id)
|
|
||||||
logic = Category(name='Logik', description='Studie der gültigen Schlussfolgerungen',
|
|
||||||
color_code='#AB47BC', icon='project-diagram', parent_id=philosophy.id)
|
|
||||||
|
|
||||||
db.session.add_all([ethics, logic])
|
|
||||||
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
# Knoten für die öffentliche Mindmap erstellen
|
|
||||||
nodes = {
|
|
||||||
'quantenmechanik': MindMapNode(
|
|
||||||
name='Quantenmechanik',
|
|
||||||
description='Physikalische Theorie zur Beschreibung der Materie auf atomarer Ebene',
|
|
||||||
color_code='#81C784',
|
|
||||||
category_id=physics.id,
|
|
||||||
created_by_id=admin.id
|
|
||||||
),
|
|
||||||
'relativitaetstheorie': MindMapNode(
|
|
||||||
name='Relativitätstheorie',
|
|
||||||
description='Einsteins Theorien zur Raumzeit und Gravitation',
|
|
||||||
color_code='#81C784',
|
|
||||||
category_id=physics.id,
|
|
||||||
created_by_id=admin.id
|
|
||||||
),
|
|
||||||
'genetik': MindMapNode(
|
|
||||||
name='Genetik',
|
|
||||||
description='Wissenschaft der Gene und Vererbung',
|
|
||||||
color_code='#66BB6A',
|
|
||||||
category_id=biology.id,
|
|
||||||
created_by_id=admin.id
|
|
||||||
),
|
|
||||||
'machine_learning': MindMapNode(
|
|
||||||
name='Machine Learning',
|
|
||||||
description='Algorithmen, die aus Daten lernen können',
|
|
||||||
color_code='#FFA726',
|
|
||||||
category_id=ai.id,
|
|
||||||
created_by_id=admin.id
|
|
||||||
),
|
|
||||||
'ki_ethik': MindMapNode(
|
|
||||||
name='KI-Ethik',
|
|
||||||
description='Moralische Implikationen künstlicher Intelligenz',
|
|
||||||
color_code='#BA68C8',
|
|
||||||
category_id=ethics.id,
|
|
||||||
created_by_id=user.id
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
for node in nodes.values():
|
|
||||||
db.session.add(node)
|
|
||||||
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
# Verknüpfungen zwischen Knoten herstellen (Hierarchie)
|
|
||||||
nodes['machine_learning'].parents.append(nodes['ki_ethik'])
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
# Gedanken erstellen
|
|
||||||
thoughts = [
|
|
||||||
{
|
|
||||||
'title': 'Künstliche Intelligenz und Bewusstsein',
|
|
||||||
'content': 'Die Frage nach maschinellem Bewusstsein ist fundamental für die KI-Ethik. Aktuelle KI-Systeme haben kein Bewusstsein, aber fortschrittliche KI könnte in Zukunft Eigenschaften entwickeln, die diesem nahekommen.',
|
|
||||||
'abstract': 'Eine Untersuchung der philosophischen Implikationen von KI-Bewusstsein.',
|
|
||||||
'keywords': 'KI, Bewusstsein, Ethik, Philosophie',
|
|
||||||
'branch': 'Philosophie',
|
|
||||||
'color_code': '#BA68C8',
|
|
||||||
'source_type': 'Markdown',
|
|
||||||
'user_id': user.id,
|
|
||||||
'node': nodes['ki_ethik']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'title': 'Quantenmechanik und Realität',
|
|
||||||
'content': 'Die Kopenhagener Deutung und ihre Auswirkungen auf unser Verständnis der Realität. Quantenmechanik stellt grundlegende Annahmen über Determinismus und Lokalität in Frage.',
|
|
||||||
'abstract': 'Eine Analyse verschiedener Interpretationen der Quantenmechanik.',
|
|
||||||
'keywords': 'Quantenmechanik, Physik, Realität',
|
|
||||||
'branch': 'Physik',
|
|
||||||
'color_code': '#81C784',
|
|
||||||
'source_type': 'PDF',
|
|
||||||
'user_id': admin.id,
|
|
||||||
'node': nodes['quantenmechanik']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'title': 'Deep Learning Fortschritte',
|
|
||||||
'content': 'Die neuesten Fortschritte im Deep Learning haben zu beeindruckenden Ergebnissen in Bereichen wie Computer Vision, Natural Language Processing und Reinforcement Learning geführt.',
|
|
||||||
'abstract': 'Überblick über aktuelle Deep Learning-Techniken und ihre Anwendungen.',
|
|
||||||
'keywords': 'Deep Learning, Neural Networks, AI',
|
|
||||||
'branch': 'Technologie',
|
|
||||||
'color_code': '#FFA726',
|
|
||||||
'source_type': 'Webpage',
|
|
||||||
'user_id': admin.id,
|
|
||||||
'node': nodes['machine_learning']
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
thought_objects = []
|
|
||||||
for t_data in thoughts:
|
|
||||||
node = t_data.pop('node')
|
|
||||||
thought = Thought(**t_data)
|
|
||||||
node.thoughts.append(thought)
|
|
||||||
thought_objects.append(thought)
|
|
||||||
db.session.add(thought)
|
|
||||||
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
# Beziehungen zwischen Gedanken
|
|
||||||
relation = ThoughtRelation(
|
|
||||||
source_id=thought_objects[0].id,
|
|
||||||
target_id=thought_objects[2].id,
|
|
||||||
relation_type=RelationType.INSPIRES,
|
|
||||||
created_by_id=user.id
|
|
||||||
)
|
|
||||||
db.session.add(relation)
|
|
||||||
|
|
||||||
# Bewertungen erstellen
|
|
||||||
rating1 = ThoughtRating(
|
|
||||||
thought_id=thought_objects[0].id,
|
|
||||||
user_id=admin.id,
|
|
||||||
relevance_score=5
|
|
||||||
)
|
|
||||||
rating2 = ThoughtRating(
|
|
||||||
thought_id=thought_objects[2].id,
|
|
||||||
user_id=user.id,
|
|
||||||
relevance_score=4
|
|
||||||
)
|
|
||||||
db.session.add_all([rating1, rating2])
|
|
||||||
|
|
||||||
# Kommentare erstellen
|
|
||||||
for thought in thought_objects:
|
|
||||||
comment = Comment(
|
|
||||||
content=f'Interessante Perspektive zu {thought.title}!',
|
|
||||||
thought_id=thought.id,
|
|
||||||
user_id=admin.id if thought.user_id != admin.id else user.id
|
|
||||||
)
|
|
||||||
db.session.add(comment)
|
|
||||||
|
|
||||||
# Benutzer-Mindmaps erstellen
|
|
||||||
user_mindmap = UserMindmap(
|
|
||||||
name='Meine KI-Forschung',
|
|
||||||
description='Meine persönliche Sammlung zu KI und Ethik',
|
|
||||||
user_id=user.id
|
|
||||||
)
|
|
||||||
db.session.add(user_mindmap)
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
# Knoten zur Benutzer-Mindmap hinzufügen
|
|
||||||
user_mindmap_nodes = [
|
|
||||||
UserMindmapNode(
|
|
||||||
user_mindmap_id=user_mindmap.id,
|
|
||||||
node_id=nodes['machine_learning'].id,
|
|
||||||
x_position=200,
|
|
||||||
y_position=300
|
|
||||||
),
|
|
||||||
UserMindmapNode(
|
|
||||||
user_mindmap_id=user_mindmap.id,
|
|
||||||
node_id=nodes['ki_ethik'].id,
|
|
||||||
x_position=500,
|
|
||||||
y_position=200
|
|
||||||
)
|
|
||||||
]
|
|
||||||
db.session.add_all(user_mindmap_nodes)
|
|
||||||
|
|
||||||
# Private Notizen
|
|
||||||
note = MindmapNote(
|
|
||||||
user_id=user.id,
|
|
||||||
mindmap_id=user_mindmap.id,
|
|
||||||
node_id=nodes['ki_ethik'].id,
|
|
||||||
content="Recherchiere mehr über aktuelle ethische Richtlinien für KI-Entwicklung!",
|
|
||||||
color_code="#FFF59D"
|
|
||||||
)
|
|
||||||
db.session.add(note)
|
|
||||||
|
|
||||||
# Gedanken zu Bookmarks hinzufügen
|
|
||||||
user.bookmarked_thoughts.append(thought_objects[0])
|
|
||||||
admin.bookmarked_thoughts.append(thought_objects[1])
|
|
||||||
|
|
||||||
# Finaler Commit
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
print("Datenbank wurde erfolgreich initialisiert!")
|
|
||||||
|
|
||||||
def init_db():
|
def init_db():
|
||||||
"""Alias für Kompatibilität mit älteren Scripts."""
|
with app.app_context():
|
||||||
init_database()
|
print("Initialisiere Datenbank...")
|
||||||
|
|
||||||
|
# Tabellen erstellen
|
||||||
|
db.create_all()
|
||||||
|
print("Tabellen wurden erstellt.")
|
||||||
|
|
||||||
|
# Standardbenutzer erstellen, falls keine vorhanden sind
|
||||||
|
if User.query.count() == 0:
|
||||||
|
print("Erstelle Standardbenutzer...")
|
||||||
|
create_default_users()
|
||||||
|
|
||||||
|
# Standardkategorien erstellen, falls keine vorhanden sind
|
||||||
|
if Category.query.count() == 0:
|
||||||
|
print("Erstelle Standardkategorien...")
|
||||||
|
create_default_categories()
|
||||||
|
|
||||||
|
# Beispiel-Mindmap erstellen, falls keine Knoten vorhanden sind
|
||||||
|
if MindMapNode.query.count() == 0:
|
||||||
|
print("Erstelle Beispiel-Mindmap...")
|
||||||
|
create_sample_mindmap()
|
||||||
|
|
||||||
|
print("Datenbankinitialisierung abgeschlossen.")
|
||||||
|
|
||||||
|
def create_default_users():
|
||||||
|
"""Erstellt Standardbenutzer für die Anwendung"""
|
||||||
|
users = [
|
||||||
|
{
|
||||||
|
'username': 'admin',
|
||||||
|
'email': 'admin@example.com',
|
||||||
|
'password': 'admin',
|
||||||
|
'role': 'admin'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'username': 'user',
|
||||||
|
'email': 'user@example.com',
|
||||||
|
'password': 'user',
|
||||||
|
'role': 'user'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
for user_data in users:
|
||||||
|
password = user_data.pop('password')
|
||||||
|
user = User(**user_data)
|
||||||
|
user.set_password(password)
|
||||||
|
db.session.add(user)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
print(f"{len(users)} Benutzer wurden erstellt.")
|
||||||
|
|
||||||
|
def create_default_categories():
|
||||||
|
"""Erstellt die Standardkategorien für die Mindmap"""
|
||||||
|
# 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__':
|
if __name__ == '__main__':
|
||||||
init_database()
|
init_db()
|
||||||
print("Datenbank wurde erfolgreich initialisiert!")
|
print("Datenbank wurde erfolgreich initialisiert!")
|
||||||
print("Sie können die Anwendung jetzt mit 'python app.py' starten")
|
print("Sie können die Anwendung jetzt mit 'python app.py' starten")
|
||||||
print("Anmelden mit:")
|
print("Anmelden mit:")
|
||||||
|
|||||||
0
instance/logs/app.log
Normal file
0
instance/logs/app.log
Normal file
0
instance/logs/errors.log
Normal file
0
instance/logs/errors.log
Normal file
0
instance/logs/social.log
Normal file
0
instance/logs/social.log
Normal file
0
logs/api.log
Normal file
0
logs/api.log
Normal file
2419
logs/app.log
Normal file
2419
logs/app.log
Normal file
File diff suppressed because it is too large
Load Diff
424
logs/errors.log
Normal file
424
logs/errors.log
Normal file
@@ -0,0 +1,424 @@
|
|||||||
|
2025-05-28 21:29:08 | ERROR | SysTades | ERROR | Fehler 500: 405 Method Not Allowed: The method is not allowed for the requested URL.
|
||||||
|
Endpoint: /api/thoughts, Method: GET, IP: 127.0.0.1
|
||||||
|
User: 1 (admin)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 867, in full_dispatch_request
|
||||||
|
rv = self.dispatch_request()
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 841, in dispatch_request
|
||||||
|
self.raise_routing_exception(req)
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 450, in raise_routing_exception
|
||||||
|
raise request.routing_exception # type: ignore
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/flask/ctx.py", line 353, in match_request
|
||||||
|
result = self.url_adapter.match(return_rule=True) # type: ignore
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/werkzeug/routing/map.py", line 619, in match
|
||||||
|
raise MethodNotAllowed(valid_methods=list(e.have_match_for)) from None
|
||||||
|
werkzeug.exceptions.MethodNotAllowed: 405 Method Not Allowed: The method is not allowed for the requested URL.
|
||||||
|
|
||||||
|
2025-05-28 21:43:40 | ERROR | SysTades | ERROR | Fehler in social_feed nach 2.83ms - Exception: AttributeError: followed_id
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/sql/base.py", line 1633, in __getattr__
|
||||||
|
return self._index[key][1]
|
||||||
|
~~~~~~~~~~~^^^^^
|
||||||
|
KeyError: 'followed_id'
|
||||||
|
|
||||||
|
The above exception was the direct cause of the following exception:
|
||||||
|
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "/home/core/dev/website/utils/logger.py", line 586, in wrapper
|
||||||
|
result = func(*args, **kwargs)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/dev/website/app.py", line 2774, in social_feed
|
||||||
|
followed_posts = current_user.get_feed_posts(limit=100)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/dev/website/models.py", line 193, in get_feed_posts
|
||||||
|
followed_users, SocialPost.user_id == followed_users.c.followed_id
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/sql/base.py", line 1635, in __getattr__
|
||||||
|
raise AttributeError(key) from err
|
||||||
|
AttributeError: followed_id
|
||||||
|
|
||||||
|
2025-05-28 21:43:40 | ERROR | SysTades | ERROR | Fehler 500: followed_id
|
||||||
|
Endpoint: /feed, Method: GET, IP: 127.0.0.1
|
||||||
|
User: 1 (admin)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/sql/base.py", line 1633, in __getattr__
|
||||||
|
return self._index[key][1]
|
||||||
|
~~~~~~~~~~~^^^^^
|
||||||
|
KeyError: 'followed_id'
|
||||||
|
|
||||||
|
The above exception was the direct cause of the following exception:
|
||||||
|
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 867, in full_dispatch_request
|
||||||
|
rv = self.dispatch_request()
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 852, in dispatch_request
|
||||||
|
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/flask_login/utils.py", line 290, in decorated_view
|
||||||
|
return current_app.ensure_sync(func)(*args, **kwargs)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/dev/website/utils/logger.py", line 586, in wrapper
|
||||||
|
result = func(*args, **kwargs)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/dev/website/app.py", line 2774, in social_feed
|
||||||
|
followed_posts = current_user.get_feed_posts(limit=100)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/dev/website/models.py", line 193, in get_feed_posts
|
||||||
|
followed_users, SocialPost.user_id == followed_users.c.followed_id
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/sql/base.py", line 1635, in __getattr__
|
||||||
|
raise AttributeError(key) from err
|
||||||
|
AttributeError: followed_id
|
||||||
|
|
||||||
|
2025-05-28 21:43:59 | ERROR | SysTades | ERROR | Fehler in discover nach 16.89ms - Exception: AttributeError: 'AppenderQuery' object has no attribute 'contains'
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "/home/core/dev/website/utils/logger.py", line 586, in wrapper
|
||||||
|
result = func(*args, **kwargs)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/dev/website/app.py", line 2800, in discover
|
||||||
|
~current_user.following.contains(User.id)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
AttributeError: 'AppenderQuery' object has no attribute 'contains'
|
||||||
|
|
||||||
|
2025-05-28 21:43:59 | ERROR | SysTades | ERROR | Fehler 500: 'AppenderQuery' object has no attribute 'contains'
|
||||||
|
Endpoint: /discover, Method: GET, IP: 127.0.0.1
|
||||||
|
User: 1 (admin)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 867, in full_dispatch_request
|
||||||
|
rv = self.dispatch_request()
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 852, in dispatch_request
|
||||||
|
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/flask_login/utils.py", line 290, in decorated_view
|
||||||
|
return current_app.ensure_sync(func)(*args, **kwargs)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/dev/website/utils/logger.py", line 586, in wrapper
|
||||||
|
result = func(*args, **kwargs)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/dev/website/app.py", line 2800, in discover
|
||||||
|
~current_user.following.contains(User.id)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
AttributeError: 'AppenderQuery' object has no attribute 'contains'
|
||||||
|
|
||||||
|
2025-05-28 21:46:15 | ERROR | SysTades | ERROR | Fehler in social_feed nach 54.92ms - Exception: OperationalError: (sqlite3.OperationalError) near "UNION": syntax error
|
||||||
|
[SQL: SELECT anon_1.social_post_id AS anon_1_social_post_id, anon_1.social_post_content AS anon_1_social_post_content, anon_1.social_post_image_url AS anon_1_social_post_image_url, anon_1.social_post_video_url AS anon_1_social_post_video_url, anon_1.social_post_link_url AS anon_1_social_post_link_url, anon_1.social_post_link_title AS anon_1_social_post_link_title, anon_1.social_post_link_description AS anon_1_social_post_link_description, anon_1.social_post_post_type AS anon_1_social_post_post_type, anon_1.social_post_visibility AS anon_1_social_post_visibility, anon_1.social_post_is_pinned AS anon_1_social_post_is_pinned, anon_1.social_post_like_count AS anon_1_social_post_like_count, anon_1.social_post_comment_count AS anon_1_social_post_comment_count, anon_1.social_post_share_count AS anon_1_social_post_share_count, anon_1.social_post_view_count AS anon_1_social_post_view_count, anon_1.social_post_created_at AS anon_1_social_post_created_at, anon_1.social_post_updated_at AS anon_1_social_post_updated_at, anon_1.social_post_user_id AS anon_1_social_post_user_id, anon_1.social_post_shared_thought_id AS anon_1_social_post_shared_thought_id, anon_1.social_post_shared_node_id AS anon_1_social_post_shared_node_id
|
||||||
|
FROM ((SELECT social_post.id AS social_post_id, social_post.content AS social_post_content, social_post.image_url AS social_post_image_url, social_post.video_url AS social_post_video_url, social_post.link_url AS social_post_link_url, social_post.link_title AS social_post_link_title, social_post.link_description AS social_post_link_description, social_post.post_type AS social_post_post_type, social_post.visibility AS social_post_visibility, social_post.is_pinned AS social_post_is_pinned, social_post.like_count AS social_post_like_count, social_post.comment_count AS social_post_comment_count, social_post.share_count AS social_post_share_count, social_post.view_count AS social_post_view_count, social_post.created_at AS social_post_created_at, social_post.updated_at AS social_post_updated_at, social_post.user_id AS social_post_user_id, social_post.shared_thought_id AS social_post_shared_thought_id, social_post.shared_node_id AS social_post_shared_node_id
|
||||||
|
FROM social_post
|
||||||
|
WHERE social_post.user_id IN (?) ORDER BY social_post.created_at DESC
|
||||||
|
LIMIT ? OFFSET ?) UNION SELECT social_post.id AS social_post_id, social_post.content AS social_post_content, social_post.image_url AS social_post_image_url, social_post.video_url AS social_post_video_url, social_post.link_url AS social_post_link_url, social_post.link_title AS social_post_link_title, social_post.link_description AS social_post_link_description, social_post.post_type AS social_post_post_type, social_post.visibility AS social_post_visibility, social_post.is_pinned AS social_post_is_pinned, social_post.like_count AS social_post_like_count, social_post.comment_count AS social_post_comment_count, social_post.share_count AS social_post_share_count, social_post.view_count AS social_post_view_count, social_post.created_at AS social_post_created_at, social_post.updated_at AS social_post_updated_at, social_post.user_id AS social_post_user_id, social_post.shared_thought_id AS social_post_shared_thought_id, social_post.shared_node_id AS social_post_shared_node_id
|
||||||
|
FROM social_post
|
||||||
|
WHERE social_post.user_id = ?) AS anon_1 ORDER BY anon_1.social_post_created_at DESC
|
||||||
|
LIMIT ? OFFSET ?]
|
||||||
|
[parameters: (1, 100, 0, 1, 10, 0)]
|
||||||
|
(Background on this error at: https://sqlalche.me/e/20/e3q8)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1969, in _exec_single_context
|
||||||
|
self.dialect.do_execute(
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/default.py", line 922, in do_execute
|
||||||
|
cursor.execute(statement, parameters)
|
||||||
|
sqlite3.OperationalError: near "UNION": syntax error
|
||||||
|
|
||||||
|
The above exception was the direct cause of the following exception:
|
||||||
|
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "/home/core/dev/website/utils/logger.py", line 586, in wrapper
|
||||||
|
result = func(*args, **kwargs)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/dev/website/app.py", line 2782, in social_feed
|
||||||
|
posts = all_posts.paginate(
|
||||||
|
^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/flask_sqlalchemy/query.py", line 98, in paginate
|
||||||
|
return QueryPagination(
|
||||||
|
^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/flask_sqlalchemy/pagination.py", line 72, in __init__
|
||||||
|
items = self._query_items()
|
||||||
|
^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/flask_sqlalchemy/pagination.py", line 358, in _query_items
|
||||||
|
out = query.limit(self.per_page).offset(self._query_offset).all()
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/query.py", line 2693, in all
|
||||||
|
return self._iter().all() # type: ignore
|
||||||
|
^^^^^^^^^^^^
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/query.py", line 2847, in _iter
|
||||||
|
result: Union[ScalarResult[_T], Result[_T]] = self.session.execute(
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/session.py", line 2308, in execute
|
||||||
|
return self._execute_internal(
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/session.py", line 2190, in _execute_internal
|
||||||
|
result: Result[Any] = compile_state_cls.orm_execute_statement(
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/context.py", line 293, in orm_execute_statement
|
||||||
|
result = conn.execute(
|
||||||
|
^^^^^^^^^^^^^
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1416, in execute
|
||||||
|
return meth(
|
||||||
|
^^^^^
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/sql/elements.py", line 516, in _execute_on_connection
|
||||||
|
return connection._execute_clauseelement(
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1639, in _execute_clauseelement
|
||||||
|
ret = self._execute_context(
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1848, in _execute_context
|
||||||
|
return self._exec_single_context(
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1988, in _exec_single_context
|
||||||
|
self._handle_dbapi_exception(
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 2343, in _handle_dbapi_exception
|
||||||
|
raise sqlalchemy_exception.with_traceback(exc_info[2]) from e
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1969, in _exec_single_context
|
||||||
|
self.dialect.do_execute(
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/default.py", line 922, in do_execute
|
||||||
|
cursor.execute(statement, parameters)
|
||||||
|
sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) near "UNION": syntax error
|
||||||
|
[SQL: SELECT anon_1.social_post_id AS anon_1_social_post_id, anon_1.social_post_content AS anon_1_social_post_content, anon_1.social_post_image_url AS anon_1_social_post_image_url, anon_1.social_post_video_url AS anon_1_social_post_video_url, anon_1.social_post_link_url AS anon_1_social_post_link_url, anon_1.social_post_link_title AS anon_1_social_post_link_title, anon_1.social_post_link_description AS anon_1_social_post_link_description, anon_1.social_post_post_type AS anon_1_social_post_post_type, anon_1.social_post_visibility AS anon_1_social_post_visibility, anon_1.social_post_is_pinned AS anon_1_social_post_is_pinned, anon_1.social_post_like_count AS anon_1_social_post_like_count, anon_1.social_post_comment_count AS anon_1_social_post_comment_count, anon_1.social_post_share_count AS anon_1_social_post_share_count, anon_1.social_post_view_count AS anon_1_social_post_view_count, anon_1.social_post_created_at AS anon_1_social_post_created_at, anon_1.social_post_updated_at AS anon_1_social_post_updated_at, anon_1.social_post_user_id AS anon_1_social_post_user_id, anon_1.social_post_shared_thought_id AS anon_1_social_post_shared_thought_id, anon_1.social_post_shared_node_id AS anon_1_social_post_shared_node_id
|
||||||
|
FROM ((SELECT social_post.id AS social_post_id, social_post.content AS social_post_content, social_post.image_url AS social_post_image_url, social_post.video_url AS social_post_video_url, social_post.link_url AS social_post_link_url, social_post.link_title AS social_post_link_title, social_post.link_description AS social_post_link_description, social_post.post_type AS social_post_post_type, social_post.visibility AS social_post_visibility, social_post.is_pinned AS social_post_is_pinned, social_post.like_count AS social_post_like_count, social_post.comment_count AS social_post_comment_count, social_post.share_count AS social_post_share_count, social_post.view_count AS social_post_view_count, social_post.created_at AS social_post_created_at, social_post.updated_at AS social_post_updated_at, social_post.user_id AS social_post_user_id, social_post.shared_thought_id AS social_post_shared_thought_id, social_post.shared_node_id AS social_post_shared_node_id
|
||||||
|
FROM social_post
|
||||||
|
WHERE social_post.user_id IN (?) ORDER BY social_post.created_at DESC
|
||||||
|
LIMIT ? OFFSET ?) UNION SELECT social_post.id AS social_post_id, social_post.content AS social_post_content, social_post.image_url AS social_post_image_url, social_post.video_url AS social_post_video_url, social_post.link_url AS social_post_link_url, social_post.link_title AS social_post_link_title, social_post.link_description AS social_post_link_description, social_post.post_type AS social_post_post_type, social_post.visibility AS social_post_visibility, social_post.is_pinned AS social_post_is_pinned, social_post.like_count AS social_post_like_count, social_post.comment_count AS social_post_comment_count, social_post.share_count AS social_post_share_count, social_post.view_count AS social_post_view_count, social_post.created_at AS social_post_created_at, social_post.updated_at AS social_post_updated_at, social_post.user_id AS social_post_user_id, social_post.shared_thought_id AS social_post_shared_thought_id, social_post.shared_node_id AS social_post_shared_node_id
|
||||||
|
FROM social_post
|
||||||
|
WHERE social_post.user_id = ?) AS anon_1 ORDER BY anon_1.social_post_created_at DESC
|
||||||
|
LIMIT ? OFFSET ?]
|
||||||
|
[parameters: (1, 100, 0, 1, 10, 0)]
|
||||||
|
(Background on this error at: https://sqlalche.me/e/20/e3q8)
|
||||||
|
|
||||||
|
2025-05-28 21:46:15 | ERROR | SysTades | ERROR | Fehler 500: (sqlite3.OperationalError) near "UNION": syntax error
|
||||||
|
[SQL: SELECT anon_1.social_post_id AS anon_1_social_post_id, anon_1.social_post_content AS anon_1_social_post_content, anon_1.social_post_image_url AS anon_1_social_post_image_url, anon_1.social_post_video_url AS anon_1_social_post_video_url, anon_1.social_post_link_url AS anon_1_social_post_link_url, anon_1.social_post_link_title AS anon_1_social_post_link_title, anon_1.social_post_link_description AS anon_1_social_post_link_description, anon_1.social_post_post_type AS anon_1_social_post_post_type, anon_1.social_post_visibility AS anon_1_social_post_visibility, anon_1.social_post_is_pinned AS anon_1_social_post_is_pinned, anon_1.social_post_like_count AS anon_1_social_post_like_count, anon_1.social_post_comment_count AS anon_1_social_post_comment_count, anon_1.social_post_share_count AS anon_1_social_post_share_count, anon_1.social_post_view_count AS anon_1_social_post_view_count, anon_1.social_post_created_at AS anon_1_social_post_created_at, anon_1.social_post_updated_at AS anon_1_social_post_updated_at, anon_1.social_post_user_id AS anon_1_social_post_user_id, anon_1.social_post_shared_thought_id AS anon_1_social_post_shared_thought_id, anon_1.social_post_shared_node_id AS anon_1_social_post_shared_node_id
|
||||||
|
FROM ((SELECT social_post.id AS social_post_id, social_post.content AS social_post_content, social_post.image_url AS social_post_image_url, social_post.video_url AS social_post_video_url, social_post.link_url AS social_post_link_url, social_post.link_title AS social_post_link_title, social_post.link_description AS social_post_link_description, social_post.post_type AS social_post_post_type, social_post.visibility AS social_post_visibility, social_post.is_pinned AS social_post_is_pinned, social_post.like_count AS social_post_like_count, social_post.comment_count AS social_post_comment_count, social_post.share_count AS social_post_share_count, social_post.view_count AS social_post_view_count, social_post.created_at AS social_post_created_at, social_post.updated_at AS social_post_updated_at, social_post.user_id AS social_post_user_id, social_post.shared_thought_id AS social_post_shared_thought_id, social_post.shared_node_id AS social_post_shared_node_id
|
||||||
|
FROM social_post
|
||||||
|
WHERE social_post.user_id IN (?) ORDER BY social_post.created_at DESC
|
||||||
|
LIMIT ? OFFSET ?) UNION SELECT social_post.id AS social_post_id, social_post.content AS social_post_content, social_post.image_url AS social_post_image_url, social_post.video_url AS social_post_video_url, social_post.link_url AS social_post_link_url, social_post.link_title AS social_post_link_title, social_post.link_description AS social_post_link_description, social_post.post_type AS social_post_post_type, social_post.visibility AS social_post_visibility, social_post.is_pinned AS social_post_is_pinned, social_post.like_count AS social_post_like_count, social_post.comment_count AS social_post_comment_count, social_post.share_count AS social_post_share_count, social_post.view_count AS social_post_view_count, social_post.created_at AS social_post_created_at, social_post.updated_at AS social_post_updated_at, social_post.user_id AS social_post_user_id, social_post.shared_thought_id AS social_post_shared_thought_id, social_post.shared_node_id AS social_post_shared_node_id
|
||||||
|
FROM social_post
|
||||||
|
WHERE social_post.user_id = ?) AS anon_1 ORDER BY anon_1.social_post_created_at DESC
|
||||||
|
LIMIT ? OFFSET ?]
|
||||||
|
[parameters: (1, 100, 0, 1, 10, 0)]
|
||||||
|
(Background on this error at: https://sqlalche.me/e/20/e3q8)
|
||||||
|
Endpoint: /feed, Method: GET, IP: 127.0.0.1
|
||||||
|
User: 1 (admin)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1969, in _exec_single_context
|
||||||
|
self.dialect.do_execute(
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/default.py", line 922, in do_execute
|
||||||
|
cursor.execute(statement, parameters)
|
||||||
|
sqlite3.OperationalError: near "UNION": syntax error
|
||||||
|
|
||||||
|
The above exception was the direct cause of the following exception:
|
||||||
|
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 867, in full_dispatch_request
|
||||||
|
rv = self.dispatch_request()
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 852, in dispatch_request
|
||||||
|
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/flask_login/utils.py", line 290, in decorated_view
|
||||||
|
return current_app.ensure_sync(func)(*args, **kwargs)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/dev/website/utils/logger.py", line 586, in wrapper
|
||||||
|
result = func(*args, **kwargs)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/dev/website/app.py", line 2782, in social_feed
|
||||||
|
posts = all_posts.paginate(
|
||||||
|
^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/flask_sqlalchemy/query.py", line 98, in paginate
|
||||||
|
return QueryPagination(
|
||||||
|
^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/flask_sqlalchemy/pagination.py", line 72, in __init__
|
||||||
|
items = self._query_items()
|
||||||
|
^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/flask_sqlalchemy/pagination.py", line 358, in _query_items
|
||||||
|
out = query.limit(self.per_page).offset(self._query_offset).all()
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/query.py", line 2693, in all
|
||||||
|
return self._iter().all() # type: ignore
|
||||||
|
^^^^^^^^^^^^
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/query.py", line 2847, in _iter
|
||||||
|
result: Union[ScalarResult[_T], Result[_T]] = self.session.execute(
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/session.py", line 2308, in execute
|
||||||
|
return self._execute_internal(
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/session.py", line 2190, in _execute_internal
|
||||||
|
result: Result[Any] = compile_state_cls.orm_execute_statement(
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/context.py", line 293, in orm_execute_statement
|
||||||
|
result = conn.execute(
|
||||||
|
^^^^^^^^^^^^^
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1416, in execute
|
||||||
|
return meth(
|
||||||
|
^^^^^
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/sql/elements.py", line 516, in _execute_on_connection
|
||||||
|
return connection._execute_clauseelement(
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1639, in _execute_clauseelement
|
||||||
|
ret = self._execute_context(
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1848, in _execute_context
|
||||||
|
return self._exec_single_context(
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1988, in _exec_single_context
|
||||||
|
self._handle_dbapi_exception(
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 2343, in _handle_dbapi_exception
|
||||||
|
raise sqlalchemy_exception.with_traceback(exc_info[2]) from e
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1969, in _exec_single_context
|
||||||
|
self.dialect.do_execute(
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/default.py", line 922, in do_execute
|
||||||
|
cursor.execute(statement, parameters)
|
||||||
|
sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) near "UNION": syntax error
|
||||||
|
[SQL: SELECT anon_1.social_post_id AS anon_1_social_post_id, anon_1.social_post_content AS anon_1_social_post_content, anon_1.social_post_image_url AS anon_1_social_post_image_url, anon_1.social_post_video_url AS anon_1_social_post_video_url, anon_1.social_post_link_url AS anon_1_social_post_link_url, anon_1.social_post_link_title AS anon_1_social_post_link_title, anon_1.social_post_link_description AS anon_1_social_post_link_description, anon_1.social_post_post_type AS anon_1_social_post_post_type, anon_1.social_post_visibility AS anon_1_social_post_visibility, anon_1.social_post_is_pinned AS anon_1_social_post_is_pinned, anon_1.social_post_like_count AS anon_1_social_post_like_count, anon_1.social_post_comment_count AS anon_1_social_post_comment_count, anon_1.social_post_share_count AS anon_1_social_post_share_count, anon_1.social_post_view_count AS anon_1_social_post_view_count, anon_1.social_post_created_at AS anon_1_social_post_created_at, anon_1.social_post_updated_at AS anon_1_social_post_updated_at, anon_1.social_post_user_id AS anon_1_social_post_user_id, anon_1.social_post_shared_thought_id AS anon_1_social_post_shared_thought_id, anon_1.social_post_shared_node_id AS anon_1_social_post_shared_node_id
|
||||||
|
FROM ((SELECT social_post.id AS social_post_id, social_post.content AS social_post_content, social_post.image_url AS social_post_image_url, social_post.video_url AS social_post_video_url, social_post.link_url AS social_post_link_url, social_post.link_title AS social_post_link_title, social_post.link_description AS social_post_link_description, social_post.post_type AS social_post_post_type, social_post.visibility AS social_post_visibility, social_post.is_pinned AS social_post_is_pinned, social_post.like_count AS social_post_like_count, social_post.comment_count AS social_post_comment_count, social_post.share_count AS social_post_share_count, social_post.view_count AS social_post_view_count, social_post.created_at AS social_post_created_at, social_post.updated_at AS social_post_updated_at, social_post.user_id AS social_post_user_id, social_post.shared_thought_id AS social_post_shared_thought_id, social_post.shared_node_id AS social_post_shared_node_id
|
||||||
|
FROM social_post
|
||||||
|
WHERE social_post.user_id IN (?) ORDER BY social_post.created_at DESC
|
||||||
|
LIMIT ? OFFSET ?) UNION SELECT social_post.id AS social_post_id, social_post.content AS social_post_content, social_post.image_url AS social_post_image_url, social_post.video_url AS social_post_video_url, social_post.link_url AS social_post_link_url, social_post.link_title AS social_post_link_title, social_post.link_description AS social_post_link_description, social_post.post_type AS social_post_post_type, social_post.visibility AS social_post_visibility, social_post.is_pinned AS social_post_is_pinned, social_post.like_count AS social_post_like_count, social_post.comment_count AS social_post_comment_count, social_post.share_count AS social_post_share_count, social_post.view_count AS social_post_view_count, social_post.created_at AS social_post_created_at, social_post.updated_at AS social_post_updated_at, social_post.user_id AS social_post_user_id, social_post.shared_thought_id AS social_post_shared_thought_id, social_post.shared_node_id AS social_post_shared_node_id
|
||||||
|
FROM social_post
|
||||||
|
WHERE social_post.user_id = ?) AS anon_1 ORDER BY anon_1.social_post_created_at DESC
|
||||||
|
LIMIT ? OFFSET ?]
|
||||||
|
[parameters: (1, 100, 0, 1, 10, 0)]
|
||||||
|
(Background on this error at: https://sqlalche.me/e/20/e3q8)
|
||||||
|
|
||||||
|
2025-05-28 21:48:48 | ERROR | SysTades | ERROR | Fehler 404: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
Endpoint: /sw.js, Method: GET, IP: 127.0.0.1
|
||||||
|
Nicht angemeldet
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 867, in full_dispatch_request
|
||||||
|
rv = self.dispatch_request()
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 841, in dispatch_request
|
||||||
|
self.raise_routing_exception(req)
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 450, in raise_routing_exception
|
||||||
|
raise request.routing_exception # type: ignore
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/flask/ctx.py", line 353, in match_request
|
||||||
|
result = self.url_adapter.match(return_rule=True) # type: ignore
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/werkzeug/routing/map.py", line 624, in match
|
||||||
|
raise NotFound() from None
|
||||||
|
werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
|
||||||
|
2025-05-28 21:48:54 | ERROR | SysTades | ERROR | Fehler 404: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
Endpoint: /static/fonts/inter-regular.woff2, Method: GET, IP: 127.0.0.1
|
||||||
|
User: 1 (admin)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 867, in full_dispatch_request
|
||||||
|
rv = self.dispatch_request()
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 852, in dispatch_request
|
||||||
|
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 257, in <lambda>
|
||||||
|
view_func=lambda **kw: self_ref().send_static_file(**kw), # type: ignore # noqa: B950
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 305, in send_static_file
|
||||||
|
return send_from_directory(
|
||||||
|
^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/flask/helpers.py", line 554, in send_from_directory
|
||||||
|
return werkzeug.utils.send_from_directory( # type: ignore[return-value]
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/werkzeug/utils.py", line 574, in send_from_directory
|
||||||
|
raise NotFound()
|
||||||
|
werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
|
||||||
|
2025-05-28 21:49:17 | ERROR | SysTades | ERROR | Fehler 404: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
Endpoint: /sw.js, Method: GET, IP: 127.0.0.1
|
||||||
|
Nicht angemeldet
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 867, in full_dispatch_request
|
||||||
|
rv = self.dispatch_request()
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 841, in dispatch_request
|
||||||
|
self.raise_routing_exception(req)
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 450, in raise_routing_exception
|
||||||
|
raise request.routing_exception # type: ignore
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/flask/ctx.py", line 353, in match_request
|
||||||
|
result = self.url_adapter.match(return_rule=True) # type: ignore
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/werkzeug/routing/map.py", line 624, in match
|
||||||
|
raise NotFound() from None
|
||||||
|
werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
|
||||||
|
2025-05-28 21:55:55 | ERROR | SysTades | ERROR | Fehler 500: name 'follows' is not defined
|
||||||
|
Endpoint: /api/discover/users, Method: GET, IP: 127.0.0.1
|
||||||
|
User: 1 (admin)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "/home/core/dev/website/app.py", line 424, in wrapper
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/dev/website/app.py", line 3141, in discover_users
|
||||||
|
not_following_subquery = db.session.query(follows.c.followed_id).filter(
|
||||||
|
^^^^^^^
|
||||||
|
NameError: name 'follows' is not defined
|
||||||
|
|
||||||
|
2025-05-28 21:55:55 | ERROR | SysTades | ERROR | Fehler 500: name 'follows' is not defined
|
||||||
|
Endpoint: /api/discover/users, Method: GET, IP: 127.0.0.1
|
||||||
|
User: 1 (admin)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "/home/core/dev/website/app.py", line 424, in wrapper
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/dev/website/app.py", line 3141, in discover_users
|
||||||
|
not_following_subquery = db.session.query(follows.c.followed_id).filter(
|
||||||
|
^^^^^^^
|
||||||
|
NameError: name 'follows' is not defined
|
||||||
|
|
||||||
|
2025-05-28 21:56:25 | ERROR | SysTades | ERROR | Fehler 404: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
Endpoint: /auth/login, Method: GET, IP: 127.0.0.1
|
||||||
|
Nicht angemeldet
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 867, in full_dispatch_request
|
||||||
|
rv = self.dispatch_request()
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 841, in dispatch_request
|
||||||
|
self.raise_routing_exception(req)
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 450, in raise_routing_exception
|
||||||
|
raise request.routing_exception # type: ignore
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/flask/ctx.py", line 353, in match_request
|
||||||
|
result = self.url_adapter.match(return_rule=True) # type: ignore
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/.local/lib/python3.11/site-packages/werkzeug/routing/map.py", line 624, in match
|
||||||
|
raise NotFound() from None
|
||||||
|
werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||||
|
|
||||||
|
2025-05-28 21:56:41 | ERROR | SysTades | ERROR | Fehler 500: name 'follows' is not defined
|
||||||
|
Endpoint: /api/discover/users, Method: GET, IP: 127.0.0.1
|
||||||
|
User: 1 (admin)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "/home/core/dev/website/app.py", line 424, in wrapper
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/dev/website/app.py", line 3141, in discover_users
|
||||||
|
not_following_subquery = db.session.query(follows.c.followed_id).filter(
|
||||||
|
^^^^^^^
|
||||||
|
NameError: name 'follows' is not defined
|
||||||
|
|
||||||
|
2025-05-28 21:57:25 | ERROR | SysTades | ERROR | Fehler 500: name 'follows' is not defined
|
||||||
|
Endpoint: /api/discover/users, Method: GET, IP: 127.0.0.1
|
||||||
|
User: 1 (admin)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "/home/core/dev/website/app.py", line 424, in wrapper
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/dev/website/app.py", line 3141, in discover_users
|
||||||
|
not_following_subquery = db.session.query(user_follows.c.followed_id).filter(
|
||||||
|
^^^^^^^
|
||||||
|
NameError: name 'follows' is not defined
|
||||||
|
|
||||||
|
2025-05-28 21:58:02 | ERROR | SysTades | ERROR | Fehler 500: name 'follows' is not defined
|
||||||
|
Endpoint: /api/discover/users, Method: GET, IP: 127.0.0.1
|
||||||
|
User: 1 (admin)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "/home/core/dev/website/app.py", line 424, in wrapper
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/core/dev/website/app.py", line 3141, in discover_users
|
||||||
|
users = User.query.filter(
|
||||||
|
|
||||||
|
NameError: name 'follows' is not defined
|
||||||
|
|
||||||
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 ###
|
||||||
489
models.py
Executable file → Normal file
489
models.py
Executable file → Normal file
@@ -6,6 +6,8 @@ from flask_login import UserMixin
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from werkzeug.security import generate_password_hash, check_password_hash
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
import uuid as uuid_pkg
|
||||||
|
import os
|
||||||
|
|
||||||
db = SQLAlchemy()
|
db = SQLAlchemy()
|
||||||
|
|
||||||
@@ -43,30 +45,157 @@ user_thought_bookmark = db.Table('user_thought_bookmark',
|
|||||||
db.Column('created_at', db.DateTime, default=datetime.utcnow)
|
db.Column('created_at', db.DateTime, default=datetime.utcnow)
|
||||||
)
|
)
|
||||||
|
|
||||||
class User(UserMixin, db.Model):
|
# Beziehungstabelle für Benutzer-Freundschaften
|
||||||
|
user_friendships = db.Table('user_friendships',
|
||||||
|
db.Column('user_id', db.Integer, db.ForeignKey('user.id'), primary_key=True),
|
||||||
|
db.Column('friend_id', db.Integer, db.ForeignKey('user.id'), primary_key=True),
|
||||||
|
db.Column('created_at', db.DateTime, default=datetime.utcnow),
|
||||||
|
db.Column('status', db.String(20), default='pending') # pending, accepted, blocked
|
||||||
|
)
|
||||||
|
|
||||||
|
# Beziehungstabelle für Benutzer-Follows
|
||||||
|
user_follows = db.Table('user_follows',
|
||||||
|
db.Column('follower_id', db.Integer, db.ForeignKey('user.id'), primary_key=True),
|
||||||
|
db.Column('followed_id', db.Integer, db.ForeignKey('user.id'), primary_key=True),
|
||||||
|
db.Column('created_at', db.DateTime, default=datetime.utcnow)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Beziehungstabelle für Post-Likes
|
||||||
|
post_likes = db.Table('post_likes',
|
||||||
|
db.Column('user_id', db.Integer, db.ForeignKey('user.id'), primary_key=True),
|
||||||
|
db.Column('post_id', db.Integer, db.ForeignKey('social_post.id'), primary_key=True),
|
||||||
|
db.Column('created_at', db.DateTime, default=datetime.utcnow)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Beziehungstabelle für Comment-Likes
|
||||||
|
comment_likes = db.Table('comment_likes',
|
||||||
|
db.Column('user_id', db.Integer, db.ForeignKey('user.id'), primary_key=True),
|
||||||
|
db.Column('comment_id', db.Integer, db.ForeignKey('social_comment.id'), primary_key=True),
|
||||||
|
db.Column('created_at', db.DateTime, default=datetime.utcnow)
|
||||||
|
)
|
||||||
|
|
||||||
|
class User(db.Model, UserMixin):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
username = db.Column(db.String(80), unique=True, nullable=False)
|
username = db.Column(db.String(80), unique=True, nullable=False)
|
||||||
email = db.Column(db.String(120), unique=True, nullable=False)
|
email = db.Column(db.String(120), unique=True, nullable=False)
|
||||||
password_hash = db.Column(db.String(128))
|
password = db.Column(db.String(512), nullable=False)
|
||||||
is_admin = db.Column(db.Boolean, default=False)
|
|
||||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
last_login = db.Column(db.DateTime)
|
is_active = db.Column(db.Boolean, default=True)
|
||||||
avatar = db.Column(db.String(200))
|
role = db.Column(db.String(20), default="user") # 'user', 'admin', 'moderator'
|
||||||
bio = db.Column(db.Text)
|
bio = db.Column(db.Text, nullable=True) # Profil-Bio
|
||||||
|
location = db.Column(db.String(100), nullable=True) # Standort
|
||||||
|
website = db.Column(db.String(200), nullable=True) # Website
|
||||||
|
avatar = db.Column(db.String(200), nullable=True) # Profilbild-URL
|
||||||
|
last_login = db.Column(db.DateTime, nullable=True) # Letzter Login
|
||||||
|
|
||||||
|
# Social Network Felder
|
||||||
|
display_name = db.Column(db.String(100), nullable=True) # Anzeigename
|
||||||
|
birth_date = db.Column(db.Date, nullable=True) # Geburtsdatum
|
||||||
|
gender = db.Column(db.String(20), nullable=True) # Geschlecht
|
||||||
|
phone = db.Column(db.String(20), nullable=True) # Telefonnummer
|
||||||
|
is_verified = db.Column(db.Boolean, default=False) # Verifizierter Account
|
||||||
|
is_private = db.Column(db.Boolean, default=False) # Privater Account
|
||||||
|
follower_count = db.Column(db.Integer, default=0) # Follower-Anzahl
|
||||||
|
following_count = db.Column(db.Integer, default=0) # Following-Anzahl
|
||||||
|
post_count = db.Column(db.Integer, default=0) # Post-Anzahl
|
||||||
|
online_status = db.Column(db.String(20), default='offline') # online, offline, away
|
||||||
|
last_seen = db.Column(db.DateTime, nullable=True) # Zuletzt gesehen
|
||||||
|
|
||||||
# Beziehungen
|
# Beziehungen
|
||||||
|
threads = db.relationship('Thread', backref='creator', lazy=True)
|
||||||
|
messages = db.relationship('Message', backref='author', lazy=True)
|
||||||
|
projects = db.relationship('Project', backref='owner', lazy=True)
|
||||||
|
mindmaps = db.relationship('UserMindmap', backref='user', lazy=True)
|
||||||
thoughts = db.relationship('Thought', backref='author', lazy=True)
|
thoughts = db.relationship('Thought', backref='author', lazy=True)
|
||||||
comments = db.relationship('Comment', backref='author', lazy=True)
|
|
||||||
user_mindmaps = db.relationship('UserMindmap', backref='user', lazy=True)
|
|
||||||
mindmap_notes = db.relationship('MindmapNote', backref='user', lazy=True)
|
|
||||||
bookmarked_thoughts = db.relationship('Thought', secondary=user_thought_bookmark,
|
bookmarked_thoughts = db.relationship('Thought', secondary=user_thought_bookmark,
|
||||||
backref=db.backref('bookmarked_by', lazy='dynamic'))
|
lazy='dynamic', backref=db.backref('bookmarked_by', lazy='dynamic'))
|
||||||
|
|
||||||
|
# Social Network Beziehungen
|
||||||
|
posts = db.relationship('SocialPost', backref='author', lazy=True, cascade="all, delete-orphan")
|
||||||
|
comments = db.relationship('SocialComment', backref='author', lazy=True, cascade="all, delete-orphan")
|
||||||
|
notifications = db.relationship('Notification', foreign_keys='Notification.user_id', backref='user', lazy=True, cascade="all, delete-orphan")
|
||||||
|
|
||||||
|
# Freundschaften (bidirektional)
|
||||||
|
friends = db.relationship(
|
||||||
|
'User',
|
||||||
|
secondary=user_friendships,
|
||||||
|
primaryjoin=id == user_friendships.c.user_id,
|
||||||
|
secondaryjoin=id == user_friendships.c.friend_id,
|
||||||
|
backref='friend_of',
|
||||||
|
lazy='dynamic'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Following/Followers
|
||||||
|
following = db.relationship(
|
||||||
|
'User',
|
||||||
|
secondary=user_follows,
|
||||||
|
primaryjoin=id == user_follows.c.follower_id,
|
||||||
|
secondaryjoin=id == user_follows.c.followed_id,
|
||||||
|
backref=db.backref('followers', lazy='dynamic'),
|
||||||
|
lazy='dynamic'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Liked Posts und Comments
|
||||||
|
liked_posts = db.relationship('SocialPost', secondary=post_likes,
|
||||||
|
backref=db.backref('liked_by', lazy='dynamic'), lazy='dynamic')
|
||||||
|
liked_comments = db.relationship('SocialComment', secondary=comment_likes,
|
||||||
|
backref=db.backref('liked_by', lazy='dynamic'), lazy='dynamic')
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<User {self.username}>'
|
||||||
|
|
||||||
def set_password(self, password):
|
def set_password(self, password):
|
||||||
self.password_hash = generate_password_hash(password)
|
self.password = generate_password_hash(password)
|
||||||
|
|
||||||
def check_password(self, password):
|
def check_password(self, password):
|
||||||
return check_password_hash(self.password_hash, password)
|
return check_password_hash(self.password, password)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_admin(self):
|
||||||
|
return self.role == 'admin'
|
||||||
|
|
||||||
|
@is_admin.setter
|
||||||
|
def is_admin(self, value):
|
||||||
|
self.role = 'admin' if value else 'user'
|
||||||
|
|
||||||
|
# Social Network Methoden
|
||||||
|
def follow(self, user):
|
||||||
|
"""Folgt einem anderen Benutzer"""
|
||||||
|
if not self.is_following(user):
|
||||||
|
self.following.append(user)
|
||||||
|
user.follower_count += 1
|
||||||
|
user.following_count += 1
|
||||||
|
|
||||||
|
# Notification erstellen
|
||||||
|
notification = Notification(
|
||||||
|
user_id=user.id,
|
||||||
|
type='follow',
|
||||||
|
message=f'{self.username} folgt dir jetzt',
|
||||||
|
related_user_id=self.id
|
||||||
|
)
|
||||||
|
db.session.add(notification)
|
||||||
|
|
||||||
|
def unfollow(self, user):
|
||||||
|
"""Entfolgt einem Benutzer"""
|
||||||
|
if self.is_following(user):
|
||||||
|
self.following.remove(user)
|
||||||
|
user.follower_count -= 1
|
||||||
|
user.following_count -= 1
|
||||||
|
|
||||||
|
def is_following(self, user):
|
||||||
|
"""Prüft ob der Benutzer einem anderen folgt"""
|
||||||
|
return self.following.filter(user_follows.c.followed_id == user.id).count() > 0
|
||||||
|
|
||||||
|
def get_feed_posts(self, limit=20):
|
||||||
|
"""Holt Posts für den Feed (von gefolgten Benutzern)"""
|
||||||
|
# Hole alle User-IDs von Benutzern, denen ich folge + meine eigene
|
||||||
|
followed_user_ids = [user.id for user in self.following]
|
||||||
|
all_user_ids = followed_user_ids + [self.id]
|
||||||
|
|
||||||
|
# Hole Posts von diesen Benutzern
|
||||||
|
return SocialPost.query.filter(
|
||||||
|
SocialPost.user_id.in_(all_user_ids)
|
||||||
|
).order_by(SocialPost.created_at.desc()).limit(limit)
|
||||||
|
|
||||||
class Category(db.Model):
|
class Category(db.Model):
|
||||||
"""Wissenschaftliche Kategorien für die Gliederung der öffentlichen Mindmap"""
|
"""Wissenschaftliche Kategorien für die Gliederung der öffentlichen Mindmap"""
|
||||||
@@ -81,6 +210,9 @@ class Category(db.Model):
|
|||||||
children = db.relationship('Category', backref=db.backref('parent', remote_side=[id]))
|
children = db.relationship('Category', backref=db.backref('parent', remote_side=[id]))
|
||||||
nodes = db.relationship('MindMapNode', backref='category', lazy=True)
|
nodes = db.relationship('MindMapNode', backref='category', lazy=True)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<Category {self.name}>'
|
||||||
|
|
||||||
class MindMapNode(db.Model):
|
class MindMapNode(db.Model):
|
||||||
"""Öffentliche Mindmap-Knoten, die für alle Benutzer sichtbar sind"""
|
"""Öffentliche Mindmap-Knoten, die für alle Benutzer sichtbar sind"""
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
@@ -93,6 +225,8 @@ class MindMapNode(db.Model):
|
|||||||
created_by_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True)
|
created_by_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True)
|
||||||
category_id = db.Column(db.Integer, db.ForeignKey('category.id'), nullable=True)
|
category_id = db.Column(db.Integer, db.ForeignKey('category.id'), nullable=True)
|
||||||
|
|
||||||
|
__table_args__ = {'extend_existing': True}
|
||||||
|
|
||||||
# Beziehungen für Baumstruktur (mehrere Eltern möglich)
|
# Beziehungen für Baumstruktur (mehrere Eltern möglich)
|
||||||
parents = db.relationship(
|
parents = db.relationship(
|
||||||
'MindMapNode',
|
'MindMapNode',
|
||||||
@@ -111,6 +245,20 @@ class MindMapNode(db.Model):
|
|||||||
# Beziehung zum Ersteller
|
# Beziehung zum Ersteller
|
||||||
created_by = db.relationship('User', backref='created_nodes')
|
created_by = db.relationship('User', backref='created_nodes')
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<MindMapNode {self.name}>'
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
'id': self.id,
|
||||||
|
'name': self.name,
|
||||||
|
'description': self.description,
|
||||||
|
'color_code': self.color_code,
|
||||||
|
'icon': self.icon,
|
||||||
|
'category_id': self.category_id,
|
||||||
|
'created_at': self.created_at.isoformat() if self.created_at else None
|
||||||
|
}
|
||||||
|
|
||||||
class UserMindmap(db.Model):
|
class UserMindmap(db.Model):
|
||||||
"""Benutzerspezifische Mindmap, die vom Benutzer personalisierbar ist"""
|
"""Benutzerspezifische Mindmap, die vom Benutzer personalisierbar ist"""
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
@@ -228,3 +376,320 @@ class Comment(db.Model):
|
|||||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
last_modified = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
last_modified = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||||
|
|
||||||
|
# Thread model
|
||||||
|
class Thread(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
title = db.Column(db.String(200), nullable=False)
|
||||||
|
description = db.Column(db.Text)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
messages = db.relationship('Message', backref='thread', lazy=True, cascade="all, delete-orphan")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<Thread {self.title}>'
|
||||||
|
|
||||||
|
# Message model
|
||||||
|
class Message(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
content = db.Column(db.Text, nullable=False)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||||
|
thread_id = db.Column(db.Integer, db.ForeignKey('thread.id'), nullable=False)
|
||||||
|
role = db.Column(db.String(20), default="user") # 'user', 'assistant', 'system'
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<Message {self.id} by {self.user_id}>'
|
||||||
|
|
||||||
|
# Project model
|
||||||
|
class Project(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
title = db.Column(db.String(150), nullable=False)
|
||||||
|
description = db.Column(db.Text)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||||
|
category_id = db.Column(db.Integer, db.ForeignKey('category.id'))
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
documents = db.relationship('Document', backref='project', lazy=True, cascade="all, delete-orphan")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<Project {self.title}>'
|
||||||
|
|
||||||
|
# Document model
|
||||||
|
class Document(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
title = db.Column(db.String(150), nullable=False)
|
||||||
|
content = db.Column(db.Text)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||||
|
project_id = db.Column(db.Integer, db.ForeignKey('project.id'), nullable=False)
|
||||||
|
filename = db.Column(db.String(150), nullable=True)
|
||||||
|
file_path = db.Column(db.String(300), nullable=True)
|
||||||
|
file_type = db.Column(db.String(50), nullable=True)
|
||||||
|
file_size = db.Column(db.Integer, nullable=True)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<Document {self.title}>'
|
||||||
|
|
||||||
|
# Forum-Kategorie-Modell - entspricht den Hauptknotenpunkten der Mindmap
|
||||||
|
class ForumCategory(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
node_id = db.Column(db.Integer, db.ForeignKey('mind_map_node.id'), nullable=False)
|
||||||
|
title = db.Column(db.String(200), nullable=False)
|
||||||
|
description = db.Column(db.Text)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
is_active = db.Column(db.Boolean, default=True)
|
||||||
|
|
||||||
|
# Beziehungen
|
||||||
|
node = db.relationship('MindMapNode', backref='forum_category')
|
||||||
|
posts = db.relationship('ForumPost', backref='category', lazy=True, cascade="all, delete-orphan")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<ForumCategory {self.title}>'
|
||||||
|
|
||||||
|
# Forum-Beitrag-Modell
|
||||||
|
class ForumPost(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
title = db.Column(db.String(200), nullable=False)
|
||||||
|
content = db.Column(db.Text, nullable=False)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||||
|
category_id = db.Column(db.Integer, db.ForeignKey('forum_category.id'), nullable=False)
|
||||||
|
parent_id = db.Column(db.Integer, db.ForeignKey('forum_post.id'), nullable=True)
|
||||||
|
is_pinned = db.Column(db.Boolean, default=False)
|
||||||
|
is_locked = db.Column(db.Boolean, default=False)
|
||||||
|
view_count = db.Column(db.Integer, default=0)
|
||||||
|
|
||||||
|
# Beziehungen
|
||||||
|
author = db.relationship('User', backref='forum_posts')
|
||||||
|
replies = db.relationship('ForumPost', backref=db.backref('parent', remote_side=[id]), lazy=True)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<ForumPost {self.title}>'
|
||||||
|
|
||||||
|
# Berechtigungstypen für Mindmap-Freigaben
|
||||||
|
class PermissionType(Enum):
|
||||||
|
READ = "Nur-Lesen"
|
||||||
|
EDIT = "Bearbeiten"
|
||||||
|
ADMIN = "Administrator"
|
||||||
|
|
||||||
|
# Freigabemodell für Mindmaps
|
||||||
|
class MindmapShare(db.Model):
|
||||||
|
"""Speichert Informationen über freigegebene Mindmaps und Berechtigungen"""
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
mindmap_id = db.Column(db.Integer, db.ForeignKey('user_mindmap.id'), nullable=False)
|
||||||
|
shared_by_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||||
|
shared_with_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||||
|
permission_type = db.Column(db.Enum(PermissionType), nullable=False, default=PermissionType.READ)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
last_accessed = db.Column(db.DateTime, nullable=True)
|
||||||
|
|
||||||
|
# Beziehungen
|
||||||
|
mindmap = db.relationship('UserMindmap', backref=db.backref('shares', lazy='dynamic'))
|
||||||
|
shared_by = db.relationship('User', foreign_keys=[shared_by_id], backref=db.backref('shared_mindmaps', lazy='dynamic'))
|
||||||
|
shared_with = db.relationship('User', foreign_keys=[shared_with_id], backref=db.backref('accessible_mindmaps', lazy='dynamic'))
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
db.UniqueConstraint('mindmap_id', 'shared_with_id', name='unique_mindmap_share'),
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<MindmapShare: {self.mindmap_id} - {self.shared_with_id} - {self.permission_type.name}>'
|
||||||
|
|
||||||
|
class SocialPost(db.Model):
|
||||||
|
"""Posts im Social Network"""
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
content = db.Column(db.Text, nullable=False)
|
||||||
|
image_url = db.Column(db.String(500), nullable=True) # Bild-URL
|
||||||
|
video_url = db.Column(db.String(500), nullable=True) # Video-URL
|
||||||
|
link_url = db.Column(db.String(500), nullable=True) # Link-URL
|
||||||
|
link_title = db.Column(db.String(200), nullable=True) # Link-Titel
|
||||||
|
link_description = db.Column(db.Text, nullable=True) # Link-Beschreibung
|
||||||
|
post_type = db.Column(db.String(20), default='text') # text, image, video, link, thought_share
|
||||||
|
visibility = db.Column(db.String(20), default='public') # public, friends, private
|
||||||
|
is_pinned = db.Column(db.Boolean, default=False)
|
||||||
|
like_count = db.Column(db.Integer, default=0)
|
||||||
|
comment_count = db.Column(db.Integer, default=0)
|
||||||
|
share_count = db.Column(db.Integer, default=0)
|
||||||
|
view_count = db.Column(db.Integer, default=0)
|
||||||
|
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)
|
||||||
|
|
||||||
|
# Verknüpfung zu Gedanken (falls der Post einen Gedanken teilt)
|
||||||
|
shared_thought_id = db.Column(db.Integer, db.ForeignKey('thought.id'), nullable=True)
|
||||||
|
shared_thought = db.relationship('Thought', backref='shared_in_posts')
|
||||||
|
|
||||||
|
# Verknüpfung zu Mindmap-Knoten
|
||||||
|
shared_node_id = db.Column(db.Integer, db.ForeignKey('mind_map_node.id'), nullable=True)
|
||||||
|
shared_node = db.relationship('MindMapNode', backref='shared_in_posts')
|
||||||
|
|
||||||
|
# Kommentare zu diesem Post
|
||||||
|
comments = db.relationship('SocialComment', backref='post', lazy=True, cascade="all, delete-orphan")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<SocialPost {self.id} by {self.author.username}>'
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
'id': self.id,
|
||||||
|
'content': self.content,
|
||||||
|
'post_type': self.post_type,
|
||||||
|
'image_url': self.image_url,
|
||||||
|
'video_url': self.video_url,
|
||||||
|
'link_url': self.link_url,
|
||||||
|
'link_title': self.link_title,
|
||||||
|
'link_description': self.link_description,
|
||||||
|
'visibility': self.visibility,
|
||||||
|
'is_pinned': self.is_pinned,
|
||||||
|
'like_count': self.like_count,
|
||||||
|
'comment_count': self.comment_count,
|
||||||
|
'share_count': self.share_count,
|
||||||
|
'view_count': self.view_count,
|
||||||
|
'created_at': self.created_at.isoformat(),
|
||||||
|
'updated_at': self.updated_at.isoformat(),
|
||||||
|
'author': {
|
||||||
|
'id': self.author.id,
|
||||||
|
'username': self.author.username,
|
||||||
|
'display_name': self.author.display_name or self.author.username,
|
||||||
|
'avatar': self.author.avatar,
|
||||||
|
'is_verified': self.author.is_verified
|
||||||
|
},
|
||||||
|
'shared_thought': self.shared_thought.to_dict() if self.shared_thought else None,
|
||||||
|
'shared_node': self.shared_node.to_dict() if self.shared_node else None
|
||||||
|
}
|
||||||
|
|
||||||
|
class SocialComment(db.Model):
|
||||||
|
"""Kommentare zu Posts"""
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
content = db.Column(db.Text, nullable=False)
|
||||||
|
like_count = db.Column(db.Integer, default=0)
|
||||||
|
reply_count = db.Column(db.Integer, default=0)
|
||||||
|
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)
|
||||||
|
post_id = db.Column(db.Integer, db.ForeignKey('social_post.id'), nullable=False)
|
||||||
|
parent_id = db.Column(db.Integer, db.ForeignKey('social_comment.id'), nullable=True)
|
||||||
|
|
||||||
|
# Antworten auf diesen Kommentar
|
||||||
|
replies = db.relationship('SocialComment', backref=db.backref('parent', remote_side=[id]), lazy=True)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<SocialComment {self.id} by {self.author.username}>'
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
'id': self.id,
|
||||||
|
'content': self.content,
|
||||||
|
'like_count': self.like_count,
|
||||||
|
'reply_count': self.reply_count,
|
||||||
|
'created_at': self.created_at.isoformat(),
|
||||||
|
'updated_at': self.updated_at.isoformat(),
|
||||||
|
'author': {
|
||||||
|
'id': self.author.id,
|
||||||
|
'username': self.author.username,
|
||||||
|
'display_name': self.author.display_name or self.author.username,
|
||||||
|
'avatar': self.author.avatar,
|
||||||
|
'is_verified': self.author.is_verified
|
||||||
|
},
|
||||||
|
'parent_id': self.parent_id
|
||||||
|
}
|
||||||
|
|
||||||
|
class Notification(db.Model):
|
||||||
|
"""Benachrichtigungen für Benutzer"""
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
type = db.Column(db.String(50), nullable=False) # follow, like, comment, mention, friend_request, etc.
|
||||||
|
message = db.Column(db.String(500), nullable=False)
|
||||||
|
is_read = db.Column(db.Boolean, default=False)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||||
|
|
||||||
|
# Verknüpfungen zu anderen Entitäten
|
||||||
|
related_user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True)
|
||||||
|
related_post_id = db.Column(db.Integer, db.ForeignKey('social_post.id'), nullable=True)
|
||||||
|
related_comment_id = db.Column(db.Integer, db.ForeignKey('social_comment.id'), nullable=True)
|
||||||
|
related_thought_id = db.Column(db.Integer, db.ForeignKey('thought.id'), nullable=True)
|
||||||
|
|
||||||
|
# Beziehungen
|
||||||
|
related_user = db.relationship('User', foreign_keys=[related_user_id])
|
||||||
|
related_post = db.relationship('SocialPost', foreign_keys=[related_post_id])
|
||||||
|
related_comment = db.relationship('SocialComment', foreign_keys=[related_comment_id])
|
||||||
|
related_thought = db.relationship('Thought', foreign_keys=[related_thought_id])
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<Notification {self.id} for {self.user.username}>'
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
'id': self.id,
|
||||||
|
'type': self.type,
|
||||||
|
'message': self.message,
|
||||||
|
'is_read': self.is_read,
|
||||||
|
'created_at': self.created_at.isoformat(),
|
||||||
|
'related_user': self.related_user.username if self.related_user else None,
|
||||||
|
'related_post_id': self.related_post_id,
|
||||||
|
'related_comment_id': self.related_comment_id,
|
||||||
|
'related_thought_id': self.related_thought_id
|
||||||
|
}
|
||||||
|
|
||||||
|
class UserSettings(db.Model):
|
||||||
|
"""Benutzereinstellungen"""
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False, unique=True)
|
||||||
|
|
||||||
|
# Datenschutz-Einstellungen
|
||||||
|
profile_visibility = db.Column(db.String(20), default='public') # public, friends, private
|
||||||
|
show_email = db.Column(db.Boolean, default=False)
|
||||||
|
show_birth_date = db.Column(db.Boolean, default=False)
|
||||||
|
show_location = db.Column(db.Boolean, default=True)
|
||||||
|
allow_friend_requests = db.Column(db.Boolean, default=True)
|
||||||
|
allow_messages = db.Column(db.String(20), default='everyone') # everyone, friends, none
|
||||||
|
|
||||||
|
# Benachrichtigungs-Einstellungen
|
||||||
|
email_notifications = db.Column(db.Boolean, default=True)
|
||||||
|
push_notifications = db.Column(db.Boolean, default=True)
|
||||||
|
notify_on_follow = db.Column(db.Boolean, default=True)
|
||||||
|
notify_on_like = db.Column(db.Boolean, default=True)
|
||||||
|
notify_on_comment = db.Column(db.Boolean, default=True)
|
||||||
|
notify_on_mention = db.Column(db.Boolean, default=True)
|
||||||
|
notify_on_friend_request = db.Column(db.Boolean, default=True)
|
||||||
|
|
||||||
|
# Interface-Einstellungen
|
||||||
|
dark_mode = db.Column(db.Boolean, default=False)
|
||||||
|
language = db.Column(db.String(10), default='de')
|
||||||
|
|
||||||
|
# Beziehung
|
||||||
|
user = db.relationship('User', backref=db.backref('settings', uselist=False))
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<UserSettings for {self.user.username}>'
|
||||||
|
|
||||||
|
class Activity(db.Model):
|
||||||
|
"""Aktivitätsprotokoll für Benutzer"""
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||||
|
action = db.Column(db.String(100), nullable=False) # login, logout, post_created, thought_shared, etc.
|
||||||
|
description = db.Column(db.String(500), nullable=True)
|
||||||
|
ip_address = db.Column(db.String(45), nullable=True) # IPv4/IPv6
|
||||||
|
user_agent = db.Column(db.String(500), nullable=True)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
|
||||||
|
# Verknüpfungen zu anderen Entitäten
|
||||||
|
related_post_id = db.Column(db.Integer, db.ForeignKey('social_post.id'), nullable=True)
|
||||||
|
related_thought_id = db.Column(db.Integer, db.ForeignKey('thought.id'), nullable=True)
|
||||||
|
related_mindmap_id = db.Column(db.Integer, db.ForeignKey('user_mindmap.id'), nullable=True)
|
||||||
|
|
||||||
|
# Beziehungen
|
||||||
|
user = db.relationship('User', backref='activities')
|
||||||
|
related_post = db.relationship('SocialPost')
|
||||||
|
related_thought = db.relationship('Thought')
|
||||||
|
related_mindmap = db.relationship('UserMindmap')
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<Activity {self.action} by {self.user.username}>'
|
||||||
@@ -5,10 +5,13 @@ email-validator
|
|||||||
python-dotenv
|
python-dotenv
|
||||||
werkzeug==2.2.3
|
werkzeug==2.2.3
|
||||||
flask-sqlalchemy==3.0.5
|
flask-sqlalchemy==3.0.5
|
||||||
openai==1.3.0
|
openai
|
||||||
requests==2.31.0
|
requests==2.31.0
|
||||||
flask-cors==4.0.0
|
|
||||||
gunicorn==21.2.0
|
gunicorn==21.2.0
|
||||||
#pillow==10.0.1
|
#pillow==10.0.1
|
||||||
pytest==7.4.0
|
pytest==7.4.0
|
||||||
pytest-flask==1.2.0
|
pytest-flask==1.2.0
|
||||||
|
Flask-Migrate
|
||||||
|
flask-socketio==5.3.6
|
||||||
|
python-engineio==4.8.2
|
||||||
|
python-socketio==5.11.1
|
||||||
22
server.log
Normal file
22
server.log
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
[2m⏰ 21:58:48.486[0m [2m│[0m [92m✅ INFO [0m [2m│[0m [33m⚙️ [SYSTEM ][0m [2m│[0m 📝 🚀 SysTades Social Network gestartet
|
||||||
|
[2m⏰ 21:58:48.486[0m [2m│[0m [92m✅ INFO [0m [2m│[0m [33m⚙️ [SYSTEM ][0m [2m│[0m 📝 🚀 SysTades Social Network gestartet (v1.0.0) in development Umgebung auf Port 5000
|
||||||
|
[2m⏰ 21:58:49.951[0m [2m│[0m [92m✅ INFO [0m [2m│[0m [33m⚙️ [SYSTEM ][0m [2m│[0m 📝 OpenAI API-Verbindung erfolgreich hergestellt
|
||||||
|
[2m⏰ 21:58:50.122[0m [2m│[0m [92m✅ INFO [0m [2m│[0m [35m🗄️ [DB ][0m [2m│[0m 🚫 Datenbank erfolgreich initialisiert
|
||||||
|
[2m⏰ 21:58:50.132[0m [2m│[0m [92m✅ INFO [0m [2m│[0m [35m🗄️ [DB ][0m [2m│[0m 🚫 Datenbanktabellen erstellt/aktualisiert
|
||||||
|
[2m⏰ 21:58:50.134[0m [2m│[0m [92m✅ INFO [0m [2m│[0m [33m⚙️ [SYSTEM ][0m [2m│[0m 📝 Starte Flask-Entwicklungsserver auf http://localhost:5000
|
||||||
|
* Serving Flask app 'app'
|
||||||
|
* Debug mode: on
|
||||||
|
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
|
||||||
|
* Running on all addresses (0.0.0.0)
|
||||||
|
* Running on http://127.0.0.1:5000
|
||||||
|
* Running on http://127.0.0.1:5000
|
||||||
|
Press CTRL+C to quit
|
||||||
|
* Restarting with watchdog (inotify)
|
||||||
|
[2m⏰ 21:58:52.225[0m [2m│[0m [92m✅ INFO [0m [2m│[0m [33m⚙️ [SYSTEM ][0m [2m│[0m 📝 🚀 SysTades Social Network gestartet
|
||||||
|
[2m⏰ 21:58:52.226[0m [2m│[0m [92m✅ INFO [0m [2m│[0m [33m⚙️ [SYSTEM ][0m [2m│[0m 📝 🚀 SysTades Social Network gestartet (v1.0.0) in development Umgebung auf Port 5000
|
||||||
|
[2m⏰ 21:58:53.848[0m [2m│[0m [92m✅ INFO [0m [2m│[0m [33m⚙️ [SYSTEM ][0m [2m│[0m 📝 OpenAI API-Verbindung erfolgreich hergestellt
|
||||||
|
[2m⏰ 21:58:53.997[0m [2m│[0m [92m✅ INFO [0m [2m│[0m [35m🗄️ [DB ][0m [2m│[0m 🚫 Datenbank erfolgreich initialisiert
|
||||||
|
[2m⏰ 21:58:54.002[0m [2m│[0m [92m✅ INFO [0m [2m│[0m [35m🗄️ [DB ][0m [2m│[0m 🚫 Datenbanktabellen erstellt/aktualisiert
|
||||||
|
[2m⏰ 21:58:54.006[0m [2m│[0m [92m✅ INFO [0m [2m│[0m [33m⚙️ [SYSTEM ][0m [2m│[0m 📝 Starte Flask-Entwicklungsserver auf http://localhost:5000
|
||||||
|
* Debugger is active!
|
||||||
|
* Debugger PIN: 114-005-893
|
||||||
53
start.sh
Normal file
53
start.sh
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
#!/usr/bin/env powershell
|
||||||
|
# Windows PowerShell-Version des Start-Skripts
|
||||||
|
# Datum: 01.05.2025
|
||||||
|
|
||||||
|
# Docker-Status prüfen
|
||||||
|
Write-Host "Prüfe Docker-Status..." -ForegroundColor Cyan
|
||||||
|
try {
|
||||||
|
$status = docker ps -q
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Host "Docker ist nicht gestartet. Bitte starten Sie Docker Desktop." -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Write-Host "Docker ist nicht verfügbar. Bitte installieren Sie Docker Desktop und starten Sie es." -ForegroundColor Red
|
||||||
|
Write-Host $_.Exception.Message
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Alte Container stoppen und entfernen
|
||||||
|
$containerExists = docker ps -a --filter "name=systades_app" -q
|
||||||
|
if ($containerExists) {
|
||||||
|
Write-Host "Stoppe und entferne alten Container..." -ForegroundColor Yellow
|
||||||
|
docker rm -f systades_app
|
||||||
|
}
|
||||||
|
|
||||||
|
# Alte Images löschen
|
||||||
|
Write-Host "Entferne altes Image..." -ForegroundColor Yellow
|
||||||
|
docker rmi -f systades_app:latest
|
||||||
|
|
||||||
|
# Stelle sicher, dass das Datenbankverzeichnis existiert
|
||||||
|
if (-not (Test-Path "database")) {
|
||||||
|
New-Item -Path "database" -ItemType Directory -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
# Docker-Compose Setup neu bauen
|
||||||
|
Write-Host "Baue Container neu..." -ForegroundColor Green
|
||||||
|
docker-compose build --no-cache
|
||||||
|
|
||||||
|
# Docker-Compose neu starten
|
||||||
|
Write-Host "Starte Container..." -ForegroundColor Green
|
||||||
|
docker-compose up -d --force-recreate
|
||||||
|
|
||||||
|
# Warte kurz und prüfe, ob der Container läuft
|
||||||
|
Write-Host "Prüfe Container-Status..." -ForegroundColor Cyan
|
||||||
|
Start-Sleep -Seconds 3
|
||||||
|
docker ps | Select-String "systades_app"
|
||||||
|
|
||||||
|
# Ausgabe
|
||||||
|
Write-Host "`nSystemstatus:" -ForegroundColor Cyan
|
||||||
|
Write-Host "----------------------------------------"
|
||||||
|
Write-Host "Systades-Anwendung ist jetzt unter http://localhost:5000 erreichbar." -ForegroundColor Green
|
||||||
|
Write-Host "Container-Logs können mit 'docker logs -f systades_app' angezeigt werden." -ForegroundColor Green
|
||||||
|
Write-Host "----------------------------------------"
|
||||||
1
static/541AABD7-4E37-44ED-B491-1459C8C19699.PNG
Normal file
1
static/541AABD7-4E37-44ED-B491-1459C8C19699.PNG
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
// Background animation with Three.js
|
|
||||||
let scene, camera, renderer, stars = [];
|
|
||||||
|
|
||||||
function initBackground() {
|
|
||||||
// Setup scene
|
|
||||||
scene = new THREE.Scene();
|
|
||||||
|
|
||||||
// Setup camera
|
|
||||||
camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 1000);
|
|
||||||
camera.position.z = 100;
|
|
||||||
|
|
||||||
// Setup renderer
|
|
||||||
renderer = new THREE.WebGLRenderer({ alpha: true });
|
|
||||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
||||||
renderer.setClearColor(0x000000, 0); // Transparent background
|
|
||||||
|
|
||||||
// Append renderer to DOM
|
|
||||||
const backgroundContainer = document.getElementById('background-container');
|
|
||||||
if (backgroundContainer) {
|
|
||||||
backgroundContainer.appendChild(renderer.domElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add stars
|
|
||||||
for (let i = 0; i < 1000; i++) {
|
|
||||||
const geometry = new THREE.SphereGeometry(0.2, 8, 8);
|
|
||||||
const material = new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true, opacity: Math.random() * 0.5 + 0.1 });
|
|
||||||
const star = new THREE.Mesh(geometry, material);
|
|
||||||
|
|
||||||
// Random position
|
|
||||||
star.position.x = Math.random() * 600 - 300;
|
|
||||||
star.position.y = Math.random() * 600 - 300;
|
|
||||||
star.position.z = Math.random() * 600 - 300;
|
|
||||||
|
|
||||||
// Store reference to move in animation
|
|
||||||
star.velocity = Math.random() * 0.02 + 0.005;
|
|
||||||
stars.push(star);
|
|
||||||
|
|
||||||
scene.add(star);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add large glowing particles
|
|
||||||
for (let i = 0; i < 15; i++) {
|
|
||||||
const size = Math.random() * 5 + 2;
|
|
||||||
const geometry = new THREE.SphereGeometry(size, 16, 16);
|
|
||||||
|
|
||||||
// Create a glowing material
|
|
||||||
const color = new THREE.Color();
|
|
||||||
color.setHSL(Math.random(), 0.7, 0.5); // Random hue
|
|
||||||
|
|
||||||
const material = new THREE.MeshBasicMaterial({
|
|
||||||
color: color,
|
|
||||||
transparent: true,
|
|
||||||
opacity: 0.2
|
|
||||||
});
|
|
||||||
|
|
||||||
const particle = new THREE.Mesh(geometry, material);
|
|
||||||
|
|
||||||
// Random position but further away
|
|
||||||
particle.position.x = Math.random() * 1000 - 500;
|
|
||||||
particle.position.y = Math.random() * 1000 - 500;
|
|
||||||
particle.position.z = Math.random() * 200 - 400;
|
|
||||||
|
|
||||||
// Store reference to move in animation
|
|
||||||
particle.velocity = Math.random() * 0.01 + 0.002;
|
|
||||||
stars.push(particle);
|
|
||||||
|
|
||||||
scene.add(particle);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle window resize
|
|
||||||
window.addEventListener('resize', onWindowResize);
|
|
||||||
|
|
||||||
// Start animation
|
|
||||||
animate();
|
|
||||||
}
|
|
||||||
|
|
||||||
function animate() {
|
|
||||||
requestAnimationFrame(animate);
|
|
||||||
|
|
||||||
// Move stars
|
|
||||||
stars.forEach(star => {
|
|
||||||
star.position.z += star.velocity;
|
|
||||||
|
|
||||||
// Reset position if star moves too close
|
|
||||||
if (star.position.z > 100) {
|
|
||||||
star.position.z = -300;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Rotate the entire scene slightly for a dreamy effect
|
|
||||||
scene.rotation.y += 0.0003;
|
|
||||||
scene.rotation.x += 0.0001;
|
|
||||||
|
|
||||||
renderer.render(scene, camera);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onWindowResize() {
|
|
||||||
camera.aspect = window.innerWidth / window.innerHeight;
|
|
||||||
camera.updateProjectionMatrix();
|
|
||||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize background when the DOM is loaded
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', initBackground);
|
|
||||||
} else {
|
|
||||||
initBackground();
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
C:\Users\firem\Downloads\background.mp4
|
|
||||||
27
static/css/all.min.css
vendored
Normal file
27
static/css/all.min.css
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* Font Awesome 6.4.0
|
||||||
|
*
|
||||||
|
* This is a placeholder file. For production, you should:
|
||||||
|
* 1. Download Font Awesome from https://fontawesome.com/download
|
||||||
|
* 2. Extract the downloaded package
|
||||||
|
* 3. Copy the 'css/all.min.css' file to this location
|
||||||
|
* 4. Copy the 'webfonts' folder to '/static/webfonts/'
|
||||||
|
*
|
||||||
|
* Alternatively, you can install via npm and copy the files:
|
||||||
|
* npm install @fortawesome/fontawesome-free
|
||||||
|
* cp -r node_modules/@fortawesome/fontawesome-free/css/all.min.css static/css/
|
||||||
|
* cp -r node_modules/@fortawesome/fontawesome-free/webfonts/ static/
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Placeholder styles for common Font Awesome icons */
|
||||||
|
.fa, .fas, .far, .fab {
|
||||||
|
display: inline-block;
|
||||||
|
width: 1em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Warning message */
|
||||||
|
body::before {
|
||||||
|
content: "Font Awesome CSS placeholder. Please replace with the actual file.";
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
@@ -1,26 +1,37 @@
|
|||||||
/* ChatGPT Assistent Styles */
|
/* ChatGPT Assistent Styles - Verbesserte Version */
|
||||||
#chatgpt-assistant {
|
#chatgpt-assistant {
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: 'Inter', sans-serif;
|
||||||
|
bottom: 5.5rem;
|
||||||
|
z-index: 100;
|
||||||
|
max-height: 85vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
#assistant-chat {
|
#assistant-chat {
|
||||||
transition: max-height 0.3s ease, opacity 0.3s ease;
|
transition: max-height 0.4s cubic-bezier(0.25, 0.1, 0.25, 1),
|
||||||
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.2);
|
opacity 0.3s cubic-bezier(0.25, 0.1, 0.25, 1);
|
||||||
|
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.25);
|
||||||
border-radius: 0.75rem;
|
border-radius: 0.75rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
max-width: calc(100vw - 2rem);
|
||||||
|
max-height: 80vh !important;
|
||||||
#assistant-toggle {
|
|
||||||
transition: transform 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
#assistant-toggle:hover {
|
|
||||||
transform: scale(1.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#assistant-history {
|
#assistant-history {
|
||||||
|
max-height: calc(80vh - 150px);
|
||||||
|
overflow-y: auto;
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: rgba(156, 163, 175, 0.5) transparent;
|
scrollbar-color: rgba(156, 163, 175, 0.5) transparent;
|
||||||
|
padding-bottom: 2rem; /* Zusätzlicher Abstand unten */
|
||||||
|
}
|
||||||
|
|
||||||
|
#assistant-toggle {
|
||||||
|
transition: transform 0.3s ease, background-color 0.2s ease;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
z-index: 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
#assistant-toggle:hover {
|
||||||
|
transform: scale(1.1) rotate(10deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
#assistant-history::-webkit-scrollbar {
|
#assistant-history::-webkit-scrollbar {
|
||||||
@@ -40,27 +51,74 @@
|
|||||||
background-color: rgba(156, 163, 175, 0.3);
|
background-color: rgba(156, 163, 175, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Verbesserte Message-Bubbles mit Schatten und Animation */
|
||||||
|
#assistant-history .flex > div {
|
||||||
|
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
|
||||||
|
animation: messageAppear 0.3s ease-out forwards;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes messageAppear {
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Verzögerte Animation für Messages */
|
||||||
|
#assistant-history .flex:nth-child(1) > div { animation-delay: 0.05s; }
|
||||||
|
#assistant-history .flex:nth-child(2) > div { animation-delay: 0.1s; }
|
||||||
|
#assistant-history .flex:nth-child(3) > div { animation-delay: 0.15s; }
|
||||||
|
#assistant-history .flex:nth-child(4) > div { animation-delay: 0.2s; }
|
||||||
|
#assistant-history .flex:nth-child(5) > div { animation-delay: 0.25s; }
|
||||||
|
|
||||||
|
/* Vorschläge styling */
|
||||||
|
#assistant-suggestions {
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestion-pill {
|
||||||
|
animation: pillAppear 0.4s ease forwards;
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.9);
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pillAppear {
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Styling für verschiedene Verzögerungen bei Vorschlägen */
|
||||||
|
#assistant-suggestions button:nth-child(1) { animation-delay: 0.1s; }
|
||||||
|
#assistant-suggestions button:nth-child(2) { animation-delay: 0.2s; }
|
||||||
|
#assistant-suggestions button:nth-child(3) { animation-delay: 0.3s; }
|
||||||
|
|
||||||
/* Mach Platz für Notifications, damit sie nicht mit dem Assistenten überlappen */
|
/* Mach Platz für Notifications, damit sie nicht mit dem Assistenten überlappen */
|
||||||
.notification-area {
|
.notification-area {
|
||||||
bottom: 5rem;
|
bottom: 5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Verbesserter Glassmorphism-Effekt */
|
/* Verbesserte Glassmorphism-Effekt */
|
||||||
.glass-morphism {
|
.glass-morphism {
|
||||||
background: rgba(255, 255, 255, 0.2);
|
background: rgba(255, 255, 255, 0.25);
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(12px);
|
||||||
-webkit-backdrop-filter: blur(10px);
|
-webkit-backdrop-filter: blur(12px);
|
||||||
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.15);
|
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.15);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .glass-morphism {
|
.dark .glass-morphism {
|
||||||
background: rgba(15, 23, 42, 0.3);
|
background: rgba(15, 23, 42, 0.35);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.4);
|
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dunkleres Dark Theme */
|
/* Verbesserte Farbpalette für Dark Theme */
|
||||||
.dark {
|
.dark {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgba(10, 15, 25, var(--tw-bg-opacity)) !important;
|
background-color: rgba(10, 15, 25, var(--tw-bg-opacity)) !important;
|
||||||
@@ -82,6 +140,62 @@
|
|||||||
background-color: rgba(23, 33, 64, var(--tw-bg-opacity)) !important;
|
background-color: rgba(23, 33, 64, var(--tw-bg-opacity)) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Typing Indicator Animation Styles */
|
||||||
|
.typing-indicator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-indicator span {
|
||||||
|
height: 8px;
|
||||||
|
width: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0 2px;
|
||||||
|
opacity: 0.6;
|
||||||
|
animation: bounce 1.4s infinite ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark .typing-indicator span {
|
||||||
|
background-color: rgba(255, 255, 255, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .typing-indicator span {
|
||||||
|
background-color: rgba(107, 114, 128, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-indicator span:nth-child(1) { animation-delay: 0s; }
|
||||||
|
.typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
|
||||||
|
.typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
|
||||||
|
|
||||||
|
@keyframes bounce {
|
||||||
|
0%, 80%, 100% { transform: translateY(0); }
|
||||||
|
40% { transform: translateY(-8px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chat Input Fokus-Effekt */
|
||||||
|
#assistant-chat input:focus {
|
||||||
|
border-color: var(--primary-500, #3B82F6);
|
||||||
|
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark #assistant-chat input:focus {
|
||||||
|
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Verbesserte Responsive Layouts */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
#assistant-chat {
|
||||||
|
width: calc(100vw - 2rem) !important;
|
||||||
|
max-height: 65vh !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chatgpt-assistant {
|
||||||
|
right: 1rem;
|
||||||
|
bottom: 6rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Footer immer unten */
|
/* Footer immer unten */
|
||||||
html, body {
|
html, body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -102,3 +216,37 @@ main {
|
|||||||
footer {
|
footer {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Verbesserte Farbkontraste für Nachrichtenblasen */
|
||||||
|
.user-message {
|
||||||
|
background-color: rgba(124, 58, 237, 0.1) !important;
|
||||||
|
color: #4B5563 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark .user-message {
|
||||||
|
background-color: rgba(124, 58, 237, 0.2) !important;
|
||||||
|
color: #F9FAFB !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-message {
|
||||||
|
background-color: #F3F4F6 !important;
|
||||||
|
color: #1F2937 !important;
|
||||||
|
border-left: 3px solid #8B5CF6;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark .assistant-message {
|
||||||
|
background-color: rgba(31, 41, 55, 0.5) !important;
|
||||||
|
color: #F9FAFB !important;
|
||||||
|
border-left: 3px solid #8B5CF6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chat-Assistent-Position im Footer-Bereich anpassen */
|
||||||
|
.chat-assistant {
|
||||||
|
max-height: 75vh;
|
||||||
|
bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-assistant .chat-messages {
|
||||||
|
max-height: calc(75vh - 180px);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
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;
|
||||||
|
}
|
||||||
915
static/css/social.css
Normal file
915
static/css/social.css
Normal file
@@ -0,0 +1,915 @@
|
|||||||
|
/* ================================
|
||||||
|
SysTades Social Network Styles
|
||||||
|
================================ */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
/* Primary Colors */
|
||||||
|
--primary-50: #f0f9ff;
|
||||||
|
--primary-100: #e0f2fe;
|
||||||
|
--primary-200: #bae6fd;
|
||||||
|
--primary-300: #7dd3fc;
|
||||||
|
--primary-400: #38bdf8;
|
||||||
|
--primary-500: #0ea5e9;
|
||||||
|
--primary-600: #0284c7;
|
||||||
|
--primary-700: #0369a1;
|
||||||
|
--primary-800: #075985;
|
||||||
|
--primary-900: #0c4a6e;
|
||||||
|
|
||||||
|
/* Neutral Colors */
|
||||||
|
--gray-50: #f9fafb;
|
||||||
|
--gray-100: #f3f4f6;
|
||||||
|
--gray-200: #e5e7eb;
|
||||||
|
--gray-300: #d1d5db;
|
||||||
|
--gray-400: #9ca3af;
|
||||||
|
--gray-500: #6b7280;
|
||||||
|
--gray-600: #4b5563;
|
||||||
|
--gray-700: #374151;
|
||||||
|
--gray-800: #1f2937;
|
||||||
|
--gray-900: #111827;
|
||||||
|
|
||||||
|
/* Semantic Colors */
|
||||||
|
--success: #10b981;
|
||||||
|
--warning: #f59e0b;
|
||||||
|
--error: #ef4444;
|
||||||
|
--info: #3b82f6;
|
||||||
|
|
||||||
|
/* Social Media Colors */
|
||||||
|
--like-color: #ec4899;
|
||||||
|
--share-color: #8b5cf6;
|
||||||
|
--bookmark-color: #f59e0b;
|
||||||
|
--comment-color: var(--primary-500);
|
||||||
|
|
||||||
|
/* Glassmorphism */
|
||||||
|
--glass-bg: rgba(255, 255, 255, 0.1);
|
||||||
|
--glass-border: rgba(255, 255, 255, 0.2);
|
||||||
|
--glass-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
--transition-fast: 0.15s ease-out;
|
||||||
|
--transition-normal: 0.3s ease-out;
|
||||||
|
--transition-slow: 0.6s ease-out;
|
||||||
|
|
||||||
|
/* Shadows */
|
||||||
|
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||||
|
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
||||||
|
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
/* Border Radius */
|
||||||
|
--radius-sm: 0.375rem;
|
||||||
|
--radius-md: 0.5rem;
|
||||||
|
--radius-lg: 0.75rem;
|
||||||
|
--radius-xl: 1rem;
|
||||||
|
--radius-2xl: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark Mode Variables */
|
||||||
|
[data-theme="dark"] {
|
||||||
|
--glass-bg: rgba(0, 0, 0, 0.1);
|
||||||
|
--glass-border: rgba(255, 255, 255, 0.1);
|
||||||
|
--glass-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================
|
||||||
|
Performance Optimizations
|
||||||
|
================================ */
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* GPU Acceleration for animations */
|
||||||
|
.accelerated {
|
||||||
|
transform: translateZ(0);
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Smooth scrolling */
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================
|
||||||
|
Social Feed Styles
|
||||||
|
================================ */
|
||||||
|
|
||||||
|
.social-feed {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-card {
|
||||||
|
background: var(--glass-bg);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
border: 1px solid var(--glass-border);
|
||||||
|
border-radius: var(--radius-xl);
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
transition: all var(--transition-normal);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: var(--shadow-xl);
|
||||||
|
border-color: var(--primary-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-card::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 3px;
|
||||||
|
background: linear-gradient(90deg, var(--primary-500), var(--primary-600));
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity var(--transition-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-card:hover::before {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Post Header */
|
||||||
|
.post-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-avatar {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid var(--primary-500);
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-avatar:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
border-color: var(--primary-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-author {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-author-name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--gray-800);
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-author-username {
|
||||||
|
color: var(--gray-500);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-time {
|
||||||
|
color: var(--gray-400);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Post Content */
|
||||||
|
.post-content {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--gray-700);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-type-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-type-text { background: var(--gray-100); color: var(--gray-600); }
|
||||||
|
.post-type-thought { background: var(--primary-100); color: var(--primary-600); }
|
||||||
|
.post-type-question { background: var(--warning); color: white; }
|
||||||
|
.post-type-insight { background: var(--success); color: white; }
|
||||||
|
|
||||||
|
/* Post Actions */
|
||||||
|
.post-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding-top: 1rem;
|
||||||
|
border-top: 1px solid var(--gray-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--gray-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn:hover {
|
||||||
|
background: var(--gray-100);
|
||||||
|
color: var(--gray-700);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn.active {
|
||||||
|
color: var(--primary-600);
|
||||||
|
background: var(--primary-50);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn i {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Specific action colors */
|
||||||
|
.action-btn.like-btn.active {
|
||||||
|
color: var(--like-color);
|
||||||
|
background: rgba(236, 72, 153, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn.share-btn:hover {
|
||||||
|
color: var(--share-color);
|
||||||
|
background: rgba(139, 92, 246, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn.bookmark-btn.active {
|
||||||
|
color: var(--bookmark-color);
|
||||||
|
background: rgba(245, 158, 11, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================
|
||||||
|
Comments Section
|
||||||
|
================================ */
|
||||||
|
|
||||||
|
.comments-section {
|
||||||
|
border-top: 1px solid var(--gray-200);
|
||||||
|
padding-top: 1rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-item {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
background: var(--gray-50);
|
||||||
|
transition: background var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-item:hover {
|
||||||
|
background: var(--gray-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-avatar {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid var(--primary-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-author {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--gray-800);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-time {
|
||||||
|
color: var(--gray-400);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-text {
|
||||||
|
color: var(--gray-700);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-action {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--gray-400);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-action:hover {
|
||||||
|
color: var(--primary-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Comment Form */
|
||||||
|
.comment-form {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-form textarea {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 1px solid var(--gray-300);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
resize: none;
|
||||||
|
min-height: 80px;
|
||||||
|
transition: border-color var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-form textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--primary-500);
|
||||||
|
box-shadow: 0 0 0 3px rgba(14, 165, 233, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-submit {
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
background: var(--primary-500);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-submit:hover {
|
||||||
|
background: var(--primary-600);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================
|
||||||
|
Create Post Form
|
||||||
|
================================ */
|
||||||
|
|
||||||
|
.create-post-form {
|
||||||
|
background: var(--glass-bg);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
border: 1px solid var(--glass-border);
|
||||||
|
border-radius: var(--radius-xl);
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-post-textarea {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 120px;
|
||||||
|
padding: 1rem;
|
||||||
|
border: 1px solid var(--gray-300);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
resize: vertical;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
transition: all var(--transition-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-post-textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--primary-500);
|
||||||
|
box-shadow: 0 0 0 3px rgba(14, 165, 233, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-post-options {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 1rem;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-type-select,
|
||||||
|
.post-visibility-select {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border: 1px solid var(--gray-300);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: white;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-type-select:focus,
|
||||||
|
.post-visibility-select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--primary-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-post-btn {
|
||||||
|
padding: 0.75rem 2rem;
|
||||||
|
background: linear-gradient(135deg, var(--primary-500), var(--primary-600));
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all var(--transition-normal);
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-post-btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-post-btn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================
|
||||||
|
Filter Tabs
|
||||||
|
================================ */
|
||||||
|
|
||||||
|
.feed-filters {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
background: var(--gray-100);
|
||||||
|
border-radius: var(--radius-xl);
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-tab {
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
color: var(--gray-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-tab:hover {
|
||||||
|
background: var(--gray-200);
|
||||||
|
color: var(--gray-800);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-tab.active {
|
||||||
|
background: var(--primary-500);
|
||||||
|
color: white;
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================
|
||||||
|
Notifications
|
||||||
|
================================ */
|
||||||
|
|
||||||
|
.notification-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-item:hover {
|
||||||
|
background: var(--gray-50);
|
||||||
|
transform: translateX(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-item.unread {
|
||||||
|
background: var(--primary-50);
|
||||||
|
border-left: 4px solid var(--primary-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-like { background: var(--like-color); }
|
||||||
|
.notification-comment { background: var(--comment-color); }
|
||||||
|
.notification-follow { background: var(--success); }
|
||||||
|
.notification-share { background: var(--share-color); }
|
||||||
|
|
||||||
|
.notification-content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-text {
|
||||||
|
margin: 0 0 0.25rem 0;
|
||||||
|
color: var(--gray-800);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-time {
|
||||||
|
color: var(--gray-500);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-delete {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--gray-400);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-delete:hover {
|
||||||
|
background: var(--error);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================
|
||||||
|
User Profile
|
||||||
|
================================ */
|
||||||
|
|
||||||
|
.profile-header {
|
||||||
|
background: linear-gradient(135deg, var(--primary-500), var(--primary-600));
|
||||||
|
color: white;
|
||||||
|
padding: 2rem;
|
||||||
|
border-radius: var(--radius-2xl);
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-header::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="grain" width="100" height="100" patternUnits="userSpaceOnUse"><circle cx="25" cy="25" r="1" fill="rgba(255,255,255,0.1)"/><circle cx="75" cy="75" r="1" fill="rgba(255,255,255,0.1)"/></pattern></defs><rect width="100" height="100" fill="url(%23grain)"/></svg>');
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-info {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-avatar {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 4px solid rgba(255, 255, 255, 0.3);
|
||||||
|
box-shadow: var(--shadow-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-details h1 {
|
||||||
|
margin: 0 0 0.5rem 0;
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-username {
|
||||||
|
opacity: 0.9;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-bio {
|
||||||
|
opacity: 0.8;
|
||||||
|
line-height: 1.5;
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-stats {
|
||||||
|
display: flex;
|
||||||
|
gap: 2rem;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-number {
|
||||||
|
display: block;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.follow-btn {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||||
|
color: white;
|
||||||
|
padding: 0.75rem 2rem;
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all var(--transition-normal);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.follow-btn:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.follow-btn.following {
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
color: var(--primary-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Profile Tabs */
|
||||||
|
.profile-tabs {
|
||||||
|
display: flex;
|
||||||
|
border-bottom: 1px solid var(--gray-200);
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-tab {
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
color: var(--gray-600);
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-tab:hover {
|
||||||
|
color: var(--primary-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-tab.active {
|
||||||
|
color: var(--primary-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-tab.active::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 3px;
|
||||||
|
background: var(--primary-500);
|
||||||
|
border-radius: 2px 2px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================
|
||||||
|
Responsive Design
|
||||||
|
================================ */
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.social-feed {
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-card {
|
||||||
|
padding: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-group {
|
||||||
|
justify-content: space-around;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-post-options {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feed-filters {
|
||||||
|
padding: 0.25rem;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-tab {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-header {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-info {
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-avatar {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-details h1 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-stats {
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-tabs {
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-tab {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================
|
||||||
|
Loading & Animations
|
||||||
|
================================ */
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
display: inline-block;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 2px solid var(--gray-300);
|
||||||
|
border-radius: 50%;
|
||||||
|
border-top-color: var(--primary-500);
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-in {
|
||||||
|
animation: fadeIn 0.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(20px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-up {
|
||||||
|
animation: slideUp 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUp {
|
||||||
|
from { transform: translateY(100%); opacity: 0; }
|
||||||
|
to { transform: translateY(0); opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.pulse {
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.5; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================
|
||||||
|
Toast Notifications
|
||||||
|
================================ */
|
||||||
|
|
||||||
|
.toast-container {
|
||||||
|
position: fixed;
|
||||||
|
top: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
z-index: 1000;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast {
|
||||||
|
background: var(--glass-bg);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
border: 1px solid var(--glass-border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
pointer-events: all;
|
||||||
|
max-width: 400px;
|
||||||
|
animation: slideInRight 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.success {
|
||||||
|
border-left: 4px solid var(--success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.error {
|
||||||
|
border-left: 4px solid var(--error);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.warning {
|
||||||
|
border-left: 4px solid var(--warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.info {
|
||||||
|
border-left: 4px solid var(--info);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideInRight {
|
||||||
|
from { transform: translateX(100%); opacity: 0; }
|
||||||
|
to { transform: translateX(0); opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================
|
||||||
|
Utilities
|
||||||
|
================================ */
|
||||||
|
|
||||||
|
.text-gradient {
|
||||||
|
background: linear-gradient(135deg, var(--primary-500), var(--primary-600));
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-effect {
|
||||||
|
background: var(--glass-bg);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
border: 1px solid var(--glass-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shadow-glow {
|
||||||
|
box-shadow: 0 0 20px rgba(14, 165, 233, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollbar-hide {
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollbar-hide::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
/* Cybertechnisches Netzwerk Hintergrund-Overlay */
|
|
||||||
.cyber-network-bg {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: -1;
|
|
||||||
pointer-events: none;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cyber-network-bg::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: linear-gradient(125deg,
|
|
||||||
rgba(14, 14, 22, 0.95) 0%,
|
|
||||||
rgba(30, 30, 46, 0.98) 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.network-grid {
|
|
||||||
position: absolute;
|
|
||||||
width: 200%;
|
|
||||||
height: 200%;
|
|
||||||
top: -50%;
|
|
||||||
left: -50%;
|
|
||||||
background-size: 40px 40px;
|
|
||||||
background-image:
|
|
||||||
linear-gradient(to right, rgba(108, 93, 211, 0.05) 1px, transparent 1px),
|
|
||||||
linear-gradient(to bottom, rgba(108, 93, 211, 0.05) 1px, transparent 1px);
|
|
||||||
transform: perspective(500px) rotateX(60deg);
|
|
||||||
animation: grid-move 20s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.node {
|
|
||||||
position: absolute;
|
|
||||||
width: 4px;
|
|
||||||
height: 4px;
|
|
||||||
background: rgba(76, 223, 255, 0.8);
|
|
||||||
border-radius: 50%;
|
|
||||||
box-shadow: 0 0 10px rgba(76, 223, 255, 0.6);
|
|
||||||
filter: blur(1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.connection {
|
|
||||||
position: absolute;
|
|
||||||
height: 1px;
|
|
||||||
background: linear-gradient(90deg,
|
|
||||||
rgba(76, 223, 255, 0.2) 0%,
|
|
||||||
rgba(108, 93, 211, 0.3) 50%,
|
|
||||||
rgba(76, 223, 255, 0.2) 100%);
|
|
||||||
transform-origin: left center;
|
|
||||||
animation: pulse 4s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.data-packet {
|
|
||||||
position: absolute;
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: rgba(118, 69, 217, 0.8);
|
|
||||||
filter: blur(1px);
|
|
||||||
animation: travel var(--travel-time, 6s) linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.glow-overlay {
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: radial-gradient(
|
|
||||||
circle at 50% 40%,
|
|
||||||
rgba(76, 223, 255, 0.03) 0%,
|
|
||||||
rgba(108, 93, 211, 0.03) 45%,
|
|
||||||
transparent 70%
|
|
||||||
);
|
|
||||||
opacity: 0.8;
|
|
||||||
animation: pulse-glow 8s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Animations */
|
|
||||||
@keyframes grid-move {
|
|
||||||
0% {
|
|
||||||
transform: perspective(500px) rotateX(60deg) translateY(0);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: perspective(500px) rotateX(60deg) translateY(40px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse {
|
|
||||||
0%, 100% {
|
|
||||||
opacity: 0.2;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
opacity: 0.4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes travel {
|
|
||||||
0% {
|
|
||||||
transform: translateX(0) translateY(0);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
10% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
90% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: translateX(var(--travel-x, 100px)) translateY(var(--travel-y, 100px));
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse-glow {
|
|
||||||
0%, 100% {
|
|
||||||
opacity: 0.4;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1434,16 +1434,211 @@ html, body {
|
|||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
background: linear-gradient(135deg, var(--background-start), var(--background-end));
|
background: linear-gradient(135deg, var(--background-start), var(--background-end));
|
||||||
background-attachment: fixed;
|
background-attachment: fixed;
|
||||||
scroll-behavior: smooth;
|
|
||||||
height: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Sticky navbar */
|
/* Sticky navbar */
|
||||||
.navbar.sticky-top {
|
.navbar.sticky-top {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 50;
|
z-index: 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Importiere das Cyber-Network CSS */
|
/* Light Mode Optimierungen für wichtige UI-Komponenten */
|
||||||
@import url('/static/css/src/cybernetwork-bg.css');
|
|
||||||
|
/* Buttons im Light Mode */
|
||||||
|
.btn-primary:not(.dark-mode .btn-primary) {
|
||||||
|
background-color: var(--light-primary, #3b82f6);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:not(.dark-mode .btn-primary):hover {
|
||||||
|
background-color: var(--light-primary-hover, #4f46e5);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:not(.dark-mode .btn-secondary) {
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
color: #374151;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:not(.dark-mode .btn-secondary):hover {
|
||||||
|
background-color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navbar im Light Mode */
|
||||||
|
.navbar:not(.dark-mode .navbar),
|
||||||
|
.nav:not(.dark-mode .nav) {
|
||||||
|
background-color: rgba(255, 255, 255, 0.95);
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar:not(.dark-mode .navbar) .nav-link,
|
||||||
|
.nav:not(.dark-mode .nav) .nav-link {
|
||||||
|
color: #1e3a8a;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar:not(.dark-mode .navbar) .nav-link:hover,
|
||||||
|
.nav:not(.dark-mode .nav) .nav-link:hover {
|
||||||
|
color: #4f46e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar:not(.dark-mode .navbar) .navbar-brand,
|
||||||
|
.nav:not(.dark-mode .nav) .navbar-brand {
|
||||||
|
color: #0f172a;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dropdown Menüs im Light Mode */
|
||||||
|
.dropdown-menu:not(.dark-mode .dropdown-menu) {
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05), 0 10px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item:not(.dark-mode .dropdown-item) {
|
||||||
|
color: #1e293b;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item:not(.dark-mode .dropdown-item):hover {
|
||||||
|
background-color: #f1f5f9;
|
||||||
|
color: #4f46e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Karten im Light Mode */
|
||||||
|
.card:not(.dark-mode .card) {
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.03), 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header:not(.dark-mode .card-header) {
|
||||||
|
background-color: #f8fafc;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-footer:not(.dark-mode .card-footer) {
|
||||||
|
background-color: #f8fafc;
|
||||||
|
border-top: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Formulare im Light Mode */
|
||||||
|
.form-control:not(.dark-mode .form-control) {
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:not(.dark-mode .form-control):focus {
|
||||||
|
border-color: #3b82f6;
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tabs im Light Mode */
|
||||||
|
.nav-tabs:not(.dark-mode .nav-tabs) {
|
||||||
|
border-bottom-color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tabs:not(.dark-mode .nav-tabs) .nav-link {
|
||||||
|
color: #64748b;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tabs:not(.dark-mode .nav-tabs) .nav-link:hover {
|
||||||
|
border-color: #e5e7eb #e5e7eb #e5e7eb;
|
||||||
|
color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tabs:not(.dark-mode .nav-tabs) .nav-link.active {
|
||||||
|
color: #0f172a;
|
||||||
|
background-color: white;
|
||||||
|
border-color: #e5e7eb #e5e7eb white;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Alerts im Light Mode */
|
||||||
|
.alert:not(.dark-mode .alert) {
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-primary:not(.dark-mode .alert-primary) {
|
||||||
|
background-color: #eff6ff;
|
||||||
|
border-color: #bfdbfe;
|
||||||
|
color: #1e40af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-success:not(.dark-mode .alert-success) {
|
||||||
|
background-color: #f0fdf4;
|
||||||
|
border-color: #bbf7d0;
|
||||||
|
color: #166534;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-warning:not(.dark-mode .alert-warning) {
|
||||||
|
background-color: #fffbeb;
|
||||||
|
border-color: #fef3c7;
|
||||||
|
color: #92400e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-danger:not(.dark-mode .alert-danger) {
|
||||||
|
background-color: #fef2f2;
|
||||||
|
border-color: #fecaca;
|
||||||
|
color: #b91c1c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Badges im Light Mode */
|
||||||
|
.badge:not(.dark-mode .badge) {
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 0.25em 0.6em;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-primary:not(.dark-mode .badge-primary) {
|
||||||
|
background-color: #3b82f6;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-secondary:not(.dark-mode .badge-secondary) {
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tabellen im Light Mode */
|
||||||
|
table:not(.dark-mode table) {
|
||||||
|
background-color: white;
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
table:not(.dark-mode table) th {
|
||||||
|
background-color: #f8fafc;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
color: #0f172a;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0.75rem;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
table:not(.dark-mode table) td {
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
padding: 0.75rem;
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
table:not(.dark-mode table) tr:hover {
|
||||||
|
background-color: #f8fafc;
|
||||||
|
}
|
||||||
6
static/css/tailwind.min.css
vendored
Normal file
6
static/css/tailwind.min.css
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* Failed to bundle using Rollup v2.79.2: the file imports a not supported node.js built-in module "fs".
|
||||||
|
* If you believe this to be an issue with jsDelivr, and not with the package itself, please open an issue at https://github.com/jsdelivr/jsdelivr
|
||||||
|
*/
|
||||||
|
|
||||||
|
throw new Error('Failed to bundle using Rollup v2.79.2: the file imports a not supported node.js built-in module "fs". If you believe this to be an issue with jsDelivr, and not with the package itself, please open an issue at https://github.com/jsdelivr/jsdelivr');
|
||||||
70
static/d3-extensions.js
vendored
70
static/d3-extensions.js
vendored
@@ -450,6 +450,76 @@ class D3Extensions {
|
|||||||
// Pulsanimation starten
|
// Pulsanimation starten
|
||||||
pulse();
|
pulse();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verarbeitet Daten aus der Datenbank für die Mindmap-Visualisierung
|
||||||
|
* @param {Array} databaseNodes - Knotendaten aus der Datenbank
|
||||||
|
* @param {Array} links - Verbindungsdaten oder null für automatische Extraktion
|
||||||
|
* @returns {Object} Aufbereitete Daten für D3.js
|
||||||
|
*/
|
||||||
|
static processDbNodesForVisualization(databaseNodes, links = null) {
|
||||||
|
// Überprüfe, ob Daten vorhanden sind
|
||||||
|
if (!databaseNodes || databaseNodes.length === 0) {
|
||||||
|
console.warn('Keine Knotendaten zum Verarbeiten vorhanden');
|
||||||
|
return { nodes: [], links: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Knoten mit D3-Kompatiblem Format erstellen
|
||||||
|
const nodes = databaseNodes.map(node => {
|
||||||
|
// Farbgenerierung, falls keine vorhanden
|
||||||
|
const nodeColor = node.color_code ||
|
||||||
|
node.color ||
|
||||||
|
D3Extensions.stringToColor(node.name || 'default');
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: node.id,
|
||||||
|
name: node.name,
|
||||||
|
description: node.description || '',
|
||||||
|
thought_count: node.thought_count || 0,
|
||||||
|
color: nodeColor,
|
||||||
|
// Zusätzliche Attribute
|
||||||
|
category_id: node.category_id,
|
||||||
|
is_public: node.is_public !== undefined ? node.is_public : true,
|
||||||
|
// Position, falls vorhanden
|
||||||
|
x: node.x_position,
|
||||||
|
y: node.y_position,
|
||||||
|
// Größe, falls vorhanden
|
||||||
|
scale: node.scale || 1.0
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verbindungen verarbeiten
|
||||||
|
let processedLinks = [];
|
||||||
|
|
||||||
|
if (links && Array.isArray(links)) {
|
||||||
|
// Verwende übergebene Verbindungen
|
||||||
|
processedLinks = links.map(link => {
|
||||||
|
return {
|
||||||
|
source: link.source,
|
||||||
|
target: link.target,
|
||||||
|
// Zusätzliche Attribute
|
||||||
|
type: link.type || 'default',
|
||||||
|
strength: link.strength || 1
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Extrahiere Verbindungen aus den Knoten
|
||||||
|
databaseNodes.forEach(node => {
|
||||||
|
if (node.connections && Array.isArray(node.connections)) {
|
||||||
|
node.connections.forEach(conn => {
|
||||||
|
processedLinks.push({
|
||||||
|
source: node.id,
|
||||||
|
target: conn.target,
|
||||||
|
type: conn.type || 'default',
|
||||||
|
strength: conn.strength || 1
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { nodes, links: processedLinks };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Globale Verfügbarkeit sicherstellen
|
// Globale Verfügbarkeit sicherstellen
|
||||||
|
|||||||
BIN
static/example.png
Normal file
BIN
static/example.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
35
static/fonts/inter.css
Normal file
35
static/fonts/inter.css
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
/* Inter font - Local version */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
src: url('../fonts/inter-light.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: url('../fonts/inter-regular.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
src: url('../fonts/inter-medium.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
src: url('../fonts/inter-semibold.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
src: url('../fonts/inter-bold.woff2') format('woff2');
|
||||||
|
}
|
||||||
21
static/fonts/jetbrains-mono.css
Normal file
21
static/fonts/jetbrains-mono.css
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
/* JetBrains Mono font - Local version */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: url('../fonts/jetbrainsmono-regular.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
src: url('../fonts/jetbrainsmono-medium.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
src: url('../fonts/jetbrainsmono-bold.woff2') format('woff2');
|
||||||
|
}
|
||||||
11
static/img/default-avatar.svg
Normal file
11
static/img/default-avatar.svg
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<svg width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="100" cy="100" r="98" fill="url(#gradient)" stroke="#7C3AED" stroke-width="4"/>
|
||||||
|
<circle cx="100" cy="80" r="36" fill="white"/>
|
||||||
|
<path d="M100 140C77.9086 140 60 157.909 60 180H140C140 157.909 122.091 140 100 140Z" fill="white"/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="gradient" x1="0" y1="0" x2="200" y2="200" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stop-color="#8B5CF6"/>
|
||||||
|
<stop offset="1" stop-color="#3B82F6"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 583 B |
@@ -1,25 +1,54 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
"""
|
||||||
|
Generate favicon.ico from SVG using cairosvg and PIL
|
||||||
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import io
|
||||||
|
from cairosvg import svg2png
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
import cairosvg
|
|
||||||
|
|
||||||
# Pfad zum SVG-Favicon
|
# Verzeichnis dieses Skripts
|
||||||
svg_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'favicon.svg')
|
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
# Ausgabepfad für das PNG
|
|
||||||
png_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'favicon.png')
|
|
||||||
# Ausgabepfad für das ICO
|
|
||||||
ico_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'favicon.ico')
|
|
||||||
|
|
||||||
# SVG zu PNG konvertieren
|
def svg_to_ico(svg_path, ico_path, sizes=[16, 32, 48, 64, 128, 256]):
|
||||||
cairosvg.svg2png(url=svg_path, write_to=png_path, output_width=512, output_height=512)
|
"""Convert SVG to multi-size ICO file"""
|
||||||
|
img_io = io.BytesIO()
|
||||||
|
|
||||||
# PNG zu ICO konvertieren
|
# Höchste Auflösung für Zwischenspeicherung
|
||||||
img = Image.open(png_path)
|
max_size = max(sizes)
|
||||||
img.save(ico_path, sizes=[(16, 16), (32, 32), (48, 48), (64, 64), (128, 128)])
|
|
||||||
|
|
||||||
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
|
# PNG in verschiedene Größen konvertieren
|
||||||
# os.remove(png_path)
|
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
|
* MindMap - Hauptdatei für globale JavaScript-Funktionen
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Import des ChatGPT-Assistenten
|
|
||||||
import ChatGPTAssistant from './modules/chatgpt-assistant.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hauptmodul für die MindMap-Anwendung
|
* Hauptmodul für die MindMap-Anwendung
|
||||||
* Verwaltet die globale Anwendungslogik
|
* Verwaltet die globale Anwendungslogik
|
||||||
*/
|
*/
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
// Initialisiere die Anwendung
|
|
||||||
MindMap.init();
|
|
||||||
|
|
||||||
// Wende Dunkel-/Hellmodus an
|
|
||||||
const isDarkMode = localStorage.getItem('darkMode') === 'dark';
|
|
||||||
document.documentElement.classList.toggle('dark', isDarkMode);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hauptobjekt der MindMap-Anwendung
|
* Hauptobjekt der MindMap-Anwendung
|
||||||
*/
|
*/
|
||||||
@@ -27,7 +15,7 @@ const MindMap = {
|
|||||||
initialized: false,
|
initialized: false,
|
||||||
darkMode: document.documentElement.classList.contains('dark'),
|
darkMode: document.documentElement.classList.contains('dark'),
|
||||||
pageInitializers: {},
|
pageInitializers: {},
|
||||||
currentPage: document.body.dataset.page,
|
currentPage: null,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialisiert die MindMap-Anwendung
|
* Initialisiert die MindMap-Anwendung
|
||||||
@@ -35,13 +23,18 @@ const MindMap = {
|
|||||||
init() {
|
init() {
|
||||||
if (this.initialized) return;
|
if (this.initialized) return;
|
||||||
|
|
||||||
|
// Setze currentPage erst jetzt, wenn DOM garantiert geladen ist
|
||||||
|
this.currentPage = document.body && document.body.dataset ? document.body.dataset.page : null;
|
||||||
|
|
||||||
console.log('MindMap-Anwendung wird initialisiert...');
|
console.log('MindMap-Anwendung wird initialisiert...');
|
||||||
|
|
||||||
// Initialisiere den ChatGPT-Assistenten
|
// Initialisiere den ChatGPT-Assistenten
|
||||||
const assistant = new ChatGPTAssistant();
|
if (typeof ChatGPTAssistant !== 'undefined') {
|
||||||
assistant.init();
|
const assistant = new ChatGPTAssistant();
|
||||||
// Speichere als Teil von MindMap
|
assistant.init();
|
||||||
this.assistant = assistant;
|
// Speichere als Teil von MindMap
|
||||||
|
this.assistant = assistant;
|
||||||
|
}
|
||||||
|
|
||||||
// Seiten-spezifische Initialisierer aufrufen
|
// Seiten-spezifische Initialisierer aufrufen
|
||||||
if (this.currentPage && this.pageInitializers[this.currentPage]) {
|
if (this.currentPage && this.pageInitializers[this.currentPage]) {
|
||||||
@@ -74,6 +67,12 @@ const MindMap = {
|
|||||||
try {
|
try {
|
||||||
console.log('Initialisiere Mindmap...');
|
console.log('Initialisiere Mindmap...');
|
||||||
|
|
||||||
|
// Prüfe, ob MindMapVisualization geladen ist
|
||||||
|
if (typeof MindMapVisualization === 'undefined') {
|
||||||
|
console.error('MindMapVisualization-Klasse ist nicht definiert!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Initialisiere die Mindmap
|
// Initialisiere die Mindmap
|
||||||
const mindmap = new MindMapVisualization('#mindmap-container', {
|
const mindmap = new MindMapVisualization('#mindmap-container', {
|
||||||
height: mindmapContainer.clientHeight || 600,
|
height: mindmapContainer.clientHeight || 600,
|
||||||
@@ -224,6 +223,13 @@ const MindMap = {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Globale Export für andere Module
|
|
||||||
window.MindMap = MindMap;
|
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.container = null;
|
||||||
this.chatHistory = null;
|
this.chatHistory = null;
|
||||||
this.inputField = null;
|
this.inputField = null;
|
||||||
|
this.suggestionArea = null;
|
||||||
|
this.maxRetries = 2;
|
||||||
|
this.retryCount = 0;
|
||||||
|
this.markdownParser = null;
|
||||||
|
this.initializeMarkdownParser();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialisiert den Markdown-Parser
|
||||||
|
*/
|
||||||
|
async initializeMarkdownParser() {
|
||||||
|
// Dynamisch marked.js laden, wenn noch nicht vorhanden
|
||||||
|
if (!window.marked) {
|
||||||
|
try {
|
||||||
|
// Prüfen, ob marked.js bereits im Dokument geladen ist
|
||||||
|
if (!document.querySelector('script[src*="marked"]')) {
|
||||||
|
// Falls nicht, Script-Tag erstellen und einfügen
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = 'https://cdn.jsdelivr.net/npm/marked/marked.min.js';
|
||||||
|
script.async = true;
|
||||||
|
|
||||||
|
// Promise erstellen, das resolved wird, wenn das Script geladen wurde
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
script.onload = resolve;
|
||||||
|
script.onerror = reject;
|
||||||
|
document.head.appendChild(script);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Marked.js erfolgreich geladen');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marked konfigurieren
|
||||||
|
this.markdownParser = window.marked;
|
||||||
|
this.markdownParser.setOptions({
|
||||||
|
gfm: true,
|
||||||
|
breaks: true,
|
||||||
|
sanitize: true,
|
||||||
|
smartLists: true,
|
||||||
|
smartypants: true
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Laden von marked.js:', error);
|
||||||
|
// Fallback-Parser, der nur einfache Absätze erkennt
|
||||||
|
this.markdownParser = {
|
||||||
|
parse: (text) => {
|
||||||
|
return text.split('\n').map(line => {
|
||||||
|
if (line.trim() === '') return '<br>';
|
||||||
|
return `<p>${line}</p>`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Marked ist bereits geladen
|
||||||
|
this.markdownParser = window.marked;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -24,7 +80,16 @@ class ChatGPTAssistant {
|
|||||||
this.setupEventListeners();
|
this.setupEventListeners();
|
||||||
|
|
||||||
// Ersten Willkommensnachricht anzeigen
|
// Ersten Willkommensnachricht anzeigen
|
||||||
this.addMessage("assistant", "Frage den KI-Assistenten");
|
this.addMessage("assistant", "Hallo! Ich bin dein KI-Assistent (4o-mini) und habe Zugriff auf die Wissensdatenbank. Wie kann ich dir helfen?\n\nDu kannst mir Fragen über:\n- **Gedanken** in der Datenbank\n- **Kategorien** und Wissenschaftsbereiche\n- **Mindmaps** und Wissensverknüpfungen\n\nstellen.");
|
||||||
|
|
||||||
|
// Vorschläge anzeigen
|
||||||
|
this.showSuggestions([
|
||||||
|
"Zeige mir Gedanken zur künstlichen Intelligenz",
|
||||||
|
"Welche Kategorien gibt es in der Datenbank?",
|
||||||
|
"Suche nach Mindmaps zum Thema Informatik"
|
||||||
|
]);
|
||||||
|
|
||||||
|
console.log('KI-Assistent initialisiert!');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -45,7 +110,7 @@ class ChatGPTAssistant {
|
|||||||
// Chat-Container
|
// Chat-Container
|
||||||
const chatContainer = document.createElement('div');
|
const chatContainer = document.createElement('div');
|
||||||
chatContainer.id = 'assistant-chat';
|
chatContainer.id = 'assistant-chat';
|
||||||
chatContainer.className = 'bg-white dark:bg-dark-800 rounded-lg shadow-xl overflow-hidden transition-all duration-300 w-80 sm:w-96 max-h-0 opacity-0';
|
chatContainer.className = 'bg-white dark:bg-dark-800 rounded-lg shadow-xl overflow-hidden transition-all duration-300 w-80 md:w-96 max-h-0 opacity-0';
|
||||||
|
|
||||||
// Chat-Header
|
// Chat-Header
|
||||||
const header = document.createElement('div');
|
const header = document.createElement('div');
|
||||||
@@ -53,7 +118,7 @@ class ChatGPTAssistant {
|
|||||||
header.innerHTML = `
|
header.innerHTML = `
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<i class="fas fa-robot mr-2"></i>
|
<i class="fas fa-robot mr-2"></i>
|
||||||
<span>KI-Assistent</span>
|
<span>KI-Assistent (4o-mini)</span>
|
||||||
</div>
|
</div>
|
||||||
<button id="assistant-close" class="text-white hover:text-gray-200">
|
<button id="assistant-close" class="text-white hover:text-gray-200">
|
||||||
<i class="fas fa-times"></i>
|
<i class="fas fa-times"></i>
|
||||||
@@ -63,7 +128,12 @@ class ChatGPTAssistant {
|
|||||||
// Chat-Verlauf
|
// Chat-Verlauf
|
||||||
this.chatHistory = document.createElement('div');
|
this.chatHistory = document.createElement('div');
|
||||||
this.chatHistory.id = 'assistant-history';
|
this.chatHistory.id = 'assistant-history';
|
||||||
this.chatHistory.className = 'p-3 overflow-y-auto max-h-80 space-y-3';
|
this.chatHistory.className = 'p-3 overflow-y-auto max-h-96 space-y-3';
|
||||||
|
|
||||||
|
// Vorschlagsbereich
|
||||||
|
this.suggestionArea = document.createElement('div');
|
||||||
|
this.suggestionArea.id = 'assistant-suggestions';
|
||||||
|
this.suggestionArea.className = 'px-3 pb-2 flex flex-wrap gap-2 overflow-x-auto hidden';
|
||||||
|
|
||||||
// Chat-Eingabe
|
// Chat-Eingabe
|
||||||
const inputContainer = document.createElement('div');
|
const inputContainer = document.createElement('div');
|
||||||
@@ -71,7 +141,7 @@ class ChatGPTAssistant {
|
|||||||
|
|
||||||
this.inputField = document.createElement('input');
|
this.inputField = document.createElement('input');
|
||||||
this.inputField.type = 'text';
|
this.inputField.type = 'text';
|
||||||
this.inputField.placeholder = 'Frage den KI-Assistenten';
|
this.inputField.placeholder = 'Stelle eine Frage zur Wissensdatenbank...';
|
||||||
this.inputField.className = 'flex-1 border border-gray-300 dark:border-dark-600 dark:bg-dark-700 dark:text-white rounded-l-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary-500';
|
this.inputField.className = 'flex-1 border border-gray-300 dark:border-dark-600 dark:bg-dark-700 dark:text-white rounded-l-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary-500';
|
||||||
|
|
||||||
const sendButton = document.createElement('button');
|
const sendButton = document.createElement('button');
|
||||||
@@ -85,6 +155,7 @@ class ChatGPTAssistant {
|
|||||||
|
|
||||||
chatContainer.appendChild(header);
|
chatContainer.appendChild(header);
|
||||||
chatContainer.appendChild(this.chatHistory);
|
chatContainer.appendChild(this.chatHistory);
|
||||||
|
chatContainer.appendChild(this.suggestionArea);
|
||||||
chatContainer.appendChild(inputContainer);
|
chatContainer.appendChild(inputContainer);
|
||||||
|
|
||||||
this.container.appendChild(toggleButton);
|
this.container.appendChild(toggleButton);
|
||||||
@@ -100,22 +171,40 @@ class ChatGPTAssistant {
|
|||||||
setupEventListeners() {
|
setupEventListeners() {
|
||||||
// Toggle-Button
|
// Toggle-Button
|
||||||
const toggleButton = document.getElementById('assistant-toggle');
|
const toggleButton = document.getElementById('assistant-toggle');
|
||||||
toggleButton.addEventListener('click', () => this.toggleAssistant());
|
if (toggleButton) {
|
||||||
|
toggleButton.addEventListener('click', () => this.toggleAssistant());
|
||||||
|
}
|
||||||
|
|
||||||
// Schließen-Button
|
// Schließen-Button
|
||||||
const closeButton = document.getElementById('assistant-close');
|
const closeButton = document.getElementById('assistant-close');
|
||||||
closeButton.addEventListener('click', () => this.toggleAssistant(false));
|
if (closeButton) {
|
||||||
|
closeButton.addEventListener('click', () => this.toggleAssistant(false));
|
||||||
|
}
|
||||||
|
|
||||||
// Senden-Button
|
// Senden-Button
|
||||||
const sendButton = document.getElementById('assistant-send');
|
const sendButton = document.getElementById('assistant-send');
|
||||||
sendButton.addEventListener('click', () => this.sendMessage());
|
if (sendButton) {
|
||||||
|
sendButton.addEventListener('click', () => this.sendMessage());
|
||||||
|
}
|
||||||
|
|
||||||
// Enter-Taste im Eingabefeld
|
// Enter-Taste im Eingabefeld
|
||||||
this.inputField.addEventListener('keyup', (e) => {
|
if (this.inputField) {
|
||||||
if (e.key === 'Enter') {
|
this.inputField.addEventListener('keyup', (e) => {
|
||||||
this.sendMessage();
|
if (e.key === 'Enter') {
|
||||||
}
|
this.sendMessage();
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vorschläge klickbar machen
|
||||||
|
if (this.suggestionArea) {
|
||||||
|
this.suggestionArea.addEventListener('click', (e) => {
|
||||||
|
if (e.target.classList.contains('suggestion-pill')) {
|
||||||
|
this.inputField.value = e.target.textContent;
|
||||||
|
this.sendMessage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -124,14 +213,21 @@ class ChatGPTAssistant {
|
|||||||
*/
|
*/
|
||||||
toggleAssistant(state = null) {
|
toggleAssistant(state = null) {
|
||||||
const chatContainer = document.getElementById('assistant-chat');
|
const chatContainer = document.getElementById('assistant-chat');
|
||||||
|
if (!chatContainer) return;
|
||||||
|
|
||||||
this.isOpen = state !== null ? state : !this.isOpen;
|
this.isOpen = state !== null ? state : !this.isOpen;
|
||||||
|
|
||||||
if (this.isOpen) {
|
if (this.isOpen) {
|
||||||
chatContainer.classList.remove('max-h-0', 'opacity-0');
|
chatContainer.classList.remove('max-h-0', 'opacity-0');
|
||||||
chatContainer.classList.add('max-h-96', 'opacity-100');
|
chatContainer.classList.add('max-h-[32rem]', 'opacity-100');
|
||||||
this.inputField.focus();
|
if (this.inputField) this.inputField.focus();
|
||||||
|
|
||||||
|
// Zeige Vorschläge wenn verfügbar
|
||||||
|
if (this.suggestionArea && this.suggestionArea.children.length > 0) {
|
||||||
|
this.suggestionArea.classList.remove('hidden');
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
chatContainer.classList.remove('max-h-96', 'opacity-100');
|
chatContainer.classList.remove('max-h-[32rem]', 'opacity-100');
|
||||||
chatContainer.classList.add('max-h-0', 'opacity-0');
|
chatContainer.classList.add('max-h-0', 'opacity-0');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -151,24 +247,79 @@ class ChatGPTAssistant {
|
|||||||
|
|
||||||
const bubble = document.createElement('div');
|
const bubble = document.createElement('div');
|
||||||
bubble.className = sender === 'user'
|
bubble.className = sender === 'user'
|
||||||
? 'bg-primary-100 dark:bg-primary-900 text-gray-800 dark:text-white rounded-lg py-2 px-3 max-w-[85%]'
|
? 'user-message rounded-lg py-2 px-3 max-w-[85%]'
|
||||||
: 'bg-gray-100 dark:bg-dark-700 text-gray-800 dark:text-white rounded-lg py-2 px-3 max-w-[85%]';
|
: 'assistant-message rounded-lg py-2 px-3 max-w-[85%]';
|
||||||
bubble.textContent = text;
|
|
||||||
|
// Nachrichtentext einfügen, falls Markdown-Parser verfügbar, nutzen
|
||||||
|
if (this.markdownParser) {
|
||||||
|
bubble.innerHTML = this.markdownParser.parse(text);
|
||||||
|
} else {
|
||||||
|
bubble.textContent = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Links in der Nachricht klickbar machen
|
||||||
|
const links = bubble.querySelectorAll('a');
|
||||||
|
links.forEach(link => {
|
||||||
|
link.target = '_blank';
|
||||||
|
link.rel = 'noopener noreferrer';
|
||||||
|
link.className = 'text-primary-600 dark:text-primary-400 underline';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Code-Blöcke stylen
|
||||||
|
const codeBlocks = bubble.querySelectorAll('pre');
|
||||||
|
codeBlocks.forEach(block => {
|
||||||
|
block.className = 'bg-gray-100 dark:bg-dark-900 p-2 rounded my-2 overflow-x-auto';
|
||||||
|
});
|
||||||
|
|
||||||
|
const inlineCode = bubble.querySelectorAll('code:not(pre code)');
|
||||||
|
inlineCode.forEach(code => {
|
||||||
|
code.className = 'bg-gray-100 dark:bg-dark-900 px-1 rounded font-mono text-sm';
|
||||||
|
});
|
||||||
|
|
||||||
messageEl.appendChild(bubble);
|
messageEl.appendChild(bubble);
|
||||||
this.chatHistory.appendChild(messageEl);
|
this.chatHistory.appendChild(messageEl);
|
||||||
|
|
||||||
// Scroll zum Ende des Verlaufs
|
// Scrolle zum Ende des Chat-Verlaufs
|
||||||
this.chatHistory.scrollTop = this.chatHistory.scrollHeight;
|
this.chatHistory.scrollTop = this.chatHistory.scrollHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zeigt Vorschläge für mögliche Fragen an
|
||||||
|
* @param {Array} suggestions - Array von Vorschlägen
|
||||||
|
*/
|
||||||
|
showSuggestions(suggestions) {
|
||||||
|
if (!this.suggestionArea || !suggestions || !suggestions.length) return;
|
||||||
|
|
||||||
|
// Vorherige Vorschläge entfernen
|
||||||
|
this.suggestionArea.innerHTML = '';
|
||||||
|
|
||||||
|
// Neue Vorschläge hinzufügen
|
||||||
|
suggestions.forEach((text, index) => {
|
||||||
|
const pill = document.createElement('button');
|
||||||
|
pill.className = 'suggestion-pill text-sm px-3 py-1.5 rounded-full bg-primary-100 dark:bg-primary-900 text-primary-800 dark:text-primary-200 hover:bg-primary-200 dark:hover:bg-primary-800 transition-all duration-200';
|
||||||
|
pill.style.animationDelay = `${index * 0.1}s`;
|
||||||
|
pill.textContent = text;
|
||||||
|
this.suggestionArea.appendChild(pill);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Vorschlagsbereich anzeigen
|
||||||
|
this.suggestionArea.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sendet die Benutzernachricht an den Server und zeigt die Antwort an
|
* Sendet die Benutzernachricht an den Server und zeigt die Antwort an
|
||||||
*/
|
*/
|
||||||
async sendMessage() {
|
async sendMessage() {
|
||||||
|
if (!this.inputField) return;
|
||||||
|
|
||||||
const userInput = this.inputField.value.trim();
|
const userInput = this.inputField.value.trim();
|
||||||
if (!userInput || this.isLoading) return;
|
if (!userInput || this.isLoading) return;
|
||||||
|
|
||||||
|
// Vorschläge ausblenden
|
||||||
|
if (this.suggestionArea) {
|
||||||
|
this.suggestionArea.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
// Benutzernachricht anzeigen
|
// Benutzernachricht anzeigen
|
||||||
this.addMessage('user', userInput);
|
this.addMessage('user', userInput);
|
||||||
|
|
||||||
@@ -180,6 +331,7 @@ class ChatGPTAssistant {
|
|||||||
this.showLoadingIndicator();
|
this.showLoadingIndicator();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
console.log('Sende Anfrage an KI-Assistent API...');
|
||||||
// Anfrage an den Server senden
|
// Anfrage an den Server senden
|
||||||
const response = await fetch('/api/assistant', {
|
const response = await fetch('/api/assistant', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -189,92 +341,206 @@ class ChatGPTAssistant {
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
messages: this.messages
|
messages: this.messages
|
||||||
}),
|
}),
|
||||||
|
cache: 'no-cache', // Kein Cache verwenden
|
||||||
|
credentials: 'same-origin', // Cookies senden
|
||||||
|
timeout: 60000 // 60 Sekunden Timeout
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Ladeindikator entfernen
|
||||||
|
this.removeLoadingIndicator();
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Netzwerkfehler oder Serverproblem');
|
const errorText = await response.text();
|
||||||
|
let errorMessage;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Versuche, die Fehlermeldung zu parsen
|
||||||
|
const errorData = JSON.parse(errorText);
|
||||||
|
errorMessage = errorData.error || `Serverfehler: ${response.status} ${response.statusText}`;
|
||||||
|
} catch {
|
||||||
|
// Bei Parsing-Fehler verwende Standardfehlermeldung
|
||||||
|
errorMessage = `Serverfehler: ${response.status} ${response.statusText}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
console.log('Antwort erhalten:', data);
|
||||||
// Ladeindikator entfernen
|
|
||||||
this.removeLoadingIndicator();
|
|
||||||
|
|
||||||
// Antwort anzeigen
|
// Antwort anzeigen
|
||||||
this.addMessage('assistant', data.response);
|
if (data.response) {
|
||||||
|
this.addMessage('assistant', data.response);
|
||||||
|
|
||||||
|
// Neue Vorschläge basierend auf dem aktuellen Kontext anzeigen
|
||||||
|
this.generateContextualSuggestions();
|
||||||
|
|
||||||
|
// Erfolgreiche Anfrage zurücksetzen
|
||||||
|
this.retryCount = 0;
|
||||||
|
} else if (data.error) {
|
||||||
|
this.addMessage('assistant', `Fehler: ${data.error}`);
|
||||||
|
} else {
|
||||||
|
throw new Error('Unerwartetes Antwortformat vom Server');
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fehler bei der Kommunikation mit dem Assistenten:', error);
|
console.error('Fehler bei der Kommunikation mit dem Assistenten:', error);
|
||||||
|
|
||||||
// Ladeindikator entfernen
|
// Ladeindikator entfernen, falls noch vorhanden
|
||||||
this.removeLoadingIndicator();
|
this.removeLoadingIndicator();
|
||||||
|
|
||||||
// Fehlermeldung anzeigen
|
// Spezielle Fehlermeldungen für bestimmte Fehlertypen
|
||||||
this.addMessage('assistant', 'Es tut mir leid, aber es gab ein Problem bei der Verarbeitung deiner Anfrage. Bitte versuche es später noch einmal.');
|
const errorMessage = error.message || '';
|
||||||
|
let userFriendlyMessage = 'Es gab ein Problem mit der Anfrage.';
|
||||||
|
|
||||||
|
if (errorMessage.includes('timeout') || errorMessage.includes('Zeitüberschreitung')) {
|
||||||
|
userFriendlyMessage = 'Die Antwort hat zu lange gedauert. Der Server ist möglicherweise überlastet.';
|
||||||
|
} else if (errorMessage.includes('500') || errorMessage.includes('Internal Server Error')) {
|
||||||
|
userFriendlyMessage = 'Ein Serverfehler ist aufgetreten. Wir arbeiten an einer Lösung.';
|
||||||
|
} else if (errorMessage.includes('429') || errorMessage.includes('rate limit')) {
|
||||||
|
userFriendlyMessage = 'Die API-Anfragelimits wurden erreicht. Bitte warte einen Moment.';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fehlermeldung anzeigen oder Wiederholungsversuch starten
|
||||||
|
if (this.retryCount < this.maxRetries) {
|
||||||
|
this.retryCount++;
|
||||||
|
this.addMessage('assistant', `${userFriendlyMessage} Ich versuche es erneut... (Versuch ${this.retryCount}/${this.maxRetries})`);
|
||||||
|
|
||||||
|
// Letzte Benutzernachricht speichern für den Wiederholungsversuch
|
||||||
|
const lastUserMessageIndex = this.messages.findLastIndex(msg => msg.role === 'user');
|
||||||
|
if (lastUserMessageIndex >= 0) {
|
||||||
|
const lastUserMessage = this.messages[lastUserMessageIndex].content;
|
||||||
|
|
||||||
|
// Kurze Verzögerung vor dem erneuten Versuch mit exponentieller Backoff-Strategie
|
||||||
|
const retryDelay = 1500 * Math.pow(2, this.retryCount - 1); // 1.5s, 3s, 6s, ...
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
// Entferne Fehlermeldung aus dem Messages-Array, behalte aber die Benutzernachricht
|
||||||
|
this.messages = this.messages.filter(msg =>
|
||||||
|
!(msg.role === 'assistant' && msg.content.includes('versuche es erneut'))
|
||||||
|
);
|
||||||
|
|
||||||
|
// Erneuter Versand mit gleicher Nachricht
|
||||||
|
this.inputField.value = lastUserMessage;
|
||||||
|
this.sendMessage();
|
||||||
|
}, retryDelay);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Maximale Anzahl an Wiederholungsversuchen erreicht
|
||||||
|
this.addMessage('assistant', 'Es tut mir leid, aber es gab ein Problem bei der Verarbeitung deiner Anfrage. Bitte versuche es später noch einmal oder kontaktiere den Support, falls das Problem weiterhin besteht.');
|
||||||
|
this.retryCount = 0; // Zurücksetzen für die nächste Anfrage
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Zeigt einen Ladeindikator im Chat an
|
* Generiert kontextbasierte Vorschläge basierend auf dem aktuellen Chat-Verlauf
|
||||||
|
*/
|
||||||
|
generateContextualSuggestions() {
|
||||||
|
// Basierend auf letzter Antwort des Assistenten, verschiedene Vorschläge generieren
|
||||||
|
const lastAssistantMessage = this.messages.findLast(msg => msg.role === 'assistant')?.content || '';
|
||||||
|
|
||||||
|
let suggestions = [];
|
||||||
|
|
||||||
|
// Intelligente Vorschläge basierend auf Kontext
|
||||||
|
if (lastAssistantMessage.includes('Künstliche Intelligenz') ||
|
||||||
|
lastAssistantMessage.includes('KI ') ||
|
||||||
|
lastAssistantMessage.includes('AI ')) {
|
||||||
|
suggestions = [
|
||||||
|
"Wie wird KI in der Wissenschaft eingesetzt?",
|
||||||
|
"Zeige mir Gedanken zum maschinellen Lernen",
|
||||||
|
"Was ist der Unterschied zwischen KI und ML?"
|
||||||
|
];
|
||||||
|
} else if (lastAssistantMessage.includes('Kategorie') ||
|
||||||
|
lastAssistantMessage.includes('Kategorien')) {
|
||||||
|
suggestions = [
|
||||||
|
"Zeige mir die Unterkategorien",
|
||||||
|
"Welche Gedanken gehören zu dieser Kategorie?",
|
||||||
|
"Liste alle Wissenschaftskategorien auf"
|
||||||
|
];
|
||||||
|
} else if (lastAssistantMessage.includes('Mindmap') ||
|
||||||
|
lastAssistantMessage.includes('Visualisierung')) {
|
||||||
|
suggestions = [
|
||||||
|
"Wie kann ich eine eigene Mindmap erstellen?",
|
||||||
|
"Zeige mir Beispiele für Mindmaps",
|
||||||
|
"Wie funktionieren die Verbindungen in Mindmaps?"
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
// Standardvorschläge
|
||||||
|
suggestions = [
|
||||||
|
"Erzähle mir mehr dazu",
|
||||||
|
"Gibt es Beispiele dafür?",
|
||||||
|
"Wie kann ich diese Information nutzen?"
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.showSuggestions(suggestions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zeigt eine Ladeanimation an
|
||||||
*/
|
*/
|
||||||
showLoadingIndicator() {
|
showLoadingIndicator() {
|
||||||
|
if (!this.chatHistory) return;
|
||||||
|
|
||||||
|
// Prüfen, ob bereits ein Ladeindikator angezeigt wird
|
||||||
|
if (document.getElementById('assistant-loading-indicator')) return;
|
||||||
|
|
||||||
const loadingEl = document.createElement('div');
|
const loadingEl = document.createElement('div');
|
||||||
loadingEl.id = 'assistant-loading';
|
|
||||||
loadingEl.className = 'flex justify-start';
|
loadingEl.className = 'flex justify-start';
|
||||||
|
loadingEl.id = 'assistant-loading-indicator';
|
||||||
|
|
||||||
const bubble = document.createElement('div');
|
const bubble = document.createElement('div');
|
||||||
bubble.className = 'bg-gray-100 dark:bg-dark-700 text-gray-800 dark:text-white rounded-lg py-2 px-3';
|
bubble.className = 'assistant-message rounded-lg py-3 px-4 max-w-[85%] flex items-center';
|
||||||
bubble.innerHTML = '<div class="typing-indicator"><span></span><span></span><span></span></div>';
|
|
||||||
|
|
||||||
loadingEl.appendChild(bubble);
|
const typingIndicator = document.createElement('div');
|
||||||
this.chatHistory.appendChild(loadingEl);
|
typingIndicator.className = 'typing-indicator';
|
||||||
|
typingIndicator.innerHTML = `
|
||||||
// Scroll zum Ende des Verlaufs
|
<span></span>
|
||||||
this.chatHistory.scrollTop = this.chatHistory.scrollHeight;
|
<span></span>
|
||||||
|
<span></span>
|
||||||
// Stil für den Typing-Indikator
|
|
||||||
const style = document.createElement('style');
|
|
||||||
style.textContent = `
|
|
||||||
.typing-indicator {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.typing-indicator span {
|
|
||||||
height: 8px;
|
|
||||||
width: 8px;
|
|
||||||
background-color: #888;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: inline-block;
|
|
||||||
margin: 0 2px;
|
|
||||||
opacity: 0.4;
|
|
||||||
animation: typing 1.5s infinite ease-in-out;
|
|
||||||
}
|
|
||||||
.typing-indicator span:nth-child(2) {
|
|
||||||
animation-delay: 0.2s;
|
|
||||||
}
|
|
||||||
.typing-indicator span:nth-child(3) {
|
|
||||||
animation-delay: 0.4s;
|
|
||||||
}
|
|
||||||
@keyframes typing {
|
|
||||||
0% { transform: translateY(0); }
|
|
||||||
50% { transform: translateY(-5px); }
|
|
||||||
100% { transform: translateY(0); }
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
document.head.appendChild(style);
|
|
||||||
|
bubble.appendChild(typingIndicator);
|
||||||
|
loadingEl.appendChild(bubble);
|
||||||
|
|
||||||
|
this.chatHistory.appendChild(loadingEl);
|
||||||
|
this.chatHistory.scrollTop = this.chatHistory.scrollHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Entfernt den Ladeindikator aus dem Chat
|
* Entfernt den Ladeindikator aus dem Chat
|
||||||
*/
|
*/
|
||||||
removeLoadingIndicator() {
|
removeLoadingIndicator() {
|
||||||
const loadingEl = document.getElementById('assistant-loading');
|
const loadingIndicator = document.getElementById('assistant-loading-indicator');
|
||||||
if (loadingEl) {
|
if (loadingIndicator) {
|
||||||
loadingEl.remove();
|
loadingIndicator.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Öffnet den Assistenten und sendet eine vorgegebene Frage
|
||||||
|
* @param {string} question - Die zu stellende Frage
|
||||||
|
*/
|
||||||
|
async sendQuestion(question) {
|
||||||
|
if (!question || this.isLoading) return;
|
||||||
|
|
||||||
|
// Assistenten öffnen
|
||||||
|
this.toggleAssistant(true);
|
||||||
|
|
||||||
|
// Kurze Verzögerung, um sicherzustellen, dass der UI vollständig geöffnet ist
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 300));
|
||||||
|
|
||||||
|
// Frage in Eingabefeld setzen
|
||||||
|
if (this.inputField) {
|
||||||
|
this.inputField.value = question;
|
||||||
|
|
||||||
|
// Sende die Frage
|
||||||
|
this.sendMessage();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exportiere die Klasse für die Verwendung in anderen Modulen
|
// Mache die Klasse global verfügbar
|
||||||
export default ChatGPTAssistant;
|
window.ChatGPTAssistant = ChatGPTAssistant;
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
/**
|
|
||||||
* Initialisierungsmodul für den CyberNetwork-Hintergrund
|
|
||||||
* Importiert und startet die Animation
|
|
||||||
*/
|
|
||||||
|
|
||||||
import CyberNetwork from './cyber-network.js';
|
|
||||||
|
|
||||||
// Beim Laden des Dokuments starten
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
console.log('CyberNetwork: Initialisierung gestartet');
|
|
||||||
|
|
||||||
// Prüfen ob das CSS bereits geladen ist, wenn nicht, dann laden
|
|
||||||
if (!document.querySelector('link[href*="cybernetwork-bg.css"]')) {
|
|
||||||
console.log('CyberNetwork: CSS wird geladen');
|
|
||||||
const cyberNetworkCss = document.createElement('link');
|
|
||||||
cyberNetworkCss.rel = 'stylesheet';
|
|
||||||
cyberNetworkCss.href = '/static/css/src/cybernetwork-bg.css';
|
|
||||||
document.head.appendChild(cyberNetworkCss);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Container-Element für das Netzwerk finden
|
|
||||||
const container = document.getElementById('cyber-background-container');
|
|
||||||
|
|
||||||
if (!container) {
|
|
||||||
console.error('CyberNetwork: Container #cyber-background-container nicht gefunden!');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('CyberNetwork: Container gefunden', container);
|
|
||||||
|
|
||||||
// Konfiguration für den Netzwerk-Hintergrund
|
|
||||||
const networkConfig = {
|
|
||||||
container: container,
|
|
||||||
nodeCount: window.innerWidth < 768 ? 15 : 30, // Weniger Nodes auf mobilen Geräten
|
|
||||||
connectionCount: window.innerWidth < 768 ? 25 : 50,
|
|
||||||
packetCount: window.innerWidth < 768 ? 8 : 15,
|
|
||||||
animationSpeed: 1.0
|
|
||||||
};
|
|
||||||
|
|
||||||
// Netzwerk erstellen und initialisieren
|
|
||||||
const cyberNetwork = new CyberNetwork(networkConfig);
|
|
||||||
cyberNetwork.init();
|
|
||||||
console.log('CyberNetwork: Netzwerk initialisiert');
|
|
||||||
|
|
||||||
// Globale Referenz für Debug-Zwecke
|
|
||||||
window.cyberNetwork = cyberNetwork;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Funktion zum manuellen Initialisieren, falls notwendig
|
|
||||||
export function initCyberNetwork(config = {}) {
|
|
||||||
console.log('CyberNetwork: Manuelle Initialisierung gestartet');
|
|
||||||
|
|
||||||
// CSS laden, falls nicht vorhanden
|
|
||||||
if (!document.querySelector('link[href*="cybernetwork-bg.css"]')) {
|
|
||||||
console.log('CyberNetwork: CSS wird geladen (manuell)');
|
|
||||||
const cyberNetworkCss = document.createElement('link');
|
|
||||||
cyberNetworkCss.rel = 'stylesheet';
|
|
||||||
cyberNetworkCss.href = '/static/css/src/cybernetwork-bg.css';
|
|
||||||
document.head.appendChild(cyberNetworkCss);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Container-Element für das Netzwerk finden
|
|
||||||
const container = document.getElementById('cyber-background-container');
|
|
||||||
|
|
||||||
if (!container) {
|
|
||||||
console.error('CyberNetwork: Container #cyber-background-container nicht gefunden!');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bestehende Instanz zurücksetzen, falls vorhanden
|
|
||||||
if (window.cyberNetwork) {
|
|
||||||
console.log('CyberNetwork: Bestehende Instanz wird zurückgesetzt');
|
|
||||||
window.cyberNetwork.reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Netzwerk mit benutzerdefinierten Optionen erstellen
|
|
||||||
const networkConfig = {
|
|
||||||
container: container,
|
|
||||||
nodeCount: window.innerWidth < 768 ? 15 : 30,
|
|
||||||
connectionCount: window.innerWidth < 768 ? 25 : 50,
|
|
||||||
packetCount: window.innerWidth < 768 ? 8 : 15,
|
|
||||||
animationSpeed: 1.0,
|
|
||||||
...config
|
|
||||||
};
|
|
||||||
|
|
||||||
// Neue Instanz erstellen und initialisieren
|
|
||||||
const cyberNetwork = new CyberNetwork(networkConfig);
|
|
||||||
cyberNetwork.init();
|
|
||||||
console.log('CyberNetwork: Netzwerk manuell initialisiert');
|
|
||||||
|
|
||||||
// Globale Referenz aktualisieren
|
|
||||||
window.cyberNetwork = cyberNetwork;
|
|
||||||
|
|
||||||
return cyberNetwork;
|
|
||||||
}
|
|
||||||
@@ -1,240 +0,0 @@
|
|||||||
/**
|
|
||||||
* Cyber Network Background Animation
|
|
||||||
* Generiert dynamisch ein animiertes Netzwerk für den Hintergrund
|
|
||||||
*/
|
|
||||||
|
|
||||||
class CyberNetwork {
|
|
||||||
constructor(options = {}) {
|
|
||||||
this.options = {
|
|
||||||
container: options.container || document.body,
|
|
||||||
nodeCount: options.nodeCount || 30,
|
|
||||||
connectionCount: options.connectionCount || 50,
|
|
||||||
packetCount: options.packetCount || 15,
|
|
||||||
animationSpeed: options.animationSpeed || 1.0,
|
|
||||||
...options
|
|
||||||
};
|
|
||||||
|
|
||||||
this.nodes = [];
|
|
||||||
this.connections = [];
|
|
||||||
this.packets = [];
|
|
||||||
this.initialized = false;
|
|
||||||
|
|
||||||
this.containerElement = null;
|
|
||||||
this.networkGridElement = null;
|
|
||||||
this.glowOverlayElement = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
if (this.initialized) return;
|
|
||||||
|
|
||||||
// Container erstellen
|
|
||||||
this.containerElement = document.createElement('div');
|
|
||||||
this.containerElement.className = 'cyber-network-bg';
|
|
||||||
|
|
||||||
// Grid erstellen
|
|
||||||
this.networkGridElement = document.createElement('div');
|
|
||||||
this.networkGridElement.className = 'network-grid';
|
|
||||||
this.containerElement.appendChild(this.networkGridElement);
|
|
||||||
|
|
||||||
// Glow Overlay erstellen
|
|
||||||
this.glowOverlayElement = document.createElement('div');
|
|
||||||
this.glowOverlayElement.className = 'glow-overlay';
|
|
||||||
this.containerElement.appendChild(this.glowOverlayElement);
|
|
||||||
|
|
||||||
// Nodes generieren
|
|
||||||
this.generateNodes();
|
|
||||||
|
|
||||||
// Connections generieren
|
|
||||||
this.generateConnections();
|
|
||||||
|
|
||||||
// Data packets generieren
|
|
||||||
this.generateDataPackets();
|
|
||||||
|
|
||||||
// Container zum DOM hinzufügen
|
|
||||||
if (typeof this.options.container === 'string') {
|
|
||||||
const container = document.querySelector(this.options.container);
|
|
||||||
if (container) {
|
|
||||||
container.appendChild(this.containerElement);
|
|
||||||
} else {
|
|
||||||
document.body.appendChild(this.containerElement);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.options.container.appendChild(this.containerElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.initialized = true;
|
|
||||||
|
|
||||||
// Animation starten
|
|
||||||
window.addEventListener('resize', this.handleResize.bind(this));
|
|
||||||
this.startAnimationCycle();
|
|
||||||
}
|
|
||||||
|
|
||||||
generateNodes() {
|
|
||||||
const containerWidth = window.innerWidth;
|
|
||||||
const containerHeight = window.innerHeight;
|
|
||||||
|
|
||||||
for (let i = 0; i < this.options.nodeCount; i++) {
|
|
||||||
const x = Math.random() * containerWidth;
|
|
||||||
const y = Math.random() * containerHeight;
|
|
||||||
|
|
||||||
const node = document.createElement('div');
|
|
||||||
node.className = 'node';
|
|
||||||
node.style.left = `${x}px`;
|
|
||||||
node.style.top = `${y}px`;
|
|
||||||
|
|
||||||
// Größen-Variation für visuelle Tiefe
|
|
||||||
const size = 2 + Math.random() * 4;
|
|
||||||
node.style.width = `${size}px`;
|
|
||||||
node.style.height = `${size}px`;
|
|
||||||
|
|
||||||
// Speichern der Position für spätere Referenz
|
|
||||||
node._data = { x, y, size };
|
|
||||||
|
|
||||||
this.containerElement.appendChild(node);
|
|
||||||
this.nodes.push(node);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
generateConnections() {
|
|
||||||
for (let i = 0; i < this.options.connectionCount; i++) {
|
|
||||||
// Zufällige Nodes auswählen
|
|
||||||
const startNodeIndex = Math.floor(Math.random() * this.nodes.length);
|
|
||||||
let endNodeIndex;
|
|
||||||
do {
|
|
||||||
endNodeIndex = Math.floor(Math.random() * this.nodes.length);
|
|
||||||
} while (endNodeIndex === startNodeIndex);
|
|
||||||
|
|
||||||
const startNode = this.nodes[startNodeIndex];
|
|
||||||
const endNode = this.nodes[endNodeIndex];
|
|
||||||
const startData = startNode._data;
|
|
||||||
const endData = endNode._data;
|
|
||||||
|
|
||||||
// Verbindung erstellen
|
|
||||||
const connection = document.createElement('div');
|
|
||||||
connection.className = 'connection';
|
|
||||||
|
|
||||||
// Position und Rotation berechnen
|
|
||||||
const dx = endData.x - startData.x;
|
|
||||||
const dy = endData.y - startData.y;
|
|
||||||
const length = Math.sqrt(dx * dx + dy * dy);
|
|
||||||
const angle = Math.atan2(dy, dx) * (180 / Math.PI);
|
|
||||||
|
|
||||||
connection.style.width = `${length}px`;
|
|
||||||
connection.style.left = `${startData.x}px`;
|
|
||||||
connection.style.top = `${startData.y}px`;
|
|
||||||
connection.style.transform = `rotate(${angle}deg)`;
|
|
||||||
|
|
||||||
// Variation in der Animations-Geschwindigkeit
|
|
||||||
connection.style.animationDuration = `${3 + Math.random() * 4}s`;
|
|
||||||
|
|
||||||
// Speichern der verbundenen Nodes
|
|
||||||
connection._data = {
|
|
||||||
startNode: startNodeIndex,
|
|
||||||
endNode: endNodeIndex,
|
|
||||||
length
|
|
||||||
};
|
|
||||||
|
|
||||||
this.containerElement.appendChild(connection);
|
|
||||||
this.connections.push(connection);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
generateDataPackets() {
|
|
||||||
for (let i = 0; i < this.options.packetCount; i++) {
|
|
||||||
this.createNewDataPacket();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
createNewDataPacket() {
|
|
||||||
if (this.connections.length === 0) return;
|
|
||||||
|
|
||||||
// Zufällige Verbindung auswählen
|
|
||||||
const connectionIndex = Math.floor(Math.random() * this.connections.length);
|
|
||||||
const connection = this.connections[connectionIndex];
|
|
||||||
const connectionData = connection._data;
|
|
||||||
|
|
||||||
const startNode = this.nodes[connectionData.startNode];
|
|
||||||
const startData = startNode._data;
|
|
||||||
|
|
||||||
// Data Packet erstellen
|
|
||||||
const packet = document.createElement('div');
|
|
||||||
packet.className = 'data-packet';
|
|
||||||
|
|
||||||
// Position auf dem Startknoten
|
|
||||||
packet.style.left = `${startData.x}px`;
|
|
||||||
packet.style.top = `${startData.y}px`;
|
|
||||||
|
|
||||||
// Zufällige Geschwindigkeit
|
|
||||||
const travelTime = (4 + Math.random() * 4) / this.options.animationSpeed;
|
|
||||||
packet.style.setProperty('--travel-time', `${travelTime}s`);
|
|
||||||
|
|
||||||
// Ziel-Koordinaten berechnen
|
|
||||||
const endNode = this.nodes[connectionData.endNode];
|
|
||||||
const endData = endNode._data;
|
|
||||||
const travelX = endData.x - startData.x;
|
|
||||||
const travelY = endData.y - startData.y;
|
|
||||||
|
|
||||||
packet.style.setProperty('--travel-x', `${travelX}px`);
|
|
||||||
packet.style.setProperty('--travel-y', `${travelY}px`);
|
|
||||||
|
|
||||||
// Farb-Variation
|
|
||||||
if (Math.random() > 0.5) {
|
|
||||||
packet.style.background = 'rgba(76, 223, 255, 0.8)'; // Akzentfarbe
|
|
||||||
}
|
|
||||||
|
|
||||||
this.containerElement.appendChild(packet);
|
|
||||||
this.packets.push(packet);
|
|
||||||
|
|
||||||
// Nach Ende der Animation neues Paket erstellen
|
|
||||||
setTimeout(() => {
|
|
||||||
if (this.containerElement.contains(packet)) {
|
|
||||||
this.containerElement.removeChild(packet);
|
|
||||||
}
|
|
||||||
|
|
||||||
const index = this.packets.indexOf(packet);
|
|
||||||
if (index > -1) {
|
|
||||||
this.packets.splice(index, 1);
|
|
||||||
this.createNewDataPacket();
|
|
||||||
}
|
|
||||||
}, travelTime * 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleResize() {
|
|
||||||
if (!this.initialized) return;
|
|
||||||
|
|
||||||
// Bei Größenänderung alles neu generieren
|
|
||||||
this.reset();
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
reset() {
|
|
||||||
if (!this.initialized) return;
|
|
||||||
|
|
||||||
// Alle Elemente entfernen
|
|
||||||
this.nodes.forEach(node => node.remove());
|
|
||||||
this.connections.forEach(connection => connection.remove());
|
|
||||||
this.packets.forEach(packet => packet.remove());
|
|
||||||
|
|
||||||
this.nodes = [];
|
|
||||||
this.connections = [];
|
|
||||||
this.packets = [];
|
|
||||||
|
|
||||||
if (this.containerElement) {
|
|
||||||
this.containerElement.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.initialized = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
startAnimationCycle() {
|
|
||||||
// Regelmäßig neue Pakete erstellen für mehr Dynamik
|
|
||||||
setInterval(() => {
|
|
||||||
if (this.packets.length < this.options.packetCount * 1.5) {
|
|
||||||
this.createNewDataPacket();
|
|
||||||
}
|
|
||||||
}, 1000 / this.options.animationSpeed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exportieren als Modul
|
|
||||||
export default CyberNetwork;
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,777 +0,0 @@
|
|||||||
/**
|
|
||||||
* MindMap D3.js Modul
|
|
||||||
* Visualisiert die Mindmap mit D3.js
|
|
||||||
*/
|
|
||||||
|
|
||||||
class MindMapVisualization {
|
|
||||||
constructor(containerSelector, options = {}) {
|
|
||||||
this.containerSelector = containerSelector;
|
|
||||||
this.container = d3.select(containerSelector);
|
|
||||||
this.width = options.width || this.container.node().clientWidth || 800;
|
|
||||||
this.height = options.height || 600;
|
|
||||||
this.nodeRadius = options.nodeRadius || 14;
|
|
||||||
this.selectedNodeRadius = options.selectedNodeRadius || 20;
|
|
||||||
this.linkDistance = options.linkDistance || 150;
|
|
||||||
this.chargeStrength = options.chargeStrength || -900;
|
|
||||||
this.centerForce = options.centerForce || 0.15;
|
|
||||||
this.onNodeClick = options.onNodeClick || ((node) => console.log('Node clicked:', node));
|
|
||||||
|
|
||||||
this.nodes = [];
|
|
||||||
this.links = [];
|
|
||||||
this.simulation = null;
|
|
||||||
this.svg = null;
|
|
||||||
this.linkElements = null;
|
|
||||||
this.nodeElements = null;
|
|
||||||
this.textElements = null;
|
|
||||||
this.tooltipEnabled = options.tooltipEnabled !== undefined ? options.tooltipEnabled : true;
|
|
||||||
|
|
||||||
this.mouseoverNode = null;
|
|
||||||
this.selectedNode = null;
|
|
||||||
|
|
||||||
this.zoomFactor = 1;
|
|
||||||
this.tooltipDiv = null;
|
|
||||||
this.isLoading = true;
|
|
||||||
|
|
||||||
// Lade die gemerkten Knoten
|
|
||||||
this.bookmarkedNodes = this.loadBookmarkedNodes();
|
|
||||||
|
|
||||||
// Sicherstellen, dass der Container bereit ist
|
|
||||||
if (this.container.node()) {
|
|
||||||
this.init();
|
|
||||||
this.setupDefaultNodes();
|
|
||||||
|
|
||||||
// Sofortige Datenladung
|
|
||||||
window.setTimeout(() => {
|
|
||||||
this.loadData();
|
|
||||||
}, 100);
|
|
||||||
} else {
|
|
||||||
console.error('Mindmap-Container nicht gefunden:', containerSelector);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Standardknoten als Fallback einrichten, falls die API nicht reagiert
|
|
||||||
setupDefaultNodes() {
|
|
||||||
// Basis-Mindmap mit Hauptthemen
|
|
||||||
const defaultNodes = [
|
|
||||||
{ id: "root", name: "Wissen", description: "Zentrale Wissensbasis", thought_count: 0 },
|
|
||||||
{ id: "philosophy", name: "Philosophie", description: "Philosophisches Denken", thought_count: 0 },
|
|
||||||
{ id: "science", name: "Wissenschaft", description: "Wissenschaftliche Erkenntnisse", thought_count: 0 },
|
|
||||||
{ id: "technology", name: "Technologie", description: "Technologische Entwicklungen", thought_count: 0 },
|
|
||||||
{ id: "arts", name: "Künste", description: "Künstlerische Ausdrucksformen", thought_count: 0 }
|
|
||||||
];
|
|
||||||
|
|
||||||
const defaultLinks = [
|
|
||||||
{ source: "root", target: "philosophy" },
|
|
||||||
{ source: "root", target: "science" },
|
|
||||||
{ source: "root", target: "technology" },
|
|
||||||
{ source: "root", target: "arts" }
|
|
||||||
];
|
|
||||||
|
|
||||||
// Als Fallback verwenden, falls die API fehlschlägt
|
|
||||||
this.defaultNodes = defaultNodes;
|
|
||||||
this.defaultLinks = defaultLinks;
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
// SVG erstellen, wenn noch nicht vorhanden
|
|
||||||
if (!this.svg) {
|
|
||||||
// Container zuerst leeren
|
|
||||||
this.container.html('');
|
|
||||||
|
|
||||||
this.svg = this.container
|
|
||||||
.append('svg')
|
|
||||||
.attr('width', '100%')
|
|
||||||
.attr('height', this.height)
|
|
||||||
.attr('viewBox', `0 0 ${this.width} ${this.height}`)
|
|
||||||
.attr('class', 'mindmap-svg')
|
|
||||||
.call(
|
|
||||||
d3.zoom()
|
|
||||||
.scaleExtent([0.1, 5])
|
|
||||||
.on('zoom', (event) => {
|
|
||||||
this.handleZoom(event.transform);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Hauptgruppe für alles, was zoom-transformierbar ist
|
|
||||||
this.g = this.svg.append('g');
|
|
||||||
|
|
||||||
// Tooltip initialisieren
|
|
||||||
if (!d3.select('body').select('.node-tooltip').size()) {
|
|
||||||
this.tooltipDiv = d3.select('body')
|
|
||||||
.append('div')
|
|
||||||
.attr('class', 'node-tooltip')
|
|
||||||
.style('opacity', 0)
|
|
||||||
.style('position', 'absolute')
|
|
||||||
.style('pointer-events', 'none')
|
|
||||||
.style('background', 'rgba(20, 20, 40, 0.9)')
|
|
||||||
.style('color', '#ffffff')
|
|
||||||
.style('border', '1px solid rgba(160, 80, 255, 0.2)')
|
|
||||||
.style('border-radius', '6px')
|
|
||||||
.style('padding', '8px 12px')
|
|
||||||
.style('font-size', '14px')
|
|
||||||
.style('max-width', '250px')
|
|
||||||
.style('box-shadow', '0 10px 25px rgba(0, 0, 0, 0.5), 0 0 10px rgba(160, 80, 255, 0.2)');
|
|
||||||
} else {
|
|
||||||
this.tooltipDiv = d3.select('body').select('.node-tooltip');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Force-Simulation initialisieren
|
|
||||||
this.simulation = d3.forceSimulation()
|
|
||||||
.force('link', d3.forceLink().id(d => d.id).distance(this.linkDistance))
|
|
||||||
.force('charge', d3.forceManyBody().strength(this.chargeStrength))
|
|
||||||
.force('center', d3.forceCenter(this.width / 2, this.height / 2).strength(this.centerForce))
|
|
||||||
.force('collision', d3.forceCollide().radius(this.nodeRadius * 2));
|
|
||||||
|
|
||||||
// Globale Mindmap-Instanz für externe Zugriffe setzen
|
|
||||||
window.mindmapInstance = this;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleZoom(transform) {
|
|
||||||
this.g.attr('transform', transform);
|
|
||||||
this.zoomFactor = transform.k;
|
|
||||||
|
|
||||||
// Knotengröße anpassen, um bei Zoom lesbar zu bleiben
|
|
||||||
if (this.nodeElements) {
|
|
||||||
this.nodeElements
|
|
||||||
.attr('r', d => (d === this.selectedNode ? this.selectedNodeRadius : this.nodeRadius) / Math.sqrt(transform.k));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Textgröße anpassen
|
|
||||||
if (this.textElements) {
|
|
||||||
this.textElements
|
|
||||||
.style('font-size', `${12 / Math.sqrt(transform.k)}px`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadData() {
|
|
||||||
try {
|
|
||||||
// Ladeindikator anzeigen
|
|
||||||
this.showLoading();
|
|
||||||
|
|
||||||
// Verwende sofort die Standarddaten für eine schnelle erste Anzeige
|
|
||||||
this.nodes = [...this.defaultNodes];
|
|
||||||
this.links = [...this.defaultLinks];
|
|
||||||
|
|
||||||
// Visualisierung sofort aktualisieren
|
|
||||||
this.isLoading = false;
|
|
||||||
this.updateVisualization();
|
|
||||||
|
|
||||||
// Status auf bereit setzen - don't wait for API
|
|
||||||
this.container.attr('data-status', 'ready');
|
|
||||||
|
|
||||||
// API-Aufruf mit kürzerem Timeout im Hintergrund durchführen
|
|
||||||
try {
|
|
||||||
const controller = new AbortController();
|
|
||||||
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 Sekunden Timeout - reduced from 10
|
|
||||||
|
|
||||||
const response = await fetch('/api/mindmap', {
|
|
||||||
signal: controller.signal,
|
|
||||||
headers: {
|
|
||||||
'Cache-Control': 'no-cache',
|
|
||||||
'Pragma': 'no-cache'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
console.warn(`HTTP Fehler: ${response.status}, verwende Standarddaten`);
|
|
||||||
return; // Keep using default data
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (!data || !data.nodes || data.nodes.length === 0) {
|
|
||||||
console.warn('Keine Mindmap-Daten vorhanden, verwende weiterhin Standard-Daten.');
|
|
||||||
return; // Keep using default data
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flache Liste von Knoten und Verbindungen erstellen
|
|
||||||
this.nodes = [];
|
|
||||||
this.links = [];
|
|
||||||
this.processHierarchicalData(data.nodes);
|
|
||||||
|
|
||||||
// Visualisierung aktualisieren mit den tatsächlichen Daten
|
|
||||||
this.updateVisualization();
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Fehler beim Laden der Mindmap-Daten, verwende Standarddaten:', error);
|
|
||||||
// Already using default data, no action needed
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Kritischer Fehler bei der Mindmap-Darstellung:', error);
|
|
||||||
this.showError('Fehler beim Laden der Mindmap-Daten. Bitte laden Sie die Seite neu.');
|
|
||||||
this.container.attr('data-status', 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
showLoading() {
|
|
||||||
// Element nur leeren, wenn es noch kein SVG enthält
|
|
||||||
if (!this.container.select('svg').size()) {
|
|
||||||
this.container.html(`
|
|
||||||
<div class="flex justify-center items-center h-full">
|
|
||||||
<div class="text-center">
|
|
||||||
<div class="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-primary-400 mx-auto mb-4"></div>
|
|
||||||
<p class="text-lg text-white">Mindmap wird geladen...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
processHierarchicalData(hierarchicalNodes, parentId = null) {
|
|
||||||
hierarchicalNodes.forEach(node => {
|
|
||||||
// Knoten hinzufügen, wenn noch nicht vorhanden
|
|
||||||
if (!this.nodes.find(n => n.id === node.id)) {
|
|
||||||
this.nodes.push({
|
|
||||||
id: node.id,
|
|
||||||
name: node.name,
|
|
||||||
description: node.description || '',
|
|
||||||
thought_count: node.thought_count || 0,
|
|
||||||
color: this.generateColorFromString(node.name),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verbindung zum Elternknoten hinzufügen
|
|
||||||
if (parentId !== null) {
|
|
||||||
this.links.push({
|
|
||||||
source: parentId,
|
|
||||||
target: node.id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rekursiv für Kindknoten aufrufen
|
|
||||||
if (node.children && node.children.length > 0) {
|
|
||||||
this.processHierarchicalData(node.children, node.id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
generateColorFromString(str) {
|
|
||||||
// Erzeugt eine deterministische Farbe basierend auf dem String
|
|
||||||
let hash = 0;
|
|
||||||
for (let i = 0; i < str.length; i++) {
|
|
||||||
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verwende deterministische Farbe aus unserem Farbschema
|
|
||||||
const colors = [
|
|
||||||
'#4080ff', // primary-400
|
|
||||||
'#a040ff', // secondary-400
|
|
||||||
'#205cf5', // primary-500
|
|
||||||
'#8020f5', // secondary-500
|
|
||||||
'#1040e0', // primary-600
|
|
||||||
'#6010e0', // secondary-600
|
|
||||||
];
|
|
||||||
|
|
||||||
return colors[Math.abs(hash) % colors.length];
|
|
||||||
}
|
|
||||||
|
|
||||||
updateVisualization() {
|
|
||||||
// Starte die Visualisierung nur, wenn nicht mehr im Ladezustand
|
|
||||||
if (this.isLoading) return;
|
|
||||||
|
|
||||||
// Container leeren, wenn Diagramm neu erstellt wird
|
|
||||||
if (!this.svg) {
|
|
||||||
this.container.html('');
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Performance-Optimierung: Deaktiviere Transition während des Datenladens
|
|
||||||
const useTransitions = false;
|
|
||||||
|
|
||||||
// Links (Edges) erstellen
|
|
||||||
this.linkElements = this.g.selectAll('.link')
|
|
||||||
.data(this.links)
|
|
||||||
.join(
|
|
||||||
enter => enter.append('line')
|
|
||||||
.attr('class', 'link')
|
|
||||||
.attr('stroke', '#ffffff30')
|
|
||||||
.attr('stroke-width', 2)
|
|
||||||
.attr('stroke-dasharray', d => d.relation_type === 'contradicts' ? '5,5' : null)
|
|
||||||
.attr('marker-end', d => d.relation_type === 'builds_upon' ? 'url(#arrowhead)' : null),
|
|
||||||
update => update
|
|
||||||
.attr('stroke', '#ffffff30')
|
|
||||||
.attr('stroke-dasharray', d => d.relation_type === 'contradicts' ? '5,5' : null)
|
|
||||||
.attr('marker-end', d => d.relation_type === 'builds_upon' ? 'url(#arrowhead)' : null),
|
|
||||||
exit => exit.remove()
|
|
||||||
);
|
|
||||||
|
|
||||||
// Pfeilspitze für gerichtete Beziehungen hinzufügen (falls noch nicht vorhanden)
|
|
||||||
if (!this.svg.select('defs').node()) {
|
|
||||||
const defs = this.svg.append('defs');
|
|
||||||
defs.append('marker')
|
|
||||||
.attr('id', 'arrowhead')
|
|
||||||
.attr('viewBox', '0 -5 10 10')
|
|
||||||
.attr('refX', 20)
|
|
||||||
.attr('refY', 0)
|
|
||||||
.attr('orient', 'auto')
|
|
||||||
.attr('markerWidth', 6)
|
|
||||||
.attr('markerHeight', 6)
|
|
||||||
.append('path')
|
|
||||||
.attr('d', 'M0,-5L10,0L0,5')
|
|
||||||
.attr('fill', '#ffffff50');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simplified Effekte definieren, falls noch nicht vorhanden
|
|
||||||
if (!this.svg.select('#glow').node()) {
|
|
||||||
const defs = this.svg.select('defs').size() ? this.svg.select('defs') : this.svg.append('defs');
|
|
||||||
|
|
||||||
// Glow-Effekt für Knoten
|
|
||||||
const filter = defs.append('filter')
|
|
||||||
.attr('id', 'glow')
|
|
||||||
.attr('x', '-50%')
|
|
||||||
.attr('y', '-50%')
|
|
||||||
.attr('width', '200%')
|
|
||||||
.attr('height', '200%');
|
|
||||||
|
|
||||||
filter.append('feGaussianBlur')
|
|
||||||
.attr('stdDeviation', '1')
|
|
||||||
.attr('result', 'blur');
|
|
||||||
|
|
||||||
filter.append('feComposite')
|
|
||||||
.attr('in', 'SourceGraphic')
|
|
||||||
.attr('in2', 'blur')
|
|
||||||
.attr('operator', 'over');
|
|
||||||
|
|
||||||
// Blur-Effekt für Schatten
|
|
||||||
const blurFilter = defs.append('filter')
|
|
||||||
.attr('id', 'blur')
|
|
||||||
.attr('x', '-50%')
|
|
||||||
.attr('y', '-50%')
|
|
||||||
.attr('width', '200%')
|
|
||||||
.attr('height', '200%');
|
|
||||||
|
|
||||||
blurFilter.append('feGaussianBlur')
|
|
||||||
.attr('stdDeviation', '1');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Knoten-Gruppe erstellen/aktualisieren
|
|
||||||
const nodeGroups = this.g.selectAll('.node-group')
|
|
||||||
.data(this.nodes)
|
|
||||||
.join(
|
|
||||||
enter => {
|
|
||||||
const group = enter.append('g')
|
|
||||||
.attr('class', 'node-group')
|
|
||||||
.call(d3.drag()
|
|
||||||
.on('start', (event, d) => this.dragStarted(event, d))
|
|
||||||
.on('drag', (event, d) => this.dragged(event, d))
|
|
||||||
.on('end', (event, d) => this.dragEnded(event, d)));
|
|
||||||
|
|
||||||
// Hintergrundschatten für besseren Kontrast
|
|
||||||
group.append('circle')
|
|
||||||
.attr('class', 'node-shadow')
|
|
||||||
.attr('r', d => this.nodeRadius * 1.2)
|
|
||||||
.attr('fill', 'rgba(0, 0, 0, 0.3)')
|
|
||||||
.attr('filter', 'url(#blur)');
|
|
||||||
|
|
||||||
// Kreis für jeden Knoten
|
|
||||||
group.append('circle')
|
|
||||||
.attr('class', d => `node ${this.isNodeBookmarked(d.id) ? 'bookmarked' : ''}`)
|
|
||||||
.attr('r', this.nodeRadius)
|
|
||||||
.attr('fill', d => d.color || this.generateColorFromString(d.name))
|
|
||||||
.attr('stroke', d => this.isNodeBookmarked(d.id) ? '#FFD700' : '#ffffff50')
|
|
||||||
.attr('stroke-width', d => this.isNodeBookmarked(d.id) ? 3 : 2)
|
|
||||||
.attr('filter', 'url(#glow)');
|
|
||||||
|
|
||||||
// Text-Label mit besserem Kontrast
|
|
||||||
group.append('text')
|
|
||||||
.attr('class', 'node-label')
|
|
||||||
.attr('dy', '0.35em')
|
|
||||||
.attr('text-anchor', 'middle')
|
|
||||||
.attr('fill', '#ffffff')
|
|
||||||
.attr('stroke', 'rgba(0, 0, 0, 0.4)')
|
|
||||||
.attr('stroke-width', '0.7px')
|
|
||||||
.attr('paint-order', 'stroke')
|
|
||||||
.style('font-size', '12px')
|
|
||||||
.style('font-weight', '500')
|
|
||||||
.style('pointer-events', 'none')
|
|
||||||
.text(d => d.name.length > 12 ? d.name.slice(0, 10) + '...' : d.name);
|
|
||||||
|
|
||||||
// Interaktivität hinzufügen
|
|
||||||
group
|
|
||||||
.on('mouseover', (event, d) => this.nodeMouseover(event, d))
|
|
||||||
.on('mouseout', (event, d) => this.nodeMouseout(event, d))
|
|
||||||
.on('click', (event, d) => this.nodeClicked(event, d));
|
|
||||||
|
|
||||||
return group;
|
|
||||||
},
|
|
||||||
update => {
|
|
||||||
// Knoten aktualisieren
|
|
||||||
update.select('.node')
|
|
||||||
.attr('class', d => `node ${this.isNodeBookmarked(d.id) ? 'bookmarked' : ''}`)
|
|
||||||
.attr('fill', d => d.color || this.generateColorFromString(d.name))
|
|
||||||
.attr('stroke', d => this.isNodeBookmarked(d.id) ? '#FFD700' : '#ffffff50')
|
|
||||||
.attr('stroke-width', d => this.isNodeBookmarked(d.id) ? 3 : 2);
|
|
||||||
|
|
||||||
// Text aktualisieren
|
|
||||||
update.select('.node-label')
|
|
||||||
.text(d => d.name.length > 12 ? d.name.slice(0, 10) + '...' : d.name);
|
|
||||||
|
|
||||||
return update;
|
|
||||||
},
|
|
||||||
exit => exit.remove()
|
|
||||||
);
|
|
||||||
|
|
||||||
// Einzelne Elemente für direkten Zugriff speichern
|
|
||||||
this.nodeElements = this.g.selectAll('.node');
|
|
||||||
this.textElements = this.g.selectAll('.node-label');
|
|
||||||
|
|
||||||
// Performance-Optimierung: Weniger Simulationsschritte für schnellere Stabilisierung
|
|
||||||
this.simulation
|
|
||||||
.nodes(this.nodes)
|
|
||||||
.on('tick', () => this.ticked())
|
|
||||||
.alpha(0.3) // Reduzierter Wert für schnellere Stabilisierung
|
|
||||||
.alphaDecay(0.05); // Erhöhter Wert für schnellere Stabilisierung
|
|
||||||
|
|
||||||
this.simulation.force('link')
|
|
||||||
.links(this.links);
|
|
||||||
|
|
||||||
// Simulation neu starten
|
|
||||||
this.simulation.restart();
|
|
||||||
|
|
||||||
// Update connection counts
|
|
||||||
this.updateConnectionCounts();
|
|
||||||
}
|
|
||||||
|
|
||||||
ticked() {
|
|
||||||
// Linienpositionen aktualisieren
|
|
||||||
this.linkElements
|
|
||||||
.attr('x1', d => d.source.x)
|
|
||||||
.attr('y1', d => d.source.y)
|
|
||||||
.attr('x2', d => d.target.x)
|
|
||||||
.attr('y2', d => d.target.y);
|
|
||||||
|
|
||||||
// Knotenpositionen aktualisieren
|
|
||||||
this.g.selectAll('.node-group')
|
|
||||||
.attr('transform', d => `translate(${d.x}, ${d.y})`);
|
|
||||||
}
|
|
||||||
|
|
||||||
dragStarted(event, d) {
|
|
||||||
if (!event.active) this.simulation.alphaTarget(0.3).restart();
|
|
||||||
d.fx = d.x;
|
|
||||||
d.fy = d.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
dragged(event, d) {
|
|
||||||
d.fx = event.x;
|
|
||||||
d.fy = event.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
dragEnded(event, d) {
|
|
||||||
if (!event.active) this.simulation.alphaTarget(0);
|
|
||||||
d.fx = null;
|
|
||||||
d.fy = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
nodeMouseover(event, d) {
|
|
||||||
this.mouseoverNode = d;
|
|
||||||
|
|
||||||
// Tooltip anzeigen
|
|
||||||
if (this.tooltipEnabled) {
|
|
||||||
const isBookmarked = this.isNodeBookmarked(d.id);
|
|
||||||
const tooltipContent = `
|
|
||||||
<div class="p-2">
|
|
||||||
<strong>${d.name}</strong>
|
|
||||||
${d.description ? `<p class="text-sm text-gray-200 mt-1">${d.description}</p>` : ''}
|
|
||||||
<div class="text-xs text-gray-300 mt-1">
|
|
||||||
Gedanken: ${d.thought_count}
|
|
||||||
</div>
|
|
||||||
<div class="mt-2">
|
|
||||||
<button id="bookmark-button" class="px-2 py-1 text-xs rounded bg-gray-700 hover:bg-gray-600 text-white"
|
|
||||||
data-nodeid="${d.id}">
|
|
||||||
${isBookmarked ? '<i class="fas fa-bookmark mr-1"></i> Gemerkt' : '<i class="far fa-bookmark mr-1"></i> Merken'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
this.tooltipDiv
|
|
||||||
.html(tooltipContent)
|
|
||||||
.style('left', (event.pageX + 10) + 'px')
|
|
||||||
.style('top', (event.pageY - 10) + 'px')
|
|
||||||
.transition()
|
|
||||||
.duration(200)
|
|
||||||
.style('opacity', 1);
|
|
||||||
|
|
||||||
// Event-Listener für den Bookmark-Button hinzufügen
|
|
||||||
document.getElementById('bookmark-button').addEventListener('click', (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
const nodeId = e.currentTarget.getAttribute('data-nodeid');
|
|
||||||
const isNowBookmarked = this.toggleBookmark(nodeId);
|
|
||||||
|
|
||||||
// Button-Text aktualisieren
|
|
||||||
if (isNowBookmarked) {
|
|
||||||
e.currentTarget.innerHTML = '<i class="fas fa-bookmark mr-1"></i> Gemerkt';
|
|
||||||
} else {
|
|
||||||
e.currentTarget.innerHTML = '<i class="far fa-bookmark mr-1"></i> Merken';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Knoten visuell hervorheben
|
|
||||||
d3.select(event.currentTarget).select('circle')
|
|
||||||
.transition()
|
|
||||||
.duration(200)
|
|
||||||
.attr('r', this.nodeRadius * 1.2)
|
|
||||||
.attr('stroke', this.isNodeBookmarked(d.id) ? '#FFD700' : '#ffffff');
|
|
||||||
}
|
|
||||||
|
|
||||||
nodeMouseout(event, d) {
|
|
||||||
this.mouseoverNode = null;
|
|
||||||
|
|
||||||
// Tooltip ausblenden
|
|
||||||
if (this.tooltipEnabled) {
|
|
||||||
this.tooltipDiv
|
|
||||||
.transition()
|
|
||||||
.duration(200)
|
|
||||||
.style('opacity', 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Knoten-Stil zurücksetzen, wenn nicht ausgewählt
|
|
||||||
const nodeElement = d3.select(event.currentTarget).select('circle');
|
|
||||||
if (d !== this.selectedNode) {
|
|
||||||
const isBookmarked = this.isNodeBookmarked(d.id);
|
|
||||||
nodeElement
|
|
||||||
.transition()
|
|
||||||
.duration(200)
|
|
||||||
.attr('r', this.nodeRadius)
|
|
||||||
.attr('stroke', isBookmarked ? '#FFD700' : '#ffffff50')
|
|
||||||
.attr('stroke-width', isBookmarked ? 3 : 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
nodeClicked(event, d) {
|
|
||||||
// Frühere Auswahl zurücksetzen
|
|
||||||
if (this.selectedNode && this.selectedNode !== d) {
|
|
||||||
this.g.selectAll('.node')
|
|
||||||
.filter(n => n === this.selectedNode)
|
|
||||||
.transition()
|
|
||||||
.duration(200)
|
|
||||||
.attr('r', this.nodeRadius)
|
|
||||||
.attr('stroke', '#ffffff50');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Neue Auswahl hervorheben
|
|
||||||
if (this.selectedNode !== d) {
|
|
||||||
this.selectedNode = d;
|
|
||||||
d3.select(event.currentTarget).select('circle')
|
|
||||||
.transition()
|
|
||||||
.duration(200)
|
|
||||||
.attr('r', this.selectedNodeRadius)
|
|
||||||
.attr('stroke', '#ffffff');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Callback mit Node-Daten aufrufen
|
|
||||||
this.onNodeClick(d);
|
|
||||||
}
|
|
||||||
|
|
||||||
showError(message) {
|
|
||||||
this.container.html(`
|
|
||||||
<div class="w-full text-center p-6">
|
|
||||||
<div class="mb-4 text-red-500">
|
|
||||||
<i class="fas fa-exclamation-triangle text-4xl"></i>
|
|
||||||
</div>
|
|
||||||
<p class="text-lg text-gray-200">${message}</p>
|
|
||||||
</div>
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fokussiert die Ansicht auf einen bestimmten Knoten
|
|
||||||
focusNode(nodeId) {
|
|
||||||
const node = this.nodes.find(n => n.id === nodeId);
|
|
||||||
if (!node) return;
|
|
||||||
|
|
||||||
// Simuliere einen Klick auf den Knoten
|
|
||||||
const nodeElement = this.g.selectAll('.node-group')
|
|
||||||
.filter(d => d.id === nodeId);
|
|
||||||
|
|
||||||
nodeElement.dispatch('click');
|
|
||||||
|
|
||||||
// Zentriere den Knoten in der Ansicht
|
|
||||||
const transform = d3.zoomIdentity
|
|
||||||
.translate(this.width / 2, this.height / 2)
|
|
||||||
.scale(1.2)
|
|
||||||
.translate(-node.x, -node.y);
|
|
||||||
|
|
||||||
this.svg.transition()
|
|
||||||
.duration(750)
|
|
||||||
.call(
|
|
||||||
d3.zoom().transform,
|
|
||||||
transform
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filtert die Mindmap basierend auf einem Suchbegriff
|
|
||||||
filterBySearchTerm(searchTerm) {
|
|
||||||
if (!searchTerm || searchTerm.trim() === '') {
|
|
||||||
// Alle Knoten anzeigen
|
|
||||||
this.g.selectAll('.node-group')
|
|
||||||
.style('opacity', 1)
|
|
||||||
.style('pointer-events', 'all');
|
|
||||||
|
|
||||||
this.g.selectAll('.link')
|
|
||||||
.style('opacity', 1);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const searchLower = searchTerm.toLowerCase();
|
|
||||||
const matchingNodes = this.nodes.filter(node =>
|
|
||||||
node.name.toLowerCase().includes(searchLower) ||
|
|
||||||
(node.description && node.description.toLowerCase().includes(searchLower))
|
|
||||||
);
|
|
||||||
|
|
||||||
const matchingNodeIds = new Set(matchingNodes.map(n => n.id));
|
|
||||||
|
|
||||||
// Passende Knoten hervorheben, andere ausblenden
|
|
||||||
this.g.selectAll('.node-group')
|
|
||||||
.style('opacity', d => matchingNodeIds.has(d.id) ? 1 : 0.2)
|
|
||||||
.style('pointer-events', d => matchingNodeIds.has(d.id) ? 'all' : 'none');
|
|
||||||
|
|
||||||
// Verbindungen zwischen passenden Knoten hervorheben
|
|
||||||
this.g.selectAll('.link')
|
|
||||||
.style('opacity', d =>
|
|
||||||
matchingNodeIds.has(d.source.id) && matchingNodeIds.has(d.target.id) ? 1 : 0.1
|
|
||||||
);
|
|
||||||
|
|
||||||
// Auf den ersten passenden Knoten fokussieren, wenn vorhanden
|
|
||||||
if (matchingNodes.length > 0) {
|
|
||||||
this.focusNode(matchingNodes[0].id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the thought_count property for each node based on existing connections
|
|
||||||
*/
|
|
||||||
updateConnectionCounts() {
|
|
||||||
// Reset all counts first
|
|
||||||
this.nodes.forEach(node => {
|
|
||||||
// Initialize thought_count if it doesn't exist
|
|
||||||
if (typeof node.thought_count !== 'number') {
|
|
||||||
node.thought_count = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Count connections for this node
|
|
||||||
const connectedNodes = this.getConnectedNodes(node);
|
|
||||||
node.thought_count = connectedNodes.length;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update UI to show counts
|
|
||||||
this.updateNodeLabels();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the visual representation of node labels to include connection counts
|
|
||||||
*/
|
|
||||||
updateNodeLabels() {
|
|
||||||
if (!this.textElements) return;
|
|
||||||
|
|
||||||
this.textElements.text(d => {
|
|
||||||
if (d.thought_count > 0) {
|
|
||||||
return `${d.name} (${d.thought_count})`;
|
|
||||||
}
|
|
||||||
return d.name;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a new connection between nodes and updates the counts
|
|
||||||
*/
|
|
||||||
addConnection(sourceNode, targetNode) {
|
|
||||||
if (!sourceNode || !targetNode) return false;
|
|
||||||
|
|
||||||
// Check if connection already exists
|
|
||||||
if (this.isConnected(sourceNode, targetNode)) return false;
|
|
||||||
|
|
||||||
// Add new connection
|
|
||||||
this.links.push({
|
|
||||||
source: sourceNode.id,
|
|
||||||
target: targetNode.id
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update counts
|
|
||||||
this.updateConnectionCounts();
|
|
||||||
|
|
||||||
// Update visualization
|
|
||||||
this.updateVisualization();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lädt gemerkete Knoten aus dem LocalStorage
|
|
||||||
loadBookmarkedNodes() {
|
|
||||||
try {
|
|
||||||
const bookmarked = localStorage.getItem('bookmarkedNodes');
|
|
||||||
return bookmarked ? JSON.parse(bookmarked) : [];
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Fehler beim Laden der gemerkten Knoten:', error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Speichert gemerkete Knoten im LocalStorage
|
|
||||||
saveBookmarkedNodes() {
|
|
||||||
try {
|
|
||||||
localStorage.setItem('bookmarkedNodes', JSON.stringify(this.bookmarkedNodes));
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Fehler beim Speichern der gemerkten Knoten:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prüft, ob ein Knoten gemerkt ist
|
|
||||||
isNodeBookmarked(nodeId) {
|
|
||||||
return this.bookmarkedNodes.includes(nodeId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merkt einen Knoten oder hebt die Markierung auf
|
|
||||||
toggleBookmark(nodeId) {
|
|
||||||
const index = this.bookmarkedNodes.indexOf(nodeId);
|
|
||||||
if (index === -1) {
|
|
||||||
// Node hinzufügen
|
|
||||||
this.bookmarkedNodes.push(nodeId);
|
|
||||||
this.updateNodeAppearance(nodeId, true);
|
|
||||||
} else {
|
|
||||||
// Node entfernen
|
|
||||||
this.bookmarkedNodes.splice(index, 1);
|
|
||||||
this.updateNodeAppearance(nodeId, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Änderungen speichern
|
|
||||||
this.saveBookmarkedNodes();
|
|
||||||
|
|
||||||
// Event auslösen für andere Komponenten
|
|
||||||
const event = new CustomEvent('nodeBookmarkToggled', {
|
|
||||||
detail: {
|
|
||||||
nodeId: nodeId,
|
|
||||||
isBookmarked: index === -1
|
|
||||||
}
|
|
||||||
});
|
|
||||||
document.dispatchEvent(event);
|
|
||||||
|
|
||||||
return index === -1; // true wenn jetzt gemerkt, false wenn Markierung aufgehoben
|
|
||||||
}
|
|
||||||
|
|
||||||
// Aktualisiert das Aussehen eines Knotens basierend auf Bookmark-Status
|
|
||||||
updateNodeAppearance(nodeId, isBookmarked) {
|
|
||||||
this.g.selectAll('.node-group')
|
|
||||||
.filter(d => d.id === nodeId)
|
|
||||||
.select('.node')
|
|
||||||
.classed('bookmarked', isBookmarked)
|
|
||||||
.attr('stroke', isBookmarked ? '#FFD700' : '#ffffff50')
|
|
||||||
.attr('stroke-width', isBookmarked ? 3 : 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Aktualisiert das Aussehen aller gemerkten Knoten
|
|
||||||
updateAllBookmarkedNodes() {
|
|
||||||
this.g.selectAll('.node-group')
|
|
||||||
.each((d) => {
|
|
||||||
const isBookmarked = this.isNodeBookmarked(d.id);
|
|
||||||
this.updateNodeAppearance(d.id, isBookmarked);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exportiere die Klasse für die Verwendung in anderen Modulen
|
|
||||||
window.MindMapVisualization = MindMapVisualization;
|
|
||||||
1133
static/js/social.js
Normal file
1133
static/js/social.js
Normal file
File diff suppressed because it is too large
Load Diff
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>
|
<title>Systades - {% block title %}{% endblock %}</title>
|
||||||
|
|
||||||
<!-- Favicon -->
|
<!-- Favicon -->
|
||||||
<link rel="icon" href="{{ url_for('static', filename='img/favicon.svg') }}" type="image/svg+xml">
|
<link rel="icon" href="{{ url_for('static', filename='img/neuron-favicon.svg') }}" type="image/svg+xml">
|
||||||
<link rel="icon" href="{{ url_for('static', filename='img/favicon.ico') }}" sizes="any">
|
|
||||||
|
|
||||||
<!-- Meta Tags -->
|
<!-- Meta Tags -->
|
||||||
<meta name="description" content="Eine interaktive Plattform zum Visualisieren, Erforschen und Teilen von Wissen">
|
<meta name="description" content="Eine interaktive Plattform zum Visualisieren, Erforschen und Teilen von Wissen">
|
||||||
<meta name="keywords" content="systades, wissen, visualisierung, lernen, gedanken, theorie">
|
<meta name="keywords" content="systades, wissen, visualisierung, lernen, gedanken, theorie">
|
||||||
<meta name="author" content="Systades-Team">
|
<meta name="author" content="Systades-Team">
|
||||||
|
|
||||||
<!-- Tailwind CSS über CDN -->
|
<!-- Tailwind CSS - CDN für Entwicklung und Produktion (laut Vorgabe) -->
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<!-- Alternative lokale Version, falls die CDN-Version blockiert wird -->
|
||||||
<script>
|
<script>
|
||||||
|
tailwind = window.tailwind || {};
|
||||||
tailwind.config = {
|
tailwind.config = {
|
||||||
darkMode: 'class',
|
darkMode: 'class',
|
||||||
theme: {
|
theme: {
|
||||||
@@ -57,23 +58,35 @@
|
|||||||
800: '#0e1220',
|
800: '#0e1220',
|
||||||
900: '#0a0e19'
|
900: '#0a0e19'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
keyframes: {
|
||||||
|
float: {
|
||||||
|
'0%, 100%': { transform: 'translateY(0)' },
|
||||||
|
'50%': { transform: 'translateY(-5px)' }
|
||||||
|
},
|
||||||
|
'bounce-slow': {
|
||||||
|
'0%, 100%': { transform: 'translateY(0)' },
|
||||||
|
'50%': { transform: 'translateY(-8px)' }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
float: 'float 3s ease-in-out infinite',
|
||||||
|
'bounce-slow': 'bounce-slow 2s ease-in-out infinite'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Fonts -->
|
<!-- Local Font Files -->
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link href="{{ url_for('static', filename='fonts/inter.css') }}" rel="stylesheet">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link href="{{ url_for('static', filename='fonts/jetbrains-mono.css') }}" rel="stylesheet">
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
|
|
||||||
|
|
||||||
<!-- Icons -->
|
<!-- Font Awesome vom CDN -->
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" rel="stylesheet">
|
||||||
|
|
||||||
<!-- Assistent CSS -->
|
<!-- Assistent CSS -->
|
||||||
<link href="{{ url_for('static', filename='css/assistant.css') }}" rel="stylesheet">
|
<link href="{{ url_for('static', filename='css/assistant.css') }}" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/src/cybernetwork-bg.css') }}">
|
|
||||||
|
|
||||||
<!-- Basis-Stylesheet -->
|
<!-- Basis-Stylesheet -->
|
||||||
<link href="{{ url_for('static', filename='style.css') }}" rel="stylesheet">
|
<link href="{{ url_for('static', filename='style.css') }}" rel="stylesheet">
|
||||||
@@ -81,101 +94,297 @@
|
|||||||
<!-- Base-Styles ausgelagert in eigene Datei -->
|
<!-- Base-Styles ausgelagert in eigene Datei -->
|
||||||
<link href="{{ url_for('static', filename='css/base-styles.css') }}" rel="stylesheet">
|
<link href="{{ url_for('static', filename='css/base-styles.css') }}" rel="stylesheet">
|
||||||
|
|
||||||
<!-- Alpine.js -->
|
<!-- Alpine.js - CDN Version -->
|
||||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.12.3/dist/cdn.min.js"></script>
|
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.12.3/dist/cdn.min.js"></script>
|
||||||
|
|
||||||
<!-- Network Background Script -->
|
<!-- Neural Network Background CSS -->
|
||||||
<script src="{{ url_for('static', filename='network-background.js') }}"></script>
|
<link href="{{ url_for('static', filename='css/neural-network-background.css') }}" rel="stylesheet">
|
||||||
|
|
||||||
<!-- Hauptmodul laden (als ES6 Modul) -->
|
<!-- Mindmap CSS -->
|
||||||
<script type="module">
|
<link href="{{ url_for('static', filename='css/mindmap.css', v='1.0.1') }}" rel="stylesheet">
|
||||||
import MindMap from "{{ url_for('static', filename='js/main.js') }}";
|
|
||||||
// Alpine.js-Integration
|
|
||||||
document.addEventListener('alpine:init', () => {
|
|
||||||
Alpine.data('layout', () => ({
|
|
||||||
darkMode: false,
|
|
||||||
mobileMenuOpen: false,
|
|
||||||
userMenuOpen: false,
|
|
||||||
showSettingsModal: false,
|
|
||||||
|
|
||||||
init() {
|
<!-- D3.js für Visualisierungen -->
|
||||||
this.fetchDarkModeFromSession();
|
<script src="https://d3js.org/d3.v7.min.js"></script>
|
||||||
},
|
|
||||||
|
|
||||||
fetchDarkModeFromSession() {
|
<!-- Marked.js für Markdown-Parsing -->
|
||||||
// Lade den Dark Mode-Status vom Server
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||||
fetch('/get_dark_mode')
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
if (data.success) {
|
|
||||||
this.darkMode = data.darkMode === 'true';
|
|
||||||
document.querySelector('html').classList.toggle('dark', this.darkMode);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Fehler beim Laden der Dark Mode-Einstellung:', error);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
toggleDarkMode() {
|
<!-- ChatGPT Assistant -->
|
||||||
this.darkMode = !this.darkMode;
|
<script src="{{ url_for('static', filename='js/modules/chatgpt-assistant.js') }}"></script>
|
||||||
document.querySelector('html').classList.toggle('dark', this.darkMode);
|
|
||||||
|
|
||||||
// Speichere den Dark Mode-Status auf dem Server
|
<!-- Neural Network Background Script -->
|
||||||
fetch('/set_dark_mode', {
|
<script src="{{ url_for('static', filename='neural-network-background.js') }}"></script>
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ darkMode: this.darkMode })
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
if (data.success) {
|
|
||||||
// Zusätzlich im localStorage speichern für sofortige Reaktion bei Seitenwechsel
|
|
||||||
localStorage.setItem('darkMode', this.darkMode ? 'dark' : 'light');
|
|
||||||
// Event auslösen für andere Komponenten
|
|
||||||
document.dispatchEvent(new CustomEvent('darkModeToggled', {
|
|
||||||
detail: { isDark: this.darkMode }
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
console.error('Fehler beim Speichern der Dark Mode-Einstellung:', data.error);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Fehler beim Speichern der Dark Mode-Einstellung:', error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
// MindMap global verfügbar machen (für Alpine.js und andere nicht-Module Skripte)
|
<!-- Hauptmodul laden (als traditionelles Skript) -->
|
||||||
window.MindMap = MindMap;
|
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Seitenspezifische Styles -->
|
<!-- Seitenspezifische Styles -->
|
||||||
{% block extra_css %}{% endblock %}
|
{% block extra_css %}{% endblock %}
|
||||||
|
|
||||||
<!-- Cybertechnisches Netzwerk-Hintergrund -->
|
<!-- Custom dark/light mode styles -->
|
||||||
<script type="module" src="{{ url_for('static', filename='js/modules/cyber-network-init.js') }}"></script>
|
<!-- ► ► Farb‑Token strikt getrennt ◄ ◄ -->
|
||||||
|
<style>
|
||||||
|
/* Light‑Mode */
|
||||||
|
:root {
|
||||||
|
--bg-primary:#f8fafc;
|
||||||
|
--bg-secondary:#f1f5f9;
|
||||||
|
--text-primary:#232837;
|
||||||
|
--text-secondary:#475569;
|
||||||
|
--accent-primary:#7c3aed;
|
||||||
|
--accent-secondary:#8b5cf6;
|
||||||
|
--glow-effect:0 0 8px rgba(139,92,246,.08);
|
||||||
|
background-image: linear-gradient(to bottom right, rgba(248, 250, 252, 0.8), rgba(241, 245, 249, 0.8));
|
||||||
|
background-attachment: fixed;
|
||||||
|
}
|
||||||
|
/* Dark‑Mode */
|
||||||
|
.dark {
|
||||||
|
--bg-primary:#181c24;
|
||||||
|
--bg-secondary:#232837;
|
||||||
|
--text-primary:#f9fafb;
|
||||||
|
--text-secondary:#e5e7eb;
|
||||||
|
--accent-primary:#6d28d9;
|
||||||
|
--accent-secondary:#8b5cf6;
|
||||||
|
--glow-effect:0 0 8px rgba(124,58,237,.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
@apply min-h-screen bg-[color:var(--bg-primary)] text-[color:var(--text-primary)];
|
||||||
|
transition: background-color 0.5s ease-in-out, color 0.3s ease-in-out, background-image 0.5s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Utilities */
|
||||||
|
.mystical-glow { text-shadow: var(--glow-effect); }
|
||||||
|
.gradient-text {
|
||||||
|
background:linear-gradient(135deg,var(--accent-primary),var(--accent-secondary));
|
||||||
|
-webkit-background-clip:text; background-clip:text; color:transparent; text-shadow:none;
|
||||||
|
}
|
||||||
|
.glass-morphism { backdrop-filter: blur(10px); }
|
||||||
|
.glass-navbar { @apply glass-morphism border backdrop-blur-xl; }
|
||||||
|
.light .glass-navbar { background-color:rgba(255,255,255,.8); border-color:rgba(0,0,0,.05); }
|
||||||
|
.dark .glass-navbar { background-color:rgba(10,14,25,.8); border-color:rgba(255,255,255,.05); }
|
||||||
|
|
||||||
|
/* Light-Mode spezifische Stile */
|
||||||
|
body:not(.dark) {
|
||||||
|
background-color: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link-light {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link-light:hover {
|
||||||
|
color: var(--text-primary);
|
||||||
|
background-color: rgba(126, 34, 206, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link-light-active {
|
||||||
|
color: var(--accent-primary);
|
||||||
|
background-color: rgba(126, 34, 206, 0.15);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Kartendesign im Light-Mode */
|
||||||
|
body:not(.dark) .card {
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .card:hover {
|
||||||
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: var(--light-primary-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light Mode Buttons */
|
||||||
|
body:not(.dark) .btn,
|
||||||
|
body:not(.dark) button:not(.toggle) {
|
||||||
|
background: linear-gradient(135deg, #7c3aed, #6d28d9);
|
||||||
|
color: white !important;
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 2px 4px rgba(124, 58, 237, 0.25);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.625rem 1.25rem;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .btn:hover,
|
||||||
|
body:not(.dark) button:not(.toggle):hover {
|
||||||
|
background: linear-gradient(135deg, #8b5cf6, #7c3aed);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(124, 58, 237, 0.3);
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* KI-Chat Button im Light-Mode */
|
||||||
|
body:not(.dark) [onclick*="MindMap.assistant.toggleAssistant"] {
|
||||||
|
background: linear-gradient(135deg, #7c3aed, #4f46e5);
|
||||||
|
color: white !important;
|
||||||
|
font-weight: 500;
|
||||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) [onclick*="MindMap.assistant.toggleAssistant"]:hover {
|
||||||
|
background: linear-gradient(135deg, #8b5cf6, #6366f1);
|
||||||
|
box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style improvements for the theme toggle button */
|
||||||
|
.theme-toggle {
|
||||||
|
position: relative;
|
||||||
|
width: 48px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 24px;
|
||||||
|
padding: 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark .theme-toggle {
|
||||||
|
background: linear-gradient(to right, #7c3aed, #3b82f6);
|
||||||
|
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.3), 0 0 10px rgba(124, 58, 237, 0.3);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .theme-toggle {
|
||||||
|
background: linear-gradient(to right, #8b5cf6, #60a5fa);
|
||||||
|
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1), 0 0 10px rgba(124, 58, 237, 0.15);
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
top: 2px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark .theme-toggle::after {
|
||||||
|
background: #f1f5f9 url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%237c3aed' width='14' height='14'%3E%3Cpath d='M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z'%3E%3C/path%3E%3C/svg%3E") no-repeat center center;
|
||||||
|
transform: translateX(24px);
|
||||||
|
box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .theme-toggle::after {
|
||||||
|
background: #fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23f59e0b' width='14' height='14'%3E%3Cpath d='M12 2.25a.75.75 0 01.75.75v2.25a.75.75 0 01-1.5 0V3a.75.75 0 01.75-.75zM7.5 12a4.5 4.5 0 119 0 4.5 4.5 0 01-9 0zM18.894 6.166a.75.75 0 00-1.06-1.06l-1.591 1.59a.75.75 0 101.06 1.061l1.591-1.59zM21.75 12a.75.75 0 01-.75.75h-2.25a.75.75 0 010-1.5H21a.75.75 0 01.75.75zM17.834 18.894a.75.75 0 001.06-1.06l-1.59-1.591a.75.75 0 10-1.061 1.06l1.59 1.591zM12 18a.75.75 0 01.75.75V21a.75.75 0 01-1.5 0v-2.25A.75.75 0 0112 18zM7.758 17.303a.75.75 0 00-1.061-1.06l-1.591 1.59a.75.75 0 001.06 1.061l1.591-1.59zM6 12a.75.75 0 01-.75.75H3a.75.75 0 010-1.5h2.25A.75.75 0 016 12zM6.697 7.757a.75.75 0 001.06-1.06l-1.59-1.591a.75.75 0 00-1.061 1.06l1.59 1.591z'%3E%3C/path%3E%3C/svg%3E") no-repeat center center;
|
||||||
|
transform: translateX(2px);
|
||||||
|
box-shadow: 0 0 8px rgba(124, 58, 237, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle:hover::after {
|
||||||
|
box-shadow: 0 0 12px rgba(124, 58, 237, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fixes for light mode button text colors */
|
||||||
|
body:not(.dark) .btn-primary {
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix for KI-Chat container */
|
||||||
|
#chatgpt-assistant {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 1.5rem;
|
||||||
|
right: 1.5rem;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-assistant {
|
||||||
|
max-height: 80vh !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-assistant .chat-messages {
|
||||||
|
max-height: calc(80vh - 160px) !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body data-page="{{ request.endpoint }}" class="relative overflow-x-hidden">
|
<body data-page="{{ request.endpoint }}" class="relative overflow-x-hidden dark bg-gray-900 text-white" x-data="{
|
||||||
<!-- Cybertechnisches Netzwerk-Hintergrund Container (wird via JavaScript befüllt) -->
|
darkMode: true,
|
||||||
<div id="cyber-background-container" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: -1; pointer-events: none; overflow: hidden;"></div>
|
mobileMenuOpen: false,
|
||||||
|
userMenuOpen: false,
|
||||||
|
showSettingsModal: false,
|
||||||
|
|
||||||
<!-- Globaler Hintergrund -->
|
init() {
|
||||||
<div class="full-page-bg"></div>
|
this.initDarkMode();
|
||||||
<!-- Statischer Fallback-Hintergrund (wird nur angezeigt, wenn JavaScript deaktiviert ist) -->
|
},
|
||||||
<div class="fixed inset-0 z-[-9] bg-cover bg-center opacity-50"></div>
|
|
||||||
|
|
||||||
|
initDarkMode() {
|
||||||
|
// Lade zuerst den Wert aus dem localStorage (client-seitig)
|
||||||
|
const storedMode = localStorage.getItem('colorMode');
|
||||||
|
if (storedMode) {
|
||||||
|
this.darkMode = storedMode === 'dark';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dann hole die Server-Einstellung, die Vorrang hat
|
||||||
|
this.fetchDarkModeFromSession();
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchDarkModeFromSession() {
|
||||||
|
fetch('/api/get_dark_mode')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
this.darkMode = data.darkMode === 'true';
|
||||||
|
this.applyDarkMode();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Fehler beim Laden der Dark Mode-Einstellung:', error);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
applyDarkMode() {
|
||||||
|
document.querySelector('html').classList.toggle('dark', this.darkMode);
|
||||||
|
document.querySelector('body').classList.toggle('dark', this.darkMode);
|
||||||
|
localStorage.setItem('colorMode', this.darkMode ? 'dark' : 'light');
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleDarkMode() {
|
||||||
|
this.darkMode = !this.darkMode;
|
||||||
|
this.applyDarkMode();
|
||||||
|
|
||||||
|
// Server über Änderung informieren
|
||||||
|
fetch('/api/set_dark_mode', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ darkMode: this.darkMode })
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
// Event auslösen für andere Komponenten
|
||||||
|
document.dispatchEvent(new CustomEvent('darkModeToggled', {
|
||||||
|
detail: { isDark: this.darkMode }
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Fehler beim Speichern der Dark Mode-Einstellung:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}">
|
||||||
<!-- App-Container -->
|
<!-- App-Container -->
|
||||||
<div id="app-container" class="flex flex-col min-h-screen" x-data="layout">
|
<div id="app-container" class="flex flex-col min-h-screen">
|
||||||
<!-- Hauptnavigation -->
|
<!-- Hauptnavigation -->
|
||||||
<nav class="sticky top-0 left-0 right-0 z-50 transition-all duration-300 py-4 px-5 border-b glass-morphism"
|
<nav class="sticky top-0 left-0 right-0 z-50 transition-all duration-300 py-4 px-5 border-b glass-morphism"
|
||||||
x-bind:class="darkMode ? 'glass-navbar-dark border-white/10' : 'glass-navbar-light border-gray-200/50'">
|
x-bind:class="darkMode ? 'glass-navbar-dark border-white/10' : 'glass-navbar-light border-gray-200/50'">
|
||||||
<div class="container mx-auto flex justify-between items-center">
|
<div class="container mx-auto flex justify-between items-center">
|
||||||
<!-- Logo -->
|
<!-- Logo -->
|
||||||
<a href="{{ url_for('index') }}" class="flex items-center group">
|
<a href="{{ url_for('index') }}" class="flex items-center group">
|
||||||
|
<img src="{{ url_for('static', filename='img/neuron-logo.svg') }}" alt="Systades Logo" class="w-8 h-8 mr-2 transform transition-transform group-hover:scale-110">
|
||||||
<span class="text-2xl font-bold gradient-text transform transition-transform group-hover:scale-105">Systades</span>
|
<span class="text-2xl font-bold gradient-text transform transition-transform group-hover:scale-105">Systades</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
@@ -195,6 +404,22 @@
|
|||||||
: '{{ 'nav-link-light-active' if request.endpoint == 'mindmap' else 'nav-link-light' }}'">
|
: '{{ 'nav-link-light-active' if request.endpoint == 'mindmap' else 'nav-link-light' }}'">
|
||||||
<i class="fa-solid fa-diagram-project mr-2"></i>Mindmap
|
<i class="fa-solid fa-diagram-project mr-2"></i>Mindmap
|
||||||
</a>
|
</a>
|
||||||
|
{% if current_user.is_authenticated %}
|
||||||
|
<a href="{{ url_for('social_feed') }}"
|
||||||
|
class="nav-link flex items-center"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? '{{ 'nav-link-active' if request.endpoint == 'social_feed' else '' }}'
|
||||||
|
: '{{ 'nav-link-light-active' if request.endpoint == 'social_feed' else 'nav-link-light' }}'">
|
||||||
|
<i class="fa-solid fa-home mr-2"></i>Feed
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('discover') }}"
|
||||||
|
class="nav-link flex items-center"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? '{{ 'nav-link-active' if request.endpoint == 'discover' else '' }}'
|
||||||
|
: '{{ 'nav-link-light-active' if request.endpoint == 'discover' else 'nav-link-light' }}'">
|
||||||
|
<i class="fa-solid fa-compass mr-2"></i>Entdecken
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
<a href="{{ url_for('search_thoughts_page') }}"
|
<a href="{{ url_for('search_thoughts_page') }}"
|
||||||
class="nav-link flex items-center"
|
class="nav-link flex items-center"
|
||||||
x-bind:class="darkMode
|
x-bind:class="darkMode
|
||||||
@@ -206,8 +431,8 @@
|
|||||||
<button onclick="window.MindMap && window.MindMap.assistant && window.MindMap.assistant.toggleAssistant(true)"
|
<button onclick="window.MindMap && window.MindMap.assistant && window.MindMap.assistant.toggleAssistant(true)"
|
||||||
class="nav-link flex items-center"
|
class="nav-link flex items-center"
|
||||||
x-bind:class="darkMode
|
x-bind:class="darkMode
|
||||||
? 'bg-gradient-to-r from-purple-600/80 to-blue-500/80 text-white font-medium px-4 py-2 rounded-xl hover:shadow-lg transition-all duration-300 hover:-translate-y-0.5'
|
? 'bg-gradient-to-r from-purple-900/90 to-indigo-800/90 text-white font-medium px-4 py-2 rounded-xl hover:shadow-lg hover:shadow-purple-800/30 transition-all duration-300'
|
||||||
: 'bg-gradient-to-r from-purple-500/20 to-blue-400/20 text-gray-800 font-medium px-4 py-2 rounded-xl hover:shadow-md transition-all duration-300 hover:-translate-y-0.5'">
|
: 'bg-gradient-to-r from-purple-600 to-indigo-500 text-white font-medium px-4 py-2 rounded-xl hover:shadow-md transition-all duration-300'">
|
||||||
<i class="fa-solid fa-robot mr-2"></i>KI-Chat
|
<i class="fa-solid fa-robot mr-2"></i>KI-Chat
|
||||||
</button>
|
</button>
|
||||||
{% if current_user.is_authenticated %}
|
{% if current_user.is_authenticated %}
|
||||||
@@ -223,25 +448,14 @@
|
|||||||
|
|
||||||
<!-- Rechte Seite -->
|
<!-- Rechte Seite -->
|
||||||
<div class="flex items-center space-x-4">
|
<div class="flex items-center space-x-4">
|
||||||
<!-- Dark Mode Toggle Switch -->
|
<!-- Dark/Light Mode Schalter -->
|
||||||
<div class="flex items-center cursor-pointer" @click="toggleDarkMode">
|
<button
|
||||||
<div class="relative w-12 h-6">
|
@click="toggleDarkMode()"
|
||||||
<input type="checkbox" id="darkModeToggle" class="sr-only" x-model="darkMode">
|
class="theme-toggle relative w-12 h-6 rounded-full transition-all duration-300 flex items-center overflow-hidden"
|
||||||
<div class="block w-12 h-6 rounded-full transition-colors duration-300"
|
aria-label="Dark Mode umschalten"
|
||||||
x-bind:class="darkMode ? 'bg-blue-400/50' : 'bg-gray-400/50'"></div>
|
>
|
||||||
<div class="dot absolute left-1 top-1 w-4 h-4 rounded-full transition-transform duration-300 shadow-md"
|
<span class="sr-only" x-text="darkMode ? 'Zum Light Mode wechseln' : 'Zum Dark Mode wechseln'"></span>
|
||||||
x-bind:class="darkMode ? 'bg-blue-500 transform translate-x-6' : 'bg-white'"></div>
|
</button>
|
||||||
</div>
|
|
||||||
<div class="ml-3 hidden sm:block"
|
|
||||||
x-bind:class="darkMode ? 'text-white/90' : 'text-gray-700'">
|
|
||||||
<span x-text="darkMode ? 'Dunkel' : 'Hell'"></span>
|
|
||||||
</div>
|
|
||||||
<div class="ml-2 sm:hidden"
|
|
||||||
x-bind:class="darkMode ? 'text-white/90' : 'text-gray-700'">
|
|
||||||
<i class="fa-solid" :class="darkMode ? 'fa-sun' : 'fa-moon'"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Profil-Link oder Login -->
|
<!-- Profil-Link oder Login -->
|
||||||
{% if current_user.is_authenticated %}
|
{% if current_user.is_authenticated %}
|
||||||
<div class="relative" x-data="{ open: false }">
|
<div class="relative" x-data="{ open: false }">
|
||||||
@@ -255,12 +469,21 @@
|
|||||||
{% if current_user.avatar %}
|
{% if current_user.avatar %}
|
||||||
<img src="{{ current_user.avatar }}" alt="{{ current_user.username }}" class="w-full h-full object-cover">
|
<img src="{{ current_user.avatar }}" alt="{{ current_user.username }}" class="w-full h-full object-cover">
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ current_user.username[0].upper() }}
|
<svg width="100%" height="100%" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="100" cy="100" r="98" fill="url(#user-gradient)" stroke="#7C3AED" stroke-width="4"/>
|
||||||
|
<circle cx="100" cy="80" r="36" fill="white"/>
|
||||||
|
<path d="M100 140C77.9086 140 60 157.909 60 180H140C140 157.909 122.091 140 100 140Z" fill="white"/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="user-gradient" x1="0" y1="0" x2="200" y2="200" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stop-color="#8B5CF6"/>
|
||||||
|
<stop offset="1" stop-color="#3B82F6"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<span class="text-sm hidden lg:block">{{ current_user.username }}</span>
|
<span class="hidden md:block">{{ current_user.username }}</span>
|
||||||
<i class="fa-solid fa-chevron-down text-xs hidden lg:block transition-transform duration-200"
|
<i class="fas fa-chevron-down text-xs opacity-60 ml-1.5"></i>
|
||||||
x-bind:class="open ? 'transform rotate-180' : ''"></i>
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Dropdown-Menü -->
|
<!-- Dropdown-Menü -->
|
||||||
@@ -308,13 +531,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{{ url_for('login') }}"
|
<div class="flex items-center space-x-2">
|
||||||
class="flex items-center px-4 py-2.5 rounded-xl font-medium transition-all duration-300"
|
<a href="{{ url_for('login') }}"
|
||||||
x-bind:class="darkMode
|
class="py-2 px-4 rounded-lg transition-all duration-300"
|
||||||
? 'bg-gray-800/80 text-white hover:bg-gray-700/80 shadow-md hover:shadow-lg hover:-translate-y-0.5'
|
x-bind:class="darkMode
|
||||||
: 'bg-gray-200/80 text-gray-800 hover:bg-gray-300/80 shadow-sm hover:shadow-md hover:-translate-y-0.5'">
|
? 'text-white/90 hover:bg-dark-700/80'
|
||||||
<i class="fa-solid fa-user mr-2"></i>Mein Konto
|
: 'text-gray-700 hover:bg-gray-100/80'">
|
||||||
</a>
|
<i class="fa-solid fa-sign-in-alt mr-2"></i>Login
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('register') }}"
|
||||||
|
class="py-2 px-4 rounded-lg transition-all duration-300 font-medium"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-purple-800/80 text-white hover:bg-purple-700/80'
|
||||||
|
: 'bg-purple-600/20 text-gray-700 hover:bg-purple-600/30'">
|
||||||
|
Registrieren
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Mobilmenü-Button -->
|
<!-- Mobilmenü-Button -->
|
||||||
@@ -331,6 +563,7 @@
|
|||||||
|
|
||||||
<!-- Mobile Menü -->
|
<!-- Mobile Menü -->
|
||||||
<div x-show="mobileMenuOpen"
|
<div x-show="mobileMenuOpen"
|
||||||
|
x-cloak
|
||||||
x-transition:enter="transition ease-out duration-200"
|
x-transition:enter="transition ease-out duration-200"
|
||||||
x-transition:enter-start="opacity-0 -translate-y-4"
|
x-transition:enter-start="opacity-0 -translate-y-4"
|
||||||
x-transition:enter-end="opacity-100 translate-y-0"
|
x-transition:enter-end="opacity-100 translate-y-0"
|
||||||
@@ -356,6 +589,22 @@
|
|||||||
: '{{ 'bg-purple-500/10 text-gray-900' if request.endpoint == 'mindmap' else 'text-gray-700 hover:bg-gray-100 hover:text-gray-900' }}'">
|
: '{{ 'bg-purple-500/10 text-gray-900' if request.endpoint == 'mindmap' else 'text-gray-700 hover:bg-gray-100 hover:text-gray-900' }}'">
|
||||||
<i class="fa-solid fa-diagram-project w-5 mr-3"></i>Mindmap
|
<i class="fa-solid fa-diagram-project w-5 mr-3"></i>Mindmap
|
||||||
</a>
|
</a>
|
||||||
|
{% if current_user.is_authenticated %}
|
||||||
|
<a href="{{ url_for('social_feed') }}"
|
||||||
|
class="block py-3.5 px-4 rounded-xl transition-all duration-200 flex items-center"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? '{{ 'bg-purple-500/20 text-white' if request.endpoint == 'social_feed' else 'text-white/80 hover:bg-gray-800/80 hover:text-white' }}'
|
||||||
|
: '{{ 'bg-purple-500/10 text-gray-900' if request.endpoint == 'social_feed' else 'text-gray-700 hover:bg-gray-100 hover:text-gray-900' }}'">
|
||||||
|
<i class="fa-solid fa-home w-5 mr-3"></i>Feed
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('discover') }}"
|
||||||
|
class="block py-3.5 px-4 rounded-xl transition-all duration-200 flex items-center"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? '{{ 'bg-purple-500/20 text-white' if request.endpoint == 'discover' else 'text-white/80 hover:bg-gray-800/80 hover:text-white' }}'
|
||||||
|
: '{{ 'bg-purple-500/10 text-gray-900' if request.endpoint == 'discover' else 'text-gray-700 hover:bg-gray-100 hover:text-gray-900' }}'">
|
||||||
|
<i class="fa-solid fa-compass w-5 mr-3"></i>Entdecken
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
<a href="{{ url_for('search_thoughts_page') }}"
|
<a href="{{ url_for('search_thoughts_page') }}"
|
||||||
class="block py-3.5 px-4 rounded-xl transition-all duration-200 flex items-center"
|
class="block py-3.5 px-4 rounded-xl transition-all duration-200 flex items-center"
|
||||||
x-bind:class="darkMode
|
x-bind:class="darkMode
|
||||||
@@ -368,7 +617,7 @@
|
|||||||
class="block w-full text-left py-3.5 px-4 rounded-xl transition-all duration-200 flex items-center"
|
class="block w-full text-left py-3.5 px-4 rounded-xl transition-all duration-200 flex items-center"
|
||||||
x-bind:class="darkMode
|
x-bind:class="darkMode
|
||||||
? 'bg-gradient-to-r from-purple-600/30 to-blue-500/30 text-white hover:from-purple-600/40 hover:to-blue-500/40'
|
? 'bg-gradient-to-r from-purple-600/30 to-blue-500/30 text-white hover:from-purple-600/40 hover:to-blue-500/40'
|
||||||
: 'bg-gradient-to-r from-purple-500/10 to-blue-400/10 text-gray-900 hover:from-purple-500/20 hover:to-blue-400/20'">
|
: 'bg-gradient-to-r from-purple-600 to-blue-500 text-white hover:from-purple-600/90 hover:to-blue-500/90'">
|
||||||
<i class="fa-solid fa-robot w-5 mr-3"></i>KI-Chat
|
<i class="fa-solid fa-robot w-5 mr-3"></i>KI-Chat
|
||||||
</button>
|
</button>
|
||||||
{% if current_user.is_authenticated %}
|
{% if current_user.is_authenticated %}
|
||||||
@@ -434,6 +683,10 @@
|
|||||||
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
||||||
Mindmap
|
Mindmap
|
||||||
</a>
|
</a>
|
||||||
|
<a href="{{ url_for('search_thoughts_page') }}" class="text-sm transition-all duration-200"
|
||||||
|
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
||||||
|
Suche
|
||||||
|
</a>
|
||||||
{% if current_user.is_authenticated %}
|
{% if current_user.is_authenticated %}
|
||||||
<a href="{{ url_for('profile') }}" class="text-sm transition-all duration-200"
|
<a href="{{ url_for('profile') }}" class="text-sm transition-all duration-200"
|
||||||
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
||||||
@@ -462,6 +715,10 @@
|
|||||||
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
||||||
Impressum
|
Impressum
|
||||||
</a>
|
</a>
|
||||||
|
<a href="{{ url_for('ueber_uns') }}" class="text-sm transition-all duration-200"
|
||||||
|
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
||||||
|
Über uns
|
||||||
|
</a>
|
||||||
<a href="{{ url_for('datenschutz') }}" class="text-sm transition-all duration-200"
|
<a href="{{ url_for('datenschutz') }}" class="text-sm transition-all duration-200"
|
||||||
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
||||||
Datenschutz
|
Datenschutz
|
||||||
@@ -509,27 +766,286 @@
|
|||||||
|
|
||||||
<!-- Hilfsscripts -->
|
<!-- Hilfsscripts -->
|
||||||
{% block scripts %}{% endblock %}
|
{% block scripts %}{% endblock %}
|
||||||
|
{% block extra_js %}{% endblock %}
|
||||||
|
|
||||||
<!-- KI-Chat Initialisierung -->
|
<!-- ChatGPT Initialisierung -->
|
||||||
<script type="module">
|
<script>
|
||||||
// Importiere und initialisiere den ChatGPT-Assistenten direkt, um sicherzustellen,
|
// Prüfe, ob ChatGPTAssistant bereits existiert
|
||||||
// dass er auf jeder Seite verfügbar ist, selbst wenn MindMap nicht geladen ist
|
if (typeof ChatGPTAssistant === 'undefined') {
|
||||||
import ChatGPTAssistant from "{{ url_for('static', filename='js/modules/chatgpt-assistant.js') }}";
|
class ChatGPTAssistant {
|
||||||
|
constructor() {
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
this.chatContainer = null;
|
||||||
// Prüfen, ob der Assistent bereits durch MindMap initialisiert wurde
|
this.messages = [];
|
||||||
if (!window.MindMap || !window.MindMap.assistant) {
|
this.isOpen = false;
|
||||||
console.log('KI-Assistent wird direkt initialisiert...');
|
}
|
||||||
const assistant = new ChatGPTAssistant();
|
|
||||||
assistant.init();
|
init() {
|
||||||
|
// Chat-Container erstellen, falls noch nicht vorhanden
|
||||||
// Speichere in window.MindMap, falls es existiert, oder erstelle es
|
if (!document.getElementById('chat-assistant-container')) {
|
||||||
if (!window.MindMap) {
|
this.createChatInterface();
|
||||||
window.MindMap = {};
|
}
|
||||||
|
|
||||||
|
// Event-Listener für Chat-Button
|
||||||
|
const chatButton = document.getElementById('chat-assistant-button');
|
||||||
|
if (chatButton) {
|
||||||
|
chatButton.addEventListener('click', () => this.toggleChat());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event-Listener für Senden-Button
|
||||||
|
const sendButton = document.getElementById('chat-send-button');
|
||||||
|
if (sendButton) {
|
||||||
|
sendButton.addEventListener('click', () => this.sendMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event-Listener für Eingabefeld (Enter-Taste)
|
||||||
|
const inputField = document.getElementById('chat-input');
|
||||||
|
if (inputField) {
|
||||||
|
inputField.addEventListener('keypress', (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
this.sendMessage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('KI-Assistent erfolgreich initialisiert');
|
||||||
|
}
|
||||||
|
|
||||||
|
createChatInterface() {
|
||||||
|
// Chat-Button erstellen
|
||||||
|
const chatButton = document.createElement('button');
|
||||||
|
chatButton.id = 'chat-assistant-button';
|
||||||
|
chatButton.className = 'fixed bottom-6 right-6 bg-primary-600 text-white rounded-full p-4 shadow-lg z-50 hover:bg-primary-700 transition-all';
|
||||||
|
chatButton.innerHTML = '<i class="fas fa-robot text-xl"></i>';
|
||||||
|
document.body.appendChild(chatButton);
|
||||||
|
|
||||||
|
// Chat-Container erstellen
|
||||||
|
const chatContainer = document.createElement('div');
|
||||||
|
chatContainer.id = 'chat-assistant-container';
|
||||||
|
chatContainer.className = 'fixed bottom-24 right-6 w-80 md:w-96 bg-white dark:bg-gray-800 rounded-xl shadow-xl z-50 flex flex-col transition-all duration-300 transform scale-0 origin-bottom-right';
|
||||||
|
chatContainer.style.height = '500px';
|
||||||
|
chatContainer.style.maxHeight = '70vh';
|
||||||
|
|
||||||
|
// Chat-Header
|
||||||
|
chatContainer.innerHTML = `
|
||||||
|
<div class="p-4 border-b dark:border-gray-700 flex justify-between items-center">
|
||||||
|
<h3 class="font-bold text-gray-800 dark:text-white">Systades Assistent</h3>
|
||||||
|
<button id="chat-close-button" class="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="chat-messages" class="flex-1 overflow-y-auto p-4 space-y-4"></div>
|
||||||
|
<div class="p-4 border-t dark:border-gray-700">
|
||||||
|
<div class="flex space-x-2">
|
||||||
|
<input id="chat-input" type="text" placeholder="Frage stellen..." class="flex-1 px-4 py-2 rounded-lg border dark:border-gray-700 dark:bg-gray-700 dark:text-white focus:outline-none focus:ring-2 focus:ring-primary-500">
|
||||||
|
<button id="chat-send-button" class="bg-primary-600 text-white px-4 py-2 rounded-lg hover:bg-primary-700 transition-all">
|
||||||
|
<i class="fas fa-paper-plane"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(chatContainer);
|
||||||
|
this.chatContainer = chatContainer;
|
||||||
|
|
||||||
|
// Event-Listener für Schließen-Button
|
||||||
|
const closeButton = document.getElementById('chat-close-button');
|
||||||
|
if (closeButton) {
|
||||||
|
closeButton.addEventListener('click', () => this.toggleChat());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleChat() {
|
||||||
|
this.isOpen = !this.isOpen;
|
||||||
|
if (this.isOpen) {
|
||||||
|
this.chatContainer.classList.remove('scale-0');
|
||||||
|
this.chatContainer.classList.add('scale-100');
|
||||||
|
} else {
|
||||||
|
this.chatContainer.classList.remove('scale-100');
|
||||||
|
this.chatContainer.classList.add('scale-0');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendMessage() {
|
||||||
|
const inputField = document.getElementById('chat-input');
|
||||||
|
const messageText = inputField.value.trim();
|
||||||
|
|
||||||
|
if (!messageText) return;
|
||||||
|
|
||||||
|
// Benutzer-Nachricht anzeigen
|
||||||
|
this.addMessage('user', messageText);
|
||||||
|
inputField.value = '';
|
||||||
|
|
||||||
|
// Lade-Indikator anzeigen
|
||||||
|
this.addMessage('assistant', '...', 'loading-message');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// API-Anfrage senden
|
||||||
|
const response = await fetch('/api/assistant', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
messages: this.messages.map(msg => ({
|
||||||
|
role: msg.role,
|
||||||
|
content: msg.content
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Lade-Nachricht entfernen
|
||||||
|
const loadingMessage = document.getElementById('loading-message');
|
||||||
|
if (loadingMessage) {
|
||||||
|
loadingMessage.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
this.addMessage('assistant', 'Entschuldigung, es ist ein Fehler aufgetreten: ' + data.error);
|
||||||
|
} else {
|
||||||
|
this.addMessage('assistant', data.response);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler bei der API-Anfrage:', error);
|
||||||
|
|
||||||
|
// Lade-Nachricht entfernen
|
||||||
|
const loadingMessage = document.getElementById('loading-message');
|
||||||
|
if (loadingMessage) {
|
||||||
|
loadingMessage.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.addMessage('assistant', 'Entschuldigung, es ist ein Fehler aufgetreten. Bitte versuche es später erneut.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addMessage(role, content, id = null) {
|
||||||
|
const messagesContainer = document.getElementById('chat-messages');
|
||||||
|
|
||||||
|
// Nachricht zum Array hinzufügen (außer Lade-Nachrichten)
|
||||||
|
if (id !== 'loading-message') {
|
||||||
|
this.messages.push({ role, content });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nachricht zum DOM hinzufügen
|
||||||
|
const messageElement = document.createElement('div');
|
||||||
|
messageElement.className = `p-3 rounded-lg ${role === 'user' ? 'bg-primary-100 dark:bg-primary-900/30 ml-6' : 'bg-gray-100 dark:bg-gray-700 mr-6'}`;
|
||||||
|
if (id) {
|
||||||
|
messageElement.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
messageElement.innerHTML = `
|
||||||
|
<div class="flex items-start">
|
||||||
|
<div class="w-8 h-8 rounded-full flex items-center justify-center ${role === 'user' ? 'bg-primary-600' : 'bg-gray-600'} text-white mr-2">
|
||||||
|
<i class="fas ${role === 'user' ? 'fa-user' : 'fa-robot'} text-xs"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 text-sm ${role === 'user' ? 'text-gray-800 dark:text-gray-200' : 'text-gray-700 dark:text-gray-300'}">
|
||||||
|
${content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
messagesContainer.appendChild(messageElement);
|
||||||
|
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||||
}
|
}
|
||||||
window.MindMap.assistant = assistant;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialisiere den ChatGPT-Assistenten direkt
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Prüfen, ob der Assistent bereits durch MindMap initialisiert wurde
|
||||||
|
if (!window.MindMap || !window.MindMap.assistant) {
|
||||||
|
console.log('KI-Assistent wird direkt initialisiert...');
|
||||||
|
const assistant = new ChatGPTAssistant();
|
||||||
|
assistant.init();
|
||||||
|
|
||||||
|
// Speichere in window.MindMap, falls es existiert, oder erstelle es
|
||||||
|
if (!window.MindMap) {
|
||||||
|
window.MindMap = {};
|
||||||
|
}
|
||||||
|
window.MindMap.assistant = assistant;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Dark/Light-Mode vereinheitlicht -->
|
||||||
|
<script>
|
||||||
|
// Globaler Zugriff für externe Skripte
|
||||||
|
window.MindMap = window.MindMap || {};
|
||||||
|
|
||||||
|
// Funktion zum Anwenden des Dark Mode, strikt getrennt
|
||||||
|
function applyDarkModeClasses(isDarkMode) {
|
||||||
|
if (isDarkMode) {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
document.body.classList.add('dark');
|
||||||
|
localStorage.setItem('colorMode', 'dark');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('dark');
|
||||||
|
document.body.classList.remove('dark');
|
||||||
|
localStorage.setItem('colorMode', 'light');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alpine.js darkMode-Variable aktualisieren, falls zutreffend
|
||||||
|
const appEl = document.querySelector('body');
|
||||||
|
if (appEl && appEl.__x) {
|
||||||
|
appEl.__x.$data.darkMode = isDarkMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event für andere Komponenten auslösen
|
||||||
|
document.dispatchEvent(new CustomEvent('darkModeToggled', {
|
||||||
|
detail: { isDark: isDarkMode }
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
window.MindMap.toggleDarkMode = function() {
|
||||||
|
const isDark = document.documentElement.classList.contains('dark');
|
||||||
|
const newIsDark = !isDark;
|
||||||
|
|
||||||
|
// DOM aktualisieren
|
||||||
|
applyDarkModeClasses(newIsDark);
|
||||||
|
|
||||||
|
// Server aktualisieren
|
||||||
|
fetch('/api/set_dark_mode', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ darkMode: newIsDark })
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialisierung beim Laden
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Reihenfolge der Prüfungen: Serverseitige Einstellung > Lokale Einstellung > Browser-Präferenz
|
||||||
|
|
||||||
|
// 1. Zuerst lokale Einstellung prüfen
|
||||||
|
const storedMode = localStorage.getItem('colorMode');
|
||||||
|
if (storedMode) {
|
||||||
|
applyDarkModeClasses(storedMode === 'dark');
|
||||||
|
} else {
|
||||||
|
// 2. Fallback auf Browser-Präferenz
|
||||||
|
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
applyDarkModeClasses(prefersDark);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Serverseitige Einstellung abrufen und anwenden
|
||||||
|
fetch('/api/get_dark_mode')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
const serverDarkMode = data.darkMode === true || data.darkMode === 'true';
|
||||||
|
applyDarkModeClasses(serverDarkMode);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => console.error('Fehler beim Abrufen des Dark Mode Status:', error));
|
||||||
|
|
||||||
|
// Listener für Änderungen der Browser-Präferenz
|
||||||
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
|
||||||
|
if (localStorage.getItem('colorMode') === null) {
|
||||||
|
applyDarkModeClasses(e.matches);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
192
templates/community/category.html
Normal file
192
templates/community/category.html
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}{{ category.title }} - Forum{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<style>
|
||||||
|
.thread-item {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
.thread-item:hover {
|
||||||
|
transform: translateX(2px);
|
||||||
|
}
|
||||||
|
.thread-pinned {
|
||||||
|
border-left-width: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mx-auto px-4 py-8">
|
||||||
|
<!-- Breadcrumb Navigation -->
|
||||||
|
<div class="mb-6 flex items-center text-sm">
|
||||||
|
<a href="{{ url_for('community') }}" class="opacity-75 hover:opacity-100 transition-opacity">
|
||||||
|
<i class="fas fa-home mr-1"></i> Forum
|
||||||
|
</a>
|
||||||
|
<span class="mx-2 opacity-50">/</span>
|
||||||
|
<span class="font-medium">{{ category.title }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Kategorie-Header -->
|
||||||
|
<div class="mb-8 flex flex-wrap items-center justify-between gap-4">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<!-- Kategorie-Icon -->
|
||||||
|
<div class="w-12 h-12 rounded-xl mr-4 flex items-center justify-center text-white"
|
||||||
|
style="background-color: {{ node.color_code or '#6d28d9' }}">
|
||||||
|
<i class="fas {{ node.icon or 'fa-folder' }} text-2xl"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Kategorie-Info -->
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-bold">{{ category.title }}</h1>
|
||||||
|
<p class="opacity-75">{{ category.description }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Neues Thema erstellen -->
|
||||||
|
<a href="{{ url_for('new_post', category_id=category.id) }}"
|
||||||
|
class="px-5 py-2.5 rounded-lg transition-all duration-300 flex items-center"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-indigo-700 hover:bg-indigo-600 text-white'
|
||||||
|
: 'bg-indigo-500 hover:bg-indigo-600 text-white'">
|
||||||
|
<i class="fas fa-plus-circle mr-2"></i>
|
||||||
|
Neues Thema
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Threads anzeigen -->
|
||||||
|
<div class="mb-8 rounded-xl overflow-hidden"
|
||||||
|
x-bind:class="darkMode ? 'bg-gray-800/60 border border-white/10' : 'bg-white border border-gray-200'">
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="p-4 border-b" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
|
||||||
|
<div class="grid grid-cols-12 gap-4">
|
||||||
|
<div class="col-span-7 font-medium">Thema</div>
|
||||||
|
<div class="col-span-1 text-center font-medium hidden md:block">Antworten</div>
|
||||||
|
<div class="col-span-2 text-center font-medium hidden md:block">Autor</div>
|
||||||
|
<div class="col-span-2 text-center font-medium hidden md:block">Letzte Antwort</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Thread-Liste -->
|
||||||
|
{% if threads_data %}
|
||||||
|
{% for thread_data in threads_data %}
|
||||||
|
{% set thread = thread_data.thread %}
|
||||||
|
<div class="thread-item p-4 border-b last:border-b-0 {{ 'thread-pinned' if thread.is_pinned }}"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'border-white/10 hover:bg-gray-700/50 {{ 'border-l-yellow-500' if thread.is_pinned }}'
|
||||||
|
: 'border-gray-200 hover:bg-gray-50 {{ 'border-l-yellow-500' if thread.is_pinned }}'">
|
||||||
|
<a href="{{ url_for('forum_post', post_id=thread.id) }}" class="block">
|
||||||
|
<div class="grid grid-cols-12 gap-4">
|
||||||
|
<!-- Thema -->
|
||||||
|
<div class="col-span-12 md:col-span-7">
|
||||||
|
<div class="flex items-start">
|
||||||
|
<!-- Status-Icons -->
|
||||||
|
<div class="flex flex-col items-center mr-3 pt-1">
|
||||||
|
{% if thread.is_pinned %}
|
||||||
|
<i class="fas fa-thumbtack text-yellow-500" title="Angepinnt"></i>
|
||||||
|
{% endif %}
|
||||||
|
{% if thread.is_locked %}
|
||||||
|
<i class="fas fa-lock text-red-500 mt-1" title="Gesperrt"></i>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Themen-Info -->
|
||||||
|
<div>
|
||||||
|
<h3 class="font-medium leading-snug mb-1 {% if thread.is_locked %}opacity-70{% endif %}">
|
||||||
|
{{ thread.title }}
|
||||||
|
</h3>
|
||||||
|
<div class="flex items-center text-xs opacity-70 mt-1">
|
||||||
|
<span><i class="fas fa-eye mr-1"></i> {{ thread.view_count }}</span>
|
||||||
|
<span class="mx-2 block md:hidden">•</span>
|
||||||
|
<span class="block md:hidden"><i class="fas fa-reply mr-1"></i> {{ thread_data.reply_count }}</span>
|
||||||
|
<span class="mx-2">•</span>
|
||||||
|
<span><i class="fas fa-clock mr-1"></i> {{ thread.created_at.strftime('%d.%m.%Y, %H:%M') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Antworten -->
|
||||||
|
<div class="col-span-1 text-center hidden md:flex items-center justify-center">
|
||||||
|
<span class="px-2.5 py-1 rounded-full text-sm font-medium"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-indigo-900/40 text-indigo-300'
|
||||||
|
: 'bg-indigo-100 text-indigo-800'">
|
||||||
|
{{ thread_data.reply_count }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Autor -->
|
||||||
|
<div class="col-span-2 text-center hidden md:flex items-center justify-center">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="w-7 h-7 rounded-full flex items-center justify-center text-white text-xs font-medium overflow-hidden mr-2"
|
||||||
|
style="background: linear-gradient(135deg, #8b5cf6, #6366f1);">
|
||||||
|
{% if thread.author.avatar %}
|
||||||
|
<img src="{{ thread.author.avatar }}" alt="{{ thread.author.username }}" class="w-full h-full object-cover">
|
||||||
|
{% else %}
|
||||||
|
{{ thread.author.username[0].upper() }}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<span class="text-sm truncate max-w-[80px]">{{ thread.author.username }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Letzte Antwort -->
|
||||||
|
<div class="col-span-2 text-center hidden md:block text-sm">
|
||||||
|
{% if thread_data.latest_reply %}
|
||||||
|
<div>{{ thread_data.latest_reply.created_at.strftime('%d.%m.%Y') }}</div>
|
||||||
|
<div class="opacity-75 text-xs">{{ thread_data.latest_reply.created_at.strftime('%H:%M') }} Uhr</div>
|
||||||
|
{% else %}
|
||||||
|
<span class="opacity-60">Keine Antworten</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<div class="p-8 text-center">
|
||||||
|
<div class="text-3xl mb-3 opacity-30"><i class="fas fa-comments"></i></div>
|
||||||
|
<h3 class="text-xl font-semibold mb-2">Keine Themen vorhanden</h3>
|
||||||
|
<p class="opacity-75 mb-4">In dieser Kategorie wurden noch keine Themen erstellt.</p>
|
||||||
|
<a href="{{ url_for('new_post', category_id=category.id) }}"
|
||||||
|
class="inline-block px-5 py-2.5 rounded-lg transition-all duration-300"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-indigo-700 hover:bg-indigo-600 text-white'
|
||||||
|
: 'bg-indigo-500 hover:bg-indigo-600 text-white'">
|
||||||
|
<i class="fas fa-plus-circle mr-2"></i>
|
||||||
|
Erstes Thema erstellen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Link zur Mindmap -->
|
||||||
|
<div class="rounded-xl p-5 mb-4 flex items-center"
|
||||||
|
x-bind:class="darkMode ? 'bg-purple-900/20 border border-purple-800/30' : 'bg-purple-50 border border-purple-100'">
|
||||||
|
<div class="text-3xl mr-4 opacity-80">
|
||||||
|
<i class="fas fa-diagram-project" style="color: {{ node.color_code }}"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-medium mb-1">Mindmap-Knotenpunkt: {{ node.name }}</h3>
|
||||||
|
<p class="text-sm opacity-75">In der Mindmap findest du weitere Informationen zu diesem Themenbereich.</p>
|
||||||
|
</div>
|
||||||
|
<div class="ml-auto">
|
||||||
|
<a href="{{ url_for('mindmap') }}"
|
||||||
|
class="px-4 py-2 rounded-lg inline-block text-sm transition-all"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-purple-800/60 hover:bg-purple-700/60 text-white'
|
||||||
|
: 'bg-white hover:bg-purple-100 text-purple-800 border border-purple-200'">
|
||||||
|
Zur Mindmap
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
// Hier können bei Bedarf kategoriespezifische Scripts eingefügt werden
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user