Compare commits
15 Commits
till-v2
...
750dba2d6f
| Author | SHA1 | Date | |
|---|---|---|---|
| 750dba2d6f | |||
| 8890a62026 | |||
| 6cf9b2a627 | |||
| 4f6aea8e20 | |||
| e5f485d9d7 | |||
| cf3fc09a63 | |||
| 10747a8336 | |||
| 7eb958f3c8 | |||
| 4a3092a4d2 | |||
| 2d8cdc052f | |||
| 968515ce2b | |||
| 88f8e98df0 | |||
| e5409eef68 | |||
| 013bf76446 | |||
| 808a3c7bbe |
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
|
||||||
2
.env
2
.env
@@ -5,7 +5,7 @@
|
|||||||
SECRET_KEY=dein-geheimer-schluessel-hier
|
SECRET_KEY=dein-geheimer-schluessel-hier
|
||||||
|
|
||||||
# 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
|
||||||
|
|||||||
173
COMMON_ERRORS.md
Normal file
173
COMMON_ERRORS.md
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
# ABSOLUTE DON'TS:
|
||||||
|
- Verwendung von npm anstelle der Tailwind CDN
|
||||||
|
- Implementierung von Content Security Policy (CSP) - UNTER KEINEN UMSTÄNDEN!
|
||||||
|
- Implementierung von Cross-Site Request Forgery (CSRF) Schutz
|
||||||
|
- Implementierung von Security Headers
|
||||||
|
- Verwendung von TypeScript anstelle von JavaScript
|
||||||
|
- Implementierung von OAuth oder anderen externen Authentifizierungsmethoden
|
||||||
|
|
||||||
|
# HÄUFIGE FEHLER:
|
||||||
|
- Verwendung der falschen Datenbank (die korrekte ist: database/systades.db)
|
||||||
|
- Falsche Pfadangaben bei statischen Dateien
|
||||||
|
- Vergessen der deutschen Spracheinstellungen in Templates
|
||||||
|
- Nicht beachten der vorhandenen Projektstruktur
|
||||||
|
- Falsche Einbindung der Neural Network Background Animation
|
||||||
|
- Verwendung von englischen Variablennamen in deutschen Funktionen
|
||||||
|
- Vergessen der Mindmap-Datenstruktur gemäß der Roadmap
|
||||||
|
|
||||||
|
# Häufige Fehler und Lösungen
|
||||||
|
D
|
||||||
|
## Content Security Policy (CSP)
|
||||||
|
|
||||||
|
### Problem: Externe Ressourcen werden nicht geladen
|
||||||
|
**Fehler:** Externe Ressourcen wie CDNs werden nicht korrekt geladen.
|
||||||
|
|
||||||
|
**Lösung:**
|
||||||
|
1. Stellen Sie sicher, dass die URLs in den Templates korrekt sind:
|
||||||
|
```html
|
||||||
|
<link href="https://cdn.tailwindcss.com" rel="stylesheet">
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Überprüfen Sie die Netzwerkverbindung und ob die CDN-Domains erreichbar sind.
|
||||||
|
|
||||||
|
3. Verwenden Sie lokale Ressourcen als Alternative:
|
||||||
|
```html
|
||||||
|
<link href="{{ url_for('static', filename='css/tailwind.min.css') }}" rel="stylesheet">
|
||||||
|
```
|
||||||
|
|
||||||
|
### Problem: CSP-Fehler (script-src-elem 'none', font-src 'none', etc.)
|
||||||
|
**Fehler:** Skripte werden aufgrund von CSP-Einschränkungen blockiert.
|
||||||
|
|
||||||
|
**Lösung:**
|
||||||
|
1. WICHTIG: CSP-Einschränkungen NICHT implementieren! Diese sind im Projekt unerwünscht.
|
||||||
|
|
||||||
|
2. Falls CSP-Header bereits vorhanden sind, entfernen Sie diese aus der Anwendung:
|
||||||
|
```python
|
||||||
|
# Entfernen Sie diesen Code, falls vorhanden:
|
||||||
|
# @app.after_request
|
||||||
|
# def add_security_headers(response):
|
||||||
|
# response.headers['Content-Security-Policy'] = '...'
|
||||||
|
# return response
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Stellen Sie sicher, dass externe Ressourcen direkt geladen werden:
|
||||||
|
```html
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||||
|
<script src="{{ url_for('static', filename='js/alpine.min.js') }}" defer></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Für Inline-Skripte keine Nonce verwenden:
|
||||||
|
```html
|
||||||
|
<script>
|
||||||
|
// Inline-JavaScript-Code ohne Einschränkungen
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Problem: Tailwind CSS CDN wird blockiert
|
||||||
|
**Fehler:** Tailwind CSS kann nicht von CDN geladen werden.
|
||||||
|
|
||||||
|
**Lösung:**
|
||||||
|
1. Verwenden Sie die lokale Version von Tailwind CSS:
|
||||||
|
```html
|
||||||
|
<link href="{{ url_for('static', filename='css/tailwind.min.css') }}" rel="stylesheet">
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Alternativ können Sie die CDN-Version direkt im Template einbinden:
|
||||||
|
```html
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Stellen Sie sicher, dass die Datei `static/css/tailwind.min.css` existiert und aktuell ist.
|
||||||
|
|
||||||
|
## Datenbank-Fehler
|
||||||
|
|
||||||
|
### Problem: Datenbank existiert nicht
|
||||||
|
**Fehler:** SQLite-Datenbank kann nicht geöffnet werden.
|
||||||
|
|
||||||
|
**Lösung:**
|
||||||
|
1. Datenbank initialisieren: `python TOOLS.py db:rebuild`
|
||||||
|
2. Sicherstellen, dass das Datenbankverzeichnis existiert und Schreibrechte hat
|
||||||
|
|
||||||
|
## Authentifizierung
|
||||||
|
|
||||||
|
### Problem: Login funktioniert nicht
|
||||||
|
**Fehler:** Benutzer kann sich nicht einloggen.
|
||||||
|
|
||||||
|
**Lösung:**
|
||||||
|
1. Standard-Admin-Benutzer erstellen: `python TOOLS.py user:admin`
|
||||||
|
2. Passwort zurücksetzen: `python TOOLS.py user:reset-pw -u USERNAME -p NEWPASSWORD`
|
||||||
|
|
||||||
|
## Neural Network Background
|
||||||
|
|
||||||
|
### Problem: Hintergrund-Animation wird nicht angezeigt
|
||||||
|
**Fehler:** Die Neural Network Animation im Hintergrund erscheint nicht.
|
||||||
|
|
||||||
|
**Lösung:**
|
||||||
|
1. Überprüfen Sie, ob die Datei `static/neural-network-background.js` korrekt eingebunden ist:
|
||||||
|
```html
|
||||||
|
<script src="{{ url_for('static', filename='neural-network-background.js') }}"></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Initialisieren Sie die Animation im Template:
|
||||||
|
```html
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const background = new NeuralNetworkBackground();
|
||||||
|
background.initialize();
|
||||||
|
background.animate();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Stellen Sie sicher, dass keine CSS-Regeln die Animation überdecken:
|
||||||
|
```css
|
||||||
|
#neural-network-background {
|
||||||
|
z-index: -10;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mindmap-Funktionalität
|
||||||
|
|
||||||
|
### Problem: Mindmap-Daten werden nicht geladen
|
||||||
|
**Fehler:** Die dynamische Mindmap zeigt keine Daten an.
|
||||||
|
|
||||||
|
**Lösung:**
|
||||||
|
1. Überprüfen Sie die API-Endpunkte für die Mindmap-Daten:
|
||||||
|
```python
|
||||||
|
@app.route('/api/mindmap/nodes', methods=['GET'])
|
||||||
|
def get_mindmap_nodes():
|
||||||
|
# Implementierung...
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Stellen Sie sicher, dass die AJAX-Anfragen korrekt implementiert sind:
|
||||||
|
```javascript
|
||||||
|
fetch('/api/mindmap/nodes')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
// Verarbeitung der Mindmap-Daten
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Überprüfen Sie die Datenbankeinträge für Mindmap-Knoten und -Verbindungen.
|
||||||
|
|
||||||
|
## ChatGPT-Assistent
|
||||||
|
|
||||||
|
### Problem: Assistent reagiert nicht auf Eingaben
|
||||||
|
**Fehler:** Der ChatGPT-Assistent verarbeitet keine Benutzereingaben.
|
||||||
|
|
||||||
|
**Lösung:**
|
||||||
|
1. Überprüfen Sie die Einbindung der JavaScript-Datei:
|
||||||
|
```html
|
||||||
|
<script src="{{ url_for('static', filename='js/modules/chatgpt-assistant.js') }}"></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Stellen Sie sicher, dass der Assistent korrekt initialisiert wird:
|
||||||
|
```javascript
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const assistant = new ChatGPTAssistant();
|
||||||
|
assistant.initialize();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Überprüfen Sie die API-Endpunkte für die Kommunikation mit dem Assistenten.
|
||||||
31
README.md
31
README.md
@@ -6,7 +6,7 @@ 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
|
||||||
- **Datenbank**: SQLite mit SQLAlchemy
|
- **Datenbank**: SQLite mit SQLAlchemy
|
||||||
@@ -131,4 +131,31 @@ Für detaillierte Hilfe: `python TOOLS.py -h`
|
|||||||
- Implementierung des Tagging-Systems für Gedanken
|
- Implementierung des Tagging-Systems für Gedanken
|
||||||
- Verbesserung der Gedankenansicht im Mindmap-Bereich
|
- Verbesserung der Gedankenansicht im Mindmap-Bereich
|
||||||
|
|
||||||
*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)
|
||||||
|
|
||||||
|
### 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: 06.06.2024*
|
||||||
27
ROADMAP.md
27
ROADMAP.md
@@ -66,6 +66,20 @@ Diese Roadmap beschreibt die geplante Entwicklung der dynamischen, benutzerorien
|
|||||||
- [ ] Caching-Strategien für bessere Performance
|
- [ ] Caching-Strategien für bessere Performance
|
||||||
- [ ] Verbesserte Fehlerbehandlung und Logging
|
- [ ] Verbesserte Fehlerbehandlung und Logging
|
||||||
|
|
||||||
|
## KI-Integration
|
||||||
|
|
||||||
|
### Aktuelle Implementation
|
||||||
|
- Integration von OpenAI mit dem gpt-4o-mini-Modell für den KI-Assistenten
|
||||||
|
- Datenbankzugriff für den KI-Assistenten, um direkt Informationen aus der Datenbank abzufragen
|
||||||
|
- Verbesserte Benutzeroberfläche für den KI-Assistenten mit kontextbezogenen Vorschlägen
|
||||||
|
|
||||||
|
### Zukünftige Verbesserungen
|
||||||
|
- Implementierung von Vektorsuche für präzisere Datenbank-Abfragen durch die KI
|
||||||
|
- Erweiterung der KI-Funktionalität für tiefere Analyse von Zusammenhängen zwischen Gedanken
|
||||||
|
- KI-gestützte Vorschläge für neue Verbindungen zwischen Gedanken basierend auf Inhaltsanalyse
|
||||||
|
- Finetuning des KI-Modells auf die spezifischen Anforderungen der Anwendung
|
||||||
|
- Erweiterung auf multimodale Fähigkeiten (Bild- und Textanalyse)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Implementierungsdetails
|
## Implementierungsdetails
|
||||||
@@ -100,3 +114,16 @@ Die implementierten API-Endpunkte umfassen:
|
|||||||
- `/api/mindmap/<id>/update_node_position` - Aktualisierung von Knotenpositionen
|
- `/api/mindmap/<id>/update_node_position` - Aktualisierung von Knotenpositionen
|
||||||
- `/api/mindmap/<id>/notes` - Verwaltung von Notizen
|
- `/api/mindmap/<id>/notes` - Verwaltung von Notizen
|
||||||
- `/api/nodes/<id>/thoughts` - Abrufen und Hinzufügen von Gedanken zu Knoten
|
- `/api/nodes/<id>/thoughts` - Abrufen und Hinzufügen von Gedanken zu Knoten
|
||||||
|
|
||||||
|
## Aktuelle Änderungen
|
||||||
|
- Tailwind CSS wurde auf CDN-Version aktualisiert (06.06.2024)
|
||||||
|
- Content Security Policy (CSP) für Tailwind CSS CDN konfiguriert
|
||||||
|
|
||||||
|
## Zukünftige Aufgaben
|
||||||
|
- Überprüfung der Kompatibilität der Tailwind CSS CDN-Version mit allen UI-Komponenten
|
||||||
|
- Optimierung der Ladezeiten für mobile Geräte
|
||||||
|
- Überarbeitung der Dark Mode Funktionalität mit neuer Tailwind Version
|
||||||
|
|
||||||
|
## Langfristige Ziele
|
||||||
|
- Migration zu einer statisch kompilierten Tailwind CSS Version für Produktivumgebungen
|
||||||
|
- Implementierung von Tailwind Plugins für erweiterte UI-Funktionen
|
||||||
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.
651
app.py
651
app.py
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from flask import Flask, render_template, request, redirect, url_for, flash, jsonify, session
|
from flask import Flask, render_template, request, redirect, url_for, flash, jsonify, session, g
|
||||||
from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user
|
from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
from werkzeug.security import generate_password_hash, check_password_hash
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
@@ -41,7 +41,29 @@ app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
|||||||
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=365) # Langlebige Session für Dark Mode-Einstellung
|
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=365) # Langlebige Session für Dark Mode-Einstellung
|
||||||
|
|
||||||
# OpenAI API-Konfiguration
|
# OpenAI API-Konfiguration
|
||||||
client = OpenAI(api_key="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")
|
api_key = os.environ.get("OPENAI_API_KEY")
|
||||||
|
if not api_key:
|
||||||
|
print("WARNUNG: Kein OPENAI_API_KEY in Umgebungsvariablen gefunden. KI-Funktionalität wird nicht verfügbar sein.")
|
||||||
|
api_key = "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"
|
||||||
|
|
||||||
|
client = OpenAI(api_key=api_key)
|
||||||
|
|
||||||
|
# Dark Mode Einstellung in Session speichern
|
||||||
|
@app.before_request
|
||||||
|
def handle_dark_mode():
|
||||||
|
if 'dark_mode' not in session:
|
||||||
|
session['dark_mode'] = False # Standardmäßig Light Mode
|
||||||
|
|
||||||
|
# Context processor für Dark Mode
|
||||||
|
@app.context_processor
|
||||||
|
def inject_dark_mode():
|
||||||
|
return {'dark_mode': session.get('dark_mode', False)}
|
||||||
|
|
||||||
|
# Route zum Umschalten des Dark Mode
|
||||||
|
@app.route('/toggle-dark-mode', methods=['POST'])
|
||||||
|
def toggle_dark_mode():
|
||||||
|
session['dark_mode'] = not session.get('dark_mode', False)
|
||||||
|
return jsonify({'success': True, 'dark_mode': session['dark_mode']})
|
||||||
|
|
||||||
# Context processor für globale Template-Variablen
|
# Context processor für globale Template-Variablen
|
||||||
@app.context_processor
|
@app.context_processor
|
||||||
@@ -51,7 +73,7 @@ def inject_globals():
|
|||||||
'current_year': datetime.now().year
|
'current_year': datetime.now().year
|
||||||
}
|
}
|
||||||
|
|
||||||
# Kontext-Prozessor für alle Templates
|
# Context-Prozessor für alle Templates
|
||||||
@app.context_processor
|
@app.context_processor
|
||||||
def inject_current_year():
|
def inject_current_year():
|
||||||
return {'current_year': datetime.now().year}
|
return {'current_year': datetime.now().year}
|
||||||
@@ -63,6 +85,214 @@ db.init_app(app)
|
|||||||
login_manager = LoginManager(app)
|
login_manager = LoginManager(app)
|
||||||
login_manager.login_view = 'login'
|
login_manager.login_view = 'login'
|
||||||
|
|
||||||
|
# Erst nach der App-Initialisierung die DB-Check-Funktionen importieren
|
||||||
|
from utils.db_check import check_db_connection, initialize_db_if_needed
|
||||||
|
|
||||||
|
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",
|
||||||
|
"subcategories": [
|
||||||
|
{"name": "Ethik", "description": "Moralische Grundsätze", "icon": "fa-balance-scale"},
|
||||||
|
{"name": "Logik", "description": "Gesetze des Denkens", "icon": "fa-project-diagram"},
|
||||||
|
{"name": "Erkenntnistheorie", "description": "Natur des Wissens", "icon": "fa-lightbulb"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Wissenschaft",
|
||||||
|
"description": "Wissenschaftliche Disziplinen und Forschung",
|
||||||
|
"color_code": "#48BB78",
|
||||||
|
"icon": "fa-flask",
|
||||||
|
"subcategories": [
|
||||||
|
{"name": "Physik", "description": "Gesetze der Materie und Energie", "icon": "fa-atom"},
|
||||||
|
{"name": "Biologie", "description": "Wissenschaft des Lebens", "icon": "fa-dna"},
|
||||||
|
{"name": "Mathematik", "description": "Abstrakte Strukturen", "icon": "fa-calculator"},
|
||||||
|
{"name": "Informatik", "description": "Wissenschaft der Datenverarbeitung", "icon": "fa-laptop-code"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Technologie",
|
||||||
|
"description": "Technologische Entwicklungen und Anwendungen",
|
||||||
|
"color_code": "#ED8936",
|
||||||
|
"icon": "fa-microchip",
|
||||||
|
"subcategories": [
|
||||||
|
{"name": "Künstliche Intelligenz", "description": "Intelligente Maschinen", "icon": "fa-robot"},
|
||||||
|
{"name": "Programmierung", "description": "Softwareentwicklung", "icon": "fa-code"},
|
||||||
|
{"name": "Elektronik", "description": "Elektronische Systeme", "icon": "fa-memory"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Künste",
|
||||||
|
"description": "Kunstformen und kulturelle Ausdrucksweisen",
|
||||||
|
"color_code": "#ED64A6",
|
||||||
|
"icon": "fa-palette",
|
||||||
|
"subcategories": [
|
||||||
|
{"name": "Literatur", "description": "Schriftliche Werke", "icon": "fa-book"},
|
||||||
|
{"name": "Musik", "description": "Klangkunst", "icon": "fa-music"},
|
||||||
|
{"name": "Bildende Kunst", "description": "Visuelle Kunstformen", "icon": "fa-paint-brush"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Psychologie",
|
||||||
|
"description": "Menschliches Verhalten und Geist",
|
||||||
|
"color_code": "#4299E1",
|
||||||
|
"icon": "fa-comments",
|
||||||
|
"subcategories": [
|
||||||
|
{"name": "Kognition", "description": "Denken und Wahrnehmen", "icon": "fa-brain"},
|
||||||
|
{"name": "Emotionen", "description": "Gefühlswelt", "icon": "fa-heart"},
|
||||||
|
{"name": "Persönlichkeit", "description": "Charaktereigenschaften", "icon": "fa-user"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
# Kategorien erstellen
|
||||||
|
for main_cat_data in main_categories:
|
||||||
|
# Prüfen, ob die Kategorie bereits existiert
|
||||||
|
existing_cat = Category.query.filter_by(name=main_cat_data["name"]).first()
|
||||||
|
if existing_cat:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Hauptkategorie erstellen
|
||||||
|
main_category = Category(
|
||||||
|
name=main_cat_data["name"],
|
||||||
|
description=main_cat_data["description"],
|
||||||
|
color_code=main_cat_data["color_code"],
|
||||||
|
icon=main_cat_data["icon"]
|
||||||
|
)
|
||||||
|
db.session.add(main_category)
|
||||||
|
db.session.flush() # Um die ID zu generieren
|
||||||
|
|
||||||
|
# Unterkategorien erstellen
|
||||||
|
for sub_cat_data in main_cat_data.get("subcategories", []):
|
||||||
|
sub_category = Category(
|
||||||
|
name=sub_cat_data["name"],
|
||||||
|
description=sub_cat_data["description"],
|
||||||
|
color_code=main_cat_data["color_code"],
|
||||||
|
icon=sub_cat_data.get("icon", main_cat_data["icon"]),
|
||||||
|
parent_id=main_category.id
|
||||||
|
)
|
||||||
|
db.session.add(sub_category)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
print("Standard-Kategorien wurden erstellt!")
|
||||||
|
|
||||||
|
def initialize_database():
|
||||||
|
"""Initialisiert die Datenbank mit Grunddaten, falls diese leer ist"""
|
||||||
|
try:
|
||||||
|
print("Initialisiere die Datenbank...")
|
||||||
|
|
||||||
|
# Erstelle alle Tabellen
|
||||||
|
db.create_all()
|
||||||
|
|
||||||
|
# Prüfe, ob bereits Benutzer existieren
|
||||||
|
if User.query.count() == 0:
|
||||||
|
print("Erstelle Admin-Benutzer...")
|
||||||
|
admin = User(
|
||||||
|
username="admin",
|
||||||
|
email="admin@example.com",
|
||||||
|
is_admin=True
|
||||||
|
)
|
||||||
|
admin.set_password("admin123") # In echter Umgebung ein sicheres Passwort verwenden!
|
||||||
|
db.session.add(admin)
|
||||||
|
|
||||||
|
# Prüfe, ob bereits Kategorien existieren
|
||||||
|
if Category.query.count() == 0:
|
||||||
|
print("Erstelle Standard-Kategorien...")
|
||||||
|
create_default_categories()
|
||||||
|
|
||||||
|
# Stelle sicher, dass die Standard-Knoten für die öffentliche Mindmap existieren
|
||||||
|
if MindMapNode.query.count() == 0:
|
||||||
|
print("Erstelle Standard-Knoten für die Mindmap...")
|
||||||
|
|
||||||
|
# Hauptknoten: Wissen
|
||||||
|
root_node = MindMapNode(
|
||||||
|
name="Wissen",
|
||||||
|
description="Zentrale Wissensbasis",
|
||||||
|
color_code="#4299E1",
|
||||||
|
is_public=True
|
||||||
|
)
|
||||||
|
db.session.add(root_node)
|
||||||
|
db.session.flush() # Um die ID zu generieren
|
||||||
|
|
||||||
|
# Verwandte Kategorien finden
|
||||||
|
philosophy = Category.query.filter_by(name="Philosophie").first()
|
||||||
|
science = Category.query.filter_by(name="Wissenschaft").first()
|
||||||
|
technology = Category.query.filter_by(name="Technologie").first()
|
||||||
|
arts = Category.query.filter_by(name="Künste").first()
|
||||||
|
|
||||||
|
# Erstelle Hauptthemenknoten
|
||||||
|
nodes = [
|
||||||
|
MindMapNode(
|
||||||
|
name="Philosophie",
|
||||||
|
description="Philosophisches Denken",
|
||||||
|
color_code="#9F7AEA",
|
||||||
|
category=philosophy,
|
||||||
|
is_public=True
|
||||||
|
),
|
||||||
|
MindMapNode(
|
||||||
|
name="Wissenschaft",
|
||||||
|
description="Wissenschaftliche Erkenntnisse",
|
||||||
|
color_code="#48BB78",
|
||||||
|
category=science,
|
||||||
|
is_public=True
|
||||||
|
),
|
||||||
|
MindMapNode(
|
||||||
|
name="Technologie",
|
||||||
|
description="Technologische Entwicklungen",
|
||||||
|
color_code="#ED8936",
|
||||||
|
category=technology,
|
||||||
|
is_public=True
|
||||||
|
),
|
||||||
|
MindMapNode(
|
||||||
|
name="Künste",
|
||||||
|
description="Künstlerische Ausdrucksformen",
|
||||||
|
color_code="#ED64A6",
|
||||||
|
category=arts,
|
||||||
|
is_public=True
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
# Füge Knoten zur Datenbank hinzu
|
||||||
|
for node in nodes:
|
||||||
|
db.session.add(node)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# Nachdem wir die IDs haben, füge die Verbindungen hinzu
|
||||||
|
all_nodes = MindMapNode.query.all()
|
||||||
|
root = MindMapNode.query.filter_by(name="Wissen").first()
|
||||||
|
|
||||||
|
if root:
|
||||||
|
for node in all_nodes:
|
||||||
|
if node.id != root.id:
|
||||||
|
root.children.append(node)
|
||||||
|
|
||||||
|
# Speichere die Änderungen
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
print("Datenbankinitialisierung abgeschlossen.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Fehler bei der Datenbankinitialisierung: {str(e)}")
|
||||||
|
db.session.rollback()
|
||||||
|
raise
|
||||||
|
|
||||||
|
# Instead of before_first_request, which is deprecated in newer Flask versions
|
||||||
|
# Use a function to initialize the database that will be called during app creation
|
||||||
|
def init_app_database(app_instance):
|
||||||
|
"""Initialisiert die Datenbank für die Flask-App"""
|
||||||
|
with app_instance.app_context():
|
||||||
|
# Überprüfe und initialisiere die Datenbank bei Bedarf
|
||||||
|
if not initialize_db_if_needed(db, initialize_database):
|
||||||
|
print("WARNUNG: Datenbankinitialisierung fehlgeschlagen. Einige Funktionen könnten eingeschränkt sein.")
|
||||||
|
|
||||||
|
# Call the function to initialize the database
|
||||||
|
init_app_database(app)
|
||||||
|
|
||||||
# Benutzerdefinierter Decorator für Admin-Zugriff
|
# Benutzerdefinierter Decorator für Admin-Zugriff
|
||||||
def admin_required(f):
|
def admin_required(f):
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
@@ -145,14 +375,22 @@ def index():
|
|||||||
@app.route('/mindmap')
|
@app.route('/mindmap')
|
||||||
def mindmap():
|
def mindmap():
|
||||||
"""Zeigt die öffentliche Mindmap an."""
|
"""Zeigt die öffentliche Mindmap an."""
|
||||||
|
try:
|
||||||
# Sicherstellen, dass wir Kategorien haben
|
# Sicherstellen, dass wir Kategorien haben
|
||||||
with app.app_context():
|
|
||||||
if Category.query.count() == 0:
|
if Category.query.count() == 0:
|
||||||
create_default_categories()
|
create_default_categories()
|
||||||
|
|
||||||
# Hole alle Kategorien der obersten Ebene
|
# Hole alle Kategorien der obersten Ebene
|
||||||
categories = Category.query.filter_by(parent_id=None).all()
|
categories = Category.query.filter_by(parent_id=None).all()
|
||||||
return render_template('mindmap.html', categories=categories)
|
|
||||||
|
# Transformiere Kategorien in ein anzeigbares Format für die Vorlage
|
||||||
|
category_tree = [build_category_tree(cat) for cat in categories]
|
||||||
|
|
||||||
|
return render_template('mindmap.html', categories=category_tree)
|
||||||
|
except Exception as e:
|
||||||
|
# Bei Fehler leere Kategorienliste übergeben und Fehler protokollieren
|
||||||
|
print(f"Fehler beim Laden der Mindmap-Kategorien: {str(e)}")
|
||||||
|
return render_template('mindmap.html', categories=[], error=str(e))
|
||||||
|
|
||||||
# Route for user profile
|
# Route for user profile
|
||||||
@app.route('/profile')
|
@app.route('/profile')
|
||||||
@@ -160,16 +398,47 @@ def mindmap():
|
|||||||
def profile():
|
def profile():
|
||||||
# Lade Benutzer-Mindmaps
|
# Lade Benutzer-Mindmaps
|
||||||
user_mindmaps = UserMindmap.query.filter_by(user_id=current_user.id).all()
|
user_mindmaps = UserMindmap.query.filter_by(user_id=current_user.id).all()
|
||||||
|
|
||||||
# Lade Statistiken
|
# Lade Statistiken
|
||||||
thought_count = Thought.query.filter_by(user_id=current_user.id).count()
|
thought_count = Thought.query.filter_by(user_id=current_user.id).count()
|
||||||
bookmark_count = db.session.query(func.count()).select_from(user_thought_bookmark).filter(
|
bookmark_count = db.session.query(user_thought_bookmark).filter(
|
||||||
user_thought_bookmark.c.user_id == current_user.id
|
user_thought_bookmark.c.user_id == current_user.id).count()
|
||||||
).scalar()
|
|
||||||
|
# Berechne tatsächliche Werte für Benutzerstatistiken
|
||||||
|
contributions_count = Comment.query.filter_by(user_id=current_user.id).count()
|
||||||
|
|
||||||
|
# Berechne Verbindungen (Anzahl der Gedankenverknüpfungen)
|
||||||
|
connections_count = ThoughtRelation.query.filter(
|
||||||
|
(ThoughtRelation.source_id.in_(
|
||||||
|
db.session.query(Thought.id).filter_by(user_id=current_user.id)
|
||||||
|
)) |
|
||||||
|
(ThoughtRelation.target_id.in_(
|
||||||
|
db.session.query(Thought.id).filter_by(user_id=current_user.id)
|
||||||
|
))
|
||||||
|
).count()
|
||||||
|
|
||||||
|
# Berechne durchschnittliche Bewertung der Gedanken des Benutzers
|
||||||
|
avg_rating = db.session.query(func.avg(ThoughtRating.relevance_score)).join(
|
||||||
|
Thought, Thought.id == ThoughtRating.thought_id
|
||||||
|
).filter(Thought.user_id == current_user.id).scalar() or 0
|
||||||
|
|
||||||
|
# Hole die Anzahl der Follower (falls implementiert)
|
||||||
|
# In diesem Beispiel nehmen wir an, dass es keine Follower-Funktionalität gibt
|
||||||
|
followers_count = 0
|
||||||
|
|
||||||
|
# Hole den Standort des Benutzers aus der Datenbank, falls vorhanden
|
||||||
|
location = "Deutschland" # Standardwert
|
||||||
|
|
||||||
return render_template('profile.html',
|
return render_template('profile.html',
|
||||||
|
user=current_user,
|
||||||
user_mindmaps=user_mindmaps,
|
user_mindmaps=user_mindmaps,
|
||||||
thought_count=thought_count,
|
thought_count=thought_count,
|
||||||
bookmark_count=bookmark_count)
|
bookmark_count=bookmark_count,
|
||||||
|
connections_count=connections_count,
|
||||||
|
contributions_count=contributions_count,
|
||||||
|
followers_count=followers_count,
|
||||||
|
rating=round(avg_rating, 1),
|
||||||
|
location=location)
|
||||||
|
|
||||||
# Route für Benutzereinstellungen
|
# Route für Benutzereinstellungen
|
||||||
@app.route('/settings', methods=['GET', 'POST'])
|
@app.route('/settings', methods=['GET', 'POST'])
|
||||||
@@ -328,34 +597,45 @@ def get_public_mindmap():
|
|||||||
return jsonify(result)
|
return jsonify(result)
|
||||||
|
|
||||||
def build_category_tree(category):
|
def build_category_tree(category):
|
||||||
"""Rekursive Funktion zum Aufbau der Kategoriestruktur."""
|
"""
|
||||||
nodes = []
|
Erstellt eine Baumstruktur für eine Kategorie mit all ihren Unterkategorien
|
||||||
# Hole alle Knoten in dieser Kategorie
|
und dazugehörigen Knoten
|
||||||
for node in category.nodes:
|
|
||||||
if node.is_public:
|
|
||||||
nodes.append({
|
|
||||||
'id': node.id,
|
|
||||||
'name': node.name,
|
|
||||||
'description': node.description,
|
|
||||||
'color_code': node.color_code,
|
|
||||||
'thought_count': len(node.thoughts)
|
|
||||||
})
|
|
||||||
|
|
||||||
# Rekursiv durch Unterkaterorien
|
Args:
|
||||||
children = []
|
category: Ein Category-Objekt
|
||||||
for child in category.children:
|
|
||||||
children.append(build_category_tree(child))
|
|
||||||
|
|
||||||
return {
|
Returns:
|
||||||
|
dict: Eine JSON-serialisierbare Darstellung der Kategoriestruktur
|
||||||
|
"""
|
||||||
|
# Kategorie-Basisinformationen
|
||||||
|
category_dict = {
|
||||||
'id': category.id,
|
'id': category.id,
|
||||||
'name': category.name,
|
'name': category.name,
|
||||||
'description': category.description,
|
'description': category.description,
|
||||||
'color_code': category.color_code,
|
'color_code': category.color_code,
|
||||||
'icon': category.icon,
|
'icon': category.icon,
|
||||||
'nodes': nodes,
|
'nodes': [],
|
||||||
'children': children
|
'children': []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Knoten zur Kategorie hinzufügen
|
||||||
|
if category.nodes:
|
||||||
|
for node in category.nodes:
|
||||||
|
category_dict['nodes'].append({
|
||||||
|
'id': node.id,
|
||||||
|
'name': node.name,
|
||||||
|
'description': node.description or '',
|
||||||
|
'color_code': node.color_code or '#9F7AEA',
|
||||||
|
'thought_count': len(node.thoughts) if hasattr(node, 'thoughts') else 0
|
||||||
|
})
|
||||||
|
|
||||||
|
# Rekursiv Unterkategorien hinzufügen
|
||||||
|
if category.children:
|
||||||
|
for child in category.children:
|
||||||
|
category_dict['children'].append(build_category_tree(child))
|
||||||
|
|
||||||
|
return category_dict
|
||||||
|
|
||||||
@app.route('/api/mindmap/user/<int:mindmap_id>')
|
@app.route('/api/mindmap/user/<int:mindmap_id>')
|
||||||
@login_required
|
@login_required
|
||||||
def get_user_mindmap(mindmap_id):
|
def get_user_mindmap(mindmap_id):
|
||||||
@@ -874,17 +1154,21 @@ def bookmark_thought(thought_id):
|
|||||||
|
|
||||||
@app.route('/api/categories')
|
@app.route('/api/categories')
|
||||||
def get_categories():
|
def get_categories():
|
||||||
"""Liefert alle verfügbaren Kategorien."""
|
"""API-Endpunkt, der alle Kategorien als hierarchische Struktur zurückgibt"""
|
||||||
categories = Category.query.all()
|
try:
|
||||||
|
# Hole alle Kategorien der obersten Ebene
|
||||||
|
categories = Category.query.filter_by(parent_id=None).all()
|
||||||
|
|
||||||
return jsonify([{
|
# Transformiere zu einer Baumstruktur
|
||||||
'id': category.id,
|
category_tree = [build_category_tree(cat) for cat in categories]
|
||||||
'name': category.name,
|
|
||||||
'description': category.description,
|
return jsonify(category_tree)
|
||||||
'color_code': category.color_code,
|
except Exception as e:
|
||||||
'icon': category.icon,
|
print(f"Fehler beim Abrufen der Kategorien: {str(e)}")
|
||||||
'parent_id': category.parent_id
|
return jsonify({
|
||||||
} for category in categories])
|
'success': False,
|
||||||
|
'error': 'Kategorien konnten nicht geladen werden'
|
||||||
|
}), 500
|
||||||
|
|
||||||
@app.route('/api/set_dark_mode', methods=['POST'])
|
@app.route('/api/set_dark_mode', methods=['POST'])
|
||||||
def set_dark_mode():
|
def set_dark_mode():
|
||||||
@@ -930,7 +1214,7 @@ def too_many_requests(e):
|
|||||||
# OpenAI-Integration für KI-Assistenz
|
# OpenAI-Integration für KI-Assistenz
|
||||||
@app.route('/api/assistant', methods=['POST'])
|
@app.route('/api/assistant', methods=['POST'])
|
||||||
def chat_with_assistant():
|
def chat_with_assistant():
|
||||||
"""Chatbot-API mit OpenAI Integration."""
|
"""Chatbot-API mit OpenAI Integration und Datenbankzugriff."""
|
||||||
data = request.json
|
data = request.json
|
||||||
|
|
||||||
# Prüfen, ob wir ein einzelnes Prompt oder ein messages-Array haben
|
# Prüfen, ob wir ein einzelnes Prompt oder ein messages-Array haben
|
||||||
@@ -943,9 +1227,9 @@ def chat_with_assistant():
|
|||||||
|
|
||||||
# Extrahiere Systemnachricht falls vorhanden, sonst Standard-Systemnachricht
|
# Extrahiere Systemnachricht falls vorhanden, sonst Standard-Systemnachricht
|
||||||
system_message = next((msg['content'] for msg in messages if msg['role'] == 'system'),
|
system_message = next((msg['content'] for msg in messages if msg['role'] == 'system'),
|
||||||
"Du bist ein hilfreicher Assistent, der Menschen dabei hilft, "
|
"Du bist ein hilfreicher Assistent, der Zugriff auf die Wissensdatenbank hat. "
|
||||||
"Wissen zu organisieren und zu verknüpfen. Liefere informative, "
|
"Du kannst Informationen zu Gedanken, Kategorien und Mindmaps liefern. "
|
||||||
"sachliche und gut strukturierte Antworten.")
|
"Antworte informativ, sachlich und gut strukturiert auf Deutsch.")
|
||||||
|
|
||||||
# Formatiere Nachrichten für OpenAI API
|
# Formatiere Nachrichten für OpenAI API
|
||||||
api_messages = [{"role": "system", "content": system_message}]
|
api_messages = [{"role": "system", "content": system_message}]
|
||||||
@@ -966,9 +1250,9 @@ def chat_with_assistant():
|
|||||||
|
|
||||||
# Zusammenfassen mehrerer Gedanken oder Analyse anfordern
|
# Zusammenfassen mehrerer Gedanken oder Analyse anfordern
|
||||||
system_message = (
|
system_message = (
|
||||||
"Du bist ein hilfreicher Assistent, der Menschen dabei hilft, "
|
"Du bist ein hilfreicher Assistent, der Zugriff auf die Wissensdatenbank hat. "
|
||||||
"Wissen zu organisieren und zu verknüpfen. Liefere informative, "
|
"Du kannst Informationen zu Gedanken, Kategorien und Mindmaps liefern. "
|
||||||
"sachliche und gut strukturierte Antworten."
|
"Antworte informativ, sachlich und gut strukturiert auf Deutsch."
|
||||||
)
|
)
|
||||||
|
|
||||||
if context:
|
if context:
|
||||||
@@ -979,14 +1263,41 @@ def chat_with_assistant():
|
|||||||
{"role": "user", "content": prompt}
|
{"role": "user", "content": prompt}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Extrahiere die letzte Benutzernachricht für Datenbankabfragen
|
||||||
|
user_message = next((msg['content'] for msg in reversed(api_messages) if msg['role'] == 'user'), '')
|
||||||
|
|
||||||
|
# Prüfen, ob die Anfrage nach Datenbankinformationen sucht
|
||||||
|
db_context = check_database_query(user_message)
|
||||||
|
|
||||||
|
if db_context:
|
||||||
|
# Erweitere den Kontext mit Datenbankinformationen
|
||||||
|
api_messages.append({
|
||||||
|
"role": "system",
|
||||||
|
"content": f"Hier sind relevante Informationen aus der Datenbank:\n\n{db_context}"
|
||||||
|
})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Überprüfen ob OpenAI API-Key konfiguriert ist
|
||||||
|
if not client.api_key or client.api_key.startswith("sk-dummy"):
|
||||||
|
print("Warnung: OpenAI API-Key ist nicht oder nur als Dummy konfiguriert!")
|
||||||
|
return jsonify({
|
||||||
|
'error': 'Der OpenAI API-Key ist nicht korrekt konfiguriert. Bitte konfigurieren Sie die Umgebungsvariable OPENAI_API_KEY.'
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
# API-Aufruf mit Timeout
|
||||||
|
import time
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
response = client.chat.completions.create(
|
response = client.chat.completions.create(
|
||||||
model="gpt-4o-mini",
|
model="gpt-4o-mini",
|
||||||
messages=api_messages,
|
messages=api_messages,
|
||||||
max_tokens=300,
|
max_tokens=600, # Erhöht für längere, detailliertere Antworten
|
||||||
temperature=0.7
|
temperature=0.7,
|
||||||
|
timeout=20 # 20 Sekunden Timeout
|
||||||
)
|
)
|
||||||
|
|
||||||
|
print(f"OpenAI API-Antwortzeit: {time.time() - start_time:.2f} Sekunden")
|
||||||
|
|
||||||
answer = response.choices[0].message.content
|
answer = response.choices[0].message.content
|
||||||
|
|
||||||
# Für das neue Format erwarten wir response statt answer
|
# Für das neue Format erwarten wir response statt answer
|
||||||
@@ -995,134 +1306,77 @@ def chat_with_assistant():
|
|||||||
})
|
})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
|
print(f"Fehler bei der OpenAI-Anfrage: {str(e)}")
|
||||||
|
print(traceback.format_exc())
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'error': f'Fehler bei der OpenAI-Anfrage: {str(e)}'
|
'error': f'Fehler bei der OpenAI-Anfrage: {str(e)}'
|
||||||
}), 500
|
}), 500
|
||||||
|
|
||||||
# App-Kontext-Funktion für Initialisierung der Datenbank
|
def check_database_query(user_message):
|
||||||
def create_default_categories():
|
"""
|
||||||
"""Erstellt die Standard-Kategorien und wissenschaftlichen Bereiche."""
|
Überprüft, ob die Benutzeranfrage nach Datenbankinformationen sucht und extrahiert
|
||||||
categories = [
|
relevante Daten aus der Datenbank.
|
||||||
{
|
"""
|
||||||
'name': 'Naturwissenschaften',
|
context = []
|
||||||
'description': 'Empirische Untersuchung und Erklärung natürlicher Phänomene',
|
|
||||||
'color_code': '#4CAF50',
|
|
||||||
'icon': 'flask',
|
|
||||||
'children': [
|
|
||||||
{
|
|
||||||
'name': 'Physik',
|
|
||||||
'description': 'Studium der Materie, Energie und deren Wechselwirkungen',
|
|
||||||
'color_code': '#81C784',
|
|
||||||
'icon': 'atom'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'Biologie',
|
|
||||||
'description': 'Wissenschaft des Lebens und lebender Organismen',
|
|
||||||
'color_code': '#66BB6A',
|
|
||||||
'icon': 'leaf'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'Chemie',
|
|
||||||
'description': 'Wissenschaft der Materie, ihrer Eigenschaften und Reaktionen',
|
|
||||||
'color_code': '#A5D6A7',
|
|
||||||
'icon': 'vial'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'Sozialwissenschaften',
|
|
||||||
'description': 'Untersuchung von Gesellschaft und menschlichem Verhalten',
|
|
||||||
'color_code': '#2196F3',
|
|
||||||
'icon': 'users',
|
|
||||||
'children': [
|
|
||||||
{
|
|
||||||
'name': 'Psychologie',
|
|
||||||
'description': 'Wissenschaftliches Studium des Geistes und Verhaltens',
|
|
||||||
'color_code': '#64B5F6',
|
|
||||||
'icon': 'brain'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'Soziologie',
|
|
||||||
'description': 'Studium sozialer Beziehungen und Institutionen',
|
|
||||||
'color_code': '#42A5F5',
|
|
||||||
'icon': 'network-wired'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'Geisteswissenschaften',
|
|
||||||
'description': 'Studium menschlicher Kultur und Kreativität',
|
|
||||||
'color_code': '#9C27B0',
|
|
||||||
'icon': 'book',
|
|
||||||
'children': [
|
|
||||||
{
|
|
||||||
'name': 'Philosophie',
|
|
||||||
'description': 'Untersuchung grundlegender Fragen über Existenz, Wissen und Ethik',
|
|
||||||
'color_code': '#BA68C8',
|
|
||||||
'icon': 'lightbulb'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'Geschichte',
|
|
||||||
'description': 'Studium der Vergangenheit und ihres Einflusses auf die Gegenwart',
|
|
||||||
'color_code': '#AB47BC',
|
|
||||||
'icon': 'landmark'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'Literatur',
|
|
||||||
'description': 'Studium literarischer Werke und ihrer Bedeutung',
|
|
||||||
'color_code': '#CE93D8',
|
|
||||||
'icon': 'feather'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'Technologie',
|
|
||||||
'description': 'Anwendung wissenschaftlicher Erkenntnisse für praktische Zwecke',
|
|
||||||
'color_code': '#FF9800',
|
|
||||||
'icon': 'microchip',
|
|
||||||
'children': [
|
|
||||||
{
|
|
||||||
'name': 'Informatik',
|
|
||||||
'description': 'Studium von Computern und Berechnungssystemen',
|
|
||||||
'color_code': '#FFB74D',
|
|
||||||
'icon': 'laptop-code'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'Künstliche Intelligenz',
|
|
||||||
'description': 'Entwicklung intelligenter Maschinen und Software',
|
|
||||||
'color_code': '#FFA726',
|
|
||||||
'icon': 'robot'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
# Kategorien in die Datenbank einfügen
|
# Prüfen auf verschiedene Datenbankabfragemuster
|
||||||
for category_data in categories:
|
if any(keyword in user_message.lower() for keyword in ['gedanken', 'thought', 'beitrag', 'inhalt']):
|
||||||
children_data = category_data.pop('children', [])
|
# Suche nach relevanten Gedanken
|
||||||
category = Category(**category_data)
|
thoughts = Thought.query.filter(
|
||||||
db.session.add(category)
|
db.or_(
|
||||||
db.session.flush() # Um die ID zu generieren
|
Thought.title.ilike(f'%{word}%') for word in user_message.split()
|
||||||
|
if len(word) > 3 # Nur längere Wörter zur Suche verwenden
|
||||||
|
)
|
||||||
|
).limit(5).all()
|
||||||
|
|
||||||
# Unterkategorien hinzufügen
|
if thoughts:
|
||||||
for child_data in children_data:
|
context.append("Relevante Gedanken:")
|
||||||
child = Category(**child_data, parent_id=category.id)
|
for thought in thoughts:
|
||||||
db.session.add(child)
|
context.append(f"- Titel: {thought.title}")
|
||||||
|
context.append(f" Zusammenfassung: {thought.abstract if thought.abstract else 'Keine Zusammenfassung verfügbar'}")
|
||||||
|
context.append(f" Keywords: {thought.keywords if thought.keywords else 'Keine Keywords verfügbar'}")
|
||||||
|
context.append("")
|
||||||
|
|
||||||
db.session.commit()
|
if any(keyword in user_message.lower() for keyword in ['kategorie', 'category', 'themengebiet', 'bereich']):
|
||||||
print("Standard-Kategorien wurden erstellt!")
|
# Suche nach Kategorien
|
||||||
|
categories = Category.query.filter(
|
||||||
|
db.or_(
|
||||||
|
Category.name.ilike(f'%{word}%') for word in user_message.split()
|
||||||
|
if len(word) > 3
|
||||||
|
)
|
||||||
|
).limit(5).all()
|
||||||
|
|
||||||
def initialize_database():
|
if categories:
|
||||||
"""Initialisiert die Datenbank, falls sie noch nicht existiert."""
|
context.append("Relevante Kategorien:")
|
||||||
db.create_all()
|
for category in categories:
|
||||||
|
context.append(f"- Name: {category.name}")
|
||||||
|
context.append(f" Beschreibung: {category.description}")
|
||||||
|
context.append("")
|
||||||
|
|
||||||
# Überprüfe, ob bereits Kategorien existieren
|
if any(keyword in user_message.lower() for keyword in ['mindmap', 'karte', 'visualisierung']):
|
||||||
if Category.query.count() == 0:
|
# Suche nach öffentlichen Mindmaps
|
||||||
create_default_categories()
|
mindmap_nodes = MindMapNode.query.filter(
|
||||||
|
db.and_(
|
||||||
|
MindMapNode.is_public == True,
|
||||||
|
db.or_(
|
||||||
|
MindMapNode.name.ilike(f'%{word}%') for word in user_message.split()
|
||||||
|
if len(word) > 3
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).limit(5).all()
|
||||||
|
|
||||||
# Führe die Datenbankinitialisierung beim Starten der App aus
|
if mindmap_nodes:
|
||||||
with app.app_context():
|
context.append("Relevante Mindmap-Knoten:")
|
||||||
initialize_database()
|
for node in mindmap_nodes:
|
||||||
|
context.append(f"- Name: {node.name}")
|
||||||
|
context.append(f" Beschreibung: {node.description if node.description else 'Keine Beschreibung verfügbar'}")
|
||||||
|
if node.category:
|
||||||
|
context.append(f" Kategorie: {node.category.name}")
|
||||||
|
context.append("")
|
||||||
|
|
||||||
|
return "\n".join(context) if context else ""
|
||||||
|
|
||||||
@app.route('/search')
|
@app.route('/search')
|
||||||
def search_thoughts_page():
|
def search_thoughts_page():
|
||||||
@@ -1145,24 +1399,10 @@ def my_account():
|
|||||||
@app.route('/static/network-bg.jpg')
|
@app.route('/static/network-bg.jpg')
|
||||||
@app.route('/static/network-bg.svg')
|
@app.route('/static/network-bg.svg')
|
||||||
def dummy_network_bg():
|
def dummy_network_bg():
|
||||||
"""Stellt einen Fallback für alte Netzwerk-Hintergrund-Anfragen bereit."""
|
"""Leere Antwort für die nicht mehr verwendeten Netzwerk-Hintergrundbilder."""
|
||||||
return redirect(url_for('static', filename='img/backgrounds/network-bg.jpg'))
|
return '', 200
|
||||||
|
|
||||||
@app.route('/static/css/src/cybernetwork-bg.css')
|
|
||||||
def serve_cybernetwork_css():
|
|
||||||
"""Stellt das CSS für den cybertechnischen Netzwerk-Hintergrund bereit."""
|
|
||||||
return app.send_static_file('css/src/cybernetwork-bg.css')
|
|
||||||
|
|
||||||
@app.route('/static/js/modules/cyber-network.js')
|
|
||||||
def serve_cybernetwork_js():
|
|
||||||
"""Stellt das JavaScript-Modul für den cybertechnischen Netzwerk-Hintergrund bereit."""
|
|
||||||
return app.send_static_file('js/modules/cyber-network.js')
|
|
||||||
|
|
||||||
@app.route('/static/js/modules/cyber-network-init.js')
|
|
||||||
def serve_cybernetwork_init_js():
|
|
||||||
"""Stellt das Initialisierungs-JavaScript für den cybertechnischen Netzwerk-Hintergrund bereit."""
|
|
||||||
return app.send_static_file('js/modules/cyber-network-init.js')
|
|
||||||
|
|
||||||
|
# Route zum expliziten Neu-Laden der Umgebungsvariablen
|
||||||
@app.route('/admin/reload-env', methods=['POST'])
|
@app.route('/admin/reload-env', methods=['POST'])
|
||||||
@admin_required
|
@admin_required
|
||||||
def reload_env():
|
def reload_env():
|
||||||
@@ -1193,3 +1433,50 @@ if __name__ == '__main__':
|
|||||||
# Make sure tables exist
|
# Make sure tables exist
|
||||||
db.create_all()
|
db.create_all()
|
||||||
app.run(host="0.0.0.0", debug=True)
|
app.run(host="0.0.0.0", debug=True)
|
||||||
|
|
||||||
|
@app.route('/api/refresh-mindmap')
|
||||||
|
def refresh_mindmap():
|
||||||
|
"""
|
||||||
|
API-Endpunkt zum Neuladen der Mindmap-Daten,
|
||||||
|
wenn die Datenbank-Verbindung vorübergehend fehlgeschlagen ist
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Stelle sicher, dass wir Kategorien haben
|
||||||
|
if Category.query.count() == 0:
|
||||||
|
create_default_categories()
|
||||||
|
|
||||||
|
# Hole alle Kategorien und Knoten
|
||||||
|
categories = Category.query.filter_by(parent_id=None).all()
|
||||||
|
category_tree = [build_category_tree(cat) for cat in categories]
|
||||||
|
|
||||||
|
# Hole alle Mindmap-Knoten
|
||||||
|
nodes = MindMapNode.query.all()
|
||||||
|
node_data = []
|
||||||
|
|
||||||
|
for node in nodes:
|
||||||
|
node_obj = {
|
||||||
|
'id': node.id,
|
||||||
|
'name': node.name,
|
||||||
|
'description': node.description or '',
|
||||||
|
'color_code': node.color_code or '#9F7AEA',
|
||||||
|
'thought_count': len(node.thoughts),
|
||||||
|
'category_id': node.category_id
|
||||||
|
}
|
||||||
|
|
||||||
|
# Verbindungen hinzufügen
|
||||||
|
node_obj['connections'] = [{'target': child.id} for child in node.children]
|
||||||
|
|
||||||
|
node_data.append(node_obj)
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'categories': category_tree,
|
||||||
|
'nodes': node_data
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Fehler beim Neuladen der Mindmap: {str(e)}")
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'error': 'Datenbankverbindung konnte nicht hergestellt werden'
|
||||||
|
}), 500
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
# Netscape HTTP Cookie File
|
|
||||||
# https://curl.se/docs/http-cookies.html
|
|
||||||
# This file was generated by libcurl! Edit at your own risk.
|
|
||||||
|
|
||||||
Binary file not shown.
@@ -5,7 +5,7 @@ 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
|
flask-cors==4.0.0
|
||||||
gunicorn==21.2.0
|
gunicorn==21.2.0
|
||||||
|
|||||||
@@ -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,21 +1,25 @@
|
|||||||
/* ChatGPT Assistent Styles */
|
/* ChatGPT Assistent Styles - Verbesserte Version */
|
||||||
#chatgpt-assistant {
|
#chatgpt-assistant {
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: 'Inter', sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
#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);
|
||||||
}
|
}
|
||||||
|
|
||||||
#assistant-toggle {
|
#assistant-toggle {
|
||||||
transition: transform 0.3s ease;
|
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 {
|
#assistant-toggle:hover {
|
||||||
transform: scale(1.1);
|
transform: scale(1.1) rotate(10deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
#assistant-history {
|
#assistant-history {
|
||||||
@@ -40,27 +44,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 +133,54 @@
|
|||||||
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;
|
||||||
|
background-color: #888;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0 2px;
|
||||||
|
opacity: 0.4;
|
||||||
|
animation: bounce 1.4s infinite ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chatgpt-assistant {
|
||||||
|
right: 1rem;
|
||||||
|
bottom: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Footer immer unten */
|
/* Footer immer unten */
|
||||||
html, body {
|
html, body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
@@ -1,426 +1,421 @@
|
|||||||
/* Globale Variablen */
|
/* Base Styles - Dark, Mystical Theme */
|
||||||
:root {
|
|
||||||
--dark-bg: #0e1220;
|
|
||||||
--dark-card-bg: rgba(24, 28, 45, 0.8);
|
|
||||||
--dark-element-bg: rgba(24, 28, 45, 0.8);
|
|
||||||
--light-bg: #f0f4f8;
|
|
||||||
--light-card-bg: rgba(255, 255, 255, 0.85);
|
|
||||||
--accent-color: #b38fff;
|
|
||||||
--accent-gradient: linear-gradient(135deg, #b38fff, #58a9ff);
|
|
||||||
--accent-gradient-hover: linear-gradient(135deg, #c7a8ff, #70b5ff);
|
|
||||||
--blur-amount: 20px;
|
|
||||||
--border-radius: 28px;
|
|
||||||
--card-border-radius: 24px;
|
|
||||||
--button-radius: 18px;
|
|
||||||
--nav-item-radius: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark Mode Einstellungen */
|
/* Global Variables */
|
||||||
html.dark {
|
:root {
|
||||||
color-scheme: dark;
|
--font-sans: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
|
--font-mono: 'JetBrains Mono', SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
|
||||||
|
/* Light Theme */
|
||||||
|
--bg-primary-light: #f8fafc;
|
||||||
|
--bg-secondary-light: #f1f5f9;
|
||||||
|
--bg-tertiary-light: #e2e8f0;
|
||||||
|
--text-primary-light: #1e293b;
|
||||||
|
--text-secondary-light: #475569;
|
||||||
|
--accent-primary-light: #7c3aed;
|
||||||
|
--accent-secondary-light: #8b5cf6;
|
||||||
|
--accent-tertiary-light: #a78bfa;
|
||||||
|
--border-light: #e2e8f0;
|
||||||
|
--shadow-light: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||||
|
--glow-light: 0 0 15px rgba(139, 92, 246, 0.3);
|
||||||
|
|
||||||
|
/* Dark Theme */
|
||||||
|
--bg-primary-dark: #0a0e19;
|
||||||
|
--bg-secondary-dark: #111827;
|
||||||
|
--bg-tertiary-dark: #1f2937;
|
||||||
|
--text-primary-dark: #f9fafb;
|
||||||
|
--text-secondary-dark: #e5e7eb;
|
||||||
|
--accent-primary-dark: #6d28d9;
|
||||||
|
--accent-secondary-dark: #8b5cf6;
|
||||||
|
--accent-tertiary-dark: #a78bfa;
|
||||||
|
--border-dark: #1f2937;
|
||||||
|
--shadow-dark: 0 4px 6px -1px rgba(0, 0, 0, 0.5), 0 2px 4px -1px rgba(0, 0, 0, 0.3);
|
||||||
|
--glow-dark: 0 0 15px rgba(124, 58, 237, 0.5);
|
||||||
|
|
||||||
|
/* Transitions */
|
||||||
|
--transition-fast: 150ms ease-in-out;
|
||||||
|
--transition-normal: 300ms ease-in-out;
|
||||||
|
--transition-slow: 500ms ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Base Styles */
|
/* Base Styles */
|
||||||
html, body {
|
html {
|
||||||
background-color: var(--dark-bg) !important;
|
font-family: var(--font-sans);
|
||||||
min-height: 100vh;
|
-webkit-font-smoothing: antialiased;
|
||||||
width: 100%;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
color: #ffffff;
|
scroll-behavior: smooth;
|
||||||
margin: 0;
|
}
|
||||||
padding: 0;
|
|
||||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
body {
|
||||||
|
transition: background-color var(--transition-normal), color var(--transition-normal);
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
transition: background-color 0.5s ease, color 0.5s ease;
|
min-height: 100vh;
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Sicherstellen, dass der dunkle Hintergrund die gesamte Seite abdeckt */
|
/* Dark Mode */
|
||||||
#app-container, .container, main, .mx-auto, .py-12, #content-wrapper {
|
html.dark body {
|
||||||
background-color: transparent !important;
|
background-color: var(--bg-primary-dark);
|
||||||
width: 100%;
|
color: var(--text-primary-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Light Mode Einstellungen */
|
/* Light Mode */
|
||||||
html.light, html.light body {
|
body {
|
||||||
background-color: var(--light-bg) !important;
|
background-color: var(--bg-primary-light);
|
||||||
color: #1a202c;
|
color: var(--text-primary-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Große Headings mit verbesserten Stilen */
|
/* Typography */
|
||||||
h1.hero-heading {
|
h1, h2, h3, h4, h5, h6 {
|
||||||
font-size: clamp(2.5rem, 8vw, 5rem);
|
font-weight: 600;
|
||||||
line-height: 1.1;
|
|
||||||
font-weight: 800;
|
|
||||||
letter-spacing: -0.03em;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2.section-heading {
|
|
||||||
font-size: clamp(1.75rem, 5vw, 3rem);
|
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-heading {
|
||||||
|
font-size: 2.75rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: -0.02em;
|
letter-spacing: -0.02em;
|
||||||
margin-bottom: 1.25rem;
|
line-height: 1.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Verbesserte Glasmorphismus-Stile */
|
.section-heading {
|
||||||
.glass-morphism {
|
font-size: 2rem;
|
||||||
background: var(--dark-card-bg);
|
font-weight: 700;
|
||||||
backdrop-filter: blur(var(--blur-amount));
|
letter-spacing: -0.01em;
|
||||||
-webkit-backdrop-filter: blur(var(--blur-amount));
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
||||||
border-radius: var(--card-border-radius);
|
|
||||||
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.35);
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.glass-morphism:hover {
|
.gradient-text {
|
||||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
background-clip: text;
|
||||||
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.45);
|
-webkit-background-clip: text;
|
||||||
transform: translateY(-2px);
|
color: transparent;
|
||||||
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.glass-morphism-light {
|
html.dark .gradient-text {
|
||||||
background: var(--light-card-bg);
|
background-image: linear-gradient(135deg, var(--accent-primary-dark), var(--accent-secondary-dark));
|
||||||
backdrop-filter: blur(var(--blur-amount));
|
text-shadow: var(--glow-dark);
|
||||||
-webkit-backdrop-filter: blur(var(--blur-amount));
|
|
||||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
|
||||||
border-radius: var(--card-border-radius);
|
|
||||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.12);
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.glass-morphism-light:hover {
|
.gradient-text {
|
||||||
border: 1px solid rgba(0, 0, 0, 0.15);
|
background-image: linear-gradient(135deg, var(--accent-primary-light), var(--accent-secondary-light));
|
||||||
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.18);
|
text-shadow: var(--glow-light);
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Verbesserte Navbar-Styles */
|
/* Mystical elements */
|
||||||
.glass-navbar-dark {
|
.mystical-border {
|
||||||
background: rgba(14, 18, 32, 0.85);
|
|
||||||
backdrop-filter: blur(15px);
|
|
||||||
-webkit-backdrop-filter: blur(15px);
|
|
||||||
border-color: rgba(255, 255, 255, 0.1);
|
|
||||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25);
|
|
||||||
border-radius: 0 0 20px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.glass-navbar-light {
|
|
||||||
background: rgba(255, 255, 255, 0.85);
|
|
||||||
backdrop-filter: blur(15px);
|
|
||||||
-webkit-backdrop-filter: blur(15px);
|
|
||||||
border-color: rgba(0, 0, 0, 0.05);
|
|
||||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08);
|
|
||||||
border-radius: 0 0 20px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Verbesserte Button-Stile mit besserer Lesbarkeit und stärkeren Farbverläufen */
|
|
||||||
.btn, button, .button, [type="button"], [type="submit"] {
|
|
||||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
border-radius: var(--button-radius);
|
|
||||||
padding: 0.75rem 1.5rem;
|
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: 0.4px;
|
|
||||||
color: rgba(255, 255, 255, 1);
|
|
||||||
background: linear-gradient(135deg, rgba(99, 102, 241, 0.8), rgba(168, 85, 247, 0.8));
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
|
||||||
box-shadow: 0 6px 14px rgba(0, 0, 0, 0.2);
|
|
||||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
|
||||||
backdrop-filter: blur(15px);
|
|
||||||
-webkit-backdrop-filter: blur(15px);
|
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
|
||||||
outline: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn:hover, button:hover, .button:hover, [type="button"]:hover, [type="submit"]:hover {
|
.mystical-border::before {
|
||||||
background: linear-gradient(135deg, rgba(129, 140, 248, 0.9), rgba(192, 132, 252, 0.9));
|
content: '';
|
||||||
transform: translateY(-3px);
|
position: absolute;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
top: 0;
|
||||||
box-shadow: 0 8px 22px rgba(0, 0, 0, 0.25), 0 0 12px rgba(179, 143, 255, 0.35);
|
left: 0;
|
||||||
color: white;
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
border: 1px solid;
|
||||||
|
border-radius: inherit;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn:active, button:active, .button:active, [type="button"]:active, [type="submit"]:active {
|
html.dark .mystical-border::before {
|
||||||
transform: translateY(1px);
|
border-color: var(--accent-primary-dark);
|
||||||
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.2);
|
box-shadow: var(--glow-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Navigation Stile mit verbesserten Farbverläufen */
|
.mystical-border::before {
|
||||||
|
border-color: var(--accent-primary-light);
|
||||||
|
box-shadow: var(--glow-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navigation Links */
|
||||||
.nav-link {
|
.nav-link {
|
||||||
transition: all 0.25s ease;
|
|
||||||
border-radius: var(--nav-item-radius);
|
|
||||||
padding: 0.625rem 1rem;
|
|
||||||
font-weight: 500;
|
|
||||||
letter-spacing: 0.3px;
|
|
||||||
color: rgba(255, 255, 255, 0.9);
|
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: visible;
|
padding: 0.5rem 0.75rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
transition: var(--transition-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .nav-link {
|
||||||
|
color: var(--text-secondary-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
color: var(--text-secondary-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .nav-link:hover {
|
||||||
|
color: var(--text-primary-dark);
|
||||||
|
background-color: rgba(31, 41, 55, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-link:hover {
|
.nav-link:hover {
|
||||||
background: rgba(179, 143, 255, 0.2);
|
color: var(--text-primary-light);
|
||||||
color: white;
|
background-color: rgba(241, 245, 249, 0.5);
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-link-active {
|
html.dark .nav-link-active {
|
||||||
background: linear-gradient(135deg, rgba(124, 58, 237, 0.3), rgba(139, 92, 246, 0.3));
|
color: var(--accent-tertiary-dark);
|
||||||
color: white;
|
background-color: rgba(109, 40, 217, 0.15);
|
||||||
font-weight: 600;
|
|
||||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.18);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-link-active::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 10%;
|
|
||||||
width: 80%;
|
|
||||||
height: 2px;
|
|
||||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.7), transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Light-Mode Navigation Stile */
|
|
||||||
.nav-link-light {
|
|
||||||
color: rgba(26, 32, 44, 0.85);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-link-light:hover {
|
|
||||||
background: rgba(179, 143, 255, 0.15);
|
|
||||||
color: rgba(26, 32, 44, 1);
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-link-light-active {
|
.nav-link-light-active {
|
||||||
background: linear-gradient(135deg, rgba(124, 58, 237, 0.2), rgba(139, 92, 246, 0.2));
|
color: var(--accent-primary-light);
|
||||||
color: rgba(26, 32, 44, 1);
|
background-color: rgba(139, 92, 246, 0.1);
|
||||||
font-weight: 600;
|
|
||||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-link-light-active::after {
|
/* Glass Morphism Effects */
|
||||||
background: linear-gradient(90deg, transparent, rgba(26, 32, 44, 0.5), transparent);
|
.glass-navbar-dark {
|
||||||
|
background-color: rgba(10, 14, 25, 0.8);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
border-color: rgba(255, 255, 255, 0.1);
|
||||||
|
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Entfernung von Gradient-Hintergrund überall */
|
.glass-navbar-light {
|
||||||
.gradient-bg, .purple-gradient, .gradient-purple-bg {
|
background-color: rgba(255, 255, 255, 0.8);
|
||||||
background: var(--dark-bg) !important;
|
backdrop-filter: blur(10px);
|
||||||
background-image: none !important;
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
border-color: rgba(226, 232, 240, 0.5);
|
||||||
|
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Verbesserte Light-Mode-Stile für Buttons */
|
.glass-morphism {
|
||||||
html.light .btn, html.light button, html.light .button,
|
transition: background-color var(--transition-normal), backdrop-filter var(--transition-normal);
|
||||||
html.light [type="button"], html.light [type="submit"] {
|
|
||||||
background: linear-gradient(135deg, rgba(124, 58, 237, 0.7), rgba(139, 92, 246, 0.7));
|
|
||||||
color: #ffffff;
|
|
||||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
|
||||||
text-shadow: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
html.light .btn:hover, html.light button:hover, html.light .button:hover,
|
/* Cards */
|
||||||
html.light [type="button"]:hover, html.light [type="submit"]:hover {
|
.mystical-card {
|
||||||
background: linear-gradient(135deg, rgba(139, 92, 246, 0.85), rgba(168, 85, 247, 0.85));
|
border-radius: 0.75rem;
|
||||||
color: #ffffff;
|
overflow: hidden;
|
||||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
transition: var(--transition-normal);
|
||||||
box-shadow: 0 8px 22px rgba(0, 0, 0, 0.12), 0 0 12px rgba(179, 143, 255, 0.2);
|
|
||||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Verbesserte Buttons mit Glasmorphismus */
|
html.dark .mystical-card {
|
||||||
.btn-primary {
|
background-color: var(--bg-secondary-dark);
|
||||||
background: linear-gradient(135deg, rgba(179, 143, 255, 0.8), rgba(88, 169, 255, 0.8));
|
border: 1px solid var(--border-dark);
|
||||||
backdrop-filter: blur(var(--blur-amount));
|
box-shadow: var(--shadow-dark);
|
||||||
-webkit-backdrop-filter: blur(var(--blur-amount));
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
||||||
border-radius: var(--button-radius);
|
|
||||||
color: white !important;
|
|
||||||
font-weight: 600;
|
|
||||||
padding: 0.75rem 1.5rem;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary:hover {
|
.mystical-card {
|
||||||
transform: translateY(-3px);
|
background-color: var(--bg-secondary-light);
|
||||||
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.35);
|
border: 1px solid var(--border-light);
|
||||||
background: linear-gradient(135deg, rgba(190, 160, 255, 0.9), rgba(100, 180, 255, 0.9));
|
box-shadow: var(--shadow-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary {
|
html.dark .mystical-card:hover {
|
||||||
background: rgba(32, 36, 55, 0.8);
|
box-shadow: var(--glow-dark), var(--shadow-dark);
|
||||||
backdrop-filter: blur(var(--blur-amount));
|
border-color: var(--accent-primary-dark);
|
||||||
-webkit-backdrop-filter: blur(var(--blur-amount));
|
}
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
border-radius: var(--button-radius);
|
.mystical-card:hover {
|
||||||
color: white;
|
box-shadow: var(--glow-light), var(--shadow-light);
|
||||||
|
border-color: var(--accent-primary-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
.mystical-button {
|
||||||
|
padding: 0.625rem 1.25rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
padding: 0.75rem 1.5rem;
|
transition: var(--transition-normal);
|
||||||
transition: all 0.3s ease;
|
position: relative;
|
||||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.2);
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary:hover {
|
.mystical-button::before {
|
||||||
transform: translateY(-3px);
|
content: '';
|
||||||
box-shadow: 0 10px 28px rgba(0, 0, 0, 0.3);
|
position: absolute;
|
||||||
background: rgba(38, 42, 65, 0.9);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Steuerungsbutton-Stil */
|
|
||||||
.control-btn {
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
background: rgba(32, 36, 55, 0.8);
|
|
||||||
color: rgba(255, 255, 255, 0.9);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
||||||
border-radius: 14px;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: 500;
|
|
||||||
transition: all 0.25s ease;
|
|
||||||
backdrop-filter: blur(15px);
|
|
||||||
-webkit-backdrop-filter: blur(15px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.control-btn:hover {
|
|
||||||
background: rgba(38, 42, 65, 0.9);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.25);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Verbesserter Farbverlauf-Text */
|
|
||||||
.gradient-text {
|
|
||||||
background: linear-gradient(135deg, rgba(200, 170, 255, 1), rgba(100, 180, 255, 1));
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
background-clip: text;
|
|
||||||
color: transparent;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: -0.02em;
|
|
||||||
text-shadow: 0 2px 10px rgba(179, 143, 255, 0.3);
|
|
||||||
filter: drop-shadow(0 2px 6px rgba(179, 143, 255, 0.3));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Globaler Hintergrund */
|
|
||||||
.full-page-bg {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100vw;
|
width: 100%;
|
||||||
height: 100vh;
|
height: 100%;
|
||||||
background-color: var(--dark-bg);
|
background: linear-gradient(135deg, transparent, rgba(255, 255, 255, 0.05), transparent);
|
||||||
z-index: -10;
|
transform: translateX(-100%);
|
||||||
|
transition: transform 0.8s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
html.light .full-page-bg {
|
.mystical-button:hover::before {
|
||||||
background-color: var(--light-bg);
|
transform: translateX(100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Animationen für Hintergrundeffekte */
|
html.dark .mystical-button-primary {
|
||||||
@keyframes float {
|
background-color: var(--accent-primary-dark);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystical-button-primary {
|
||||||
|
background-color: var(--accent-primary-light);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .mystical-button-primary:hover {
|
||||||
|
background-color: var(--accent-secondary-dark);
|
||||||
|
box-shadow: var(--glow-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystical-button-primary:hover {
|
||||||
|
background-color: var(--accent-secondary-light);
|
||||||
|
box-shadow: var(--glow-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .mystical-button-secondary {
|
||||||
|
background-color: var(--bg-tertiary-dark);
|
||||||
|
color: var(--text-primary-dark);
|
||||||
|
border: 1px solid var(--border-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystical-button-secondary {
|
||||||
|
background-color: var(--bg-tertiary-light);
|
||||||
|
color: var(--text-primary-light);
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .mystical-button-secondary:hover {
|
||||||
|
background-color: var(--bg-secondary-dark);
|
||||||
|
border-color: var(--accent-tertiary-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystical-button-secondary:hover {
|
||||||
|
background-color: var(--bg-secondary-light);
|
||||||
|
border-color: var(--accent-tertiary-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Inputs */
|
||||||
|
.mystical-input {
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
transition: var(--transition-normal);
|
||||||
|
width: 100%;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .mystical-input {
|
||||||
|
background-color: var(--bg-tertiary-dark);
|
||||||
|
border: 1px solid var(--border-dark);
|
||||||
|
color: var(--text-primary-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystical-input {
|
||||||
|
background-color: var(--bg-tertiary-light);
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
color: var(--text-primary-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .mystical-input:focus {
|
||||||
|
border-color: var(--accent-tertiary-dark);
|
||||||
|
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystical-input:focus {
|
||||||
|
border-color: var(--accent-tertiary-light);
|
||||||
|
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
@keyframes floatAnimation {
|
||||||
0% { transform: translateY(0); }
|
0% { transform: translateY(0); }
|
||||||
50% { transform: translateY(-12px); }
|
50% { transform: translateY(-5px); }
|
||||||
100% { transform: translateY(0); }
|
100% { transform: translateY(0); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulse {
|
|
||||||
0% { opacity: 0.7; transform: scale(1); }
|
|
||||||
50% { opacity: 1; transform: scale(1.05); }
|
|
||||||
100% { opacity: 0.7; transform: scale(1); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.animate-float {
|
.animate-float {
|
||||||
animation: float 6s ease-in-out infinite;
|
animation: floatAnimation 3s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate-pulse {
|
@keyframes fadeIn {
|
||||||
animation: pulse 3s ease-in-out infinite;
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Verbesserter Container für konsistente Layouts */
|
.animate-fade-in {
|
||||||
.page-container {
|
animation: fadeIn 0.5s ease-out forwards;
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 2rem 1rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dark Mode Toggle Stile */
|
/* Scroll Bars */
|
||||||
.dot {
|
::-webkit-scrollbar {
|
||||||
transform: translateX(0);
|
width: 0.5rem;
|
||||||
transition: transform 0.3s ease-in-out, background-color 0.3s ease;
|
height: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
input:checked ~ .dot {
|
html.dark ::-webkit-scrollbar-track {
|
||||||
transform: translateX(100%);
|
background: var(--bg-secondary-dark);
|
||||||
background-color: #58a9ff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
input:checked ~ .block {
|
::-webkit-scrollbar-track {
|
||||||
background-color: rgba(88, 169, 255, 0.4);
|
background: var(--bg-secondary-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Feature Cards mit Glasmorphismus und Farbverlauf */
|
html.dark ::-webkit-scrollbar-thumb {
|
||||||
.feature-card {
|
background: var(--accent-primary-dark);
|
||||||
border-radius: var(--card-border-radius);
|
border-radius: 0.25rem;
|
||||||
padding: 2rem;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
background: linear-gradient(145deg, rgba(32, 36, 55, 0.7), rgba(24, 28, 45, 0.9));
|
|
||||||
backdrop-filter: blur(var(--blur-amount));
|
|
||||||
-webkit-backdrop-filter: blur(var(--blur-amount));
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.feature-card:hover {
|
::-webkit-scrollbar-thumb {
|
||||||
transform: translateY(-5px);
|
background: var(--accent-primary-light);
|
||||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.3);
|
border-radius: 0.25rem;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
|
||||||
background: linear-gradient(145deg, rgba(40, 44, 65, 0.8), rgba(28, 32, 50, 0.95));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
html.light .feature-card {
|
html.dark ::-webkit-scrollbar-thumb:hover {
|
||||||
background: linear-gradient(145deg, rgba(255, 255, 255, 0.8), rgba(240, 240, 250, 0.9));
|
background: var(--accent-secondary-dark);
|
||||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
|
||||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
html.light .feature-card:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
background: linear-gradient(145deg, rgba(255, 255, 255, 0.9), rgba(245, 245, 255, 0.95));
|
background: var(--accent-secondary-light);
|
||||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
|
||||||
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.12);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.feature-card .icon {
|
/* Responsive Utilities */
|
||||||
width: 60px;
|
@media (max-width: 640px) {
|
||||||
height: 60px;
|
.hero-heading {
|
||||||
border-radius: 18px;
|
font-size: 2rem;
|
||||||
display: flex;
|
}
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
.section-heading {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
margin-bottom: 1.25rem;
|
}
|
||||||
background: linear-gradient(135deg, rgba(124, 58, 237, 0.8), rgba(139, 92, 246, 0.6));
|
|
||||||
color: white;
|
|
||||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.feature-card h3 {
|
/* Additional background elements */
|
||||||
font-size: 1.25rem;
|
.mystical-dot {
|
||||||
font-weight: 600;
|
position: absolute;
|
||||||
margin-bottom: 0.75rem;
|
border-radius: 50%;
|
||||||
color: rgba(255, 255, 255, 0.95);
|
opacity: 0.15;
|
||||||
|
filter: blur(3px);
|
||||||
|
z-index: -1;
|
||||||
|
transition: opacity var(--transition-normal);
|
||||||
}
|
}
|
||||||
|
|
||||||
html.light .feature-card h3 {
|
html.dark .mystical-dot {
|
||||||
color: rgba(26, 32, 44, 0.95);
|
background-color: var(--accent-primary-dark);
|
||||||
|
box-shadow: 0 0 15px var(--accent-primary-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
.feature-card p {
|
.mystical-dot {
|
||||||
color: rgba(255, 255, 255, 0.75);
|
background-color: var(--accent-primary-light);
|
||||||
line-height: 1.6;
|
box-shadow: 0 0 15px var(--accent-primary-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
html.light .feature-card p {
|
/* Accessibility */
|
||||||
color: rgba(26, 32, 44, 0.75);
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Focus styles for keyboard navigation */
|
||||||
|
:focus-visible {
|
||||||
|
outline: 2px solid var(--accent-primary-light);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark :focus-visible {
|
||||||
|
outline-color: var(--accent-primary-dark);
|
||||||
}
|
}
|
||||||
47
static/css/neural-network-background.css
Normal file
47
static/css/neural-network-background.css
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
/* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark .glass-navbar-dark {
|
||||||
|
background-color: rgba(10, 14, 25, 0.7) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body .glass-navbar-light {
|
||||||
|
background-color: rgba(255, 255, 255, 0.7) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make sure footer has proper transparency */
|
||||||
|
footer {
|
||||||
|
background-color: rgba(10, 14, 25, 0.7) !important;
|
||||||
|
}
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
/* Cybertechnisches Netzwerk Hintergrund-Overlay */
|
|
||||||
.cyber-network-bg {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: -1;
|
|
||||||
pointer-events: none;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cyber-network-bg::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: linear-gradient(125deg,
|
|
||||||
rgba(14, 14, 22, 0.95) 0%,
|
|
||||||
rgba(30, 30, 46, 0.98) 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.network-grid {
|
|
||||||
position: absolute;
|
|
||||||
width: 200%;
|
|
||||||
height: 200%;
|
|
||||||
top: -50%;
|
|
||||||
left: -50%;
|
|
||||||
background-size: 40px 40px;
|
|
||||||
background-image:
|
|
||||||
linear-gradient(to right, rgba(108, 93, 211, 0.05) 1px, transparent 1px),
|
|
||||||
linear-gradient(to bottom, rgba(108, 93, 211, 0.05) 1px, transparent 1px);
|
|
||||||
transform: perspective(500px) rotateX(60deg);
|
|
||||||
animation: grid-move 20s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.node {
|
|
||||||
position: absolute;
|
|
||||||
width: 4px;
|
|
||||||
height: 4px;
|
|
||||||
background: rgba(76, 223, 255, 0.8);
|
|
||||||
border-radius: 50%;
|
|
||||||
box-shadow: 0 0 10px rgba(76, 223, 255, 0.6);
|
|
||||||
filter: blur(1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.connection {
|
|
||||||
position: absolute;
|
|
||||||
height: 1px;
|
|
||||||
background: linear-gradient(90deg,
|
|
||||||
rgba(76, 223, 255, 0.2) 0%,
|
|
||||||
rgba(108, 93, 211, 0.3) 50%,
|
|
||||||
rgba(76, 223, 255, 0.2) 100%);
|
|
||||||
transform-origin: left center;
|
|
||||||
animation: pulse 4s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.data-packet {
|
|
||||||
position: absolute;
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: rgba(118, 69, 217, 0.8);
|
|
||||||
filter: blur(1px);
|
|
||||||
animation: travel var(--travel-time, 6s) linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.glow-overlay {
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: radial-gradient(
|
|
||||||
circle at 50% 40%,
|
|
||||||
rgba(76, 223, 255, 0.03) 0%,
|
|
||||||
rgba(108, 93, 211, 0.03) 45%,
|
|
||||||
transparent 70%
|
|
||||||
);
|
|
||||||
opacity: 0.8;
|
|
||||||
animation: pulse-glow 8s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Animations */
|
|
||||||
@keyframes grid-move {
|
|
||||||
0% {
|
|
||||||
transform: perspective(500px) rotateX(60deg) translateY(0);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: perspective(500px) rotateX(60deg) translateY(40px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse {
|
|
||||||
0%, 100% {
|
|
||||||
opacity: 0.2;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
opacity: 0.4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes travel {
|
|
||||||
0% {
|
|
||||||
transform: translateX(0) translateY(0);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
10% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
90% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: translateX(var(--travel-x, 100px)) translateY(var(--travel-y, 100px));
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse-glow {
|
|
||||||
0%, 100% {
|
|
||||||
opacity: 0.4;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1434,16 +1434,11 @@ 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 */
|
|
||||||
@import url('/static/css/src/cybernetwork-bg.css');
|
|
||||||
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');
|
||||||
|
}
|
||||||
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,9 +2,6 @@
|
|||||||
* 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
|
||||||
@@ -38,10 +35,12 @@ const MindMap = {
|
|||||||
console.log('MindMap-Anwendung wird initialisiert...');
|
console.log('MindMap-Anwendung wird initialisiert...');
|
||||||
|
|
||||||
// Initialisiere den ChatGPT-Assistenten
|
// Initialisiere den ChatGPT-Assistenten
|
||||||
|
if (typeof ChatGPTAssistant !== 'undefined') {
|
||||||
const assistant = new ChatGPTAssistant();
|
const assistant = new ChatGPTAssistant();
|
||||||
assistant.init();
|
assistant.init();
|
||||||
// Speichere als Teil von MindMap
|
// Speichere als Teil von MindMap
|
||||||
this.assistant = assistant;
|
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 +73,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,
|
||||||
|
|||||||
@@ -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,17 +171,24 @@ class ChatGPTAssistant {
|
|||||||
setupEventListeners() {
|
setupEventListeners() {
|
||||||
// Toggle-Button
|
// Toggle-Button
|
||||||
const toggleButton = document.getElementById('assistant-toggle');
|
const toggleButton = document.getElementById('assistant-toggle');
|
||||||
|
if (toggleButton) {
|
||||||
toggleButton.addEventListener('click', () => this.toggleAssistant());
|
toggleButton.addEventListener('click', () => this.toggleAssistant());
|
||||||
|
}
|
||||||
|
|
||||||
// Schließen-Button
|
// Schließen-Button
|
||||||
const closeButton = document.getElementById('assistant-close');
|
const closeButton = document.getElementById('assistant-close');
|
||||||
|
if (closeButton) {
|
||||||
closeButton.addEventListener('click', () => this.toggleAssistant(false));
|
closeButton.addEventListener('click', () => this.toggleAssistant(false));
|
||||||
|
}
|
||||||
|
|
||||||
// Senden-Button
|
// Senden-Button
|
||||||
const sendButton = document.getElementById('assistant-send');
|
const sendButton = document.getElementById('assistant-send');
|
||||||
|
if (sendButton) {
|
||||||
sendButton.addEventListener('click', () => this.sendMessage());
|
sendButton.addEventListener('click', () => this.sendMessage());
|
||||||
|
}
|
||||||
|
|
||||||
// Enter-Taste im Eingabefeld
|
// Enter-Taste im Eingabefeld
|
||||||
|
if (this.inputField) {
|
||||||
this.inputField.addEventListener('keyup', (e) => {
|
this.inputField.addEventListener('keyup', (e) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
this.sendMessage();
|
this.sendMessage();
|
||||||
@@ -118,20 +196,38 @@ class ChatGPTAssistant {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Öffnet oder schließt den Assistenten
|
* Öffnet oder schließt den Assistenten
|
||||||
* @param {boolean} state - Optional: erzwingt einen bestimmten Zustand
|
* @param {boolean} state - Optional: erzwingt einen bestimmten Zustand
|
||||||
*/
|
*/
|
||||||
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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -153,22 +249,144 @@ class ChatGPTAssistant {
|
|||||||
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%]'
|
? 'bg-primary-100 dark:bg-primary-900 text-gray-800 dark:text-white rounded-lg py-2 px-3 max-w-[85%]'
|
||||||
: 'bg-gray-100 dark:bg-dark-700 text-gray-800 dark:text-white rounded-lg py-2 px-3 max-w-[85%]';
|
: 'bg-gray-100 dark:bg-dark-700 text-gray-800 dark:text-white rounded-lg py-2 px-3 max-w-[85%]';
|
||||||
bubble.textContent = text;
|
|
||||||
|
// Formatierung des Texts (mit Markdown für Assistent-Nachrichten)
|
||||||
|
let formattedText = '';
|
||||||
|
|
||||||
|
if (sender === 'assistant' && this.markdownParser) {
|
||||||
|
// Für Assistentnachrichten Markdown verwenden
|
||||||
|
try {
|
||||||
|
formattedText = this.markdownParser.parse(text);
|
||||||
|
|
||||||
|
// CSS für Markdown-Formatierung hinzufügen
|
||||||
|
const markdownStyles = `
|
||||||
|
.markdown-bubble h1, .markdown-bubble h2, .markdown-bubble h3,
|
||||||
|
.markdown-bubble h4, .markdown-bubble h5, .markdown-bubble h6 {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
.markdown-bubble h1 { font-size: 1.4rem; }
|
||||||
|
.markdown-bubble h2 { font-size: 1.3rem; }
|
||||||
|
.markdown-bubble h3 { font-size: 1.2rem; }
|
||||||
|
.markdown-bubble h4 { font-size: 1.1rem; }
|
||||||
|
.markdown-bubble ul, .markdown-bubble ol {
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
|
.markdown-bubble ul { list-style-type: disc; }
|
||||||
|
.markdown-bubble ol { list-style-type: decimal; }
|
||||||
|
.markdown-bubble p { margin: 0.5rem 0; }
|
||||||
|
.markdown-bubble code {
|
||||||
|
font-family: monospace;
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
padding: 1px 4px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
.markdown-bubble pre {
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
|
.markdown-bubble pre code {
|
||||||
|
background-color: transparent;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.markdown-bubble blockquote {
|
||||||
|
border-left: 3px solid rgba(0, 0, 0, 0.2);
|
||||||
|
padding-left: 0.8rem;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
.dark .markdown-bubble code {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
.dark .markdown-bubble pre {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
.dark .markdown-bubble blockquote {
|
||||||
|
border-left-color: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Füge die Styles hinzu, wenn sie noch nicht vorhanden sind
|
||||||
|
if (!document.querySelector('#markdown-chat-styles')) {
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.id = 'markdown-chat-styles';
|
||||||
|
style.textContent = markdownStyles;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Klasse für Markdown-Formatierung hinzufügen
|
||||||
|
bubble.classList.add('markdown-bubble');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler bei der Markdown-Formatierung:', error);
|
||||||
|
// Fallback zur einfachen Formatierung
|
||||||
|
formattedText = text.split('\n').map(line => {
|
||||||
|
if (line.trim() === '') return '<br>';
|
||||||
|
return `<p>${line}</p>`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Für Benutzernachrichten einfache Formatierung
|
||||||
|
formattedText = text.split('\n').map(line => {
|
||||||
|
if (line.trim() === '') return '<br>';
|
||||||
|
return `<p>${line}</p>`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
bubble.innerHTML = formattedText;
|
||||||
|
|
||||||
messageEl.appendChild(bubble);
|
messageEl.appendChild(bubble);
|
||||||
|
|
||||||
|
if (this.chatHistory) {
|
||||||
this.chatHistory.appendChild(messageEl);
|
this.chatHistory.appendChild(messageEl);
|
||||||
|
|
||||||
// Scroll zum Ende des Verlaufs
|
// Scroll zum Ende des Verlaufs
|
||||||
this.chatHistory.scrollTop = this.chatHistory.scrollHeight;
|
this.chatHistory.scrollTop = this.chatHistory.scrollHeight;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zeigt Vorschläge als klickbare Pills an
|
||||||
|
* @param {string[]} suggestions - Liste von Vorschlägen
|
||||||
|
*/
|
||||||
|
showSuggestions(suggestions) {
|
||||||
|
if (!this.suggestionArea) return;
|
||||||
|
|
||||||
|
// Vorherige Vorschläge entfernen
|
||||||
|
this.suggestionArea.innerHTML = '';
|
||||||
|
|
||||||
|
if (suggestions && suggestions.length > 0) {
|
||||||
|
suggestions.forEach(suggestion => {
|
||||||
|
const pill = document.createElement('button');
|
||||||
|
pill.className = 'suggestion-pill text-sm bg-gray-200 dark:bg-dark-600 hover:bg-gray-300 dark:hover:bg-dark-500 text-gray-800 dark:text-gray-200 rounded-full px-3 py-1 mb-2 transition-colors';
|
||||||
|
pill.textContent = suggestion;
|
||||||
|
this.suggestionArea.appendChild(pill);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.suggestionArea.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
this.suggestionArea.classList.add('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 +398,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,36 +408,118 @@ 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
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Ladeindikator entfernen
|
||||||
|
this.removeLoadingIndicator();
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Netzwerkfehler oder Serverproblem');
|
throw new Error(`Serverfehler: ${response.status} ${response.statusText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
console.log('Antwort erhalten:', data);
|
||||||
// Ladeindikator entfernen
|
|
||||||
this.removeLoadingIndicator();
|
|
||||||
|
|
||||||
// Antwort anzeigen
|
// Antwort anzeigen
|
||||||
|
if (data.response) {
|
||||||
this.addMessage('assistant', 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
|
// Fehlermeldung anzeigen oder Wiederholungsversuch starten
|
||||||
|
if (this.retryCount < this.maxRetries) {
|
||||||
|
this.retryCount++;
|
||||||
|
this.addMessage('assistant', 'Es gab ein Problem mit der Anfrage. Ich versuche es erneut...');
|
||||||
|
|
||||||
|
// Kurze Verzögerung vor dem erneuten Versuch
|
||||||
|
setTimeout(() => {
|
||||||
|
// Letzte Benutzernachricht aus dem Messages-Array entfernen
|
||||||
|
const lastUserMessage = this.messages[this.messages.length - 2].content;
|
||||||
|
this.messages = this.messages.slice(0, -2); // Entferne Benutzernachricht und Fehlermeldung
|
||||||
|
|
||||||
|
// Erneuter Versand mit gleicher Nachricht
|
||||||
|
this.inputField.value = lastUserMessage;
|
||||||
|
this.sendMessage();
|
||||||
|
}, 1500);
|
||||||
|
} 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.');
|
this.addMessage('assistant', 'Es tut mir leid, aber es gab ein Problem bei der Verarbeitung deiner Anfrage. Bitte versuche es später noch einmal.');
|
||||||
|
this.retryCount = 0; // Zurücksetzen für die nächste Anfrage
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 einen Ladeindikator im Chat an
|
* Zeigt einen Ladeindikator im Chat an
|
||||||
*/
|
*/
|
||||||
showLoadingIndicator() {
|
showLoadingIndicator() {
|
||||||
|
if (!this.chatHistory) return;
|
||||||
|
|
||||||
|
// Entferne vorhandenen Ladeindikator (falls vorhanden)
|
||||||
|
this.removeLoadingIndicator();
|
||||||
|
|
||||||
const loadingEl = document.createElement('div');
|
const loadingEl = document.createElement('div');
|
||||||
loadingEl.id = 'assistant-loading';
|
loadingEl.id = 'assistant-loading';
|
||||||
loadingEl.className = 'flex justify-start';
|
loadingEl.className = 'flex justify-start';
|
||||||
@@ -232,49 +533,40 @@ class ChatGPTAssistant {
|
|||||||
|
|
||||||
// Scroll zum Ende des Verlaufs
|
// Scroll zum Ende des Verlaufs
|
||||||
this.chatHistory.scrollTop = this.chatHistory.scrollHeight;
|
this.chatHistory.scrollTop = this.chatHistory.scrollHeight;
|
||||||
|
|
||||||
// Stil für den Typing-Indikator
|
|
||||||
const style = document.createElement('style');
|
|
||||||
style.textContent = `
|
|
||||||
.typing-indicator {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.typing-indicator span {
|
|
||||||
height: 8px;
|
|
||||||
width: 8px;
|
|
||||||
background-color: #888;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: inline-block;
|
|
||||||
margin: 0 2px;
|
|
||||||
opacity: 0.4;
|
|
||||||
animation: typing 1.5s infinite ease-in-out;
|
|
||||||
}
|
|
||||||
.typing-indicator span:nth-child(2) {
|
|
||||||
animation-delay: 0.2s;
|
|
||||||
}
|
|
||||||
.typing-indicator span:nth-child(3) {
|
|
||||||
animation-delay: 0.4s;
|
|
||||||
}
|
|
||||||
@keyframes typing {
|
|
||||||
0% { transform: translateY(0); }
|
|
||||||
50% { transform: translateY(-5px); }
|
|
||||||
100% { transform: translateY(0); }
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
document.head.appendChild(style);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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');
|
||||||
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;
|
|
||||||
@@ -3,12 +3,18 @@
|
|||||||
* Spezifische Funktionen für die Mindmap-Seite
|
* Spezifische Funktionen für die Mindmap-Seite
|
||||||
*/
|
*/
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
// Füge das Modul zum globalen MindMap-Objekt hinzu
|
||||||
|
if (!window.MindMap) {
|
||||||
|
window.MindMap = {};
|
||||||
|
}
|
||||||
|
|
||||||
// Registriere den Initialisierer im MindMap-Objekt
|
// Registriere den Initialisierer im MindMap-Objekt
|
||||||
if (window.MindMap) {
|
if (window.MindMap) {
|
||||||
|
window.MindMap.pageInitializers = window.MindMap.pageInitializers || {};
|
||||||
window.MindMap.pageInitializers.mindmap = initMindmapPage;
|
window.MindMap.pageInitializers.mindmap = initMindmapPage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
// Prüfe, ob wir auf der Mindmap-Seite sind und initialisiere
|
// Prüfe, ob wir auf der Mindmap-Seite sind und initialisiere
|
||||||
if (document.body.dataset.page === 'mindmap') {
|
if (document.body.dataset.page === 'mindmap') {
|
||||||
initMindmapPage();
|
initMindmapPage();
|
||||||
@@ -19,6 +25,10 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
* Initialisiert die Mindmap-Seite
|
* Initialisiert die Mindmap-Seite
|
||||||
*/
|
*/
|
||||||
function initMindmapPage() {
|
function initMindmapPage() {
|
||||||
|
console.log('Mindmap-Seite Initialisierung startet...');
|
||||||
|
console.log('D3 Bibliothek verfügbar:', typeof d3 !== 'undefined');
|
||||||
|
console.log('MindMapVisualization verfügbar:', typeof MindMapVisualization !== 'undefined');
|
||||||
|
|
||||||
const mindmapContainer = document.getElementById('mindmap-container');
|
const mindmapContainer = document.getElementById('mindmap-container');
|
||||||
const thoughtsContainer = document.getElementById('thoughts-container');
|
const thoughtsContainer = document.getElementById('thoughts-container');
|
||||||
|
|
||||||
@@ -26,6 +36,7 @@ function initMindmapPage() {
|
|||||||
console.error('Mindmap-Container nicht gefunden!');
|
console.error('Mindmap-Container nicht gefunden!');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
console.log('Mindmap-Container gefunden:', mindmapContainer);
|
||||||
|
|
||||||
// Prüfe, ob D3.js geladen ist
|
// Prüfe, ob D3.js geladen ist
|
||||||
if (typeof d3 === 'undefined') {
|
if (typeof d3 === 'undefined') {
|
||||||
@@ -41,7 +52,23 @@ function initMindmapPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prüfe, ob MindMapVisualization definiert ist
|
||||||
|
if (typeof MindMapVisualization === 'undefined') {
|
||||||
|
console.error('MindMapVisualization-Klasse ist nicht definiert!');
|
||||||
|
mindmapContainer.innerHTML = `
|
||||||
|
<div class="glass-effect p-6 text-center">
|
||||||
|
<div class="text-red-500 mb-4">
|
||||||
|
<i class="fa-solid fa-triangle-exclamation text-4xl"></i>
|
||||||
|
</div>
|
||||||
|
<p class="text-xl">MindMap-Visualisierung konnte nicht geladen werden. Bitte laden Sie die Seite neu.</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Erstelle die Mindmap-Visualisierung
|
// Erstelle die Mindmap-Visualisierung
|
||||||
|
try {
|
||||||
|
console.log('Versuche, MindMapVisualization zu erstellen...');
|
||||||
const mindmap = new MindMapVisualization('#mindmap-container', {
|
const mindmap = new MindMapVisualization('#mindmap-container', {
|
||||||
height: 600,
|
height: 600,
|
||||||
onNodeClick: handleNodeClick
|
onNodeClick: handleNodeClick
|
||||||
@@ -52,6 +79,20 @@ function initMindmapPage() {
|
|||||||
|
|
||||||
// Lade die Mindmap-Daten
|
// Lade die Mindmap-Daten
|
||||||
mindmap.loadData();
|
mindmap.loadData();
|
||||||
|
console.log('MindMapVisualization erfolgreich erstellt und geladen');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Erstellen der MindMapVisualization:', error);
|
||||||
|
mindmapContainer.innerHTML = `
|
||||||
|
<div class="glass-effect p-6 text-center">
|
||||||
|
<div class="text-red-500 mb-4">
|
||||||
|
<i class="fa-solid fa-triangle-exclamation text-4xl"></i>
|
||||||
|
</div>
|
||||||
|
<p class="text-xl">Fehler beim Erstellen der Mindmap-Visualisierung:</p>
|
||||||
|
<p class="text-md mt-2">${error.message}</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Suchfunktion für die Mindmap
|
// Suchfunktion für die Mindmap
|
||||||
const searchInput = document.getElementById('mindmap-search');
|
const searchInput = document.getElementById('mindmap-search');
|
||||||
@@ -426,13 +467,13 @@ function initMindmapPage() {
|
|||||||
|
|
||||||
// Erfolgsbenachrichtigung
|
// Erfolgsbenachrichtigung
|
||||||
if (window.MindMap && window.MindMap.showNotification) {
|
if (window.MindMap && window.MindMap.showNotification) {
|
||||||
MindMap.showNotification('Gedanke erfolgreich gespeichert.', 'success');
|
window.MindMap.showNotification('Gedanke erfolgreich gespeichert.', 'success');
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fehler beim Speichern:', error);
|
console.error('Fehler beim Speichern:', error);
|
||||||
if (window.MindMap && window.MindMap.showNotification) {
|
if (window.MindMap && window.MindMap.showNotification) {
|
||||||
MindMap.showNotification('Fehler beim Speichern des Gedankens.', 'error');
|
window.MindMap.showNotification('Fehler beim Speichern des Gedankens.', 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -448,7 +489,7 @@ function initMindmapPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Zeigt die Kommentare zu einem Gedanken an
|
* Füge globale Funktionen für das Mindmap-Objekt hinzu
|
||||||
*/
|
*/
|
||||||
window.showComments = async function(thoughtId) {
|
window.showComments = async function(thoughtId) {
|
||||||
try {
|
try {
|
||||||
@@ -470,7 +511,11 @@ window.showComments = async function(thoughtId) {
|
|||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fehler beim Laden der Kommentare:', error);
|
console.error('Fehler beim Laden der Kommentare:', error);
|
||||||
MindMap.showNotification('Fehler beim Laden der Kommentare.', 'error');
|
if (window.MindMap && window.MindMap.showNotification) {
|
||||||
|
window.MindMap.showNotification('Fehler beim Laden der Kommentare.', 'error');
|
||||||
|
} else {
|
||||||
|
alert('Fehler beim Laden der Kommentare.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -497,7 +542,11 @@ window.showRelations = async function(thoughtId) {
|
|||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fehler beim Laden der Beziehungen:', error);
|
console.error('Fehler beim Laden der Beziehungen:', error);
|
||||||
MindMap.showNotification('Fehler beim Laden der Beziehungen.', 'error');
|
if (window.MindMap && window.MindMap.showNotification) {
|
||||||
|
window.MindMap.showNotification('Fehler beim Laden der Beziehungen.', 'error');
|
||||||
|
} else {
|
||||||
|
alert('Fehler beim Laden der Beziehungen.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ class MindMapVisualization {
|
|||||||
// API-Aufruf mit kürzerem Timeout im Hintergrund durchführen
|
// API-Aufruf mit kürzerem Timeout im Hintergrund durchführen
|
||||||
try {
|
try {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 Sekunden Timeout - reduced from 10
|
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 Sekunden Timeout
|
||||||
|
|
||||||
const response = await fetch('/api/mindmap', {
|
const response = await fetch('/api/mindmap', {
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
@@ -175,10 +175,57 @@ class MindMapVisualization {
|
|||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
console.warn(`HTTP Fehler: ${response.status}, verwende Standarddaten`);
|
console.warn(`HTTP Fehler: ${response.status}, versuche erneute Verbindung`);
|
||||||
|
|
||||||
|
// Bei Verbindungsfehler versuchen, die Verbindung neu herzustellen
|
||||||
|
const retryResponse = await fetch('/api/refresh-mindmap', {
|
||||||
|
headers: {
|
||||||
|
'Cache-Control': 'no-cache',
|
||||||
|
'Pragma': 'no-cache'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!retryResponse.ok) {
|
||||||
|
throw new Error(`Retry failed with status: ${retryResponse.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const retryData = await retryResponse.json();
|
||||||
|
|
||||||
|
if (!retryData.success || !retryData.nodes || retryData.nodes.length === 0) {
|
||||||
|
console.warn('Keine Mindmap-Daten nach Neuversuch, verwende weiterhin Standard-Daten.');
|
||||||
return; // Keep using default data
|
return; // Keep using default data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Flache Liste von Knoten und Verbindungen erstellen
|
||||||
|
this.nodes = [];
|
||||||
|
this.links = [];
|
||||||
|
|
||||||
|
// Knoten direkt übernehmen
|
||||||
|
retryData.nodes.forEach(node => {
|
||||||
|
this.nodes.push({
|
||||||
|
id: node.id,
|
||||||
|
name: node.name,
|
||||||
|
description: node.description || '',
|
||||||
|
thought_count: node.thought_count || 0,
|
||||||
|
color: this.generateColorFromString(node.name),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verbindungen hinzufügen
|
||||||
|
if (node.connections && node.connections.length > 0) {
|
||||||
|
node.connections.forEach(conn => {
|
||||||
|
this.links.push({
|
||||||
|
source: node.id,
|
||||||
|
target: conn.target
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Visualisierung aktualisieren mit den tatsächlichen Daten
|
||||||
|
this.updateVisualization();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (!data || !data.nodes || data.nodes.length === 0) {
|
if (!data || !data.nodes || data.nodes.length === 0) {
|
||||||
|
|||||||
@@ -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);
|
|
||||||
955
static/neural-network-background.js
Normal file
955
static/neural-network-background.js
Normal file
@@ -0,0 +1,955 @@
|
|||||||
|
/**
|
||||||
|
* Neural Network Background Animation
|
||||||
|
* Modern, darker, mystical theme using WebGL
|
||||||
|
* Subtle flowing network aesthetic
|
||||||
|
*/
|
||||||
|
|
||||||
|
class NeuralNetworkBackground {
|
||||||
|
constructor() {
|
||||||
|
// Canvas setup
|
||||||
|
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'; // Ensure it's behind content but visible
|
||||||
|
this.canvas.style.pointerEvents = 'none';
|
||||||
|
this.canvas.style.opacity = '1'; // Force visibility
|
||||||
|
|
||||||
|
// If canvas already exists, remove it first
|
||||||
|
const existingCanvas = document.getElementById('neural-network-background');
|
||||||
|
if (existingCanvas) {
|
||||||
|
existingCanvas.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append to body as first child to ensure it's behind everything
|
||||||
|
if (document.body.firstChild) {
|
||||||
|
document.body.insertBefore(this.canvas, document.body.firstChild);
|
||||||
|
} else {
|
||||||
|
document.body.appendChild(this.canvas);
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebGL context
|
||||||
|
this.gl = this.canvas.getContext('webgl') || this.canvas.getContext('experimental-webgl');
|
||||||
|
if (!this.gl) {
|
||||||
|
console.warn('WebGL not supported, falling back to canvas rendering');
|
||||||
|
this.gl = null;
|
||||||
|
this.ctx = this.canvas.getContext('2d');
|
||||||
|
this.useWebGL = false;
|
||||||
|
} else {
|
||||||
|
this.useWebGL = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Animation properties
|
||||||
|
this.nodes = [];
|
||||||
|
this.connections = [];
|
||||||
|
this.flows = []; // Flow animations along connections
|
||||||
|
this.animationFrameId = null;
|
||||||
|
this.isDarkMode = true; // Always use dark mode for the background
|
||||||
|
|
||||||
|
// Colors - Updated for intense visibility and neural effect
|
||||||
|
this.darkModeColors = {
|
||||||
|
background: '#030610', // Noch dunklerer Hintergrund für besseren Kontrast
|
||||||
|
nodeColor: '#88a5ff', // Hellere, leuchtende Knoten
|
||||||
|
nodePulse: '#c0d5ff', // Strahlend helles Pulsieren
|
||||||
|
connectionColor: '#5a6ca8', // Hellere, sichtbarere Verbindungen
|
||||||
|
flowColor: '#90c8ffee' // Sehr leuchtende, fast undurchsichtige Flüsse
|
||||||
|
};
|
||||||
|
|
||||||
|
this.lightModeColors = {
|
||||||
|
background: '#f9fafb',
|
||||||
|
nodeColor: '#8c4aff',
|
||||||
|
nodePulse: '#ab7cff',
|
||||||
|
connectionColor: '#b798ff',
|
||||||
|
flowColor: '#d4c5ff'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Config - Drastisch verstärkt für strahlende Animationen und neuronale Vernetzung
|
||||||
|
this.config = {
|
||||||
|
nodeCount: 120, // Viel mehr Knoten für ein dichtes Netzwerk
|
||||||
|
nodeSize: 1.4, // Größere Knoten für mehr Sichtbarkeit
|
||||||
|
nodeVariation: 0.6, // Mehr Variation für organisches Aussehen
|
||||||
|
connectionDistance: 220, // Deutlich längere Verbindungen für mehr Vernetzung
|
||||||
|
connectionOpacity: 0.4, // Wesentlich stärkere Verbindungen
|
||||||
|
animationSpeed: 0.08, // Schnellere Bewegung
|
||||||
|
pulseSpeed: 0.006, // Schnelleres Pulsieren für lebendiges Aussehen
|
||||||
|
flowSpeed: 0.8, // Schnellere Flussanimationen
|
||||||
|
flowDensity: 0.005, // Viel mehr Flussanimationen
|
||||||
|
flowLength: 0.25, // Längere Flussanimationen
|
||||||
|
maxConnections: 6, // NEW: Neue Eigenschaft für mehr neuronale Verbindungen pro Knoten
|
||||||
|
clusteringFactor: 0.3 // NEW: Erzeugt Cluster wie in einem neuronalen Netzwerk
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
this.init();
|
||||||
|
|
||||||
|
// Event listeners
|
||||||
|
window.addEventListener('resize', this.resizeCanvas.bind(this));
|
||||||
|
document.addEventListener('darkModeToggled', (event) => {
|
||||||
|
this.isDarkMode = event.detail.isDark;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Log that the background is initialized
|
||||||
|
console.log('Neural Network Background initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.resizeCanvas();
|
||||||
|
|
||||||
|
if (this.useWebGL) {
|
||||||
|
this.initWebGL();
|
||||||
|
}
|
||||||
|
|
||||||
|
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.useWebGL) {
|
||||||
|
this.gl.viewport(0, 0, this.canvas.width, this.canvas.height);
|
||||||
|
} else if (this.ctx) {
|
||||||
|
this.ctx.scale(pixelRatio, pixelRatio);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recalculate node positions after resize
|
||||||
|
if (this.nodes.length) {
|
||||||
|
this.createNodes();
|
||||||
|
this.createConnections();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initWebGL() {
|
||||||
|
// Vertex shader
|
||||||
|
const vsSource = `
|
||||||
|
attribute vec2 aVertexPosition;
|
||||||
|
attribute float aPointSize;
|
||||||
|
uniform vec2 uResolution;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
// Convert from pixel to clip space
|
||||||
|
vec2 position = (aVertexPosition / uResolution) * 2.0 - 1.0;
|
||||||
|
// Flip Y coordinate
|
||||||
|
position.y = -position.y;
|
||||||
|
|
||||||
|
gl_Position = vec4(position, 0, 1);
|
||||||
|
gl_PointSize = aPointSize;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Fragment shader - Softer glow effect
|
||||||
|
const fsSource = `
|
||||||
|
precision mediump float;
|
||||||
|
uniform vec4 uColor;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
float distance = length(gl_PointCoord - vec2(0.5, 0.5));
|
||||||
|
|
||||||
|
// Softer glow with smoother falloff
|
||||||
|
float alpha = 1.0 - smoothstep(0.1, 0.5, distance);
|
||||||
|
alpha = pow(alpha, 1.5); // Make the glow even softer
|
||||||
|
|
||||||
|
gl_FragColor = vec4(uColor.rgb, uColor.a * alpha);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Initialize shaders
|
||||||
|
const vertexShader = this.loadShader(this.gl.VERTEX_SHADER, vsSource);
|
||||||
|
const fragmentShader = this.loadShader(this.gl.FRAGMENT_SHADER, fsSource);
|
||||||
|
|
||||||
|
// Create shader program
|
||||||
|
this.shaderProgram = this.gl.createProgram();
|
||||||
|
this.gl.attachShader(this.shaderProgram, vertexShader);
|
||||||
|
this.gl.attachShader(this.shaderProgram, fragmentShader);
|
||||||
|
this.gl.linkProgram(this.shaderProgram);
|
||||||
|
|
||||||
|
if (!this.gl.getProgramParameter(this.shaderProgram, this.gl.LINK_STATUS)) {
|
||||||
|
console.error('Unable to initialize the shader program: ' +
|
||||||
|
this.gl.getProgramInfoLog(this.shaderProgram));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get attribute and uniform locations
|
||||||
|
this.programInfo = {
|
||||||
|
program: this.shaderProgram,
|
||||||
|
attribLocations: {
|
||||||
|
vertexPosition: this.gl.getAttribLocation(this.shaderProgram, 'aVertexPosition'),
|
||||||
|
pointSize: this.gl.getAttribLocation(this.shaderProgram, 'aPointSize')
|
||||||
|
},
|
||||||
|
uniformLocations: {
|
||||||
|
resolution: this.gl.getUniformLocation(this.shaderProgram, 'uResolution'),
|
||||||
|
color: this.gl.getUniformLocation(this.shaderProgram, 'uColor')
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create buffers
|
||||||
|
this.positionBuffer = this.gl.createBuffer();
|
||||||
|
this.sizeBuffer = this.gl.createBuffer();
|
||||||
|
|
||||||
|
// Set clear color for WebGL context
|
||||||
|
const bgColor = this.hexToRgb(this.darkModeColors.background);
|
||||||
|
|
||||||
|
this.gl.clearColor(bgColor.r/255, bgColor.g/255, bgColor.b/255, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadShader(type, source) {
|
||||||
|
const shader = this.gl.createShader(type);
|
||||||
|
this.gl.shaderSource(shader, source);
|
||||||
|
this.gl.compileShader(shader);
|
||||||
|
|
||||||
|
if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
|
||||||
|
console.error('An error occurred compiling the shaders: ' +
|
||||||
|
this.gl.getShaderInfoLog(shader));
|
||||||
|
this.gl.deleteShader(shader);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return shader;
|
||||||
|
}
|
||||||
|
|
||||||
|
createNodes() {
|
||||||
|
this.nodes = [];
|
||||||
|
const width = this.canvas.width / (window.devicePixelRatio || 1);
|
||||||
|
const height = this.canvas.height / (window.devicePixelRatio || 1);
|
||||||
|
|
||||||
|
// Erstelle Cluster-Zentren für neuronale Netzwerkmuster
|
||||||
|
const clusterCount = Math.floor(5 + Math.random() * 4); // 5-8 Cluster
|
||||||
|
const clusters = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < clusterCount; i++) {
|
||||||
|
clusters.push({
|
||||||
|
x: Math.random() * width,
|
||||||
|
y: Math.random() * height,
|
||||||
|
radius: 100 + Math.random() * 150
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create nodes with random positions and properties
|
||||||
|
for (let i = 0; i < this.config.nodeCount; i++) {
|
||||||
|
// Entscheide, ob dieser Knoten zu einem Cluster gehört oder nicht
|
||||||
|
const useCluster = Math.random() < this.config.clusteringFactor;
|
||||||
|
let x, y;
|
||||||
|
|
||||||
|
if (useCluster && clusters.length > 0) {
|
||||||
|
// Wähle ein zufälliges Cluster
|
||||||
|
const cluster = clusters[Math.floor(Math.random() * clusters.length)];
|
||||||
|
const angle = Math.random() * Math.PI * 2;
|
||||||
|
const distance = Math.random() * cluster.radius;
|
||||||
|
|
||||||
|
// Platziere in der Nähe des Clusters mit einiger Streuung
|
||||||
|
x = cluster.x + Math.cos(angle) * distance;
|
||||||
|
y = cluster.y + Math.sin(angle) * distance;
|
||||||
|
|
||||||
|
// Stelle sicher, dass es innerhalb des Bildschirms bleibt
|
||||||
|
x = Math.max(0, Math.min(width, x));
|
||||||
|
y = Math.max(0, Math.min(height, y));
|
||||||
|
} else {
|
||||||
|
// Zufällige Position außerhalb von Clustern
|
||||||
|
x = Math.random() * width;
|
||||||
|
y = Math.random() * height;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bestimme die Knotengröße - wichtigere Knoten (in Clustern) sind größer
|
||||||
|
const nodeImportance = useCluster ? 1.2 : 0.8;
|
||||||
|
const size = this.config.nodeSize * nodeImportance + Math.random() * this.config.nodeVariation;
|
||||||
|
|
||||||
|
const node = {
|
||||||
|
x: x,
|
||||||
|
y: y,
|
||||||
|
size: size,
|
||||||
|
speed: {
|
||||||
|
x: (Math.random() - 0.5) * this.config.animationSpeed,
|
||||||
|
y: (Math.random() - 0.5) * this.config.animationSpeed
|
||||||
|
},
|
||||||
|
pulsePhase: Math.random() * Math.PI * 2, // Random starting phase
|
||||||
|
connections: [],
|
||||||
|
isActive: Math.random() < 0.3, // Some nodes start active for neural firing effect
|
||||||
|
lastFired: 0, // For neural firing animation
|
||||||
|
firingRate: 1000 + Math.random() * 4000 // Random firing rate in ms
|
||||||
|
};
|
||||||
|
|
||||||
|
this.nodes.push(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createConnections() {
|
||||||
|
this.connections = [];
|
||||||
|
this.flows = []; // Reset flows
|
||||||
|
|
||||||
|
// Create connections between nearby nodes
|
||||||
|
for (let i = 0; i < this.nodes.length; i++) {
|
||||||
|
const nodeA = this.nodes[i];
|
||||||
|
nodeA.connections = [];
|
||||||
|
|
||||||
|
// Sortiere andere Knoten nach Entfernung für bevorzugte nahe Verbindungen
|
||||||
|
const potentialConnections = [];
|
||||||
|
|
||||||
|
for (let j = 0; j < this.nodes.length; j++) {
|
||||||
|
if (i === j) continue;
|
||||||
|
|
||||||
|
const nodeB = this.nodes[j];
|
||||||
|
const dx = nodeB.x - nodeA.x;
|
||||||
|
const dy = nodeB.y - nodeA.y;
|
||||||
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
|
||||||
|
if (distance < this.config.connectionDistance) {
|
||||||
|
potentialConnections.push({
|
||||||
|
index: j,
|
||||||
|
distance: distance
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sortiere nach Entfernung
|
||||||
|
potentialConnections.sort((a, b) => a.distance - b.distance);
|
||||||
|
|
||||||
|
// Wähle die nächsten N Verbindungen, maximal maxConnections
|
||||||
|
const maxConn = Math.min(
|
||||||
|
this.config.maxConnections,
|
||||||
|
potentialConnections.length,
|
||||||
|
1 + Math.floor(Math.random() * this.config.maxConnections)
|
||||||
|
);
|
||||||
|
|
||||||
|
for (let c = 0; c < maxConn; c++) {
|
||||||
|
const connection = potentialConnections[c];
|
||||||
|
const j = connection.index;
|
||||||
|
const nodeB = this.nodes[j];
|
||||||
|
const distance = connection.distance;
|
||||||
|
|
||||||
|
// Create weighted connection (closer = stronger)
|
||||||
|
const connectionStrength = Math.max(0, 1 - distance / this.config.connectionDistance);
|
||||||
|
const connOpacity = connectionStrength * this.config.connectionOpacity;
|
||||||
|
|
||||||
|
// Check if connection already exists
|
||||||
|
if (!this.connections.some(conn =>
|
||||||
|
(conn.from === i && conn.to === j) || (conn.from === j && conn.to === i)
|
||||||
|
)) {
|
||||||
|
// Create connection
|
||||||
|
this.connections.push({
|
||||||
|
from: i,
|
||||||
|
to: j,
|
||||||
|
distance: distance,
|
||||||
|
opacity: connOpacity,
|
||||||
|
strength: connectionStrength,
|
||||||
|
hasFlow: false, // Each connection can have a flow
|
||||||
|
lastActivated: 0 // For neural firing animation
|
||||||
|
});
|
||||||
|
|
||||||
|
nodeA.connections.push(j);
|
||||||
|
nodeB.connections.push(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startAnimation() {
|
||||||
|
this.animationFrameId = requestAnimationFrame(this.animate.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
animate() {
|
||||||
|
// Update nodes
|
||||||
|
const width = this.canvas.width / (window.devicePixelRatio || 1);
|
||||||
|
const height = this.canvas.height / (window.devicePixelRatio || 1);
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
// Simulate neural firing
|
||||||
|
for (let i = 0; i < this.nodes.length; i++) {
|
||||||
|
const node = this.nodes[i];
|
||||||
|
|
||||||
|
// Check if node should fire based on its firing rate
|
||||||
|
if (now - node.lastFired > node.firingRate) {
|
||||||
|
node.isActive = true;
|
||||||
|
node.lastFired = now;
|
||||||
|
|
||||||
|
// Activate connected nodes with probability based on connection strength
|
||||||
|
for (const connIndex of node.connections) {
|
||||||
|
// Find the connection
|
||||||
|
const conn = this.connections.find(c =>
|
||||||
|
(c.from === i && c.to === connIndex) || (c.from === connIndex && c.to === i)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (conn) {
|
||||||
|
// Mark connection as recently activated
|
||||||
|
conn.lastActivated = now;
|
||||||
|
|
||||||
|
// Create a flow along this connection
|
||||||
|
if (Math.random() < conn.strength * 0.8) {
|
||||||
|
this.flows.push({
|
||||||
|
connection: conn,
|
||||||
|
progress: 0,
|
||||||
|
direction: conn.from === i, // Flow from activated node
|
||||||
|
length: this.config.flowLength + Math.random() * 0.1,
|
||||||
|
intensity: 0.7 + Math.random() * 0.3 // Random intensity for variation
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Probability for connected node to activate
|
||||||
|
if (Math.random() < conn.strength * 0.5) {
|
||||||
|
this.nodes[connIndex].isActive = true;
|
||||||
|
this.nodes[connIndex].lastFired = now - Math.random() * 500; // Slight variation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (now - node.lastFired > 300) { // Deactivate after short period
|
||||||
|
node.isActive = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update flows
|
||||||
|
this.updateFlows();
|
||||||
|
|
||||||
|
// Occasionally create new flows along connections
|
||||||
|
if (Math.random() < this.config.flowDensity) {
|
||||||
|
this.createNewFlow();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recalculate connections occasionally for a living network
|
||||||
|
if (Math.random() < 0.01) { // Only recalculate 1% of the time for performance
|
||||||
|
this.createConnections();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render
|
||||||
|
if (this.useWebGL) {
|
||||||
|
this.renderWebGL();
|
||||||
|
} else {
|
||||||
|
this.renderCanvas();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continue animation
|
||||||
|
this.animationFrameId = requestAnimationFrame(this.animate.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
// New method to update flow animations
|
||||||
|
updateFlows() {
|
||||||
|
// Update existing flows
|
||||||
|
for (let i = this.flows.length - 1; i >= 0; i--) {
|
||||||
|
const flow = this.flows[i];
|
||||||
|
flow.progress += this.config.flowSpeed / flow.connection.distance;
|
||||||
|
|
||||||
|
// Remove completed flows
|
||||||
|
if (flow.progress > 1.0) {
|
||||||
|
this.flows.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate more flows for enhanced visibility
|
||||||
|
if (Math.random() < this.config.flowDensity * 2) {
|
||||||
|
this.createNewFlow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// New method to create flow animations
|
||||||
|
createNewFlow() {
|
||||||
|
if (this.connections.length === 0) return;
|
||||||
|
|
||||||
|
// Select a random connection with preference for more connected nodes
|
||||||
|
let connectionIdx = Math.floor(Math.random() * this.connections.length);
|
||||||
|
let attempts = 0;
|
||||||
|
|
||||||
|
// Try to find a connection with more connected nodes
|
||||||
|
while (attempts < 5) {
|
||||||
|
const testIdx = Math.floor(Math.random() * this.connections.length);
|
||||||
|
const testConn = this.connections[testIdx];
|
||||||
|
const fromNode = this.nodes[testConn.from];
|
||||||
|
|
||||||
|
if (fromNode.connections.length > 2) {
|
||||||
|
connectionIdx = testIdx;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
attempts++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const connection = this.connections[connectionIdx];
|
||||||
|
|
||||||
|
// Create a new flow along this connection
|
||||||
|
this.flows.push({
|
||||||
|
connection: connection,
|
||||||
|
progress: 0,
|
||||||
|
direction: Math.random() > 0.5, // Randomly decide direction
|
||||||
|
length: this.config.flowLength + Math.random() * 0.1 // Slightly vary lengths
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderWebGL() {
|
||||||
|
this.gl.clear(this.gl.COLOR_BUFFER_BIT);
|
||||||
|
|
||||||
|
const width = this.canvas.width / (window.devicePixelRatio || 1);
|
||||||
|
const height = this.canvas.height / (window.devicePixelRatio || 1);
|
||||||
|
|
||||||
|
// Select shader program
|
||||||
|
this.gl.useProgram(this.programInfo.program);
|
||||||
|
|
||||||
|
// Set resolution uniform
|
||||||
|
this.gl.uniform2f(this.programInfo.uniformLocations.resolution, width, height);
|
||||||
|
|
||||||
|
// Draw connections first (behind nodes)
|
||||||
|
this.renderConnectionsWebGL();
|
||||||
|
|
||||||
|
// Draw flows on top of connections
|
||||||
|
this.renderFlowsWebGL();
|
||||||
|
|
||||||
|
// Draw nodes
|
||||||
|
this.renderNodesWebGL();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderNodesWebGL() {
|
||||||
|
// Prepare node positions for WebGL
|
||||||
|
const positions = new Float32Array(this.nodes.length * 2);
|
||||||
|
const sizes = new Float32Array(this.nodes.length);
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
for (let i = 0; i < this.nodes.length; i++) {
|
||||||
|
const node = this.nodes[i];
|
||||||
|
positions[i * 2] = node.x;
|
||||||
|
positions[i * 2 + 1] = node.y;
|
||||||
|
|
||||||
|
// Enhanced pulse effect with additional boost for active nodes
|
||||||
|
let pulse = Math.sin(node.pulsePhase) * 0.4 + 1;
|
||||||
|
|
||||||
|
// Make active nodes pulse more intensely (neural firing effect)
|
||||||
|
if (node.isActive) {
|
||||||
|
const timeSinceFired = now - node.lastFired;
|
||||||
|
if (timeSinceFired < 300) {
|
||||||
|
// Quick expand then contract effect
|
||||||
|
const normalizedTime = timeSinceFired / 300;
|
||||||
|
const fireBoost = 1.5 * (1 - normalizedTime);
|
||||||
|
pulse += fireBoost;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nodes with more connections are larger (hub neurons)
|
||||||
|
const connectivityFactor = 1 + (node.connections.length / this.config.maxConnections) * 0.8;
|
||||||
|
sizes[i] = node.size * pulse * connectivityFactor;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bind position buffer
|
||||||
|
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer);
|
||||||
|
this.gl.bufferData(this.gl.ARRAY_BUFFER, positions, this.gl.STATIC_DRAW);
|
||||||
|
this.gl.vertexAttribPointer(
|
||||||
|
this.programInfo.attribLocations.vertexPosition,
|
||||||
|
2, // components per vertex
|
||||||
|
this.gl.FLOAT, // data type
|
||||||
|
false, // normalize
|
||||||
|
0, // stride
|
||||||
|
0 // offset
|
||||||
|
);
|
||||||
|
this.gl.enableVertexAttribArray(this.programInfo.attribLocations.vertexPosition);
|
||||||
|
|
||||||
|
// Bind size buffer
|
||||||
|
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.sizeBuffer);
|
||||||
|
this.gl.bufferData(this.gl.ARRAY_BUFFER, sizes, this.gl.STATIC_DRAW);
|
||||||
|
this.gl.vertexAttribPointer(
|
||||||
|
this.programInfo.attribLocations.pointSize,
|
||||||
|
1, // components per vertex
|
||||||
|
this.gl.FLOAT, // data type
|
||||||
|
false, // normalize
|
||||||
|
0, // stride
|
||||||
|
0 // offset
|
||||||
|
);
|
||||||
|
this.gl.enableVertexAttribArray(this.programInfo.attribLocations.pointSize);
|
||||||
|
|
||||||
|
// Enable blending for all nodes
|
||||||
|
this.gl.enable(this.gl.BLEND);
|
||||||
|
this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE); // Additive blending for glow
|
||||||
|
|
||||||
|
// Draw each node individually with its own color
|
||||||
|
for (let i = 0; i < this.nodes.length; i++) {
|
||||||
|
const node = this.nodes[i];
|
||||||
|
|
||||||
|
// Set node color - more visible with active highlighting
|
||||||
|
const colorObj = this.isDarkMode ? this.darkModeColors : this.lightModeColors;
|
||||||
|
const nodeColor = this.hexToRgb(colorObj.nodeColor);
|
||||||
|
const nodePulseColor = this.hexToRgb(colorObj.nodePulse);
|
||||||
|
|
||||||
|
// Use pulse color for active nodes
|
||||||
|
let r = nodeColor.r / 255;
|
||||||
|
let g = nodeColor.g / 255;
|
||||||
|
let b = nodeColor.b / 255;
|
||||||
|
|
||||||
|
// Active nodes get brighter color
|
||||||
|
if (node.isActive) {
|
||||||
|
r = (r + nodePulseColor.r / 255) / 2;
|
||||||
|
g = (g + nodePulseColor.g / 255) / 2;
|
||||||
|
b = (b + nodePulseColor.b / 255) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.gl.uniform4f(
|
||||||
|
this.programInfo.uniformLocations.color,
|
||||||
|
r, g, b,
|
||||||
|
0.95 // Higher opacity for maximum visibility
|
||||||
|
);
|
||||||
|
|
||||||
|
// Draw each node individually for better control
|
||||||
|
this.gl.drawArrays(this.gl.POINTS, i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderConnectionsWebGL() {
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
// For each connection, draw a line
|
||||||
|
for (const connection of this.connections) {
|
||||||
|
const fromNode = this.nodes[connection.from];
|
||||||
|
const toNode = this.nodes[connection.to];
|
||||||
|
|
||||||
|
// Line positions
|
||||||
|
const positions = new Float32Array([
|
||||||
|
fromNode.x, fromNode.y,
|
||||||
|
toNode.x, toNode.y
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Bind position buffer
|
||||||
|
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer);
|
||||||
|
this.gl.bufferData(this.gl.ARRAY_BUFFER, positions, this.gl.STATIC_DRAW);
|
||||||
|
this.gl.vertexAttribPointer(
|
||||||
|
this.programInfo.attribLocations.vertexPosition,
|
||||||
|
2, // components per vertex
|
||||||
|
this.gl.FLOAT, // data type
|
||||||
|
false, // normalize
|
||||||
|
0, // stride
|
||||||
|
0 // offset
|
||||||
|
);
|
||||||
|
this.gl.enableVertexAttribArray(this.programInfo.attribLocations.vertexPosition);
|
||||||
|
|
||||||
|
// Disable point size attribute for lines
|
||||||
|
this.gl.disableVertexAttribArray(this.programInfo.attribLocations.pointSize);
|
||||||
|
|
||||||
|
// Set line color with connection opacity
|
||||||
|
const colorObj = this.isDarkMode ? this.darkModeColors : this.lightModeColors;
|
||||||
|
let lineColor = this.hexToRgb(colorObj.connectionColor);
|
||||||
|
|
||||||
|
// Highlight recently activated connections for neural pathway effect
|
||||||
|
let opacity = connection.opacity * 1.4; // Base increased visibility
|
||||||
|
|
||||||
|
if (now - connection.lastActivated < 800) {
|
||||||
|
// Make recently activated connections brighter
|
||||||
|
lineColor = this.hexToRgb(colorObj.flowColor);
|
||||||
|
|
||||||
|
// Fade out effect
|
||||||
|
const timeFactor = 1 - ((now - connection.lastActivated) / 800);
|
||||||
|
opacity = Math.max(opacity, timeFactor * 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.gl.uniform4f(
|
||||||
|
this.programInfo.uniformLocations.color,
|
||||||
|
lineColor.r / 255,
|
||||||
|
lineColor.g / 255,
|
||||||
|
lineColor.b / 255,
|
||||||
|
opacity
|
||||||
|
);
|
||||||
|
|
||||||
|
// Draw the line
|
||||||
|
this.gl.enable(this.gl.BLEND);
|
||||||
|
this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE);
|
||||||
|
this.gl.lineWidth(1);
|
||||||
|
this.gl.drawArrays(this.gl.LINES, 0, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// New method to render the flowing animations
|
||||||
|
renderFlowsWebGL() {
|
||||||
|
// For each flow, draw a segment along its connection
|
||||||
|
for (const flow of this.flows) {
|
||||||
|
const connection = flow.connection;
|
||||||
|
const fromNode = this.nodes[connection.from];
|
||||||
|
const toNode = this.nodes[connection.to];
|
||||||
|
|
||||||
|
// Calculate flow position
|
||||||
|
const startProgress = flow.progress;
|
||||||
|
const endProgress = Math.min(1, startProgress + flow.length);
|
||||||
|
|
||||||
|
// If flow hasn't started yet or has finished
|
||||||
|
if (startProgress >= 1 || endProgress <= 0) continue;
|
||||||
|
|
||||||
|
// Calculate actual positions
|
||||||
|
const direction = flow.direction ? 1 : -1;
|
||||||
|
let p1, p2;
|
||||||
|
|
||||||
|
if (direction > 0) {
|
||||||
|
p1 = {
|
||||||
|
x: fromNode.x + (toNode.x - fromNode.x) * startProgress,
|
||||||
|
y: fromNode.y + (toNode.y - fromNode.y) * startProgress
|
||||||
|
};
|
||||||
|
p2 = {
|
||||||
|
x: fromNode.x + (toNode.x - fromNode.x) * endProgress,
|
||||||
|
y: fromNode.y + (toNode.y - fromNode.y) * endProgress
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
p1 = {
|
||||||
|
x: toNode.x + (fromNode.x - toNode.x) * startProgress,
|
||||||
|
y: toNode.y + (fromNode.y - toNode.y) * startProgress
|
||||||
|
};
|
||||||
|
p2 = {
|
||||||
|
x: toNode.x + (fromNode.x - toNode.x) * endProgress,
|
||||||
|
y: toNode.y + (fromNode.y - toNode.y) * endProgress
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Line positions for the flow
|
||||||
|
const positions = new Float32Array([
|
||||||
|
p1.x, p1.y,
|
||||||
|
p2.x, p2.y
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Bind position buffer
|
||||||
|
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer);
|
||||||
|
this.gl.bufferData(this.gl.ARRAY_BUFFER, positions, this.gl.STATIC_DRAW);
|
||||||
|
this.gl.vertexAttribPointer(
|
||||||
|
this.programInfo.attribLocations.vertexPosition,
|
||||||
|
2, // components per vertex
|
||||||
|
this.gl.FLOAT, // data type
|
||||||
|
false, // normalize
|
||||||
|
0, // stride
|
||||||
|
0 // offset
|
||||||
|
);
|
||||||
|
this.gl.enableVertexAttribArray(this.programInfo.attribLocations.vertexPosition);
|
||||||
|
|
||||||
|
// Disable point size attribute for lines
|
||||||
|
this.gl.disableVertexAttribArray(this.programInfo.attribLocations.pointSize);
|
||||||
|
|
||||||
|
// Fade the flow at the beginning and end
|
||||||
|
const fadeEdge = 0.2;
|
||||||
|
const fadeOpacity = Math.min(
|
||||||
|
startProgress / fadeEdge,
|
||||||
|
(1 - endProgress) / fadeEdge,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
|
||||||
|
// Flow color - much stronger glow
|
||||||
|
const colorObj = this.isDarkMode ? this.darkModeColors : this.lightModeColors;
|
||||||
|
const flowColor = this.hexToRgb(colorObj.flowColor);
|
||||||
|
this.gl.uniform4f(
|
||||||
|
this.programInfo.uniformLocations.color,
|
||||||
|
flowColor.r / 255,
|
||||||
|
flowColor.g / 255,
|
||||||
|
flowColor.b / 255,
|
||||||
|
0.9 * fadeOpacity * flow.intensity // Much stronger flow opacity with intensity variation
|
||||||
|
);
|
||||||
|
|
||||||
|
// Draw the flow line
|
||||||
|
this.gl.enable(this.gl.BLEND);
|
||||||
|
this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE);
|
||||||
|
this.gl.lineWidth(2.5); // Even thicker for dramatic visibility
|
||||||
|
this.gl.drawArrays(this.gl.LINES, 0, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderCanvas() {
|
||||||
|
// Clear canvas
|
||||||
|
const width = this.canvas.width / (window.devicePixelRatio || 1);
|
||||||
|
const height = this.canvas.height / (window.devicePixelRatio || 1);
|
||||||
|
|
||||||
|
this.ctx.clearRect(0, 0, width, height);
|
||||||
|
|
||||||
|
// Set background
|
||||||
|
const backgroundColor = this.isDarkMode
|
||||||
|
? this.darkModeColors.background
|
||||||
|
: this.lightModeColors.background;
|
||||||
|
|
||||||
|
this.ctx.fillStyle = backgroundColor;
|
||||||
|
this.ctx.fillRect(0, 0, width, height);
|
||||||
|
|
||||||
|
// Draw connections
|
||||||
|
const connectionColor = this.isDarkMode
|
||||||
|
? this.darkModeColors.connectionColor
|
||||||
|
: this.lightModeColors.connectionColor;
|
||||||
|
|
||||||
|
for (const connection of this.connections) {
|
||||||
|
const fromNode = this.nodes[connection.from];
|
||||||
|
const toNode = this.nodes[connection.to];
|
||||||
|
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.moveTo(fromNode.x, fromNode.y);
|
||||||
|
this.ctx.lineTo(toNode.x, toNode.y);
|
||||||
|
|
||||||
|
const rgbColor = this.hexToRgb(connectionColor);
|
||||||
|
this.ctx.strokeStyle = `rgba(${rgbColor.r}, ${rgbColor.g}, ${rgbColor.b}, ${connection.opacity})`;
|
||||||
|
this.ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw flows
|
||||||
|
this.renderFlowsCanvas();
|
||||||
|
|
||||||
|
// Draw nodes
|
||||||
|
const nodeColor = this.isDarkMode
|
||||||
|
? this.darkModeColors.nodeColor
|
||||||
|
: this.lightModeColors.nodeColor;
|
||||||
|
|
||||||
|
const nodePulse = this.isDarkMode
|
||||||
|
? this.darkModeColors.nodePulse
|
||||||
|
: this.lightModeColors.nodePulse;
|
||||||
|
|
||||||
|
for (const node of this.nodes) {
|
||||||
|
// Node with subtle glow effect
|
||||||
|
const pulse = Math.sin(node.pulsePhase) * 0.2 + 1;
|
||||||
|
const nodeSize = node.size * pulse * (node.connections.length > 3 ? 1.3 : 1);
|
||||||
|
|
||||||
|
// Glow effect
|
||||||
|
const glow = this.ctx.createRadialGradient(
|
||||||
|
node.x, node.y, 0,
|
||||||
|
node.x, node.y, nodeSize * 2
|
||||||
|
);
|
||||||
|
|
||||||
|
const rgbNodeColor = this.hexToRgb(nodeColor);
|
||||||
|
const rgbPulseColor = this.hexToRgb(nodePulse);
|
||||||
|
|
||||||
|
glow.addColorStop(0, `rgba(${rgbPulseColor.r}, ${rgbPulseColor.g}, ${rgbPulseColor.b}, 0.6)`);
|
||||||
|
glow.addColorStop(0.5, `rgba(${rgbNodeColor.r}, ${rgbNodeColor.g}, ${rgbNodeColor.b}, 0.2)`);
|
||||||
|
glow.addColorStop(1, `rgba(${rgbNodeColor.r}, ${rgbNodeColor.g}, ${rgbNodeColor.b}, 0)`);
|
||||||
|
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.arc(node.x, node.y, nodeSize * 2, 0, Math.PI * 2);
|
||||||
|
this.ctx.fillStyle = glow;
|
||||||
|
this.ctx.fill();
|
||||||
|
|
||||||
|
// Main node
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.arc(node.x, node.y, nodeSize, 0, Math.PI * 2);
|
||||||
|
this.ctx.fillStyle = nodeColor;
|
||||||
|
this.ctx.fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// New method to render flows in Canvas mode
|
||||||
|
renderFlowsCanvas() {
|
||||||
|
if (!this.ctx) return;
|
||||||
|
|
||||||
|
const flowColor = this.isDarkMode
|
||||||
|
? this.darkModeColors.flowColor
|
||||||
|
: this.lightModeColors.flowColor;
|
||||||
|
|
||||||
|
const rgbFlowColor = this.hexToRgb(flowColor);
|
||||||
|
|
||||||
|
for (const flow of this.flows) {
|
||||||
|
const connection = flow.connection;
|
||||||
|
const fromNode = this.nodes[connection.from];
|
||||||
|
const toNode = this.nodes[connection.to];
|
||||||
|
|
||||||
|
// Calculate flow position
|
||||||
|
const startProgress = flow.progress;
|
||||||
|
const endProgress = Math.min(1, startProgress + flow.length);
|
||||||
|
|
||||||
|
// If flow hasn't started yet or has finished
|
||||||
|
if (startProgress >= 1 || endProgress <= 0) continue;
|
||||||
|
|
||||||
|
// Calculate actual positions
|
||||||
|
const direction = flow.direction ? 1 : -1;
|
||||||
|
let p1, p2;
|
||||||
|
|
||||||
|
if (direction > 0) {
|
||||||
|
p1 = {
|
||||||
|
x: fromNode.x + (toNode.x - fromNode.x) * startProgress,
|
||||||
|
y: fromNode.y + (toNode.y - fromNode.y) * startProgress
|
||||||
|
};
|
||||||
|
p2 = {
|
||||||
|
x: fromNode.x + (toNode.x - fromNode.x) * endProgress,
|
||||||
|
y: fromNode.y + (toNode.y - fromNode.y) * startProgress
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
p1 = {
|
||||||
|
x: toNode.x + (fromNode.x - toNode.x) * startProgress,
|
||||||
|
y: toNode.y + (fromNode.y - toNode.y) * startProgress
|
||||||
|
};
|
||||||
|
p2 = {
|
||||||
|
x: toNode.x + (fromNode.x - toNode.x) * endProgress,
|
||||||
|
y: toNode.y + (fromNode.y - toNode.y) * endProgress
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fade the flow at the beginning and end
|
||||||
|
const fadeEdge = 0.2;
|
||||||
|
const fadeOpacity = Math.min(
|
||||||
|
startProgress / fadeEdge,
|
||||||
|
(1 - endProgress) / fadeEdge,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
|
||||||
|
// Draw flow
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.moveTo(p1.x, p1.y);
|
||||||
|
this.ctx.lineTo(p2.x, p2.y);
|
||||||
|
this.ctx.strokeStyle = `rgba(${rgbFlowColor.r}, ${rgbFlowColor.g}, ${rgbFlowColor.b}, ${0.4 * fadeOpacity})`;
|
||||||
|
this.ctx.lineWidth = 1.5;
|
||||||
|
this.ctx.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper method to convert hex to RGB
|
||||||
|
hexToRgb(hex) {
|
||||||
|
// Remove # if present
|
||||||
|
hex = hex.replace(/^#/, '');
|
||||||
|
|
||||||
|
// Handle rgba hex format
|
||||||
|
let alpha = 1;
|
||||||
|
if (hex.length === 8) {
|
||||||
|
alpha = parseInt(hex.slice(6, 8), 16) / 255;
|
||||||
|
hex = hex.slice(0, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse hex values
|
||||||
|
const bigint = parseInt(hex, 16);
|
||||||
|
const r = (bigint >> 16) & 255;
|
||||||
|
const g = (bigint >> 8) & 255;
|
||||||
|
const b = bigint & 255;
|
||||||
|
|
||||||
|
return { r, g, b, a: alpha };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup method
|
||||||
|
destroy() {
|
||||||
|
if (this.animationFrameId) {
|
||||||
|
cancelAnimationFrame(this.animationFrameId);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.removeEventListener('resize', this.resizeCanvas.bind(this));
|
||||||
|
|
||||||
|
if (this.canvas && this.canvas.parentNode) {
|
||||||
|
this.canvas.parentNode.removeChild(this.canvas);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.gl) {
|
||||||
|
// Clean up WebGL resources
|
||||||
|
this.gl.deleteBuffer(this.positionBuffer);
|
||||||
|
this.gl.deleteBuffer(this.sizeBuffer);
|
||||||
|
this.gl.deleteProgram(this.shaderProgram);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize when DOM is loaded
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// Short delay to ensure DOM is fully loaded
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!window.neuralNetworkBackground) {
|
||||||
|
console.log('Creating Neural Network Background');
|
||||||
|
window.neuralNetworkBackground = new NeuralNetworkBackground();
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Re-initialize when page is fully loaded (for safety)
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
if (!window.neuralNetworkBackground) {
|
||||||
|
console.log('Re-initializing Neural Network Background on full load');
|
||||||
|
window.neuralNetworkBackground = new NeuralNetworkBackground();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clean up when window is closed
|
||||||
|
window.addEventListener('beforeunload', () => {
|
||||||
|
if (window.neuralNetworkBackground) {
|
||||||
|
window.neuralNetworkBackground.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
492
static/style.css
Normal file
492
static/style.css
Normal file
@@ -0,0 +1,492 @@
|
|||||||
|
/* 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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
|
|
||||||
],
|
|
||||||
}
|
|
||||||
@@ -14,8 +14,10 @@
|
|||||||
<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 - Beide Optionen verfügbar -->
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<!-- Alternative lokale Version, falls die CDN-Version blockiert wird -->
|
||||||
|
<link href="{{ url_for('static', filename='css/tailwind.min.css') }}" rel="stylesheet">
|
||||||
<script>
|
<script>
|
||||||
tailwind.config = {
|
tailwind.config = {
|
||||||
darkMode: 'class',
|
darkMode: 'class',
|
||||||
@@ -63,17 +65,15 @@
|
|||||||
}
|
}
|
||||||
</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 -->
|
<!-- Icons - Self-hosted Font Awesome -->
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
<link href="{{ url_for('static', filename='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,19 +81,116 @@
|
|||||||
<!-- 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) -->
|
<!-- D3.js für Visualisierungen -->
|
||||||
<script type="module">
|
<script src="https://d3js.org/d3.v7.min.js"></script>
|
||||||
import MindMap from "{{ url_for('static', filename='js/main.js') }}";
|
|
||||||
// Alpine.js-Integration
|
<!-- Marked.js für Markdown-Parsing -->
|
||||||
document.addEventListener('alpine:init', () => {
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||||
Alpine.data('layout', () => ({
|
|
||||||
darkMode: false,
|
<!-- ChatGPT Assistant -->
|
||||||
|
<script src="{{ url_for('static', filename='js/modules/chatgpt-assistant.js') }}"></script>
|
||||||
|
|
||||||
|
<!-- MindMap Visualization Module -->
|
||||||
|
<script src="{{ url_for('static', filename='js/modules/mindmap.js') }}"></script>
|
||||||
|
|
||||||
|
<!-- MindMap Page Module -->
|
||||||
|
<script src="{{ url_for('static', filename='js/modules/mindmap-page.js') }}"></script>
|
||||||
|
|
||||||
|
<!-- Neural Network Background Script -->
|
||||||
|
<script src="{{ url_for('static', filename='neural-network-background.js') }}"></script>
|
||||||
|
|
||||||
|
<!-- Hauptmodul laden (als traditionelles Skript) -->
|
||||||
|
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
|
||||||
|
|
||||||
|
<!-- Seitenspezifische Styles -->
|
||||||
|
{% block extra_css %}{% endblock %}
|
||||||
|
|
||||||
|
<!-- Custom dark mode styles -->
|
||||||
|
<style>
|
||||||
|
/* Dark mystical theme */
|
||||||
|
.dark {
|
||||||
|
--bg-primary: #0a0e19;
|
||||||
|
--bg-secondary: #111827;
|
||||||
|
--text-primary: #f9fafb;
|
||||||
|
--text-secondary: #e5e7eb;
|
||||||
|
--accent-primary: #6d28d9;
|
||||||
|
--accent-secondary: #8b5cf6;
|
||||||
|
--glow-effect: 0 0 15px rgba(124, 58, 237, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light theme with mystical tones */
|
||||||
|
:root {
|
||||||
|
--bg-primary: #f8fafc;
|
||||||
|
--bg-secondary: #f1f5f9;
|
||||||
|
--text-primary: #1e293b;
|
||||||
|
--text-secondary: #475569;
|
||||||
|
--accent-primary: #7c3aed;
|
||||||
|
--accent-secondary: #8b5cf6;
|
||||||
|
--glow-effect: 0 0 15px rgba(139, 92, 246, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark {
|
||||||
|
background-color: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mystical glowing effects */
|
||||||
|
.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 effects */
|
||||||
|
.glass-morphism {
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .glass-navbar-dark {
|
||||||
|
background-color: rgba(10, 14, 25, 0.8);
|
||||||
|
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-navbar-light {
|
||||||
|
background-color: rgba(255, 255, 255, 0.8);
|
||||||
|
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Alpine.js x-cloak für ausgeblendete Elemente */
|
||||||
|
[x-cloak] { display: none !important; }
|
||||||
|
|
||||||
|
/* Grundlegende Klassen, um sicherzustellen, dass Tailwind geladen wird */
|
||||||
|
.nav-link {
|
||||||
|
@apply text-gray-300 hover:text-white transition-colors duration-200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link-active {
|
||||||
|
@apply text-white font-medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link-light {
|
||||||
|
@apply text-gray-600 hover:text-gray-900 transition-colors duration-200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link-light-active {
|
||||||
|
@apply text-gray-900 font-medium;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body data-page="{{ request.endpoint }}" class="relative overflow-x-hidden dark bg-gray-900 text-white" x-data="{
|
||||||
|
darkMode: true,
|
||||||
mobileMenuOpen: false,
|
mobileMenuOpen: false,
|
||||||
userMenuOpen: false,
|
userMenuOpen: false,
|
||||||
showSettingsModal: false,
|
showSettingsModal: false,
|
||||||
@@ -103,8 +200,7 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
fetchDarkModeFromSession() {
|
fetchDarkModeFromSession() {
|
||||||
// Lade den Dark Mode-Status vom Server
|
fetch('/api/get_dark_mode')
|
||||||
fetch('/get_dark_mode')
|
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
@@ -121,8 +217,7 @@
|
|||||||
this.darkMode = !this.darkMode;
|
this.darkMode = !this.darkMode;
|
||||||
document.querySelector('html').classList.toggle('dark', this.darkMode);
|
document.querySelector('html').classList.toggle('dark', this.darkMode);
|
||||||
|
|
||||||
// Speichere den Dark Mode-Status auf dem Server
|
fetch('/api/set_dark_mode', {
|
||||||
fetch('/set_dark_mode', {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
@@ -132,9 +227,7 @@
|
|||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
// Zusätzlich im localStorage speichern für sofortige Reaktion bei Seitenwechsel
|
|
||||||
localStorage.setItem('darkMode', this.darkMode ? 'dark' : 'light');
|
localStorage.setItem('darkMode', this.darkMode ? 'dark' : 'light');
|
||||||
// Event auslösen für andere Komponenten
|
|
||||||
document.dispatchEvent(new CustomEvent('darkModeToggled', {
|
document.dispatchEvent(new CustomEvent('darkModeToggled', {
|
||||||
detail: { isDark: this.darkMode }
|
detail: { isDark: this.darkMode }
|
||||||
}));
|
}));
|
||||||
@@ -146,30 +239,9 @@
|
|||||||
console.error('Fehler beim Speichern der Dark Mode-Einstellung:', error);
|
console.error('Fehler beim Speichern der Dark Mode-Einstellung:', error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}));
|
}">
|
||||||
});
|
|
||||||
|
|
||||||
// MindMap global verfügbar machen (für Alpine.js und andere nicht-Module Skripte)
|
|
||||||
window.MindMap = MindMap;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Seitenspezifische Styles -->
|
|
||||||
{% block extra_css %}{% endblock %}
|
|
||||||
|
|
||||||
<!-- Cybertechnisches Netzwerk-Hintergrund -->
|
|
||||||
<script type="module" src="{{ url_for('static', filename='js/modules/cyber-network-init.js') }}"></script>
|
|
||||||
</head>
|
|
||||||
<body data-page="{{ request.endpoint }}" class="relative overflow-x-hidden">
|
|
||||||
<!-- Cybertechnisches Netzwerk-Hintergrund Container (wird via JavaScript befüllt) -->
|
|
||||||
<div id="cyber-background-container" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: -1; pointer-events: none; overflow: hidden;"></div>
|
|
||||||
|
|
||||||
<!-- Globaler Hintergrund -->
|
|
||||||
<div class="full-page-bg"></div>
|
|
||||||
<!-- Statischer Fallback-Hintergrund (wird nur angezeigt, wenn JavaScript deaktiviert ist) -->
|
|
||||||
<div class="fixed inset-0 z-[-9] bg-cover bg-center opacity-50"></div>
|
|
||||||
|
|
||||||
<!-- App-Container -->
|
<!-- 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'">
|
||||||
@@ -206,8 +278,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/30 to-indigo-500/30 text-gray-800 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 %}
|
||||||
@@ -228,9 +300,9 @@
|
|||||||
<div class="relative w-12 h-6">
|
<div class="relative w-12 h-6">
|
||||||
<input type="checkbox" id="darkModeToggle" class="sr-only" x-model="darkMode">
|
<input type="checkbox" id="darkModeToggle" class="sr-only" x-model="darkMode">
|
||||||
<div class="block w-12 h-6 rounded-full transition-colors duration-300"
|
<div class="block w-12 h-6 rounded-full transition-colors duration-300"
|
||||||
x-bind:class="darkMode ? 'bg-blue-400/50' : 'bg-gray-400/50'"></div>
|
x-bind:class="darkMode ? 'bg-purple-800/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"
|
<div class="dot absolute left-1 top-1 w-4 h-4 rounded-full transition-transform duration-300 shadow-md"
|
||||||
x-bind:class="darkMode ? 'bg-blue-500 transform translate-x-6' : 'bg-white'"></div>
|
x-bind:class="darkMode ? 'bg-purple-600 transform translate-x-6' : 'bg-white'"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-3 hidden sm:block"
|
<div class="ml-3 hidden sm:block"
|
||||||
x-bind:class="darkMode ? 'text-white/90' : 'text-gray-700'">
|
x-bind:class="darkMode ? 'text-white/90' : 'text-gray-700'">
|
||||||
@@ -308,13 +380,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
<a href="{{ url_for('login') }}"
|
<a href="{{ url_for('login') }}"
|
||||||
class="flex items-center px-4 py-2.5 rounded-xl font-medium transition-all duration-300"
|
class="py-2 px-4 rounded-lg transition-all duration-300"
|
||||||
x-bind:class="darkMode
|
x-bind:class="darkMode
|
||||||
? 'bg-gray-800/80 text-white hover:bg-gray-700/80 shadow-md hover:shadow-lg hover:-translate-y-0.5'
|
? 'text-white/90 hover:bg-dark-700/80'
|
||||||
: 'bg-gray-200/80 text-gray-800 hover:bg-gray-300/80 shadow-sm hover:shadow-md hover:-translate-y-0.5'">
|
: 'text-gray-700 hover:bg-gray-100/80'">
|
||||||
<i class="fa-solid fa-user mr-2"></i>Mein Konto
|
<i class="fa-solid fa-sign-in-alt mr-2"></i>Login
|
||||||
</a>
|
</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 +412,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"
|
||||||
@@ -511,11 +593,9 @@
|
|||||||
{% block scripts %}{% endblock %}
|
{% block scripts %}{% endblock %}
|
||||||
|
|
||||||
<!-- KI-Chat Initialisierung -->
|
<!-- KI-Chat Initialisierung -->
|
||||||
<script type="module">
|
<script>
|
||||||
// Importiere und initialisiere den ChatGPT-Assistenten direkt, um sicherzustellen,
|
// Initialisiere den ChatGPT-Assistenten direkt, um sicherzustellen,
|
||||||
// dass er auf jeder Seite verfügbar ist, selbst wenn MindMap nicht geladen ist
|
// dass er auf jeder Seite verfügbar ist, selbst wenn MindMap nicht geladen ist
|
||||||
import ChatGPTAssistant from "{{ url_for('static', filename='js/modules/chatgpt-assistant.js') }}";
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
// Prüfen, ob der Assistent bereits durch MindMap initialisiert wurde
|
// Prüfen, ob der Assistent bereits durch MindMap initialisiert wurde
|
||||||
if (!window.MindMap || !window.MindMap.assistant) {
|
if (!window.MindMap || !window.MindMap.assistant) {
|
||||||
|
|||||||
@@ -3,21 +3,31 @@
|
|||||||
{% block title %}403 - Zugriff verweigert{% endblock %}
|
{% block title %}403 - Zugriff verweigert{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="min-h-[65vh] flex flex-col items-center justify-center px-4 py-12">
|
<div class="min-h-[75vh] flex flex-col items-center justify-center px-4 py-12 bg-gradient-to-b from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800">
|
||||||
<div class="glass-effect max-w-2xl w-full p-8 rounded-lg border border-gray-300/20 dark:border-gray-700/30 shadow-lg">
|
<div class="glass-effect max-w-2xl w-full p-6 md:p-10 rounded-xl border border-gray-300/20 dark:border-gray-700/30 shadow-xl transform transition-all duration-300 hover:shadow-2xl">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<h1 class="text-6xl font-bold text-primary-600 dark:text-primary-400 mb-4">403</h1>
|
<div class="flex justify-center mb-6">
|
||||||
<h2 class="text-2xl font-semibold mb-4">Zugriff verweigert</h2>
|
<div class="relative">
|
||||||
<p class="text-gray-600 dark:text-gray-300 mb-8">Sie haben nicht die erforderlichen Berechtigungen, um auf diese Seite zuzugreifen. Bitte melden Sie sich an oder nutzen Sie ein Konto mit entsprechenden Rechten.</p>
|
<h1 class="text-7xl md:text-8xl font-extrabold text-primary-600 dark:text-primary-400 opacity-90">403</h1>
|
||||||
|
<div class="absolute -top-4 -right-4 w-12 h-12 bg-red-500 rounded-full flex items-center justify-center animate-pulse">
|
||||||
|
<i class="fa-solid fa-lock text-white text-xl"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h2 class="text-2xl md:text-3xl font-semibold mb-4 text-gray-800 dark:text-gray-200">Zugriff verweigert</h2>
|
||||||
|
<p class="text-gray-600 dark:text-gray-300 mb-8 max-w-lg mx-auto text-base md:text-lg">Sie haben nicht die erforderlichen Berechtigungen, um auf diese Seite zuzugreifen. Bitte melden Sie sich an oder nutzen Sie ein Konto mit entsprechenden Rechten.</p>
|
||||||
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||||
<a href="{{ url_for('index') }}" class="btn-primary">
|
<a href="{{ url_for('index') }}" class="btn-primary transform transition-transform duration-300 hover:scale-105 px-6 py-3 rounded-lg">
|
||||||
<i class="fa-solid fa-home mr-2"></i>Zur Startseite
|
<i class="fa-solid fa-home mr-2"></i>Zur Startseite
|
||||||
</a>
|
</a>
|
||||||
<a href="javascript:history.back()" class="btn-secondary">
|
<a href="javascript:history.back()" class="btn-secondary transform transition-transform duration-300 hover:scale-105 px-6 py-3 rounded-lg">
|
||||||
<i class="fa-solid fa-arrow-left mr-2"></i>Zurück
|
<i class="fa-solid fa-arrow-left mr-2"></i>Zurück
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mt-8 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
<p>Benötigen Sie Hilfe? <a href="#" class="text-primary-600 dark:text-primary-400 hover:underline">Kontaktieren Sie uns</a></p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -3,21 +3,31 @@
|
|||||||
{% block title %}404 - Seite nicht gefunden{% endblock %}
|
{% block title %}404 - Seite nicht gefunden{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="min-h-[65vh] flex flex-col items-center justify-center px-4 py-12">
|
<div class="min-h-[75vh] flex flex-col items-center justify-center px-4 py-12 bg-gradient-to-b from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800">
|
||||||
<div class="glass-effect max-w-2xl w-full p-8 rounded-lg border border-gray-300/20 dark:border-gray-700/30 shadow-lg">
|
<div class="glass-effect max-w-2xl w-full p-6 md:p-10 rounded-xl border border-gray-300/20 dark:border-gray-700/30 shadow-xl transform transition-all duration-300 hover:shadow-2xl">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<h1 class="text-6xl font-bold text-primary-600 dark:text-primary-400 mb-4">404</h1>
|
<div class="flex justify-center mb-6">
|
||||||
<h2 class="text-2xl font-semibold mb-4">Seite nicht gefunden</h2>
|
<div class="relative">
|
||||||
<p class="text-gray-600 dark:text-gray-300 mb-8">Die gesuchte Seite existiert nicht oder wurde verschoben. Bitte prüfen Sie die URL oder nutzen Sie die Navigation.</p>
|
<h1 class="text-7xl md:text-8xl font-extrabold text-primary-600 dark:text-primary-400 opacity-90">404</h1>
|
||||||
|
<div class="absolute -top-4 -right-4 w-12 h-12 bg-yellow-500 rounded-full flex items-center justify-center animate-pulse">
|
||||||
|
<i class="fa-solid fa-question text-white text-xl"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h2 class="text-2xl md:text-3xl font-semibold mb-4 text-gray-800 dark:text-gray-200">Seite nicht gefunden</h2>
|
||||||
|
<p class="text-gray-600 dark:text-gray-300 mb-8 max-w-lg mx-auto text-base md:text-lg">Die gesuchte Seite existiert nicht oder wurde verschoben. Bitte prüfen Sie die URL oder nutzen Sie die Navigation.</p>
|
||||||
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||||
<a href="{{ url_for('index') }}" class="btn-primary">
|
<a href="{{ url_for('index') }}" class="btn-primary transform transition-transform duration-300 hover:scale-105 px-6 py-3 rounded-lg">
|
||||||
<i class="fa-solid fa-home mr-2"></i>Zur Startseite
|
<i class="fa-solid fa-home mr-2"></i>Zur Startseite
|
||||||
</a>
|
</a>
|
||||||
<a href="javascript:history.back()" class="btn-secondary">
|
<a href="javascript:history.back()" class="btn-secondary transform transition-transform duration-300 hover:scale-105 px-6 py-3 rounded-lg">
|
||||||
<i class="fa-solid fa-arrow-left mr-2"></i>Zurück
|
<i class="fa-solid fa-arrow-left mr-2"></i>Zurück
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mt-8 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
<p>Benötigen Sie Hilfe? <a href="#" class="text-primary-600 dark:text-primary-400 hover:underline">Kontaktieren Sie uns</a></p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -3,21 +3,31 @@
|
|||||||
{% block title %}429 - Zu viele Anfragen{% endblock %}
|
{% block title %}429 - Zu viele Anfragen{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="min-h-[65vh] flex flex-col items-center justify-center px-4 py-12">
|
<div class="min-h-[75vh] flex flex-col items-center justify-center px-4 py-12 bg-gradient-to-b from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800">
|
||||||
<div class="glass-effect max-w-2xl w-full p-8 rounded-lg border border-gray-300/20 dark:border-gray-700/30 shadow-lg">
|
<div class="glass-effect max-w-2xl w-full p-6 md:p-10 rounded-xl border border-gray-300/20 dark:border-gray-700/30 shadow-xl transform transition-all duration-300 hover:shadow-2xl">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<h1 class="text-6xl font-bold text-primary-600 dark:text-primary-400 mb-4">429</h1>
|
<div class="flex justify-center mb-6">
|
||||||
<h2 class="text-2xl font-semibold mb-4">Zu viele Anfragen</h2>
|
<div class="relative">
|
||||||
<p class="text-gray-600 dark:text-gray-300 mb-8">Sie haben zu viele Anfragen in kurzer Zeit gestellt. Bitte warten Sie einen Moment und versuchen Sie es dann erneut.</p>
|
<h1 class="text-7xl md:text-8xl font-extrabold text-primary-600 dark:text-primary-400 opacity-90">429</h1>
|
||||||
|
<div class="absolute -top-4 -right-4 w-12 h-12 bg-orange-500 rounded-full flex items-center justify-center animate-pulse">
|
||||||
|
<i class="fa-solid fa-hourglass-half text-white text-xl"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h2 class="text-2xl md:text-3xl font-semibold mb-4 text-gray-800 dark:text-gray-200">Zu viele Anfragen</h2>
|
||||||
|
<p class="text-gray-600 dark:text-gray-300 mb-8 max-w-lg mx-auto text-base md:text-lg">Sie haben zu viele Anfragen in kurzer Zeit gestellt. Bitte warten Sie einen Moment und versuchen Sie es dann erneut.</p>
|
||||||
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||||
<a href="{{ url_for('index') }}" class="btn-primary">
|
<a href="{{ url_for('index') }}" class="btn-primary transform transition-transform duration-300 hover:scale-105 px-6 py-3 rounded-lg">
|
||||||
<i class="fa-solid fa-home mr-2"></i>Zur Startseite
|
<i class="fa-solid fa-home mr-2"></i>Zur Startseite
|
||||||
</a>
|
</a>
|
||||||
<a href="javascript:history.back()" class="btn-secondary">
|
<a href="javascript:history.back()" class="btn-secondary transform transition-transform duration-300 hover:scale-105 px-6 py-3 rounded-lg">
|
||||||
<i class="fa-solid fa-arrow-left mr-2"></i>Zurück
|
<i class="fa-solid fa-arrow-left mr-2"></i>Zurück
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mt-8 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
<p>Benötigen Sie Hilfe? <a href="#" class="text-primary-600 dark:text-primary-400 hover:underline">Kontaktieren Sie uns</a></p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -3,21 +3,31 @@
|
|||||||
{% block title %}500 - Serverfehler{% endblock %}
|
{% block title %}500 - Serverfehler{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="min-h-[65vh] flex flex-col items-center justify-center px-4 py-12">
|
<div class="min-h-[75vh] flex flex-col items-center justify-center px-4 py-12 bg-gradient-to-b from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800">
|
||||||
<div class="glass-effect max-w-2xl w-full p-8 rounded-lg border border-gray-300/20 dark:border-gray-700/30 shadow-lg">
|
<div class="glass-effect max-w-2xl w-full p-6 md:p-10 rounded-xl border border-gray-300/20 dark:border-gray-700/30 shadow-xl transform transition-all duration-300 hover:shadow-2xl">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<h1 class="text-6xl font-bold text-primary-600 dark:text-primary-400 mb-4">500</h1>
|
<div class="flex justify-center mb-6">
|
||||||
<h2 class="text-2xl font-semibold mb-4">Interner Serverfehler</h2>
|
<div class="relative">
|
||||||
<p class="text-gray-600 dark:text-gray-300 mb-8">Es ist ein Fehler auf unserem Server aufgetreten. Unser Team wurde informiert und arbeitet bereits an einer Lösung. Bitte versuchen Sie es später erneut.</p>
|
<h1 class="text-7xl md:text-8xl font-extrabold text-primary-600 dark:text-primary-400 opacity-90">500</h1>
|
||||||
|
<div class="absolute -top-4 -right-4 w-12 h-12 bg-red-600 rounded-full flex items-center justify-center animate-pulse">
|
||||||
|
<i class="fa-solid fa-exclamation-triangle text-white text-xl"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h2 class="text-2xl md:text-3xl font-semibold mb-4 text-gray-800 dark:text-gray-200">Interner Serverfehler</h2>
|
||||||
|
<p class="text-gray-600 dark:text-gray-300 mb-8 max-w-lg mx-auto text-base md:text-lg">Es ist ein Fehler auf unserem Server aufgetreten. Unser Team wurde informiert und arbeitet bereits an einer Lösung. Bitte versuchen Sie es später erneut.</p>
|
||||||
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||||
<a href="{{ url_for('index') }}" class="btn-primary">
|
<a href="{{ url_for('index') }}" class="btn-primary transform transition-transform duration-300 hover:scale-105 px-6 py-3 rounded-lg">
|
||||||
<i class="fa-solid fa-home mr-2"></i>Zur Startseite
|
<i class="fa-solid fa-home mr-2"></i>Zur Startseite
|
||||||
</a>
|
</a>
|
||||||
<a href="javascript:history.back()" class="btn-secondary">
|
<a href="javascript:history.back()" class="btn-secondary transform transition-transform duration-300 hover:scale-105 px-6 py-3 rounded-lg">
|
||||||
<i class="fa-solid fa-arrow-left mr-2"></i>Zurück
|
<i class="fa-solid fa-arrow-left mr-2"></i>Zurück
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mt-8 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
<p>Benötigen Sie Hilfe? <a href="#" class="text-primary-600 dark:text-primary-400 hover:underline">Kontaktieren Sie uns</a></p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
{% block extra_css %}
|
{% block extra_css %}
|
||||||
<style>
|
<style>
|
||||||
/* Hintergrund über die gesamte Seite erstrecken */
|
/* Full height and width for the page */
|
||||||
html, body {
|
html, body {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -13,152 +13,146 @@
|
|||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Entferne den Gradient-Hintergrund vollständig */
|
/* Remove gradient backgrounds */
|
||||||
.hero-gradient, .bg-fade {
|
.hero-gradient, .bg-fade {
|
||||||
background: none !important;
|
background: none !important;
|
||||||
clip-path: none !important;
|
clip-path: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tech-line {
|
/* Style elements */
|
||||||
|
.mystical-line {
|
||||||
height: 1px;
|
height: 1px;
|
||||||
background: linear-gradient(to right, transparent, rgba(100, 100, 100, 0.1), transparent);
|
background: linear-gradient(to right, transparent, rgba(109, 40, 217, 0.2), transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tech-dot {
|
.dark .mystical-line {
|
||||||
width: 4px;
|
background: linear-gradient(to right, transparent, rgba(139, 92, 246, 0.2), transparent);
|
||||||
height: 4px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: rgba(100, 100, 100, 0.2);
|
|
||||||
position: absolute;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .tech-line {
|
/* Text reveal animation */
|
||||||
background: linear-gradient(to right, transparent, rgba(255, 255, 255, 0.1), transparent);
|
@keyframes textReveal {
|
||||||
|
0% { clip-path: polygon(0 0, 0 0, 0 100%, 0 100%); }
|
||||||
|
100% { clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%); }
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .tech-dot {
|
.text-reveal {
|
||||||
background-color: rgba(255, 255, 255, 0.3);
|
animation: textReveal 1s cubic-bezier(0.77, 0, 0.18, 1) forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulse {
|
.delay-1 { animation-delay: 0.2s; }
|
||||||
0% { r: 10; opacity: 0.7; }
|
.delay-2 { animation-delay: 0.4s; }
|
||||||
50% { r: 12; opacity: 1; }
|
.delay-3 { animation-delay: 0.6s; }
|
||||||
100% { r: 10; opacity: 0.7; }
|
|
||||||
|
/* Home page specific styles */
|
||||||
|
.featured-card {
|
||||||
|
transition: transform 0.5s ease, box-shadow 0.5s ease;
|
||||||
|
border: 1px solid;
|
||||||
|
border-color: rgba(139, 92, 246, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate-pulse {
|
.dark .featured-card {
|
||||||
animation: pulse 3s ease-in-out infinite;
|
border-color: rgba(109, 40, 217, 0.2);
|
||||||
|
background-color: rgba(17, 24, 39, 0.7);
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes iconPulse {
|
.featured-card:hover {
|
||||||
0% { transform: scale(1); }
|
transform: translateY(-5px);
|
||||||
50% { transform: scale(1.1); }
|
|
||||||
100% { transform: scale(1); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-pulse {
|
.dark .featured-card:hover {
|
||||||
animation: iconPulse 3s ease-in-out infinite;
|
box-shadow: 0 5px 15px rgba(109, 40, 217, 0.2);
|
||||||
display: inline-block;
|
border-color: rgba(109, 40, 217, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Volle Seitenbreite für Container */
|
.featured-card:hover {
|
||||||
#app-container, .container, main, .mx-auto, .py-12 {
|
box-shadow: 0 5px 15px rgba(139, 92, 246, 0.1);
|
||||||
width: 100%;
|
border-color: rgba(139, 92, 246, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Sicherstellen dass der Hintergrund die ganze Seite abdeckt */
|
/* Chat section styles */
|
||||||
.full-page-bg {
|
.embedded-chat {
|
||||||
position: fixed;
|
height: 350px;
|
||||||
top: 0;
|
border-radius: 1rem;
|
||||||
left: 0;
|
overflow: hidden;
|
||||||
width: 100vw;
|
transition: all 0.3s ease;
|
||||||
height: 100vh;
|
border: 1px solid;
|
||||||
z-index: -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Chat-Animationen */
|
.dark .embedded-chat {
|
||||||
.typing-dots span {
|
background-color: rgba(17, 24, 39, 0.7);
|
||||||
animation-duration: 1.2s;
|
border-color: rgba(109, 40, 217, 0.2);
|
||||||
animation-iteration-count: infinite;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Chat-Nachrichten-Animation */
|
.embedded-chat {
|
||||||
@keyframes fadeInUp {
|
background-color: rgba(255, 255, 255, 0.7);
|
||||||
from {
|
border-color: rgba(139, 92, 246, 0.1);
|
||||||
opacity: 0;
|
|
||||||
transform: translate3d(0, 10px, 0);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translate3d(0, 0, 0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#embedded-chat-messages > div {
|
|
||||||
animation: fadeInUp 0.3s ease-out forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Sanftes Scrollen im Chat */
|
|
||||||
#embedded-chat-messages {
|
#embedded-chat-messages {
|
||||||
scroll-behavior: smooth;
|
height: 250px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Benutzerdefinierter Scrollbar für den Chat */
|
/* Chat typing indicator */
|
||||||
#embedded-chat-messages::-webkit-scrollbar {
|
.typing-dots span {
|
||||||
width: 6px;
|
display: inline-block;
|
||||||
|
width: 5px;
|
||||||
|
height: 5px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-right: 3px;
|
||||||
|
background-color: currentColor;
|
||||||
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
#embedded-chat-messages::-webkit-scrollbar-track {
|
.typing-dots span:nth-child(1) {
|
||||||
background-color: rgba(0, 0, 0, 0.05);
|
animation: dot-pulse 1.2s infinite ease-in-out;
|
||||||
border-radius: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#embedded-chat-messages::-webkit-scrollbar-thumb {
|
.typing-dots span:nth-child(2) {
|
||||||
background-color: rgba(139, 92, 246, 0.3);
|
animation: dot-pulse 1.2s infinite ease-in-out 0.2s;
|
||||||
border-radius: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark #embedded-chat-messages::-webkit-scrollbar-thumb {
|
.typing-dots span:nth-child(3) {
|
||||||
background-color: rgba(139, 92, 246, 0.5);
|
animation: dot-pulse 1.2s infinite ease-in-out 0.4s;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hover-Effekt für Quick-Query-Buttons */
|
@keyframes dot-pulse {
|
||||||
.quick-query-btn:hover {
|
0%, 100% { transform: scale(1); opacity: 0.5; }
|
||||||
cursor: pointer;
|
50% { transform: scale(1.3); opacity: 1; }
|
||||||
background: linear-gradient(to right, rgba(139, 92, 246, 0.1), rgba(96, 165, 250, 0.1));
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .quick-query-btn:hover {
|
|
||||||
background: linear-gradient(to right, rgba(139, 92, 246, 0.2), rgba(96, 165, 250, 0.2));
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<!-- Hintergrund für die gesamte Seite -->
|
|
||||||
<div class="full-page-bg gradient-background"></div>
|
|
||||||
|
|
||||||
<!-- Hero Section -->
|
<!-- Hero Section -->
|
||||||
<section class="relative pt-20 pb-32">
|
<section class="relative pt-20 pb-24">
|
||||||
<!-- Hero Content -->
|
<!-- Hero Content -->
|
||||||
<div class="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
<div class="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||||
<div class="text-center mb-16">
|
<div class="text-center mb-16">
|
||||||
<h1 class="hero-heading mb-8 text-gray-900 dark:text-white">
|
<h1 class="hero-heading mb-8 text-gray-900 dark:text-white">
|
||||||
<span class="gradient-text inline-block transform transition-all duration-700 hover:scale-105">Wissen</span> neu
|
<div class="overflow-hidden">
|
||||||
<div class="mt-2 relative">
|
<span class="gradient-text inline-block text-reveal">Wissen</span>
|
||||||
<span class="relative inline-block">vernetzen
|
</div>
|
||||||
|
<div class="overflow-hidden mt-2">
|
||||||
|
<span class="inline-block text-reveal delay-1">neu</span>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 relative overflow-hidden">
|
||||||
|
<span class="relative inline-block text-reveal delay-2">vernetzen
|
||||||
<div class="absolute -bottom-2 left-0 right-0 h-1 bg-gradient-to-r from-purple-500/0 via-purple-500/70 to-purple-500/0 rounded-full"></div>
|
<div class="absolute -bottom-2 left-0 right-0 h-1 bg-gradient-to-r from-purple-500/0 via-purple-500/70 to-purple-500/0 rounded-full"></div>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-xl md:text-2xl text-gray-700 dark:text-gray-300 max-w-3xl mx-auto mb-12">
|
<div class="overflow-hidden">
|
||||||
|
<p class="text-xl md:text-2xl text-gray-700 dark:text-gray-300 max-w-3xl mx-auto mb-12 text-reveal delay-3">
|
||||||
Erkunde komplexe Ideen visuell, schaffe Verbindungen und teile deine Gedanken
|
Erkunde komplexe Ideen visuell, schaffe Verbindungen und teile deine Gedanken
|
||||||
in einem interaktiven Wissensnetzwerk.
|
in einem interaktiven Wissensnetzwerk.
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
<div class="flex flex-col sm:flex-row gap-5 justify-center">
|
<div class="flex flex-col sm:flex-row gap-5 justify-center">
|
||||||
<a href="{{ url_for('mindmap') }}" class="group transition-all duration-300 bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-700 hover:to-indigo-700 text-white font-medium text-lg px-8 py-4 rounded-2xl shadow-lg hover:shadow-xl hover:shadow-purple-500/20 transform hover:-translate-y-1">
|
<a href="{{ url_for('mindmap') }}" class="mystical-button mystical-button-primary group transition-all duration-300">
|
||||||
<span class="flex items-center justify-center">
|
<span class="flex items-center justify-center">
|
||||||
<i class="fa-solid fa-diagram-project mr-3 text-purple-200 group-hover:text-white transition-all duration-300 animate-pulse"></i>
|
<i class="fa-solid fa-diagram-project mr-3 text-purple-200 group-hover:text-white transition-all duration-300"></i>
|
||||||
<span class="relative">
|
<span class="relative">
|
||||||
Mindmap erkunden
|
Mindmap erkunden
|
||||||
<span class="absolute -bottom-1 left-0 w-0 h-0.5 bg-white group-hover:w-full transition-all duration-300"></span>
|
<span class="absolute -bottom-1 left-0 w-0 h-0.5 bg-white group-hover:w-full transition-all duration-300"></span>
|
||||||
@@ -166,12 +160,12 @@
|
|||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
{% if not current_user.is_authenticated %}
|
{% if not current_user.is_authenticated %}
|
||||||
<a href="{{ url_for('register') }}" class="group transition-all duration-300 bg-gradient-to-r from-blue-500 to-cyan-500 hover:from-blue-600 hover:to-cyan-600 text-white font-medium text-lg px-8 py-4 rounded-2xl shadow-lg hover:shadow-xl hover:shadow-blue-500/20 transform hover:-translate-y-1">
|
<a href="{{ url_for('register') }}" class="mystical-button mystical-button-secondary group transition-all duration-300">
|
||||||
<span class="flex items-center justify-center">
|
<span class="flex items-center justify-center">
|
||||||
<i class="fa-solid fa-user-plus mr-3 text-blue-200 group-hover:text-white transition-all duration-300 icon-pulse"></i>
|
<i class="fa-solid fa-user-plus mr-3 group-hover:text-accent-secondary-dark dark:group-hover:text-accent-secondary-light transition-all duration-300"></i>
|
||||||
<span class="relative">
|
<span class="relative">
|
||||||
Konto erstellen
|
Konto erstellen
|
||||||
<span class="absolute -bottom-1 left-0 w-0 h-0.5 bg-white group-hover:w-full transition-all duration-300"></span>
|
<span class="absolute -bottom-1 left-0 w-0 h-0.5 bg-accent-primary-light dark:bg-accent-primary-dark group-hover:w-full transition-all duration-300"></span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
@@ -179,61 +173,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tech illustration -->
|
<!-- Central logo and name -->
|
||||||
<div class="relative w-full max-w-4xl mx-auto h-80 sm:h-96">
|
<div class="relative w-full max-w-4xl mx-auto h-40 sm:h-60 mb-16">
|
||||||
<div class="absolute inset-0 flex items-center justify-center">
|
<div class="absolute inset-0 flex items-center justify-center">
|
||||||
<div class="hidden md:block text-center">
|
<div class="text-center">
|
||||||
<div class="text-3xl font-bold gradient-text mb-2 animate-float">Systades</div>
|
<div class="text-3xl font-bold gradient-text mb-2 animate-float">Systades</div>
|
||||||
<div class="text-lg text-gray-700 dark:text-gray-300">WISSEN VERNETZEN</div>
|
<div class="text-lg text-gray-700 dark:text-gray-300">WISSEN VERNETZEN</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Network Visualization with SVG -->
|
|
||||||
<svg class="absolute inset-0 w-full h-full" viewBox="0 0 800 600" preserveAspectRatio="xMidYMid meet">
|
|
||||||
<!-- Glossy Nodes and Lines -->
|
|
||||||
<defs>
|
|
||||||
<radialGradient id="nodeGradient" cx="50%" cy="50%" r="50%" fx="50%" fy="50%">
|
|
||||||
<stop offset="0%" stop-color="rgba(139, 92, 246, 0.9)" />
|
|
||||||
<stop offset="100%" stop-color="rgba(79, 70, 229, 0.5)" />
|
|
||||||
</radialGradient>
|
|
||||||
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
|
|
||||||
<feGaussianBlur stdDeviation="5" result="blur" />
|
|
||||||
<feComposite in="SourceGraphic" in2="blur" operator="over" />
|
|
||||||
</filter>
|
|
||||||
</defs>
|
|
||||||
|
|
||||||
<!-- Network Lines -->
|
|
||||||
<g class="lines">
|
|
||||||
<!-- Connection network -->
|
|
||||||
<line x1="200" y1="250" x2="400" y2="150" stroke="rgba(0,0,0,0.1)" stroke-width="1" class="dark:hidden" />
|
|
||||||
<line x1="400" y1="150" x2="600" y2="250" stroke="rgba(0,0,0,0.1)" stroke-width="1" class="dark:hidden" />
|
|
||||||
<line x1="600" y1="250" x2="400" y2="350" stroke="rgba(0,0,0,0.1)" stroke-width="1" class="dark:hidden" />
|
|
||||||
<line x1="400" y1="350" x2="200" y2="250" stroke="rgba(0,0,0,0.1)" stroke-width="1" class="dark:hidden" />
|
|
||||||
<line x1="400" y1="150" x2="400" y2="350" stroke="rgba(0,0,0,0.1)" stroke-width="1" class="dark:hidden" />
|
|
||||||
<line x1="200" y1="250" x2="600" y2="250" stroke="rgba(0,0,0,0.1)" stroke-width="1" class="dark:hidden" />
|
|
||||||
|
|
||||||
<!-- Dark mode connections -->
|
|
||||||
<line x1="200" y1="250" x2="400" y2="150" stroke="rgba(255,255,255,0.1)" stroke-width="1" class="hidden dark:inline" />
|
|
||||||
<line x1="400" y1="150" x2="600" y2="250" stroke="rgba(255,255,255,0.1)" stroke-width="1" class="hidden dark:inline" />
|
|
||||||
<line x1="600" y1="250" x2="400" y2="350" stroke="rgba(255,255,255,0.1)" stroke-width="1" class="hidden dark:inline" />
|
|
||||||
<line x1="400" y1="350" x2="200" y2="250" stroke="rgba(255,255,255,0.1)" stroke-width="1" class="hidden dark:inline" />
|
|
||||||
<line x1="400" y1="150" x2="400" y2="350" stroke="rgba(255,255,255,0.1)" stroke-width="1" class="hidden dark:inline" />
|
|
||||||
<line x1="200" y1="250" x2="600" y2="250" stroke="rgba(255,255,255,0.1)" stroke-width="1" class="hidden dark:inline" />
|
|
||||||
|
|
||||||
<!-- Pulse animation for some lines -->
|
|
||||||
<line class="animate-pulse" x1="400" y1="150" x2="300" y2="200" stroke="rgba(139, 92, 246, 0.5)" stroke-width="2" />
|
|
||||||
<line class="animate-pulse" x1="400" y1="350" x2="500" y2="300" stroke="rgba(168, 85, 247, 0.5)" stroke-width="2" />
|
|
||||||
</g>
|
|
||||||
|
|
||||||
<!-- Network Nodes -->
|
|
||||||
<g class="nodes">
|
|
||||||
<circle cx="400" cy="150" r="15" fill="url(#nodeGradient)" filter="url(#glow)" class="animate-pulse float-animation" />
|
|
||||||
<circle cx="200" cy="250" r="10" fill="url(#nodeGradient)" class="float-animation" />
|
|
||||||
<circle cx="600" cy="250" r="10" fill="url(#nodeGradient)" class="float-animation" />
|
|
||||||
<circle cx="400" cy="350" r="15" fill="url(#nodeGradient)" filter="url(#glow)" class="animate-pulse float-animation" />
|
|
||||||
<circle cx="300" cy="200" r="8" fill="url(#nodeGradient)" class="float-animation" />
|
|
||||||
<circle cx="500" cy="300" r="8" fill="url(#nodeGradient)" class="float-animation" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -241,363 +187,334 @@
|
|||||||
|
|
||||||
<!-- Features Section -->
|
<!-- Features Section -->
|
||||||
<section class="py-20 relative">
|
<section class="py-20 relative">
|
||||||
<div class="tech-line absolute top-0 left-1/2 transform -translate-x-1/2 w-1/3"></div>
|
<div class="mystical-line absolute top-0 left-1/2 transform -translate-x-1/2 w-1/3"></div>
|
||||||
|
|
||||||
<div class="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div class="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div class="text-center mb-16">
|
<div class="text-center mb-16">
|
||||||
<h2 class="section-heading mb-4 text-gray-900 dark:text-white">Was ist <span class="gradient-text">Systades?</span></h2>
|
<h2 class="section-heading mb-4 text-gray-900 dark:text-white">Was ist <span class="gradient-text">Systades?</span></h2>
|
||||||
<p class="text-lg text-gray-700 dark:text-gray-300 max-w-3xl mx-auto">
|
<p class="text-lg text-gray-700 dark:text-gray-300 max-w-3xl mx-auto">
|
||||||
Ein modernes Werkzeug zum Visualisieren, Erforschen und Teilen von Wissen
|
Ein modernes Werkzeug zum Visualisieren, Erforschen und Teilen von Wissen in einem interaktiven
|
||||||
in einer intuitiven, interaktiven Umgebung.
|
Netzwerk, das dir hilft, Verbindungen zu entdecken und dein Wissen zu organisieren.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
<!-- Feature Cards -->
|
||||||
<!-- Feature Card 1 -->
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-8 mb-12">
|
||||||
<div class="feature-card p-8 rounded-3xl hover:-translate-y-3 transform transition-all duration-300">
|
<!-- Feature 1: Visualize -->
|
||||||
<div class="icon mb-6 rounded-2xl shadow-lg">
|
<div class="featured-card rounded-2xl p-6 bg-white/80 dark:bg-gray-800/30 backdrop-blur-sm">
|
||||||
<i class="fa-solid fa-brain"></i>
|
<div class="mb-4 text-purple-600 dark:text-purple-400">
|
||||||
|
<i class="fa-solid fa-diagram-project text-3xl"></i>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-xl font-semibold mb-3">Visualisiere Wissen</h3>
|
<h3 class="text-xl font-semibold mb-2 text-gray-900 dark:text-white">Visualisiere Wissen</h3>
|
||||||
<p>
|
<p class="text-gray-700 dark:text-gray-300">
|
||||||
Sieh Wissen als vernetztes System, entdecke Zusammenhänge und erkenne überraschende
|
Organisiere Gedanken und Ideen in einem interaktiven Netzwerk, das komplexe Beziehungen
|
||||||
Verbindungen zwischen verschiedenen Themengebieten.
|
visuell darstellt und neue Verbindungen offenbart.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Feature Card 2 -->
|
<!-- Feature 2: Connect -->
|
||||||
<div class="feature-card p-8 rounded-3xl hover:-translate-y-3 transform transition-all duration-300">
|
<div class="featured-card rounded-2xl p-6 bg-white/80 dark:bg-gray-800/30 backdrop-blur-sm">
|
||||||
<div class="icon mb-6 rounded-2xl shadow-lg">
|
<div class="mb-4 text-indigo-600 dark:text-indigo-400">
|
||||||
<i class="fa-solid fa-lightbulb"></i>
|
<i class="fa-solid fa-network-wired text-3xl"></i>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-xl font-semibold mb-3">Teile Gedanken</h3>
|
<h3 class="text-xl font-semibold mb-2 text-gray-900 dark:text-white">Verknüpfe Gedanken</h3>
|
||||||
<p>
|
<p class="text-gray-700 dark:text-gray-300">
|
||||||
Füge deine eigenen Ideen und Perspektiven hinzu. Erstelle Verbindungen zu
|
Entdecke Zusammenhänge zwischen scheinbar unzusammenhängenden Ideen und schaffe dein
|
||||||
vorhandenen Gedanken und bereichere die wachsende Wissensbasis.
|
persönliches Wissensnetzwerk über verschiedene Bereiche hinweg.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Feature Card 3 -->
|
<!-- Feature 3: Share -->
|
||||||
<div class="feature-card p-8 rounded-3xl hover:-translate-y-3 transform transition-all duration-300">
|
<div class="featured-card rounded-2xl p-6 bg-white/80 dark:bg-gray-800/30 backdrop-blur-sm">
|
||||||
<div class="icon mb-6 rounded-2xl shadow-lg">
|
<div class="mb-4 text-purple-600 dark:text-purple-400">
|
||||||
<i class="fa-solid fa-users"></i>
|
<i class="fa-solid fa-share-nodes text-3xl"></i>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-xl font-semibold mb-3">Community</h3>
|
<h3 class="text-xl font-semibold mb-2 text-gray-900 dark:text-white">Teile und Entdecke</h3>
|
||||||
<p>
|
<p class="text-gray-700 dark:text-gray-300">
|
||||||
Sei Teil einer Gemeinschaft, die gemeinsam ein verteiltes Wissensarchiv aufbaut
|
Tausche Erkenntnisse mit anderen aus und erweitere dein Wissen durch die
|
||||||
und sich in thematisch fokussierten Bereichen austauscht.
|
Perspektiven und Gedanken der Community.
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Feature Card 4 -->
|
|
||||||
<div class="feature-card p-8 rounded-3xl hover:-translate-y-3 transform transition-all duration-300">
|
|
||||||
<div class="icon mb-6 rounded-2xl shadow-lg">
|
|
||||||
<i class="fa-solid fa-robot"></i>
|
|
||||||
</div>
|
|
||||||
<h3 class="text-xl font-semibold mb-3">KI-Assistenz</h3>
|
|
||||||
<p>
|
|
||||||
Lass dir von künstlicher Intelligenz helfen, neue Zusammenhänge zu entdecken,
|
|
||||||
Inhalte zusammenzufassen und Fragen zu beantworten.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Feature Card 5 -->
|
|
||||||
<div class="feature-card p-8 rounded-3xl hover:-translate-y-3 transform transition-all duration-300">
|
|
||||||
<div class="icon mb-6 rounded-2xl shadow-lg">
|
|
||||||
<i class="fa-solid fa-search"></i>
|
|
||||||
</div>
|
|
||||||
<h3 class="text-xl font-semibold mb-3">Intelligente Suche</h3>
|
|
||||||
<p>
|
|
||||||
Finde genau die Informationen, die du suchst, mit fortschrittlichen Such- und
|
|
||||||
Filterfunktionen für eine präzise Navigation durch das Wissen.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Feature Card 6 -->
|
|
||||||
<div class="feature-card p-8 rounded-3xl hover:-translate-y-3 transform transition-all duration-300">
|
|
||||||
<div class="icon mb-6 rounded-2xl shadow-lg">
|
|
||||||
<i class="fa-solid fa-route"></i>
|
|
||||||
</div>
|
|
||||||
<h3 class="text-xl font-semibold mb-3">Geführte Pfade</h3>
|
|
||||||
<p>
|
|
||||||
Folge kuratierten Lernpfaden durch komplexe Themen oder erschaffe selbst
|
|
||||||
Routen für andere, die deinen Gedankengängen folgen möchten.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Call to Action Section -->
|
<!-- AI Assistant Preview -->
|
||||||
<section class="py-16 sm:py-20 md:py-24 relative overflow-hidden">
|
<section class="py-20 relative">
|
||||||
<div class="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
<div class="mystical-line absolute top-0 left-1/2 transform -translate-x-1/2 w-1/3"></div>
|
||||||
<div class="glass-effect p-6 sm:p-8 md:p-12 rounded-3xl transform transition-all duration-500 hover:-translate-y-2 hover:shadow-2xl bg-gradient-to-br from-purple-500/15 to-blue-500/15 backdrop-blur-xl border border-white/10 shadow-lg">
|
|
||||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-6">
|
|
||||||
<div class="md:w-2/3">
|
|
||||||
<h2 class="text-2xl sm:text-3xl lg:text-4xl font-bold mb-3 text-gray-900 dark:text-white leading-tight">
|
|
||||||
Bereit, <span class="gradient-text bg-clip-text text-transparent bg-gradient-to-r from-purple-500 to-blue-500">Wissen</span> neu zu entdecken?
|
|
||||||
</h2>
|
|
||||||
<p class="text-gray-700 dark:text-gray-300 text-base sm:text-lg mb-6 md:mb-0 max-w-2xl">
|
|
||||||
Starte jetzt deine Reise durch das Wissensnetzwerk und erschließe neue Perspektiven.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="md:w-1/3 text-center md:text-right">
|
|
||||||
<a href="{{ url_for('mindmap') }}" class="inline-flex items-center justify-center w-full md:w-auto btn-primary font-bold py-3 sm:py-3.5 px-6 sm:px-8 rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1 hover:scale-105 bg-gradient-to-r from-purple-600 to-blue-600 text-white">
|
|
||||||
<span class="flex items-center justify-center">
|
|
||||||
<i class="fa-solid fa-arrow-right mr-2"></i>
|
|
||||||
<span>Zur Mindmap</span>
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Quick Access Section -->
|
|
||||||
<section class="py-16 sm:py-20">
|
|
||||||
<div class="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div class="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 lg:gap-8">
|
<div class="text-center mb-12">
|
||||||
<!-- Themen-Übersicht -->
|
<h2 class="section-heading mb-4 text-gray-900 dark:text-white">Dein <span class="gradient-text">KI-Assistent</span></h2>
|
||||||
<div class="glass-morphism p-6 sm:p-8 rounded-3xl transition-all duration-500 hover:-translate-y-3 hover:shadow-xl border border-white/10 backdrop-blur-md">
|
<p class="text-lg text-gray-700 dark:text-gray-300 max-w-3xl mx-auto">
|
||||||
<h3 class="text-xl font-bold mb-4 flex items-center text-gray-800 dark:text-white">
|
Unser integrierter KI-Assistent hilft dir, Wissen zu organisieren, Verbindungen zu finden und
|
||||||
<div class="w-10 h-10 sm:w-12 sm:h-12 rounded-2xl bg-gradient-to-r from-violet-500 to-fuchsia-500 flex items-center justify-center mr-3 sm:mr-4 shadow-md transform transition-transform duration-300 hover:scale-110">
|
Einsichten zu gewinnen.
|
||||||
<i class="fa-solid fa-fire text-white text-base sm:text-lg"></i>
|
</p>
|
||||||
</div>
|
|
||||||
<span class="text-lg sm:text-xl md:text-2xl">Themen-Übersicht</span>
|
|
||||||
</h3>
|
|
||||||
<div class="space-y-3 sm:space-y-4 mb-6">
|
|
||||||
<a href="{{ url_for('mindmap') }}" class="flex items-center p-3 sm:p-3.5 rounded-xl hover:bg-gray-100/50 dark:hover:bg-white/5 transition-all duration-200 group">
|
|
||||||
<div class="w-3 h-3 rounded-full bg-purple-400 mr-3 group-hover:scale-125 transition-transform"></div>
|
|
||||||
<div class="flex-grow">
|
|
||||||
<p class="font-medium text-gray-800 dark:text-gray-200">Wissensbereiche <span class="text-xs text-gray-500">(12)</span></p>
|
|
||||||
<p class="text-xs sm:text-sm text-gray-500 dark:text-gray-400">Überblick über Themenbereiche</p>
|
|
||||||
</div>
|
|
||||||
<i class="fa-solid fa-chevron-right text-gray-500 group-hover:translate-x-1 transition-transform"></i>
|
|
||||||
</a>
|
|
||||||
<a href="{{ url_for('search_thoughts_page') }}" class="flex items-center p-3 sm:p-3.5 rounded-xl hover:bg-gray-100/50 dark:hover:bg-white/5 transition-all duration-200 group">
|
|
||||||
<div class="w-3 h-3 rounded-full bg-blue-400 mr-3 group-hover:scale-125 transition-transform"></div>
|
|
||||||
<div class="flex-grow">
|
|
||||||
<p class="font-medium text-gray-800 dark:text-gray-200">Gedanken <span class="text-xs text-gray-500">(87)</span></p>
|
|
||||||
<p class="text-xs sm:text-sm text-gray-500 dark:text-gray-400">Konkrete Einträge durchsuchen</p>
|
|
||||||
</div>
|
|
||||||
<i class="fa-solid fa-chevron-right text-gray-500 group-hover:translate-x-1 transition-transform"></i>
|
|
||||||
</a>
|
|
||||||
<a href="#" class="flex items-center p-3 sm:p-3.5 rounded-xl hover:bg-gray-100/50 dark:hover:bg-white/5 transition-all duration-200 group">
|
|
||||||
<div class="w-3 h-3 rounded-full bg-green-400 mr-3 group-hover:scale-125 transition-transform"></div>
|
|
||||||
<div class="flex-grow">
|
|
||||||
<p class="font-medium text-gray-800 dark:text-gray-200">Verbindungen <span class="text-xs text-gray-500">(34)</span></p>
|
|
||||||
<p class="text-xs sm:text-sm text-gray-500 dark:text-gray-400">Beziehungen zwischen Gedanken</p>
|
|
||||||
</div>
|
|
||||||
<i class="fa-solid fa-chevron-right text-gray-500 group-hover:translate-x-1 transition-transform"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<a href="{{ url_for('search_thoughts_page') }}" class="btn-primary w-full text-center rounded-xl py-3 sm:py-3.5 transform transition-all duration-300 hover:-translate-y-1 hover:shadow-lg flex items-center justify-center">
|
|
||||||
<span>Alle Themen entdecken</span>
|
|
||||||
<i class="fa-solid fa-arrow-right ml-2"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- KI-Assistent mit eingebettetem Chat -->
|
<!-- Chat Interface Preview -->
|
||||||
<div class="glass-morphism p-6 sm:p-8 rounded-3xl transition-all duration-500 hover:-translate-y-1 hover:shadow-xl backdrop-blur-md border border-white/10">
|
<div class="max-w-3xl mx-auto">
|
||||||
<h3 class="text-xl md:text-2xl font-bold mb-4 flex flex-wrap sm:flex-nowrap items-center text-gray-800 dark:text-white">
|
<div class="embedded-chat" id="demo-chat">
|
||||||
<div class="w-10 h-10 sm:w-12 sm:h-12 rounded-2xl bg-gradient-to-r from-purple-600 to-blue-600 flex items-center justify-center mr-3 sm:mr-4 shadow-lg transform transition-transform duration-300 hover:scale-110">
|
<!-- Chat Header -->
|
||||||
<i class="fa-solid fa-robot text-white text-base sm:text-lg"></i>
|
<div class="p-4 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="w-8 h-8 rounded-full bg-gradient-to-r from-purple-600 to-indigo-600 flex items-center justify-center text-white mr-3">
|
||||||
|
<i class="fa-solid fa-robot text-sm"></i>
|
||||||
|
</div>
|
||||||
|
<span class="font-medium text-gray-800 dark:text-gray-200">Systades Assistent (4o-mini)</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button id="open-real-assistant" class="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
||||||
|
<i class="fa-solid fa-expand"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="mt-1 sm:mt-0">KI-Assistent</span>
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<!-- Eingebettetes Chat-Interface -->
|
<!-- Chat Messages -->
|
||||||
<div id="embedded-assistant" class="rounded-xl border border-gray-200/50 dark:border-gray-700/50 overflow-hidden flex flex-col h-[300px]">
|
<div id="embedded-chat-messages" class="border-b border-gray-200 dark:border-gray-700">
|
||||||
<!-- Chat Verlauf -->
|
<!-- Assistant Message -->
|
||||||
<div id="embedded-chat-messages" class="flex-grow p-4 overflow-y-auto space-y-3 bg-white/70 dark:bg-gray-800/70">
|
<div class="mb-4 flex">
|
||||||
<!-- Begrüßungsnachricht -->
|
<div class="w-8 h-8 rounded-full bg-gradient-to-r from-purple-600 to-indigo-600 flex items-center justify-center text-white mr-2 flex-shrink-0">
|
||||||
<div class="flex items-start space-x-2">
|
<i class="fa-solid fa-robot text-sm"></i>
|
||||||
<div class="w-8 h-8 rounded-full bg-gradient-to-r from-purple-600 to-blue-600 flex items-center justify-center flex-shrink-0">
|
|
||||||
<i class="fa-solid fa-robot text-white text-xs"></i>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="max-w-[85%] bg-purple-100 dark:bg-gray-700 p-3 rounded-xl rounded-tl-none shadow-sm">
|
<div class="bg-gray-100 dark:bg-gray-800 rounded-lg p-3 max-w-[80%]">
|
||||||
<p class="text-sm text-gray-700 dark:text-gray-200">Hallo! Ich bin dein KI-Assistent. Wie kann ich dir helfen?</p>
|
<div class="text-gray-700 dark:text-gray-300 markdown-content">
|
||||||
|
<p>Hallo! Ich bin dein Systades-Assistent. Wie kann ich dir heute helfen?</p>
|
||||||
|
<p>Du kannst mir Fragen zu:</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Gedanken</strong> in der Datenbank</li>
|
||||||
|
<li><strong>Kategorien</strong> und Wissensgebieten</li>
|
||||||
|
<li><strong>Mindmaps</strong> und Visualisierungsmöglichkeiten</li>
|
||||||
|
</ul>
|
||||||
|
<p>stellen.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Chat Eingabe -->
|
<!-- User Message -->
|
||||||
<div class="p-3 border-t border-gray-200/70 dark:border-gray-700/70 bg-gray-50/90 dark:bg-gray-800/90">
|
<div class="mb-4 flex justify-end">
|
||||||
<form id="embedded-chat-form" class="flex items-center space-x-2">
|
<div class="bg-purple-100 dark:bg-purple-900/30 rounded-lg p-3 max-w-[80%]">
|
||||||
<input type="text" id="embedded-chat-input"
|
<p class="text-gray-800 dark:text-gray-200">
|
||||||
placeholder="Stelle eine Frage..."
|
Kann ich mit deiner Hilfe eine Mindmap zum Thema Künstliche Intelligenz erstellen?
|
||||||
class="flex-grow px-4 py-2 rounded-xl border bg-white/90 dark:bg-gray-700/90 border-gray-300 dark:border-gray-600 shadow-sm focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-all duration-200 placeholder-gray-400 dark:placeholder-gray-500 text-gray-700 dark:text-gray-200">
|
</p>
|
||||||
<button type="submit"
|
</div>
|
||||||
class="p-2 rounded-xl bg-gradient-to-r from-purple-600 to-blue-600 text-white shadow-md hover:shadow-lg transition-all duration-200 hover:-translate-y-0.5">
|
<div class="w-8 h-8 rounded-full bg-gray-300 dark:bg-gray-600 flex items-center justify-center text-gray-700 dark:text-gray-300 ml-2 flex-shrink-0">
|
||||||
|
<i class="fa-solid fa-user text-sm"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Assistant Response -->
|
||||||
|
<div class="mb-4 flex" id="demo-ai-response">
|
||||||
|
<div class="w-8 h-8 rounded-full bg-gradient-to-r from-purple-600 to-indigo-600 flex items-center justify-center text-white mr-2 flex-shrink-0">
|
||||||
|
<i class="fa-solid fa-robot text-sm"></i>
|
||||||
|
</div>
|
||||||
|
<div class="bg-gray-100 dark:bg-gray-800 rounded-lg p-3 max-w-[80%]">
|
||||||
|
<div class="text-gray-700 dark:text-gray-300 markdown-content">
|
||||||
|
<p>Ja, natürlich! Ich kann dir dabei helfen, eine Mindmap zum Thema <strong>Künstliche Intelligenz</strong> zu erstellen.</p>
|
||||||
|
<p>Du kannst wie folgt vorgehen:</p>
|
||||||
|
<ol>
|
||||||
|
<li>Gehe zur <strong>Mindmap</strong>-Ansicht</li>
|
||||||
|
<li>Suche nach dem Knoten "Künstliche Intelligenz" unter der Kategorie "Technologie"</li>
|
||||||
|
<li>Füge diesen Knoten zu deiner persönlichen Mindmap hinzu</li>
|
||||||
|
<li>Ergänze verwandte Themen wie <em>Machine Learning, Neural Networks oder Data Science</em></li>
|
||||||
|
</ol>
|
||||||
|
<p>Soll ich dir noch mehr spezifische Informationen zu KI-Teilgebieten geben?</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Chat Input -->
|
||||||
|
<div class="p-4">
|
||||||
|
<div class="flex">
|
||||||
|
<input type="text" placeholder="Stelle eine Frage..." class="mystical-input flex-grow" disabled>
|
||||||
|
<button class="ml-2 bg-gradient-to-r from-purple-600 to-indigo-600 text-white p-2 rounded-lg disabled:opacity-50" disabled>
|
||||||
<i class="fa-solid fa-paper-plane"></i>
|
<i class="fa-solid fa-paper-plane"></i>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</div>
|
||||||
|
<!-- Quick Queries -->
|
||||||
|
<div class="mt-3 flex flex-wrap gap-2">
|
||||||
|
<span class="text-xs text-gray-500 dark:text-gray-400 mr-1">Beispiele:</span>
|
||||||
|
<button data-question="Was sind die wichtigsten Grundlagen der Künstlichen Intelligenz?" class="quick-query-btn text-xs px-2 py-1 rounded-full bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600 cursor-pointer transition-colors">KI-Grundlagen</button>
|
||||||
|
<button data-question="Wie kann ich eine Mindmap zum Thema Neuronale Netzwerke erstellen?" class="quick-query-btn text-xs px-2 py-1 rounded-full bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600 cursor-pointer transition-colors">Mindmap erstellen</button>
|
||||||
|
<button data-question="Zeige mir alle verfügbaren Kategorien in der Datenbank" class="quick-query-btn text-xs px-2 py-1 rounded-full bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600 cursor-pointer transition-colors">Datenbank durchsuchen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Schnelllinks unter dem Chat -->
|
<!-- Try it Button -->
|
||||||
<div class="mt-4 flex flex-wrap gap-2">
|
<div class="text-center mt-10">
|
||||||
<button class="quick-query-btn px-2 sm:px-3 py-1 sm:py-1.5 bg-gray-100 hover:bg-gray-200 dark:bg-gray-800/70 dark:hover:bg-gray-700/80 rounded-lg sm:rounded-xl text-xs text-gray-700 dark:text-gray-300 transition-all duration-200 hover:-translate-y-0.5 shadow-sm hover:shadow">
|
<button onclick="window.MindMap && window.MindMap.assistant && window.MindMap.assistant.sendQuestion('Hallo! Ich möchte mehr über die Funktionen der Wissensdatenbank erfahren. Was kann ich hier alles machen?')"
|
||||||
Was ist Systades?
|
class="mystical-button mystical-button-primary inline-flex items-center">
|
||||||
</button>
|
<i class="fa-solid fa-robot mr-2"></i>
|
||||||
<button class="quick-query-btn px-2 sm:px-3 py-1 sm:py-1.5 bg-gray-100 hover:bg-gray-200 dark:bg-gray-800/70 dark:hover:bg-gray-700/80 rounded-lg sm:rounded-xl text-xs text-gray-700 dark:text-gray-300 transition-all duration-200 hover:-translate-y-0.5 shadow-sm hover:shadow">
|
KI-Assistenten ausprobieren
|
||||||
Wie erstelle ich eine Mindmap?
|
|
||||||
</button>
|
|
||||||
<button class="quick-query-btn px-2 sm:px-3 py-1 sm:py-1.5 bg-gray-100 hover:bg-gray-200 dark:bg-gray-800/70 dark:hover:bg-gray-700/80 rounded-lg sm:rounded-xl text-xs text-gray-700 dark:text-gray-300 transition-all duration-200 hover:-translate-y-0.5 shadow-sm hover:shadow">
|
|
||||||
Zeige neueste Gedanken
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Vollständigen KI-Chat öffnen -->
|
<!-- Getting Started Section -->
|
||||||
<button onclick="window.MindMap.assistant.toggleAssistant(true)" class="mt-4 btn-primary w-full text-center rounded-xl py-2 sm:py-2.5 shadow-md hover:shadow-lg transition-all duration-300 hover:-translate-y-1 flex items-center justify-center">
|
<section class="py-20 relative">
|
||||||
<i class="fa-solid fa-expand mr-2"></i>
|
<div class="mystical-line absolute top-0 left-1/2 transform -translate-x-1/2 w-1/3"></div>
|
||||||
<span>Chat in Vollansicht öffnen</span>
|
|
||||||
</button>
|
<div class="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="text-center mb-12">
|
||||||
|
<h2 class="section-heading mb-4 text-gray-900 dark:text-white">So <span class="gradient-text">funktioniert's</span></h2>
|
||||||
|
<p class="text-lg text-gray-700 dark:text-gray-300 max-w-3xl mx-auto">
|
||||||
|
In wenigen einfachen Schritten kannst du mit Systades beginnen, dein Wissen zu organisieren.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step Cards -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||||
|
<!-- Step 1 -->
|
||||||
|
<div class="featured-card rounded-2xl p-6 bg-white/80 dark:bg-gray-800/30 backdrop-blur-sm relative overflow-hidden">
|
||||||
|
<div class="absolute top-4 right-4 w-10 h-10 rounded-full bg-purple-100 dark:bg-purple-900/30 flex items-center justify-center text-purple-600 dark:text-purple-400 font-bold">
|
||||||
|
1
|
||||||
|
</div>
|
||||||
|
<h3 class="text-xl font-semibold mb-4 text-gray-900 dark:text-white">Konto erstellen</h3>
|
||||||
|
<p class="text-gray-700 dark:text-gray-300 mb-4">
|
||||||
|
Registriere dich für ein kostenloses Konto, um deine persönliche Wissenslandschaft zu erstellen.
|
||||||
|
</p>
|
||||||
|
{% if not current_user.is_authenticated %}
|
||||||
|
<a href="{{ url_for('register') }}" class="text-purple-600 dark:text-purple-400 hover:underline">
|
||||||
|
Jetzt registrieren <i class="fa-solid fa-arrow-right ml-1"></i>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 2 -->
|
||||||
|
<div class="featured-card rounded-2xl p-6 bg-white/80 dark:bg-gray-800/30 backdrop-blur-sm relative overflow-hidden">
|
||||||
|
<div class="absolute top-4 right-4 w-10 h-10 rounded-full bg-purple-100 dark:bg-purple-900/30 flex items-center justify-center text-purple-600 dark:text-purple-400 font-bold">
|
||||||
|
2
|
||||||
|
</div>
|
||||||
|
<h3 class="text-xl font-semibold mb-4 text-gray-900 dark:text-white">Mindmap erkunden</h3>
|
||||||
|
<p class="text-gray-700 dark:text-gray-300 mb-4">
|
||||||
|
Entdecke die öffentliche Wissensmindmap und füge Knoten zu deiner persönlichen Landschaft hinzu.
|
||||||
|
</p>
|
||||||
|
<a href="{{ url_for('mindmap') }}" class="text-purple-600 dark:text-purple-400 hover:underline">
|
||||||
|
Zur Mindmap <i class="fa-solid fa-arrow-right ml-1"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 3 -->
|
||||||
|
<div class="featured-card rounded-2xl p-6 bg-white/80 dark:bg-gray-800/30 backdrop-blur-sm relative overflow-hidden">
|
||||||
|
<div class="absolute top-4 right-4 w-10 h-10 rounded-full bg-purple-100 dark:bg-purple-900/30 flex items-center justify-center text-purple-600 dark:text-purple-400 font-bold">
|
||||||
|
3
|
||||||
|
</div>
|
||||||
|
<h3 class="text-xl font-semibold mb-4 text-gray-900 dark:text-white">Gedanken teilen</h3>
|
||||||
|
<p class="text-gray-700 dark:text-gray-300 mb-4">
|
||||||
|
Teile deine eigenen Gedanken, verbinde sie mit vorhandenen Knoten und baue das kollektive Wissen aus.
|
||||||
|
</p>
|
||||||
|
{% if current_user.is_authenticated %}
|
||||||
|
<a href="{{ url_for('profile') }}" class="text-purple-600 dark:text-purple-400 hover:underline">
|
||||||
|
Zum Profil <i class="fa-solid fa-arrow-right ml-1"></i>
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="{{ url_for('login') }}" class="text-purple-600 dark:text-purple-400 hover:underline">
|
||||||
|
Jetzt anmelden <i class="fa-solid fa-arrow-right ml-1"></i>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- Call to Action -->
|
||||||
|
<section class="py-20 relative">
|
||||||
|
<div class="mystical-line absolute top-0 left-1/2 transform -translate-x-1/2 w-1/3"></div>
|
||||||
|
|
||||||
|
<div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||||
|
<h2 class="section-heading mb-6 text-gray-900 dark:text-white">Bereit, dein <span class="gradient-text">Wissen zu vernetzen</span>?</h2>
|
||||||
|
<p class="text-lg text-gray-700 dark:text-gray-300 mb-8">
|
||||||
|
Tritt unserer wachsenden Community bei und entdecke eine neue Art, Wissen zu organisieren und zu teilen.
|
||||||
|
</p>
|
||||||
|
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||||
|
<a href="{{ url_for('mindmap') }}" class="mystical-button mystical-button-primary">
|
||||||
|
<i class="fa-solid fa-diagram-project mr-2"></i> Mindmap erkunden
|
||||||
|
</a>
|
||||||
|
{% if not current_user.is_authenticated %}
|
||||||
|
<a href="{{ url_for('register') }}" class="mystical-button mystical-button-secondary">
|
||||||
|
<i class="fa-solid fa-user-plus mr-2"></i> Konto erstellen
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
<!-- JavaScript für eingebetteten Chat -->
|
{% block extra_js %}
|
||||||
{% block scripts %}
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
// Warten bis MindMap und der Assistent initialisiert sind
|
// Expand-Button mit dem echten Assistenten verknüpfen
|
||||||
const waitForAssistant = setInterval(() => {
|
const openRealAssistantBtn = document.getElementById('open-real-assistant');
|
||||||
|
if (openRealAssistantBtn) {
|
||||||
|
openRealAssistantBtn.addEventListener('click', function() {
|
||||||
if (window.MindMap && window.MindMap.assistant) {
|
if (window.MindMap && window.MindMap.assistant) {
|
||||||
clearInterval(waitForAssistant);
|
window.MindMap.assistant.toggleAssistant(true);
|
||||||
initEmbeddedChat();
|
|
||||||
}
|
}
|
||||||
}, 200);
|
|
||||||
|
|
||||||
function initEmbeddedChat() {
|
|
||||||
const chatForm = document.getElementById('embedded-chat-form');
|
|
||||||
const chatInput = document.getElementById('embedded-chat-input');
|
|
||||||
const messagesContainer = document.getElementById('embedded-chat-messages');
|
|
||||||
const quickQueryBtns = document.querySelectorAll('.quick-query-btn');
|
|
||||||
|
|
||||||
// Event-Listener für das Chat-Formular
|
|
||||||
chatForm.addEventListener('submit', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
const userMessage = chatInput.value.trim();
|
|
||||||
if (!userMessage) return;
|
|
||||||
|
|
||||||
// Nachricht des Benutzers anzeigen
|
|
||||||
appendMessage('user', userMessage);
|
|
||||||
chatInput.value = '';
|
|
||||||
|
|
||||||
// Anzeigen, dass der Assistent antwortet
|
|
||||||
const typingIndicator = appendTypingIndicator();
|
|
||||||
|
|
||||||
// API-Anfrage an den Assistenten senden
|
|
||||||
sendToAssistant(userMessage)
|
|
||||||
.then(response => {
|
|
||||||
// Entferne Tipp-Indikator
|
|
||||||
typingIndicator.remove();
|
|
||||||
// Zeige Antwort des Assistenten an
|
|
||||||
appendMessage('assistant', response);
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
typingIndicator.remove();
|
|
||||||
appendMessage('assistant', 'Es tut mir leid, ich konnte deine Nachricht nicht verarbeiten. Bitte versuche es später noch einmal.');
|
|
||||||
console.error('Fehler bei der Kommunikation mit dem Assistenten:', error);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Schnellabfragen-Buttons
|
// Auch die Beispiel-Buttons im Demo-Chat klickbar machen
|
||||||
quickQueryBtns.forEach(btn => {
|
const quickQueryButtons = document.querySelectorAll('.quick-query-btn');
|
||||||
btn.addEventListener('click', function() {
|
quickQueryButtons.forEach(button => {
|
||||||
const query = this.textContent.trim();
|
button.addEventListener('click', function() {
|
||||||
chatInput.value = query;
|
if (window.MindMap && window.MindMap.assistant) {
|
||||||
chatForm.dispatchEvent(new Event('submit'));
|
const question = button.getAttribute('data-question');
|
||||||
});
|
if (question) {
|
||||||
});
|
window.MindMap.assistant.sendQuestion(question);
|
||||||
|
|
||||||
// Funktion zum Hinzufügen einer Nachricht zum Chat
|
|
||||||
function appendMessage(sender, message) {
|
|
||||||
const messageElement = document.createElement('div');
|
|
||||||
messageElement.className = 'flex items-start space-x-2';
|
|
||||||
|
|
||||||
if (sender === 'user') {
|
|
||||||
messageElement.innerHTML = `
|
|
||||||
<div class="flex-grow"></div>
|
|
||||||
<div class="max-w-[85%] bg-blue-100 dark:bg-blue-900/40 p-3 rounded-xl rounded-tr-none shadow-sm">
|
|
||||||
<p class="text-sm text-gray-700 dark:text-gray-200">${message}</p>
|
|
||||||
</div>
|
|
||||||
<div class="w-8 h-8 rounded-full bg-gradient-to-r from-blue-500 to-indigo-500 flex items-center justify-center flex-shrink-0">
|
|
||||||
<i class="fa-solid fa-user text-white text-xs"></i>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
} else {
|
} else {
|
||||||
messageElement.innerHTML = `
|
// Fallback auf den Button-Text, falls kein data-question Attribut gesetzt ist
|
||||||
<div class="w-8 h-8 rounded-full bg-gradient-to-r from-purple-600 to-blue-600 flex items-center justify-center flex-shrink-0">
|
const queryText = button.textContent.trim();
|
||||||
<i class="fa-solid fa-robot text-white text-xs"></i>
|
window.MindMap.assistant.sendQuestion(queryText);
|
||||||
</div>
|
|
||||||
<div class="max-w-[85%] bg-purple-100 dark:bg-gray-700 p-3 rounded-xl rounded-tl-none shadow-sm">
|
|
||||||
<p class="text-sm text-gray-700 dark:text-gray-200">${message}</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
messagesContainer.appendChild(messageElement);
|
|
||||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
// Tipp-Indikator für "Assistent schreibt..."
|
|
||||||
function appendTypingIndicator() {
|
|
||||||
const indicatorElement = document.createElement('div');
|
|
||||||
indicatorElement.className = 'flex items-start space-x-2 typing-indicator';
|
|
||||||
indicatorElement.innerHTML = `
|
|
||||||
<div class="w-8 h-8 rounded-full bg-gradient-to-r from-purple-600 to-blue-600 flex items-center justify-center flex-shrink-0">
|
|
||||||
<i class="fa-solid fa-robot text-white text-xs"></i>
|
|
||||||
</div>
|
|
||||||
<div class="max-w-[85%] bg-purple-100 dark:bg-gray-700 p-3 rounded-xl rounded-tl-none shadow-sm">
|
|
||||||
<p class="text-sm text-gray-500 dark:text-gray-400 flex items-center">
|
|
||||||
<span class="mr-1">Tipp</span>
|
|
||||||
<span class="typing-dots flex space-x-1">
|
|
||||||
<span class="w-1.5 h-1.5 bg-gray-500 dark:bg-gray-400 rounded-full animate-bounce" style="animation-delay: 0ms;"></span>
|
|
||||||
<span class="w-1.5 h-1.5 bg-gray-500 dark:bg-gray-400 rounded-full animate-bounce" style="animation-delay: 150ms;"></span>
|
|
||||||
<span class="w-1.5 h-1.5 bg-gray-500 dark:bg-gray-400 rounded-full animate-bounce" style="animation-delay: 300ms;"></span>
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
messagesContainer.appendChild(indicatorElement);
|
|
||||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
|
||||||
return indicatorElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sende Nachricht an den Assistenten und erhalte Antwort
|
|
||||||
async function sendToAssistant(message) {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/assistant', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
messages: [
|
|
||||||
{ role: "system", content: "Du bist ein hilfreicher Assistent für das Wissensnetzwerk Systades." },
|
|
||||||
{ role: "user", content: message }
|
|
||||||
]
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
// Styling für die markdown-content hinzufügen
|
||||||
if (!response.ok) {
|
const style = document.createElement('style');
|
||||||
throw new Error(data.error || 'Unbekannter Fehler');
|
style.textContent = `
|
||||||
|
.markdown-content h1, .markdown-content h2, .markdown-content h3,
|
||||||
|
.markdown-content h4, .markdown-content h5, .markdown-content h6 {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
.markdown-content h1 { font-size: 1.4rem; }
|
||||||
return data.response || data.answer || 'Ich habe keine Antwort erhalten.';
|
.markdown-content h2 { font-size: 1.3rem; }
|
||||||
} catch (error) {
|
.markdown-content h3 { font-size: 1.2rem; }
|
||||||
console.error('Fehler bei der API-Anfrage:', error);
|
.markdown-content h4 { font-size: 1.1rem; }
|
||||||
throw error;
|
.markdown-content ul, .markdown-content ol {
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
margin: 0.5rem 0;
|
||||||
}
|
}
|
||||||
|
.markdown-content ul { list-style-type: disc; }
|
||||||
|
.markdown-content ol { list-style-type: decimal; }
|
||||||
|
.markdown-content p { margin: 0.5rem 0; }
|
||||||
|
.markdown-content code {
|
||||||
|
font-family: monospace;
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
padding: 1px 4px;
|
||||||
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
.markdown-content pre {
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 0.5rem 0;
|
||||||
}
|
}
|
||||||
|
.dark .markdown-content code {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
.dark .markdown-content pre {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -32,7 +32,7 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-avatar {
|
.avatar-container {
|
||||||
width: 180px;
|
width: 180px;
|
||||||
height: 180px;
|
height: 180px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
@@ -50,40 +50,48 @@
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-avatar:hover {
|
.avatar-container:hover {
|
||||||
transform: scale(1.05);
|
transform: scale(1.05);
|
||||||
border: 3px solid rgba(179, 143, 255, 0.5);
|
border: 3px solid rgba(179, 143, 255, 0.5);
|
||||||
box-shadow: 0 12px 45px rgba(0, 0, 0, 0.3), 0 0 25px rgba(179, 143, 255, 0.35);
|
box-shadow: 0 12px 45px rgba(0, 0, 0, 0.3), 0 0 25px rgba(179, 143, 255, 0.35);
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-avatar img {
|
.avatar-container img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
transition: filter 0.3s ease;
|
transition: filter 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-avatar:hover img {
|
.avatar-container:hover img {
|
||||||
filter: brightness(1.1);
|
filter: brightness(1.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-avatar-placeholder {
|
.avatar-edit {
|
||||||
font-size: 5rem;
|
position: absolute;
|
||||||
color: rgba(255, 255, 255, 0.6);
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-avatar:hover .profile-avatar-placeholder {
|
.avatar-edit:hover {
|
||||||
color: rgba(255, 255, 255, 0.9);
|
background: rgba(255, 255, 255, 0.3);
|
||||||
transform: scale(1.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-info {
|
.user-info {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding-top: 0.5rem;
|
padding-top: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-name {
|
.user-info h1 {
|
||||||
font-size: 2.75rem;
|
font-size: 2.75rem;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
margin-bottom: 0.75rem;
|
margin-bottom: 0.75rem;
|
||||||
@@ -96,33 +104,7 @@
|
|||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-username {
|
.user-bio {
|
||||||
font-size: 1.35rem;
|
|
||||||
color: rgba(255, 255, 255, 0.85);
|
|
||||||
margin-bottom: 1.25rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.username-badge {
|
|
||||||
background: rgba(179, 143, 255, 0.2);
|
|
||||||
border: 1px solid rgba(179, 143, 255, 0.3);
|
|
||||||
color: #b38fff;
|
|
||||||
padding: 0.3rem 1rem;
|
|
||||||
border-radius: 20px;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-weight: 600;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.username-badge:hover {
|
|
||||||
background: rgba(179, 143, 255, 0.3);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2), 0 0 8px rgba(179, 143, 255, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-bio {
|
|
||||||
font-size: 1.15rem;
|
font-size: 1.15rem;
|
||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
color: rgba(255, 255, 255, 0.9);
|
color: rgba(255, 255, 255, 0.9);
|
||||||
@@ -131,7 +113,7 @@
|
|||||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-meta {
|
.user-meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 1.5rem;
|
gap: 1.5rem;
|
||||||
@@ -140,118 +122,22 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-meta-item {
|
.user-meta span {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
transition: all 0.25s ease;
|
transition: all 0.25s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-meta-item:hover {
|
.user-meta span:hover {
|
||||||
color: rgba(255, 255, 255, 1);
|
color: rgba(255, 255, 255, 1);
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-meta-icon {
|
.user-meta i {
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Verbesserte Statistik-Karten mit interaktiven Effekten */
|
|
||||||
.profile-stats {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
||||||
gap: 1.5rem;
|
|
||||||
margin-top: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-item {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
padding: 1.75rem 1.25rem;
|
|
||||||
background: rgba(32, 36, 55, 0.7);
|
|
||||||
backdrop-filter: blur(15px);
|
|
||||||
-webkit-backdrop-filter: blur(15px);
|
|
||||||
border-radius: 20px;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
text-align: center;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-item::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: radial-gradient(circle at center, rgba(179, 143, 255, 0.15) 0%, transparent 70%);
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.5s ease;
|
|
||||||
z-index: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-item:hover::before {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-item:hover {
|
|
||||||
transform: translateY(-5px);
|
|
||||||
background: rgba(32, 36, 55, 0.8);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
|
||||||
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.25), 0 0 20px rgba(179, 143, 255, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-value {
|
|
||||||
font-size: 2.25rem;
|
|
||||||
font-weight: 800;
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
background: linear-gradient(135deg, #b38fff, #58a9ff);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
background-clip: text;
|
|
||||||
color: transparent;
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-item:hover .stat-value {
|
|
||||||
transform: scale(1.1);
|
|
||||||
text-shadow: 0 0 15px rgba(179, 143, 255, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-label {
|
|
||||||
font-size: 0.95rem;
|
|
||||||
color: rgba(255, 255, 255, 0.75);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 1px;
|
|
||||||
font-weight: 600;
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-item:hover .stat-label {
|
|
||||||
color: rgba(255, 255, 255, 0.95);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Stat-Icon für visuelle Verstärkung */
|
|
||||||
.stat-icon {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
color: rgba(179, 143, 255, 0.7);
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-item:hover .stat-icon {
|
|
||||||
transform: scale(1.2) translateY(-3px);
|
|
||||||
color: rgba(179, 143, 255, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Benutzer-Aktionsbereich */
|
/* Benutzer-Aktionsbereich */
|
||||||
.profile-actions {
|
.profile-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -504,31 +390,24 @@
|
|||||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
html.light .profile-avatar {
|
html.light .avatar-container {
|
||||||
background: rgba(255, 255, 255, 0.9);
|
background: rgba(255, 255, 255, 0.9);
|
||||||
border: 3px solid rgba(126, 63, 242, 0.3);
|
border: 3px solid rgba(126, 63, 242, 0.3);
|
||||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1), 0 0 15px rgba(126, 63, 242, 0.15);
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1), 0 0 15px rgba(126, 63, 242, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
html.light .profile-name {
|
html.light .user-info h1 {
|
||||||
background: linear-gradient(135deg, #7e3ff2, #3282f6);
|
background: linear-gradient(135deg, #7e3ff2, #3282f6);
|
||||||
text-shadow: none;
|
text-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
html.light .profile-username,
|
html.light .user-bio,
|
||||||
html.light .profile-bio,
|
|
||||||
html.light .activity-content {
|
html.light .activity-content {
|
||||||
color: #1a202c;
|
color: #1a202c;
|
||||||
text-shadow: none;
|
text-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
html.light .username-badge {
|
html.light .user-meta span {
|
||||||
background: rgba(126, 63, 242, 0.15);
|
|
||||||
border: 1px solid rgba(126, 63, 242, 0.3);
|
|
||||||
color: #7e3ff2;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.light .profile-meta {
|
|
||||||
color: #4a5568;
|
color: #4a5568;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -606,63 +485,22 @@
|
|||||||
<div class="container mx-auto px-4 py-10">
|
<div class="container mx-auto px-4 py-10">
|
||||||
<!-- Profil-Container -->
|
<!-- Profil-Container -->
|
||||||
<div class="profile-container">
|
<div class="profile-container">
|
||||||
<!-- Profil-Header mit Benutzerinformationen -->
|
<!-- User Info Section -->
|
||||||
<div class="profile-header">
|
<div class="profile-header">
|
||||||
<!-- Profilbild -->
|
<div class="avatar-container">
|
||||||
<div class="profile-avatar">
|
<img src="{{ user.avatar if user.avatar else url_for('static', filename='img/default-avatar.png') }}" alt="Profilbild" class="avatar">
|
||||||
{% if user.profile_image %}
|
<div class="avatar-edit">
|
||||||
<img src="{{ user.profile_image }}" alt="{{ user.name }}" />
|
<i class="fas fa-camera"></i>
|
||||||
{% else %}
|
|
||||||
<div class="profile-avatar-placeholder">
|
|
||||||
<i class="fas fa-user"></i>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Profilinformationen -->
|
|
||||||
<div class="profile-info">
|
|
||||||
<h1 class="profile-name">{{ user.name|default('Max Mustermann') }}</h1>
|
|
||||||
<div class="profile-username">
|
|
||||||
@{{ user.username|default('maxmustermann') }}
|
|
||||||
{% if user.verified %}
|
|
||||||
<span class="username-badge">
|
|
||||||
<i class="fas fa-check-circle mr-1"></i> Verifiziert
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="profile-bio">
|
|
||||||
{{ user.bio|default('Willkommen auf meinem Profil! Ich bin daran interessiert, Wissen zu vernetzen und neue Verbindungen zwischen verschiedenen Themengebieten zu entdecken. Mein Ziel ist es, ein tieferes Verständnis für komplexe Zusammenhänge zu entwickeln.') }}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<!-- Meta-Informationen -->
|
|
||||||
<div class="profile-meta">
|
|
||||||
<div class="profile-meta-item">
|
|
||||||
<i class="fas fa-map-marker-alt profile-meta-icon"></i>
|
|
||||||
<span>{{ user.location|default('Berlin, Deutschland') }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="profile-meta-item">
|
|
||||||
<i class="fas fa-calendar-alt profile-meta-icon"></i>
|
|
||||||
<span>Mitglied seit {{ user.joined_date|default('Januar 2023') }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="profile-meta-item">
|
|
||||||
<i class="fas fa-globe profile-meta-icon"></i>
|
|
||||||
<span>{{ user.website|default('www.beispiel.de') }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="user-info">
|
||||||
<!-- Profil-Aktionen -->
|
<h1>{{ user.username }}</h1>
|
||||||
<div class="profile-actions">
|
<p class="user-bio">{{ user.bio if user.bio else 'Keine Bio vorhanden. Klicke auf bearbeiten, um eine hinzuzufügen.' }}</p>
|
||||||
<button class="profile-action-btn primary">
|
<div class="user-meta">
|
||||||
<i class="fas fa-user-plus mr-1"></i> Folgen
|
<span><i class="fas fa-map-marker-alt"></i> {{ user.location if user.location else 'Kein Standort angegeben' }}</span>
|
||||||
</button>
|
<span><i class="fas fa-calendar-alt"></i> Mitglied seit {{ user.created_at.strftime('%d.%m.%Y') }}</span>
|
||||||
<button class="profile-action-btn">
|
|
||||||
<i class="fas fa-comment mr-1"></i> Nachricht
|
|
||||||
</button>
|
|
||||||
<button class="profile-action-btn">
|
|
||||||
<i class="fas fa-share-alt mr-1"></i> Teilen
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<button class="edit-profile-btn">Profil bearbeiten</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -673,7 +511,7 @@
|
|||||||
<div class="stat-icon">
|
<div class="stat-icon">
|
||||||
<i class="fas fa-lightbulb"></i>
|
<i class="fas fa-lightbulb"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-value">{{ user.thoughts_count|default('42') }}</div>
|
<div class="stat-value">{{ stats.thought_count if stats and stats.thought_count else 0 }}</div>
|
||||||
<div class="stat-label">Gedanken</div>
|
<div class="stat-label">Gedanken</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -682,7 +520,7 @@
|
|||||||
<div class="stat-icon">
|
<div class="stat-icon">
|
||||||
<i class="fas fa-project-diagram"></i>
|
<i class="fas fa-project-diagram"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-value">{{ user.connections_count|default('128') }}</div>
|
<div class="stat-value">{{ stats.connections_count if stats and stats.connections_count else 0 }}</div>
|
||||||
<div class="stat-label">Verbindungen</div>
|
<div class="stat-label">Verbindungen</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -691,7 +529,7 @@
|
|||||||
<div class="stat-icon">
|
<div class="stat-icon">
|
||||||
<i class="fas fa-users"></i>
|
<i class="fas fa-users"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-value">{{ user.followers_count|default('567') }}</div>
|
<div class="stat-value">{{ stats.followers_count if stats and stats.followers_count else 0 }}</div>
|
||||||
<div class="stat-label">Follower</div>
|
<div class="stat-label">Follower</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -700,7 +538,7 @@
|
|||||||
<div class="stat-icon">
|
<div class="stat-icon">
|
||||||
<i class="fas fa-comment-dots"></i>
|
<i class="fas fa-comment-dots"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-value">{{ user.contributions_count|default('89') }}</div>
|
<div class="stat-value">{{ stats.contributions_count if stats and stats.contributions_count else 0 }}</div>
|
||||||
<div class="stat-label">Beiträge</div>
|
<div class="stat-label">Beiträge</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -709,7 +547,7 @@
|
|||||||
<div class="stat-icon">
|
<div class="stat-icon">
|
||||||
<i class="fas fa-star"></i>
|
<i class="fas fa-star"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-value">{{ user.rating|default('4.8') }}</div>
|
<div class="stat-value">{{ stats.rating if stats and stats.rating else '0.0' }}</div>
|
||||||
<div class="stat-label">Bewertung</div>
|
<div class="stat-label">Bewertung</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -731,25 +569,26 @@
|
|||||||
<!-- Aktivitäts-Tab (Standardmäßig angezeigt) -->
|
<!-- Aktivitäts-Tab (Standardmäßig angezeigt) -->
|
||||||
<div class="tab-content" id="activity-tab">
|
<div class="tab-content" id="activity-tab">
|
||||||
<div class="activity-feed">
|
<div class="activity-feed">
|
||||||
<!-- Aktivität 1 -->
|
{% if activities %}
|
||||||
|
{% for activity in activities %}
|
||||||
<div class="activity-card">
|
<div class="activity-card">
|
||||||
<div class="activity-header">
|
<div class="activity-header">
|
||||||
<div class="activity-title">Neuer Gedanke hinzugefügt</div>
|
<div class="activity-title">{{ activity.title }}</div>
|
||||||
<div class="activity-date">vor 2 Stunden</div>
|
<div class="activity-date">{{ activity.date }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="activity-content">
|
<div class="activity-content">
|
||||||
<p>Ich habe einen neuen Gedanken zum Thema "Künstliche Intelligenz und Kreativität" hinzugefügt. Wie können KI-Tools uns dabei helfen, kreativer zu denken?</p>
|
<p>{{ activity.content }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="activity-footer">
|
<div class="activity-footer">
|
||||||
<div class="activity-reactions">
|
<div class="activity-reactions">
|
||||||
<button class="reaction-button">
|
<button class="reaction-button {% if activity.user_liked %}active{% endif %}">
|
||||||
<i class="fas fa-thumbs-up"></i> <span>24</span>
|
<i class="fas fa-thumbs-up"></i> <span>{{ activity.likes }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="reaction-button">
|
<button class="reaction-button">
|
||||||
<i class="fas fa-comment"></i> <span>8</span>
|
<i class="fas fa-comment"></i> <span>{{ activity.comments }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="reaction-button">
|
<button class="reaction-button">
|
||||||
<i class="fas fa-share"></i> <span>3</span>
|
<i class="fas fa-share"></i> <span>{{ activity.shares }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="activity-actions">
|
<div class="activity-actions">
|
||||||
@@ -759,87 +598,95 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endfor %}
|
||||||
<!-- Aktivität 2 -->
|
{% else %}
|
||||||
<div class="activity-card">
|
<div class="text-center py-12">
|
||||||
<div class="activity-header">
|
<i class="fas fa-history text-5xl text-gray-400 mb-4"></i>
|
||||||
<div class="activity-title">Verbindung erstellt</div>
|
<p class="text-gray-500">Noch keine Aktivitäten vorhanden</p>
|
||||||
<div class="activity-date">vor 1 Tag</div>
|
|
||||||
</div>
|
|
||||||
<div class="activity-content">
|
|
||||||
<p>Ich habe eine neue Verbindung zwischen "Nachhaltige Entwicklung" und "Digitale Transformation" hergestellt. Es gibt interessante Überschneidungen in diesen Bereichen.</p>
|
|
||||||
</div>
|
|
||||||
<div class="activity-footer">
|
|
||||||
<div class="activity-reactions">
|
|
||||||
<button class="reaction-button active">
|
|
||||||
<i class="fas fa-thumbs-up"></i> <span>42</span>
|
|
||||||
</button>
|
|
||||||
<button class="reaction-button">
|
|
||||||
<i class="fas fa-comment"></i> <span>12</span>
|
|
||||||
</button>
|
|
||||||
<button class="reaction-button">
|
|
||||||
<i class="fas fa-share"></i> <span>7</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="activity-actions">
|
|
||||||
<button class="action-button">
|
|
||||||
Ansehen
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Aktivität 3 -->
|
|
||||||
<div class="activity-card">
|
|
||||||
<div class="activity-header">
|
|
||||||
<div class="activity-title">Sammlung erstellt</div>
|
|
||||||
<div class="activity-date">vor 3 Tagen</div>
|
|
||||||
</div>
|
|
||||||
<div class="activity-content">
|
|
||||||
<p>Ich habe eine neue Sammlung zum Thema "Zukunftstechnologien" erstellt. Diese Sammlung enthält Gedanken zu KI, Quantencomputing, Biotechnologie und mehr.</p>
|
|
||||||
</div>
|
|
||||||
<div class="activity-footer">
|
|
||||||
<div class="activity-reactions">
|
|
||||||
<button class="reaction-button">
|
|
||||||
<i class="fas fa-thumbs-up"></i> <span>17</span>
|
|
||||||
</button>
|
|
||||||
<button class="reaction-button">
|
|
||||||
<i class="fas fa-comment"></i> <span>4</span>
|
|
||||||
</button>
|
|
||||||
<button class="reaction-button">
|
|
||||||
<i class="fas fa-share"></i> <span>2</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="activity-actions">
|
|
||||||
<button class="action-button">
|
|
||||||
Ansehen
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Weitere Tab-Inhalte (werden per JavaScript angezeigt) -->
|
<!-- Weitere Tab-Inhalte (werden per JavaScript angezeigt) -->
|
||||||
<div class="tab-content hidden" id="thoughts-tab">
|
<div class="tab-content hidden" id="thoughts-tab">
|
||||||
<p class="text-center text-gray-400 py-12">
|
<div id="thoughts-container">
|
||||||
<i class="fas fa-spinner fa-spin text-3xl mb-4 block"></i>
|
{% if thoughts %}
|
||||||
Gedanken werden geladen...
|
{% for thought in thoughts %}
|
||||||
</p>
|
<div class="thought-item">
|
||||||
|
<h3>{{ thought.title }}</h3>
|
||||||
|
<p>{{ thought.content }}</p>
|
||||||
|
<div class="thought-meta">
|
||||||
|
<span>{{ thought.date }}</span>
|
||||||
|
<span>{{ thought.category }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-12">
|
||||||
|
<i class="fas fa-lightbulb text-5xl text-gray-400 mb-4"></i>
|
||||||
|
<p class="text-gray-500">Noch keine Gedanken erstellt</p>
|
||||||
|
<a href="{{ url_for('create_thought') }}" class="mt-4 inline-block px-4 py-2 bg-purple-600 text-white rounded-lg">Ersten Gedanken erstellen</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tab-content hidden" id="collections-tab">
|
<div class="tab-content hidden" id="collections-tab">
|
||||||
<p class="text-center text-gray-400 py-12">
|
<div id="collections-container">
|
||||||
<i class="fas fa-spinner fa-spin text-3xl mb-4 block"></i>
|
{% if collections %}
|
||||||
Sammlungen werden geladen...
|
{% for collection in collections %}
|
||||||
</p>
|
<div class="collection-item">
|
||||||
|
<h3>{{ collection.title }}</h3>
|
||||||
|
<p>{{ collection.description }}</p>
|
||||||
|
<div class="collection-meta">
|
||||||
|
<span>{{ collection.thoughts_count }} Gedanken</span>
|
||||||
|
<span>{{ collection.date }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-12">
|
||||||
|
<i class="fas fa-folder-open text-5xl text-gray-400 mb-4"></i>
|
||||||
|
<p class="text-gray-500">Noch keine Sammlungen erstellt</p>
|
||||||
|
<a href="{{ url_for('create_collection') }}" class="mt-4 inline-block px-4 py-2 bg-purple-600 text-white rounded-lg">Erste Sammlung erstellen</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tab-content hidden" id="connections-tab">
|
<div class="tab-content hidden" id="connections-tab">
|
||||||
<p class="text-center text-gray-400 py-12">
|
<div id="connections-container">
|
||||||
<i class="fas fa-spinner fa-spin text-3xl mb-4 block"></i>
|
{% if connections %}
|
||||||
Verbindungen werden geladen...
|
{% for connection in connections %}
|
||||||
</p>
|
<div class="connection-item">
|
||||||
|
<div class="connection-nodes">
|
||||||
|
<div class="connection-node">
|
||||||
|
<h4>{{ connection.source.title }}</h4>
|
||||||
|
<p>{{ connection.source.excerpt }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="connection-type">
|
||||||
|
<i class="fas fa-arrow-right"></i>
|
||||||
|
<span>{{ connection.relation_type }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="connection-node">
|
||||||
|
<h4>{{ connection.target.title }}</h4>
|
||||||
|
<p>{{ connection.target.excerpt }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="connection-meta">
|
||||||
|
<span>{{ connection.date }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-12">
|
||||||
|
<i class="fas fa-project-diagram text-5xl text-gray-400 mb-4"></i>
|
||||||
|
<p class="text-gray-500">Noch keine Verbindungen erstellt</p>
|
||||||
|
<a href="{{ url_for('mindmap') }}" class="mt-4 inline-block px-4 py-2 bg-purple-600 text-white rounded-lg">Verbindungen in der Mindmap erstellen</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tab-content hidden" id="settings-tab">
|
<div class="tab-content hidden" id="settings-tab">
|
||||||
@@ -849,22 +696,22 @@
|
|||||||
<div class="settings-card-body">
|
<div class="settings-card-body">
|
||||||
<div class="settings-group">
|
<div class="settings-group">
|
||||||
<label class="settings-label" for="name">Name</label>
|
<label class="settings-label" for="name">Name</label>
|
||||||
<input type="text" id="name" class="settings-input" value="{{ user.name|default('Max Mustermann') }}" />
|
<input type="text" id="name" class="settings-input" value="{{ user.name }}" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-group">
|
<div class="settings-group">
|
||||||
<label class="settings-label" for="bio">Über mich</label>
|
<label class="settings-label" for="bio">Über mich</label>
|
||||||
<textarea id="bio" class="settings-input" rows="4">{{ user.bio|default('Willkommen auf meinem Profil! Ich bin daran interessiert, Wissen zu vernetzen und neue Verbindungen zwischen verschiedenen Themengebieten zu entdecken.') }}</textarea>
|
<textarea id="bio" class="settings-input" rows="4">{{ user.bio }}</textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-group">
|
<div class="settings-group">
|
||||||
<label class="settings-label" for="location">Standort</label>
|
<label class="settings-label" for="location">Standort</label>
|
||||||
<input type="text" id="location" class="settings-input" value="{{ user.location|default('Berlin, Deutschland') }}" />
|
<input type="text" id="location" class="settings-input" value="{{ user.location }}" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-group">
|
<div class="settings-group">
|
||||||
<label class="settings-label" for="website">Website</label>
|
<label class="settings-label" for="website">Website</label>
|
||||||
<input type="url" id="website" class="settings-input" value="{{ user.website|default('https://www.beispiel.de') }}" />
|
<input type="url" id="website" class="settings-input" value="{{ user.website }}" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="profile-action-btn primary mt-4">
|
<button class="profile-action-btn primary mt-4">
|
||||||
@@ -878,7 +725,7 @@
|
|||||||
<div class="settings-card-body">
|
<div class="settings-card-body">
|
||||||
<div class="settings-group">
|
<div class="settings-group">
|
||||||
<label class="settings-label" for="email">E-Mail-Adresse</label>
|
<label class="settings-label" for="email">E-Mail-Adresse</label>
|
||||||
<input type="email" id="email" class="settings-input" value="{{ user.email|default('beispiel@email.com') }}" />
|
<input type="email" id="email" class="settings-input" value="{{ user.email }}" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-group">
|
<div class="settings-group">
|
||||||
@@ -903,7 +750,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
<script>
|
<script nonce="{{ csp_nonce }}">
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
// Profil-Tab-Funktionalität
|
// Profil-Tab-Funktionalität
|
||||||
const tabs = document.querySelectorAll('.profile-tab');
|
const tabs = document.querySelectorAll('.profile-tab');
|
||||||
|
|||||||
@@ -7,14 +7,14 @@ This package contains various utilities for database management,
|
|||||||
user management, and server administration.
|
user management, and server administration.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from .db_fix import fix_database_schema
|
# Import utilities that don't depend on app.py first
|
||||||
from .db_rebuild import rebuild_database
|
from .db_check import check_db_connection, initialize_db_if_needed
|
||||||
from .db_test import test_database_connection, test_models, print_database_stats, run_all_tests
|
|
||||||
from .user_manager import list_users, create_user, reset_password, delete_user, create_admin_user
|
|
||||||
from .server import run_development_server
|
|
||||||
|
|
||||||
|
# Define the list of all available utilities
|
||||||
__all__ = [
|
__all__ = [
|
||||||
# Database utilities
|
# Database utilities
|
||||||
|
'check_db_connection',
|
||||||
|
'initialize_db_if_needed',
|
||||||
'fix_database_schema',
|
'fix_database_schema',
|
||||||
'rebuild_database',
|
'rebuild_database',
|
||||||
'test_database_connection',
|
'test_database_connection',
|
||||||
@@ -32,3 +32,10 @@ __all__ = [
|
|||||||
# Server management
|
# Server management
|
||||||
'run_development_server',
|
'run_development_server',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Import remaining modules that might depend on app
|
||||||
|
from .db_fix import fix_database_schema
|
||||||
|
from .db_rebuild import rebuild_database
|
||||||
|
from .db_test import test_database_connection, test_models, print_database_stats, run_all_tests
|
||||||
|
from .user_manager import list_users, create_user, reset_password, delete_user, create_admin_user
|
||||||
|
from .server import run_development_server
|
||||||
Binary file not shown.
BIN
utils/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
utils/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/db_check.cpython-311.pyc
Normal file
BIN
utils/__pycache__/db_check.cpython-311.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/db_check.cpython-313.pyc
Normal file
BIN
utils/__pycache__/db_check.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
utils/__pycache__/db_fix.cpython-313.pyc
Normal file
BIN
utils/__pycache__/db_fix.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
utils/__pycache__/db_rebuild.cpython-313.pyc
Normal file
BIN
utils/__pycache__/db_rebuild.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
utils/__pycache__/db_test.cpython-313.pyc
Normal file
BIN
utils/__pycache__/db_test.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
utils/__pycache__/server.cpython-313.pyc
Normal file
BIN
utils/__pycache__/server.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
utils/__pycache__/user_manager.cpython-313.pyc
Normal file
BIN
utils/__pycache__/user_manager.cpython-313.pyc
Normal file
Binary file not shown.
79
utils/db_check.py
Normal file
79
utils/db_check.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from flask import current_app
|
||||||
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
|
from sqlalchemy import text
|
||||||
|
import time
|
||||||
|
|
||||||
|
def check_db_connection(db):
|
||||||
|
"""
|
||||||
|
Überprüft die Datenbankverbindung und versucht ggf. die Verbindung wiederherzustellen
|
||||||
|
|
||||||
|
Args:
|
||||||
|
db: SQLAlchemy-Instanz
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True, wenn die Verbindung erfolgreich ist, sonst False
|
||||||
|
"""
|
||||||
|
max_retries = 3
|
||||||
|
retry_count = 0
|
||||||
|
|
||||||
|
while retry_count < max_retries:
|
||||||
|
try:
|
||||||
|
# Führe eine einfache Abfrage durch, um die Verbindung zu testen
|
||||||
|
with current_app.app_context():
|
||||||
|
db.session.execute(text('SELECT 1'))
|
||||||
|
return True
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
retry_count += 1
|
||||||
|
print(f"DB-Verbindungsfehler (Versuch {retry_count}/{max_retries}): {str(e)}")
|
||||||
|
|
||||||
|
if retry_count < max_retries:
|
||||||
|
# Warte vor dem nächsten Versuch
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# Versuche die Verbindung zurückzusetzen
|
||||||
|
try:
|
||||||
|
db.session.rollback()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def initialize_db_if_needed(db, initialize_function=None):
|
||||||
|
"""
|
||||||
|
Initialisiert die Datenbank, falls erforderlich
|
||||||
|
|
||||||
|
Args:
|
||||||
|
db: SQLAlchemy-Instanz
|
||||||
|
initialize_function: Funktion, die aufgerufen wird, um die Datenbank zu initialisieren
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True, wenn die Datenbank bereit ist, sonst False
|
||||||
|
"""
|
||||||
|
# Prüfe die Verbindung
|
||||||
|
if not check_db_connection(db):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Prüfe, ob die Tabellen existieren
|
||||||
|
try:
|
||||||
|
with current_app.app_context():
|
||||||
|
# Führe eine Testabfrage auf einer Tabelle durch
|
||||||
|
db.session.execute(text('SELECT COUNT(*) FROM user'))
|
||||||
|
except SQLAlchemyError:
|
||||||
|
# Tabellen existieren nicht, erstelle sie
|
||||||
|
try:
|
||||||
|
with current_app.app_context():
|
||||||
|
db.create_all()
|
||||||
|
|
||||||
|
# Rufe die Initialisierungsfunktion auf, falls vorhanden
|
||||||
|
if initialize_function and callable(initialize_function):
|
||||||
|
initialize_function()
|
||||||
|
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Fehler bei DB-Initialisierung: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
@@ -11,12 +11,20 @@ import importlib.util
|
|||||||
parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
sys.path.insert(0, parent_dir)
|
sys.path.insert(0, parent_dir)
|
||||||
|
|
||||||
from app import app, db_path, create_default_categories
|
# Import models directly to avoid circular import
|
||||||
from models import db, User, Category
|
from models import db, User, Category
|
||||||
|
|
||||||
def rebuild_database():
|
def rebuild_database(app_instance=None):
|
||||||
"""Completely rebuilds the database by dropping and recreating all tables."""
|
"""Completely rebuilds the database by dropping and recreating all tables."""
|
||||||
with app.app_context():
|
if app_instance is None:
|
||||||
|
# Only import app if it's not provided as a parameter
|
||||||
|
from app import app as app_instance
|
||||||
|
from app import db_path
|
||||||
|
else:
|
||||||
|
# Get db_path from app_instance config
|
||||||
|
db_path = app_instance.config['SQLALCHEMY_DATABASE_URI'].replace('sqlite:///', '')
|
||||||
|
|
||||||
|
with app_instance.app_context():
|
||||||
print(f"Database path: {db_path}")
|
print(f"Database path: {db_path}")
|
||||||
|
|
||||||
# Back up existing database if it exists
|
# Back up existing database if it exists
|
||||||
@@ -68,7 +76,9 @@ def rebuild_database():
|
|||||||
|
|
||||||
# Create default categories
|
# Create default categories
|
||||||
print("Creating default categories...")
|
print("Creating default categories...")
|
||||||
create_default_categories()
|
# Instead of directly importing create_default_categories, call it through app_instance
|
||||||
|
create_default_categories_func = getattr(sys.modules['app'], 'create_default_categories')
|
||||||
|
create_default_categories_func()
|
||||||
|
|
||||||
print("Database rebuild completed successfully!")
|
print("Database rebuild completed successfully!")
|
||||||
return True
|
return True
|
||||||
|
|||||||
225
utils/download_resources.py
Executable file
225
utils/download_resources.py
Executable file
@@ -0,0 +1,225 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
Dieses Skript lädt externe Ressourcen wie Font Awesome, Tailwind CSS und Alpine.js herunter
|
||||||
|
und installiert sie lokal, um die Abhängigkeit von externen CDNs zu vermeiden und
|
||||||
|
die Content Security Policy zu erfüllen.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import requests
|
||||||
|
import zipfile
|
||||||
|
import io
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
# URLs für die Ressourcen
|
||||||
|
RESOURCES = {
|
||||||
|
'alpine.js': 'https://cdn.jsdelivr.net/npm/alpinejs@3.12.3/dist/cdn.min.js',
|
||||||
|
'font_awesome': 'https://use.fontawesome.com/releases/v6.4.0/fontawesome-free-6.4.0-web.zip',
|
||||||
|
'inter_font_300': 'https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1ZL7.woff2',
|
||||||
|
'inter_font_400': 'https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2ZL7SUc.woff2',
|
||||||
|
'inter_font_500': 'https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1pL7SUc.woff2',
|
||||||
|
'inter_font_600': 'https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2pL7SUc.woff2',
|
||||||
|
'inter_font_700': 'https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa25L7SUc.woff2',
|
||||||
|
'jetbrains_font_400': 'https://fonts.gstatic.com/s/jetbrainsmono/v20/tDbv2o-flEEny0FZhsfKu5WU4zr3E_BX0PnT8RD8yKwBNntkaToggR7BYRbKPxDcwg.woff2',
|
||||||
|
'jetbrains_font_500': 'https://fonts.gstatic.com/s/jetbrainsmono/v20/tDbv2o-flEEny0FZhsfKu5WU4zr3E_BX0PnT8RD8yKwBNntkaToggR7BYRbKPx3cwhsk.woff2',
|
||||||
|
'jetbrains_font_700': 'https://fonts.gstatic.com/s/jetbrainsmono/v20/tDbv2o-flEEny0FZhsfKu5WU4zr3E_BX0PnT8RD8yKwBNntkaToggR7BYRbKPxTcwhsk.woff2',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Zielverzeichnisse
|
||||||
|
DIRECTORIES = {
|
||||||
|
'js': os.path.join(BASE_DIR, 'static', 'js'),
|
||||||
|
'css': os.path.join(BASE_DIR, 'static', 'css'),
|
||||||
|
'fonts': os.path.join(BASE_DIR, 'static', 'fonts'),
|
||||||
|
'webfonts': os.path.join(BASE_DIR, 'static', 'webfonts'),
|
||||||
|
}
|
||||||
|
|
||||||
|
def download_file(url, filepath):
|
||||||
|
"""Lädt eine Datei von einer URL herunter und speichert sie lokal."""
|
||||||
|
response = requests.get(url, stream=True)
|
||||||
|
if response.status_code == 200:
|
||||||
|
with open(filepath, 'wb') as f:
|
||||||
|
for chunk in response.iter_content(chunk_size=8192):
|
||||||
|
f.write(chunk)
|
||||||
|
print(f"✅ Heruntergeladen: {os.path.basename(filepath)}")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f"❌ Fehler beim Herunterladen von {url}: {response.status_code}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def extract_zip(zip_data, extract_to):
|
||||||
|
"""Extrahiert eine ZIP-Datei in das angegebene Verzeichnis."""
|
||||||
|
with zipfile.ZipFile(io.BytesIO(zip_data)) as zip_ref:
|
||||||
|
zip_ref.extractall(extract_to)
|
||||||
|
|
||||||
|
def setup_directories():
|
||||||
|
"""Erstellt die benötigten Verzeichnisse, falls sie nicht existieren."""
|
||||||
|
for directory in DIRECTORIES.values():
|
||||||
|
os.makedirs(directory, exist_ok=True)
|
||||||
|
print(f"📁 Verzeichnis erstellt/überprüft: {directory}")
|
||||||
|
|
||||||
|
def download_alpine():
|
||||||
|
"""Lädt Alpine.js herunter."""
|
||||||
|
url = RESOURCES['alpine.js']
|
||||||
|
filepath = os.path.join(DIRECTORIES['js'], 'alpine.min.js')
|
||||||
|
download_file(url, filepath)
|
||||||
|
|
||||||
|
def download_font_awesome():
|
||||||
|
"""Lädt Font Awesome herunter und extrahiert die Dateien."""
|
||||||
|
url = RESOURCES['font_awesome']
|
||||||
|
response = requests.get(url)
|
||||||
|
if response.status_code == 200:
|
||||||
|
# Temporäres Verzeichnis für die Extraktion
|
||||||
|
temp_dir = os.path.join(BASE_DIR, 'temp_fontawesome')
|
||||||
|
os.makedirs(temp_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# ZIP-Datei extrahieren
|
||||||
|
extract_zip(response.content, temp_dir)
|
||||||
|
|
||||||
|
# CSS-Datei kopieren
|
||||||
|
fa_dir = os.path.join(temp_dir, 'fontawesome-free-6.4.0-web')
|
||||||
|
css_source = os.path.join(fa_dir, 'css', 'all.min.css')
|
||||||
|
css_dest = os.path.join(DIRECTORIES['css'], 'all.min.css')
|
||||||
|
shutil.copyfile(css_source, css_dest)
|
||||||
|
print(f"✅ Font Awesome CSS kopiert nach {css_dest}")
|
||||||
|
|
||||||
|
# Webfonts-Verzeichnis kopieren
|
||||||
|
webfonts_source = os.path.join(fa_dir, 'webfonts')
|
||||||
|
shutil.rmtree(DIRECTORIES['webfonts'], ignore_errors=True)
|
||||||
|
shutil.copytree(webfonts_source, DIRECTORIES['webfonts'])
|
||||||
|
print(f"✅ Font Awesome Webfonts kopiert nach {DIRECTORIES['webfonts']}")
|
||||||
|
|
||||||
|
# Temporäres Verzeichnis löschen
|
||||||
|
shutil.rmtree(temp_dir)
|
||||||
|
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f"❌ Fehler beim Herunterladen von Font Awesome: {response.status_code}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def download_google_fonts():
|
||||||
|
"""Lädt die Google Fonts (Inter und JetBrains Mono) herunter."""
|
||||||
|
font_files = {
|
||||||
|
'inter-light.woff2': RESOURCES['inter_font_300'],
|
||||||
|
'inter-regular.woff2': RESOURCES['inter_font_400'],
|
||||||
|
'inter-medium.woff2': RESOURCES['inter_font_500'],
|
||||||
|
'inter-semibold.woff2': RESOURCES['inter_font_600'],
|
||||||
|
'inter-bold.woff2': RESOURCES['inter_font_700'],
|
||||||
|
'jetbrainsmono-regular.woff2': RESOURCES['jetbrains_font_400'],
|
||||||
|
'jetbrainsmono-medium.woff2': RESOURCES['jetbrains_font_500'],
|
||||||
|
'jetbrainsmono-bold.woff2': RESOURCES['jetbrains_font_700'],
|
||||||
|
}
|
||||||
|
|
||||||
|
for filename, url in font_files.items():
|
||||||
|
filepath = os.path.join(DIRECTORIES['fonts'], filename)
|
||||||
|
download_file(url, filepath)
|
||||||
|
|
||||||
|
def setup_tailwind():
|
||||||
|
"""Richtet Tailwind CSS ein."""
|
||||||
|
# Tailwind-Konfiguration erstellen, falls sie nicht existiert
|
||||||
|
tailwind_config = os.path.join(BASE_DIR, 'tailwind.config.js')
|
||||||
|
if not os.path.exists(tailwind_config):
|
||||||
|
with open(tailwind_config, 'w') as f:
|
||||||
|
f.write("""/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
darkMode: 'class',
|
||||||
|
content: [
|
||||||
|
"./templates/**/*.html",
|
||||||
|
"./static/**/*.js",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['Inter', 'ui-sans-serif', 'system-ui', '-apple-system', 'sans-serif'],
|
||||||
|
mono: ['JetBrains Mono', 'ui-monospace', 'monospace']
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
primary: {
|
||||||
|
50: '#f5f3ff',
|
||||||
|
100: '#ede9fe',
|
||||||
|
200: '#ddd6fe',
|
||||||
|
300: '#c4b5fd',
|
||||||
|
400: '#a78bfa',
|
||||||
|
500: '#8b5cf6',
|
||||||
|
600: '#7c3aed',
|
||||||
|
700: '#6d28d9',
|
||||||
|
800: '#5b21b6',
|
||||||
|
900: '#4c1d95'
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
50: '#ecfdf5',
|
||||||
|
100: '#d1fae5',
|
||||||
|
200: '#a7f3d0',
|
||||||
|
300: '#6ee7b7',
|
||||||
|
400: '#34d399',
|
||||||
|
500: '#10b981',
|
||||||
|
600: '#059669',
|
||||||
|
700: '#047857',
|
||||||
|
800: '#065f46',
|
||||||
|
900: '#064e3b'
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
500: '#374151',
|
||||||
|
600: '#1f2937',
|
||||||
|
700: '#111827',
|
||||||
|
800: '#0e1220',
|
||||||
|
900: '#0a0e19'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
print(f"✅ Tailwind-Konfiguration erstellt: {tailwind_config}")
|
||||||
|
|
||||||
|
# Input-CSS-Datei erstellen
|
||||||
|
input_css_dir = os.path.join(DIRECTORIES['css'], 'src')
|
||||||
|
os.makedirs(input_css_dir, exist_ok=True)
|
||||||
|
|
||||||
|
input_css = os.path.join(input_css_dir, 'input.css')
|
||||||
|
if not os.path.exists(input_css):
|
||||||
|
with open(input_css, 'w') as f:
|
||||||
|
f.write("""@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
""")
|
||||||
|
print(f"✅ Tailwind Input-CSS erstellt: {input_css}")
|
||||||
|
|
||||||
|
# Hinweis zur Kompilierung anzeigen
|
||||||
|
print("\n📋 Um Tailwind CSS zu kompilieren, führe folgenden Befehl aus:")
|
||||||
|
print("npm install -D tailwindcss")
|
||||||
|
print(f"npx tailwindcss -i {input_css} -o {os.path.join(DIRECTORIES['css'], 'tailwind.min.css')} --minify")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Hauptfunktion: Lädt alle benötigten Ressourcen herunter."""
|
||||||
|
print("🚀 Starte den Download externer Ressourcen...")
|
||||||
|
setup_directories()
|
||||||
|
|
||||||
|
# Alpine.js herunterladen
|
||||||
|
print("\n📦 Lade Alpine.js herunter...")
|
||||||
|
download_alpine()
|
||||||
|
|
||||||
|
# Font Awesome herunterladen
|
||||||
|
print("\n📦 Lade Font Awesome herunter...")
|
||||||
|
download_font_awesome()
|
||||||
|
|
||||||
|
# Google Fonts herunterladen
|
||||||
|
print("\n📦 Lade Google Fonts herunter...")
|
||||||
|
download_google_fonts()
|
||||||
|
|
||||||
|
# Tailwind CSS einrichten
|
||||||
|
print("\n📦 Richte Tailwind CSS ein...")
|
||||||
|
setup_tailwind()
|
||||||
|
|
||||||
|
print("\n✅ Alle Ressourcen wurden erfolgreich heruntergeladen und eingerichtet!")
|
||||||
|
print("🔒 Die Webseite sollte nun ohne externe CDNs funktionieren und die Content Security Policy erfüllen.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user