chore: automatic commit 2025-04-30 12:48
This commit is contained in:
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
Normal file
2
.env
Normal file
@@ -0,0 +1,2 @@
|
||||
SECRET_KEY=eed9298856dc9363cd32778265780d6904ba24e6a6b815a2cc382bcdd767ea7b
|
||||
OPENAI_API_KEY=sk-dein-openai-api-schluessel-hier
|
||||
8
.vscode/jsconfig.json
vendored
Normal file
8
.vscode/jsconfig.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"lib": [
|
||||
"esnext"
|
||||
]
|
||||
}
|
||||
}
|
||||
68
.vscode/main.js
vendored
Normal file
68
.vscode/main.js
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
/// <reference types="vscode" />
|
||||
// @ts-check
|
||||
// API: https://code.visualstudio.com/api/references/vscode-api
|
||||
// @ts-ignore
|
||||
const vscode = require('vscode');
|
||||
* @typedef {import('vscode').ExtensionContext} ExtensionContext
|
||||
* @typedef {import('vscode').commands} commands
|
||||
* @typedef {import('vscode').window} window
|
||||
* @typedef {import('vscode').TextEditor} TextEditor
|
||||
* @typedef {import('vscode').TextDocument} TextDocument
|
||||
*/
|
||||
|
||||
/**
|
||||
* Aktiviert die Erweiterung und registriert den Auto-Resume-Befehl
|
||||
* @param {vscode.ExtensionContext} context - Der Erweiterungskontext
|
||||
*/
|
||||
function activate(context) {
|
||||
const disposable = vscode.commands.registerCommand('extension.autoResume', () => {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (!editor) return;
|
||||
|
||||
const document = editor.document;
|
||||
const text = document.getText();
|
||||
|
||||
// Track last click time to avoid multiple clicks
|
||||
let lastClickTime = 0;
|
||||
|
||||
// Main function that looks for and clicks the resume link
|
||||
function clickResumeLink() {
|
||||
// Prevent clicking too frequently (3 second cooldown)
|
||||
const now = Date.now();
|
||||
if (now - lastClickTime < 3000) return;
|
||||
|
||||
// Check if text contains rate limit text
|
||||
if (text.includes('stop the agent after 25 tool calls') ||
|
||||
text.includes('Note: we default stop')) {
|
||||
|
||||
// Find the resume link position
|
||||
const resumePos = text.indexOf('resume the conversation');
|
||||
if (resumePos !== -1) {
|
||||
vscode.window.showInformationMessage('Auto-resuming conversation...');
|
||||
lastClickTime = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Führe periodisch aus
|
||||
const interval = global.setInterval(clickResumeLink, 1000);
|
||||
|
||||
// Speichere das Intervall in den Subscriptions
|
||||
context.subscriptions.push({
|
||||
dispose: () => global.clearInterval(interval)
|
||||
});
|
||||
// Führe die Funktion sofort aus
|
||||
clickResumeLink();
|
||||
});
|
||||
|
||||
context.subscriptions.push(disposable);
|
||||
}
|
||||
|
||||
function deactivate() {
|
||||
// Cleanup if needed
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
activate,
|
||||
deactivate
|
||||
}
|
||||
239
COMMON_ERRORS.md
Normal file
239
COMMON_ERRORS.md
Normal file
@@ -0,0 +1,239 @@
|
||||
# ABSOLUTE DON'TS:
|
||||
- Verwendung von npm anstelle der Tailwind CDN
|
||||
- Implementierung von Content Security Policy (CSP) - UNTER KEINEN UMSTÄNDEN!
|
||||
- Implementierung von Cross-Site Request Forgery (CSRF) Schutz
|
||||
- Implementierung von Security Headers
|
||||
- Verwendung von TypeScript anstelle von JavaScript
|
||||
- Implementierung von OAuth oder anderen externen Authentifizierungsmethoden
|
||||
|
||||
# HÄUFIGE FEHLER:
|
||||
- Verwendung der falschen Datenbank (die korrekte ist: database/systades.db)
|
||||
- Falsche Pfadangaben bei statischen Dateien
|
||||
- Vergessen der deutschen Spracheinstellungen in Templates
|
||||
- Nicht beachten der vorhandenen Projektstruktur
|
||||
- Falsche Einbindung der Neural Network Background Animation
|
||||
- Verwendung von englischen Variablennamen in deutschen Funktionen
|
||||
- Vergessen der Mindmap-Datenstruktur gemäß der Roadmap
|
||||
|
||||
# Häufige Fehler und Lösungen
|
||||
|
||||
## Datenbankfehler
|
||||
|
||||
### Fehler: "no such column: user.password"
|
||||
|
||||
**Fehlerbeschreibung:**
|
||||
```
|
||||
sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) no such column: user.password
|
||||
[SQL: SELECT user.id AS user_id, user.username AS user_username, user.email AS user_email, user.password AS user_password, user.created_at AS user_created_at, user.is_active AS user_is_active, user.role AS user_role
|
||||
FROM user
|
||||
WHERE user.id = ?]
|
||||
```
|
||||
|
||||
**Ursache:**
|
||||
Die Spalte `password` fehlt in der Tabelle `user` der SQLite-Datenbank. Dies kann durch eine unvollständige Datenbankinitialisierung oder ein fehlerhaftes Schema-Update verursacht worden sein.
|
||||
|
||||
**Lösung:**
|
||||
|
||||
1. **Datenbank reparieren mit dem Fix-Skript**
|
||||
|
||||
```bash
|
||||
python fix_user_table.py
|
||||
```
|
||||
|
||||
Dieses Skript:
|
||||
- Prüft, ob die Tabelle `user` existiert und erstellt sie, falls nicht
|
||||
- Prüft, ob die Spalte `password` existiert und fügt sie hinzu, falls nicht
|
||||
|
||||
2. **Standardbenutzer erstellen**
|
||||
|
||||
```bash
|
||||
python create_default_users.py
|
||||
```
|
||||
|
||||
Dieses Skript:
|
||||
- Erstellt Standardbenutzer (admin, user), falls keine vorhanden sind
|
||||
- Setzt Passwörter mit korrektem Hashing
|
||||
|
||||
3. **Datenbank testen**
|
||||
|
||||
```bash
|
||||
python test_app.py
|
||||
```
|
||||
|
||||
Dieses Skript überprüft:
|
||||
- Ob die Datenbank existiert
|
||||
- Ob die Tabelle `user` korrekt konfiguriert ist
|
||||
- Ob Benutzer vorhanden sind
|
||||
|
||||
## Häufige Ursachen für Datenbankfehler
|
||||
|
||||
1. **Inkonsistente Datenbankschemas**
|
||||
- Unterschiede zwischen dem SQLAlchemy-Modell und der tatsächlichen Datenbankstruktur
|
||||
- Fehlende Spalten, die in den Modellen definiert sind
|
||||
|
||||
2. **Falsche Datenbankinitialisierung**
|
||||
- Die Datenbank wurde nicht korrekt initialisiert
|
||||
- Fehler bei der Migration oder dem Schema-Update
|
||||
|
||||
3. **Datenbankdatei-Korrumpierung**
|
||||
- Die SQLite-Datenbankdatei wurde beschädigt
|
||||
- Lösung: Sicherung wiederherstellen oder Datenbank neu erstellen
|
||||
|
||||
## Vorbeugende Maßnahmen
|
||||
|
||||
1. **Regelmäßige Backups**
|
||||
- Tägliche Sicherung der Datenbankdatei
|
||||
|
||||
2. **Schema-Validierung**
|
||||
- Regelmäßige Überprüfung der Datenbankstruktur
|
||||
- Automatisierte Tests für Datenbankschema
|
||||
|
||||
3. **Datenbankmigration**
|
||||
- Verwenden Sie Flask-Migrate für strukturierte Datenbank-Updates
|
||||
- Dokumentieren Sie alle Schemaänderungen
|
||||
|
||||
## Content Security Policy (CSP)
|
||||
|
||||
### Problem: Externe Ressourcen werden nicht geladen
|
||||
**Fehler:** Externe Ressourcen wie CDNs werden nicht korrekt geladen.
|
||||
|
||||
**Lösung:**
|
||||
1. Stellen Sie sicher, dass die URLs in den Templates korrekt sind:
|
||||
```html
|
||||
<link href="https://cdn.tailwindcss.com" rel="stylesheet">
|
||||
```
|
||||
|
||||
2. Überprüfen Sie die Netzwerkverbindung und ob die CDN-Domains erreichbar sind.
|
||||
|
||||
3. Verwenden Sie lokale Ressourcen als Alternative:
|
||||
```html
|
||||
<link href="{{ url_for('static', filename='css/tailwind.min.css') }}" rel="stylesheet">
|
||||
```
|
||||
|
||||
### Problem: CSP-Fehler (script-src-elem 'none', font-src 'none', etc.)
|
||||
**Fehler:** Skripte werden aufgrund von CSP-Einschränkungen blockiert.
|
||||
|
||||
**Lösung:**
|
||||
1. WICHTIG: CSP-Einschränkungen NICHT implementieren! Diese sind im Projekt unerwünscht.
|
||||
|
||||
2. Falls CSP-Header bereits vorhanden sind, entfernen Sie diese aus der Anwendung:
|
||||
```python
|
||||
# Entfernen Sie diesen Code, falls vorhanden:
|
||||
# @app.after_request
|
||||
# def add_security_headers(response):
|
||||
# response.headers['Content-Security-Policy'] = '...'
|
||||
# return response
|
||||
```
|
||||
|
||||
3. Stellen Sie sicher, dass externe Ressourcen direkt geladen werden:
|
||||
```html
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
<script src="{{ url_for('static', filename='js/alpine.min.js') }}" defer></script>
|
||||
```
|
||||
|
||||
4. Für Inline-Skripte keine Nonce verwenden:
|
||||
```html
|
||||
<script>
|
||||
// Inline-JavaScript-Code ohne Einschränkungen
|
||||
</script>
|
||||
```
|
||||
|
||||
### Problem: Tailwind CSS CDN wird blockiert
|
||||
**Fehler:** Tailwind CSS kann nicht von CDN geladen werden.
|
||||
|
||||
**Lösung:**
|
||||
1. Verwenden Sie die lokale Version von Tailwind CSS:
|
||||
```html
|
||||
<link href="{{ url_for('static', filename='css/tailwind.min.css') }}" rel="stylesheet">
|
||||
```
|
||||
|
||||
2. Alternativ können Sie die CDN-Version direkt im Template einbinden:
|
||||
```html
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
```
|
||||
|
||||
3. Stellen Sie sicher, dass die Datei `static/css/tailwind.min.css` existiert und aktuell ist.
|
||||
|
||||
## Authentifizierung
|
||||
|
||||
### Problem: Login funktioniert nicht
|
||||
**Fehler:** Benutzer kann sich nicht einloggen.
|
||||
|
||||
**Lösung:**
|
||||
1. Standard-Admin-Benutzer erstellen: `python TOOLS.py user:admin`
|
||||
2. Passwort zurücksetzen: `python TOOLS.py user:reset-pw -u USERNAME -p NEWPASSWORD`
|
||||
|
||||
## Neural Network Background
|
||||
|
||||
### Problem: Hintergrund-Animation wird nicht angezeigt
|
||||
**Fehler:** Die Neural Network Animation im Hintergrund erscheint nicht.
|
||||
|
||||
**Lösung:**
|
||||
1. Überprüfen Sie, ob die Datei `static/neural-network-background.js` korrekt eingebunden ist:
|
||||
```html
|
||||
<script src="{{ url_for('static', filename='neural-network-background.js') }}"></script>
|
||||
```
|
||||
|
||||
2. Initialisieren Sie die Animation im Template:
|
||||
```html
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const background = new NeuralNetworkBackground();
|
||||
background.initialize();
|
||||
background.animate();
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
3. Stellen Sie sicher, dass keine CSS-Regeln die Animation überdecken:
|
||||
```css
|
||||
#neural-network-background {
|
||||
z-index: -10;
|
||||
opacity: 1;
|
||||
}
|
||||
```
|
||||
|
||||
## Mindmap-Funktionalität
|
||||
|
||||
### Problem: Mindmap-Daten werden nicht geladen
|
||||
**Fehler:** Die dynamische Mindmap zeigt keine Daten an.
|
||||
|
||||
**Lösung:**
|
||||
1. Überprüfen Sie die API-Endpunkte für die Mindmap-Daten:
|
||||
```python
|
||||
@app.route('/api/mindmap/nodes', methods=['GET'])
|
||||
def get_mindmap_nodes():
|
||||
# Implementierung...
|
||||
```
|
||||
|
||||
2. Stellen Sie sicher, dass die AJAX-Anfragen korrekt implementiert sind:
|
||||
```javascript
|
||||
fetch('/api/mindmap/nodes')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// Verarbeitung der Mindmap-Daten
|
||||
});
|
||||
```
|
||||
|
||||
3. Überprüfen Sie die Datenbankeinträge für Mindmap-Knoten und -Verbindungen.
|
||||
|
||||
## ChatGPT-Assistent
|
||||
|
||||
### Problem: Assistent reagiert nicht auf Eingaben
|
||||
**Fehler:** Der ChatGPT-Assistent verarbeitet keine Benutzereingaben.
|
||||
|
||||
**Lösung:**
|
||||
1. Überprüfen Sie die Einbindung der JavaScript-Datei:
|
||||
```html
|
||||
<script src="{{ url_for('static', filename='js/modules/chatgpt-assistant.js') }}"></script>
|
||||
```
|
||||
|
||||
2. Stellen Sie sicher, dass der Assistent korrekt initialisiert wird:
|
||||
```javascript
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const assistant = new ChatGPTAssistant();
|
||||
assistant.initialize();
|
||||
});
|
||||
```
|
||||
|
||||
3. Überprüfen Sie die API-Endpunkte für die Kommunikation mit dem Assistenten.
|
||||
196
README.md
Normal file
196
README.md
Normal file
@@ -0,0 +1,196 @@
|
||||
# MindMapProjekt - Roadmap
|
||||
|
||||
## Projektübersicht
|
||||
Das MindMapProjekt ist eine interaktive Plattform zum Visualisieren, Erforschen und Teilen von Wissen. Das Projekt wird umfassend überarbeitet, um ein modernes, benutzerfreundliches Design und erweiterte Funktionalitäten zu bieten.
|
||||
|
||||
## Technischer Stack
|
||||
- **Backend**: Python/Flask
|
||||
- **Frontend**:
|
||||
- Tailwind CSS (via CDN) für moderne UI
|
||||
- SVG-Bibliotheken für Visualisierungen (D3.js)
|
||||
- JavaScript/Alpine.js für interaktive Komponenten
|
||||
- WebGL für animierte Hintergrundeffekte
|
||||
- **Datenbank**: SQLite mit SQLAlchemy
|
||||
- **KI-Integration**: OpenAI API für intelligente Assistenz
|
||||
|
||||
## Installation und Verwendung
|
||||
|
||||
### Installation
|
||||
1. Repository klonen
|
||||
2. Virtuelle Umgebung erstellen: `python -m venv venv`
|
||||
3. Virtuelle Umgebung aktivieren:
|
||||
- Windows: `venv\Scripts\activate`
|
||||
- Unix/MacOS: `source venv/bin/activate`
|
||||
4. Abhängigkeiten installieren: `pip install -r requirements.txt`
|
||||
5. Datenbank initialisieren: `python TOOLS.py db:rebuild`
|
||||
6. Admin-Benutzer erstellen: `python TOOLS.py user:admin`
|
||||
7. Server starten: `python TOOLS.py server:run`
|
||||
|
||||
### Standardbenutzer
|
||||
- **Admin-Benutzer**: Username: `admin` / Passwort: `admin`
|
||||
- **Testbenutzer**: Username: `user` / Passwort: `user`
|
||||
|
||||
### Verwaltungswerkzeuge mit TOOLS.py
|
||||
Das Projekt enthält ein zentrales Verwaltungsskript `TOOLS.py`, das verschiedene Hilfsfunktionen bietet:
|
||||
|
||||
#### Datenbankverwaltung
|
||||
- `python TOOLS.py db:fix` - Reparieren der Datenbankstruktur
|
||||
- `python TOOLS.py db:rebuild` - Datenbank neu aufbauen (löscht alle Daten!)
|
||||
- `python TOOLS.py db:test` - Datenbankverbindung und Modelle testen
|
||||
- `python TOOLS.py db:stats` - Datenbankstatistiken anzeigen
|
||||
|
||||
#### Benutzerverwaltung
|
||||
- `python TOOLS.py user:list` - Alle Benutzer anzeigen
|
||||
- `python TOOLS.py user:create -u USERNAME -e EMAIL -p PASSWORD [-a]` - Neuen Benutzer erstellen
|
||||
- `python TOOLS.py user:admin` - Admin-Benutzer erstellen (admin/admin)
|
||||
- `python TOOLS.py user:reset-pw -u USERNAME -p NEWPASSWORD` - Benutzerpasswort zurücksetzen
|
||||
- `python TOOLS.py user:delete -u USERNAME` - Benutzer löschen
|
||||
|
||||
#### Serververwaltung
|
||||
- `python TOOLS.py server:run [--host HOST] [--port PORT] [--no-debug]` - Entwicklungsserver starten
|
||||
|
||||
Für detaillierte Hilfe: `python TOOLS.py -h`
|
||||
|
||||
## Roadmap der Überarbeitung
|
||||
|
||||
### Phase 1: Grundlegende Infrastruktur ✅
|
||||
- [x] Bestandsaufnahme des aktuellen Projekts
|
||||
- [x] Erstellung der Roadmap
|
||||
- [x] Aktualisierung der Abhängigkeiten
|
||||
- [x] Integration von Tailwind CSS
|
||||
- [x] Einrichtung der SVG-Bibliotheken (D3.js)
|
||||
- [x] Favicon erstellen
|
||||
- [x] Setup-Skript für einfache Installation
|
||||
|
||||
### Phase 2: Design-Überarbeitung ✅
|
||||
- [x] Implementierung des Dark Mode
|
||||
- [x] Erstellung eines modernen, minimalistischen UI mit Tech-Ästhetik
|
||||
- [x] Responsive Design für alle Geräte
|
||||
- [x] Gestaltung der Landing Page mit großer Typografie
|
||||
- [x] Animierter Neurales Netzwerk-Hintergrund mit WebGL
|
||||
|
||||
### Phase 3: Mindmap-Funktionalitäten 🔄
|
||||
- [x] Verbesserte Visualisierung mit SVG und D3.js
|
||||
- [x] Implementierung der Mouseover-Funktion
|
||||
- [x] Entwicklung der Suchfunktion für Knoten
|
||||
- [x] Clustertopologie für neuronale Netzwerkdarstellung
|
||||
- [x] Fehlerbehandlung für robuste Datenverarbeitung und Knotenverweise
|
||||
- [x] Verbesserte Verbindungserkennung zwischen Knoten
|
||||
- [ ] Tagging-System für Inhalte
|
||||
- [ ] Quellenmanagement und -verlinkung
|
||||
- [ ] Upload-Funktionalität an Knotenpunkten
|
||||
|
||||
### Phase 4: Kernseitenentwicklung
|
||||
- [ ] Überarbeitung der Startseite mit neuen Features
|
||||
- [ ] Entwicklung der "Wer sind wir?"-Seite
|
||||
- [ ] Implementierung von Impressum und Datenschutzerklärung
|
||||
- [ ] Erstellung der Kontaktseite mit FAQs
|
||||
- [ ] Überarbeitung des Benutzerprofilbereichs
|
||||
|
||||
### Phase 5: Community-Features
|
||||
- [ ] Entwicklung des Autorenbereichs
|
||||
- [ ] Implementierung von Community-Bereichen für Themenbereiche
|
||||
- [ ] Verbesserter Kommentarbereich
|
||||
- [ ] Benutzerrechtemanagement
|
||||
|
||||
### Phase 6: KI-Integration
|
||||
- [ ] Implementierung des Frage-Antwort-Systems
|
||||
- [ ] KI-generierte Themeneinleitungen
|
||||
- [ ] Intelligente Suchunterstützung
|
||||
- [ ] Geführte Pfade durch Themenbereiche
|
||||
- [ ] Vorgeschlagene Chat-Möglichkeiten
|
||||
|
||||
### Phase 7: Benutzerprofilfunktionen
|
||||
- [ ] Speichern von Thematiken
|
||||
- [ ] Persönliche Mindmap/Pinboard
|
||||
- [ ] Beitragsmanagement
|
||||
- [ ] Benutzerstatistiken und -aktivitäten
|
||||
|
||||
### Phase 8: Testing und Optimierung
|
||||
- [ ] Umfassende Tests aller Funktionen
|
||||
- [ ] Performance-Optimierung
|
||||
- [ ] SEO-Implementierung
|
||||
- [ ] Barrierefreiheit prüfen und verbessern
|
||||
|
||||
### Phase 9: Dokumentation und Einführung
|
||||
- [ ] Erstellung von Benutzeranleitungen
|
||||
- [ ] Entwicklerdokumentation
|
||||
- [ ] Administratorenhandbuch
|
||||
- [ ] Guided Tour für neue Benutzer
|
||||
|
||||
## Aktueller Status
|
||||
- **Phase 1**: ✅ Abgeschlossen
|
||||
- **Phase 2**: ✅ Abgeschlossen
|
||||
- **Phase 3**: 🔄 In Bearbeitung (75% abgeschlossen)
|
||||
|
||||
## Aktuelle Fortschritte
|
||||
- Grundlegende UI modernisiert mit Tailwind CSS und Dark Mode
|
||||
- Neues Favicon für bessere visuelle Identität erstellt
|
||||
- Setup-Prozess vereinfacht mit einem Shell-Skript
|
||||
- Mindmap-Visualisierung komplett überarbeitet mit D3.js für eine interaktivere Erfahrung
|
||||
- Responsive Design für optimale Darstellung auf allen Geräten
|
||||
- Animierter neuronaler Netzwerk-Hintergrund mit WebGL implementiert
|
||||
- Verbesserte neuronale Cluster-Darstellung in der MindMap-Ansicht
|
||||
- Behebung von kritischen Fehlern in der Knotenvisualisierung und Verbindungserkennung
|
||||
- Robustere Datenverarbeitung für Mindmap-Knoten implementiert
|
||||
- Fehlerbehandlung für verschiedene API-Datenformate verbessert
|
||||
|
||||
## Neuronaler Netzwerk-Hintergrund
|
||||
|
||||
Ein wesentliches neues Feature ist der animierte Hintergrund, der ein neuronales Netzwerk simuliert:
|
||||
|
||||
- **WebGL-basierte Rendering-Engine** für hohe Performance
|
||||
- **Dynamische Knoten und Verbindungen** mit realistischem Bewegungsverhalten
|
||||
- **Neuronenfeuer-Simulation** mit Signalweiterleitung zwischen Knoten
|
||||
- **Clustertopologie** für realistisches Erscheinungsbild
|
||||
- **Anpassbare Farbgebung und Animationsparameter**
|
||||
- **Flüssige Animationen** mit über 100 Knotenpunkten
|
||||
|
||||
Die Animation ist vollständig responsiv und passt sich automatisch an verschiedene Bildschirmgrößen an, ohne die Browser-Performance zu beeinträchtigen.
|
||||
|
||||
## Mindmap-Verbesserungen
|
||||
|
||||
Die Mindmap-Darstellung wurde grundlegend überarbeitet:
|
||||
|
||||
- **D3.js Force-Directed Graph** für intuitive Knotenpositionierung
|
||||
- **Verbesserte Fehlerbehandlung** für robustere Datenverarbeitung
|
||||
- **Neuronale Cluster-Gruppierung** von thematisch zusammengehörigen Inhalten
|
||||
- **Glasmorphismus-Effekte** für moderne visuelle Darstellung
|
||||
- **Verbesserte Hover- und Selektionseffekte**
|
||||
- **Flüssige Animationen** bei Knotenauswahl und -fokussierung
|
||||
|
||||
## Nächste Schritte
|
||||
- Fertigstellung des Tagging-Systems für Gedanken
|
||||
- Verbesserung der Gedankenansicht im Mindmap-Bereich
|
||||
- Implementierung von Quellenmanagement
|
||||
- Überarbeitung der Startseite mit neuen Features
|
||||
|
||||
## Content Security Policy (CSP)
|
||||
|
||||
Die Anwendung implementiert eine Content Security Policy, um die Sicherheit zu erhöhen und unerwünschte externe Ressourcen zu blockieren. CSP wird in `app.py` konfiguriert und schränkt ein, welche Ressourcen geladen werden dürfen.
|
||||
|
||||
### Aktualisierung (06.06.2024)
|
||||
Die Anwendung verwendet nun die Tailwind CSS CDN für vereinfachte Entwicklung. Die CSP wurde entsprechend angepasst, um die Domain `cdn.tailwindcss.com` zu erlauben.
|
||||
|
||||
### Lokale und CDN-Ressourcen
|
||||
|
||||
Die Anwendung nutzt eine Mischung aus lokalen Ressourcen und CDNs:
|
||||
- **CDN-Ressourcen**:
|
||||
- Tailwind CSS (cdn.tailwindcss.com)
|
||||
- **Lokale Ressourcen**:
|
||||
- Alpine.js
|
||||
- Font Awesome
|
||||
- Google Fonts (Inter und JetBrains Mono)
|
||||
- WebGL-Animation (neural-network-background.js)
|
||||
|
||||
### CSP-Nonces
|
||||
|
||||
Die Anwendung verwendet Nonces für Inline-Skripte. In den Templates wird `{{ csp_nonce }}` verwendet, um den Nonce-Wert einzufügen:
|
||||
|
||||
```html
|
||||
<script nonce="{{ csp_nonce }}">
|
||||
// JavaScript Code
|
||||
</script>
|
||||
```
|
||||
|
||||
*Zuletzt aktualisiert: 15.06.2024*
|
||||
172
ROADMAP.md
Normal file
172
ROADMAP.md
Normal file
@@ -0,0 +1,172 @@
|
||||
# Systades Mindmap - Entwicklungs-Roadmap
|
||||
|
||||
Diese Roadmap beschreibt die geplante Entwicklung der dynamischen, benutzerorientierten Mindmap-Funktionalität für das Systades-Projekt.
|
||||
|
||||
## Phase 1: Grundlegendes Datenmodell und Backend (Abgeschlossen ✅)
|
||||
|
||||
- [x] Entwurf des Datenbankschemas für benutzerorientierte Mindmaps
|
||||
- [x] Implementierung der Modelle in models.py
|
||||
- [x] Erstellung der API-Endpunkte für CRUD-Operationen
|
||||
- [x] Integration mit der bestehenden Benutzerauthentifizierung
|
||||
- [x] Seed-Daten für die Entwicklung und Tests
|
||||
|
||||
## Phase 2: Dynamische Mindmap-Visualisierung (Abgeschlossen ✅)
|
||||
|
||||
- [x] Anpassung des Frontend-Codes zur Verwendung der DB-Daten anstelle des SVG
|
||||
- [x] Implementierung von AJAX-Anfragen zum Laden der Mindmap-Daten
|
||||
- [x] Dynamisches Rendering der Knoten, Verbindungen und Labels
|
||||
- [x] Drag-and-Drop-Funktionalität für die Bewegung von Knoten
|
||||
- [x] Zoom- und Pan-Funktionalität mit Persistenz der Ansicht
|
||||
- [x] Verbesserte Fehlerbehandlung in der Knotenvisualisierung
|
||||
- [x] Robustere Verbindungserkennung zwischen Knoten
|
||||
- [x] Implementierung von Glasmorphismus-Effekten für moderneres UI
|
||||
|
||||
## Phase 3: Visuelles Design und UX (Abgeschlossen ✅)
|
||||
|
||||
- [x] Implementierung des Dark Mode
|
||||
- [x] Entwicklung eines modernen, minimalistischen UI
|
||||
- [x] Animierter neuronaler Netzwerk-Hintergrund mit WebGL
|
||||
- [x] Responsive Design für alle Geräte
|
||||
- [x] Verbesserte Hover- und Selektionseffekte
|
||||
- [x] Clustertopologie für neuronale Netzwerkdarstellung
|
||||
- [x] Animierte Neuronenfeuer-Simulation mit Signalweiterleitung
|
||||
|
||||
## Phase 4: Benutzerdefinierte Mindmaps (Aktuell 🔄)
|
||||
|
||||
- [x] UI für das Betrachten bestehender Mindmaps
|
||||
- [ ] UI für das Erstellen und Bearbeiten eigener Mindmaps
|
||||
- [ ] Funktion zum Hinzufügen/Entfernen von Knoten aus der öffentlichen Mindmap
|
||||
- [ ] Speichern der Knotenpositionen und Ansichtseinstellungen
|
||||
- [ ] Benutzerspezifische Visualisierungseinstellungen
|
||||
- [ ] Dashboard mit Übersicht aller Mindmaps des Benutzers
|
||||
|
||||
## Phase 5: Notizen und Annotationen
|
||||
|
||||
- [x] Anzeige von Gedanken zu Mindmap-Knoten
|
||||
- [ ] UI für das Hinzufügen privater Notizen zu Knoten
|
||||
- [ ] Visuelle Anzeige von Notizen in der Mindmap
|
||||
- [ ] Texteditor mit Markdown-Unterstützung für Notizen
|
||||
- [ ] Kategorisierung und Farbkodierung von Notizen
|
||||
- [ ] Suchfunktion für Notizen
|
||||
|
||||
## Phase 6: Tagging und Quellenmanagement
|
||||
|
||||
- [ ] Tagging-System für Inhalte implementieren
|
||||
- [ ] Verknüpfen von Quellen mit Mindmap-Knoten
|
||||
- [ ] Upload-Funktionalität für Dateien und Medien
|
||||
- [ ] Verwaltung von Zitaten und Referenzen
|
||||
- [ ] Visuelles Feedback für Tags und Quellen in der Mindmap
|
||||
|
||||
## Phase 7: Integrationen und Erweiterungen
|
||||
|
||||
- [ ] Import/Export-Funktionalität für Mindmaps (JSON, PNG)
|
||||
- [ ] Teilen von Mindmaps (öffentlich/privat/mit bestimmten Benutzern)
|
||||
- [ ] Kollaborative Bearbeitung von Mindmaps
|
||||
- [ ] Verknüpfung mit externen Ressourcen (Links, Dateien)
|
||||
- [ ] Versionierung von Mindmaps
|
||||
|
||||
## Phase 8: KI-Integration und Analyse
|
||||
|
||||
- [ ] KI-gestützte Vorschläge für Verbindungen zwischen Knoten
|
||||
- [ ] Automatische Kategorisierung von Inhalten
|
||||
- [ ] Visualisierung von Beziehungsstärken und -typen
|
||||
- [ ] Mindmap-Statistiken und Analysen
|
||||
- [ ] KI-basierte Zusammenfassung von Teilbereichen der Mindmap
|
||||
|
||||
## Phase 9: Optimierung und Skalierung
|
||||
|
||||
- [ ] Performance-Optimierung für große Mindmaps
|
||||
- [ ] Verbesserung der Benutzerfreundlichkeit basierend auf Feedback
|
||||
- [ ] Erweiterte Such- und Filterfunktionen
|
||||
- [ ] Mobile Optimierung
|
||||
- [ ] Offline-Funktionalität mit Synchronisierung
|
||||
|
||||
## Technische Schulden und Refactoring
|
||||
|
||||
- [ ] Trennung der Datenbank-Logik vom Flask-App-Code
|
||||
- [ ] Einführung von Unit-Tests und Integration-Tests
|
||||
- [ ] Überarbeitung der API-Dokumentation
|
||||
- [ ] Caching-Strategien für bessere Performance
|
||||
- [ ] Verbesserte Fehlerbehandlung und Logging
|
||||
|
||||
## 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
|
||||
|
||||
### Datenbankschema
|
||||
|
||||
Das Datenbankschema umfasst folgende Hauptentitäten:
|
||||
|
||||
1. **Category** - Wissenschaftliche Kategorien für die öffentliche Mindmap
|
||||
2. **MindMapNode** - Öffentliche Mindmap-Knoten mit Metadaten
|
||||
3. **UserMindmap** - Benutzerdefinierte Mindmaps
|
||||
4. **UserMindmapNode** - Verknüpfung zwischen Benutzermindmaps und öffentlichen Knoten
|
||||
5. **MindmapNote** - Benutzerspezifische Notizen
|
||||
6. **Thought** - Gedanken und Inhalte, die Knoten zugeordnet sind
|
||||
7. **ThoughtRelation** - Beziehungen zwischen Gedanken
|
||||
|
||||
### Frontend-Technologien
|
||||
|
||||
- D3.js für die Visualisierung der Mindmap
|
||||
- WebGL für den neuronalen Netzwerk-Hintergrund
|
||||
- AJAX für dynamisches Laden von Daten
|
||||
- Interaktive Bedienelemente mit JavaScript
|
||||
- Responsive Design mit Tailwind CSS
|
||||
|
||||
### Backend-APIs
|
||||
|
||||
Die implementierten API-Endpunkte umfassen:
|
||||
|
||||
- `/api/mindmap/public` - Abrufen der öffentlichen Mindmap-Struktur
|
||||
- `/api/mindmap/user/<id>` - Abrufen benutzerdefinierter Mindmaps
|
||||
- `/api/mindmap/<id>/add_node` - Hinzufügen eines Knotens zur Benutzer-Mindmap
|
||||
- `/api/mindmap/<id>/remove_node/<node_id>` - Entfernen eines Knotens
|
||||
- `/api/mindmap/<id>/update_node_position` - Aktualisierung von Knotenpositionen
|
||||
- `/api/mindmap/<id>/notes` - Verwaltung von Notizen
|
||||
- `/api/nodes/<id>/thoughts` - Abrufen und Hinzufügen von Gedanken zu Knoten
|
||||
- `/api/get_dark_mode` - Abrufen der Dark Mode Einstellung
|
||||
|
||||
## Neuronaler Netzwerk-Hintergrund
|
||||
|
||||
Der neue WebGL-basierte Hintergrund bietet:
|
||||
|
||||
- WebGL-basierte Rendering-Engine für optimale Performance
|
||||
- Dynamische Knoten und Verbindungen mit realistischem Verhalten
|
||||
- Clustering von neuronalen Knoten für natürlicheres Erscheinungsbild
|
||||
- Simulation von neuronaler Aktivität und Signalweiterleitung
|
||||
- Anpassbare visuelle Parameter (Helligkeit, Dichte, Geschwindigkeit)
|
||||
- Vollständig responsives Design für alle Bildschirmgrößen
|
||||
|
||||
## Aktuelle Verbesserungen
|
||||
- Tailwind CSS wurde auf CDN-Version aktualisiert (06.06.2024)
|
||||
- Content Security Policy (CSP) für Tailwind CSS CDN und WebGL konfiguriert
|
||||
- Behebung kritischer Fehler in der Mindmap-Knotenvisualisierung (15.06.2024)
|
||||
- Verbesserte Verbindungserkennung zwischen Knoten implementiert
|
||||
- Robuste Fehlerbehandlung für verschiedene API-Datenformate
|
||||
|
||||
## Zukünftige Aufgaben (Q3 2024)
|
||||
- Implementierung des Tagging-Systems für Gedanken
|
||||
- Quellenmanagement für Mindmap-Knoten
|
||||
- Erweiterte Benutzerprofilfunktionen
|
||||
- Verbesserung der mobilen Benutzererfahrung
|
||||
- Integration von Exportfunktionen für Mindmaps
|
||||
|
||||
*Zuletzt aktualisiert: 15.06.2024*
|
||||
|
||||
## [Entfernt] CORS-Unterstützung (flask-cors)
|
||||
- Die flask-cors-Bibliothek und alle zugehörigen Initialisierungen wurden entfernt.
|
||||
- CORS wird nicht mehr unterstützt oder benötigt.
|
||||
125
TOOLS.py
Normal file
125
TOOLS.py
Normal file
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
TOOLS.py - Main utility script for the website application.
|
||||
|
||||
This script provides a command-line interface to all utilities
|
||||
for database management, user management, and server administration.
|
||||
|
||||
Usage:
|
||||
python3 TOOLS.py [command] [options]
|
||||
|
||||
Available commands:
|
||||
- db:fix Fix database schema
|
||||
- db:rebuild Completely rebuild the database
|
||||
- db:test Test database connection and models
|
||||
- db:stats Show database statistics
|
||||
|
||||
- user:list List all users
|
||||
- user:create Create a new user
|
||||
- user:admin Create admin user (username: admin, password: admin)
|
||||
- user:reset-pw Reset user password
|
||||
- user:delete Delete a user
|
||||
|
||||
- server:run Run the development server
|
||||
|
||||
Examples:
|
||||
python3 TOOLS.py db:rebuild
|
||||
python3 TOOLS.py user:admin
|
||||
python3 TOOLS.py server:run --port 8080
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
from utils import (
|
||||
fix_database_schema, rebuild_database, run_all_tests, print_database_stats,
|
||||
list_users, create_user, reset_password, delete_user, create_admin_user,
|
||||
run_development_server
|
||||
)
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Website Administration Tools',
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog=__doc__
|
||||
)
|
||||
|
||||
# Main command argument
|
||||
parser.add_argument('command', help='Command to execute')
|
||||
|
||||
# Additional arguments
|
||||
parser.add_argument('--username', '-u', help='Username for user commands')
|
||||
parser.add_argument('--email', '-e', help='Email for user creation')
|
||||
parser.add_argument('--password', '-p', help='Password for user creation/reset')
|
||||
parser.add_argument('--admin', '-a', action='store_true', help='Make user an admin')
|
||||
parser.add_argument('--host', help='Host for server (default: 127.0.0.1)')
|
||||
parser.add_argument('--port', type=int, help='Port for server (default: 5000)')
|
||||
parser.add_argument('--no-debug', action='store_true', help='Disable debug mode for server')
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
|
||||
# Database commands
|
||||
if args.command == 'db:fix':
|
||||
fix_database_schema()
|
||||
|
||||
elif args.command == 'db:rebuild':
|
||||
print("WARNING: This will delete all data in the database!")
|
||||
confirm = input("Are you sure you want to continue? (y/n): ").lower()
|
||||
if confirm == 'y':
|
||||
rebuild_database()
|
||||
else:
|
||||
print("Aborted.")
|
||||
|
||||
elif args.command == 'db:test':
|
||||
run_all_tests()
|
||||
|
||||
elif args.command == 'db:stats':
|
||||
print_database_stats()
|
||||
|
||||
# User commands
|
||||
elif args.command == 'user:list':
|
||||
list_users()
|
||||
|
||||
elif args.command == 'user:create':
|
||||
if not args.username or not args.email or not args.password:
|
||||
print("Error: Username, email, and password are required.")
|
||||
print("Example: python3 TOOLS.py user:create -u username -e email -p password [-a]")
|
||||
sys.exit(1)
|
||||
create_user(args.username, args.email, args.password, args.admin)
|
||||
|
||||
elif args.command == 'user:admin':
|
||||
create_admin_user()
|
||||
|
||||
elif args.command == 'user:reset-pw':
|
||||
if not args.username or not args.password:
|
||||
print("Error: Username and password are required.")
|
||||
print("Example: python3 TOOLS.py user:reset-pw -u username -p new_password")
|
||||
sys.exit(1)
|
||||
reset_password(args.username, args.password)
|
||||
|
||||
elif args.command == 'user:delete':
|
||||
if not args.username:
|
||||
print("Error: Username is required.")
|
||||
print("Example: python3 TOOLS.py user:delete -u username")
|
||||
sys.exit(1)
|
||||
delete_user(args.username)
|
||||
|
||||
# Server commands
|
||||
elif args.command == 'server:run':
|
||||
host = args.host or '127.0.0.1'
|
||||
port = args.port or 5000
|
||||
debug = not args.no_debug
|
||||
run_development_server(host=host, port=port, debug=debug)
|
||||
|
||||
else:
|
||||
print(f"Unknown command: {args.command}")
|
||||
print("Run 'python3 TOOLS.py -h' for usage information")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
BIN
__pycache__/app.cpython-311.pyc
Normal file
BIN
__pycache__/app.cpython-311.pyc
Normal file
Binary file not shown.
BIN
__pycache__/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.
BIN
__pycache__/models.cpython-311.pyc
Normal file
BIN
__pycache__/models.cpython-311.pyc
Normal file
Binary file not shown.
BIN
__pycache__/models.cpython-313.pyc
Normal file
BIN
__pycache__/models.cpython-313.pyc
Normal file
Binary file not shown.
34
check_schema.py
Normal file
34
check_schema.py
Normal file
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import sqlite3
|
||||
|
||||
# Verbindung zur Datenbank herstellen
|
||||
conn = sqlite3.connect('systades.db')
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Liste aller Tabellen abrufen
|
||||
print("Alle Tabellen in der Datenbank:")
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table';")
|
||||
tables = cursor.fetchall()
|
||||
for table in tables:
|
||||
print(f"- {table[0]}")
|
||||
|
||||
# Schema der Datenbank abrufen
|
||||
cursor.execute("SELECT sql FROM sqlite_master WHERE type='table';")
|
||||
schemas = cursor.fetchall()
|
||||
|
||||
# Schematische Informationen ausgeben
|
||||
print("\nDatenbankschema:")
|
||||
for schema in schemas:
|
||||
print("\n" + str(schema[0]))
|
||||
|
||||
# Schema der User-Tabelle genauer untersuchen, falls vorhanden
|
||||
if ('user',) in tables:
|
||||
print("\n\nBenutzer-Tabellenschema:")
|
||||
cursor.execute("PRAGMA table_info(user);")
|
||||
user_columns = cursor.fetchall()
|
||||
for column in user_columns:
|
||||
print(f"Column: {column[1]}, Type: {column[2]}, NOT NULL: {column[3]}, Default: {column[4]}, Primary Key: {column[5]}")
|
||||
|
||||
conn.close()
|
||||
72
create_default_users.py
Normal file
72
create_default_users.py
Normal file
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import sqlite3
|
||||
import os
|
||||
from werkzeug.security import generate_password_hash
|
||||
from datetime import datetime
|
||||
|
||||
# Prüfen, ob die Datenbank existiert
|
||||
db_path = 'systades.db'
|
||||
if not os.path.exists(db_path):
|
||||
print(f"Datenbank {db_path} existiert nicht.")
|
||||
exit(1)
|
||||
|
||||
# Verbindung zur Datenbank herstellen
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Überprüfen, ob bereits Benutzer vorhanden sind
|
||||
cursor.execute("SELECT COUNT(*) FROM user;")
|
||||
user_count = cursor.fetchone()[0]
|
||||
|
||||
if user_count == 0:
|
||||
print("Keine Benutzer in der Datenbank gefunden. Erstelle Standardbenutzer...")
|
||||
|
||||
# Standardbenutzer definieren
|
||||
default_users = [
|
||||
{
|
||||
'username': 'admin',
|
||||
'email': 'admin@example.com',
|
||||
'password': generate_password_hash('admin'),
|
||||
'created_at': datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'is_active': 1,
|
||||
'role': 'admin'
|
||||
},
|
||||
{
|
||||
'username': 'user',
|
||||
'email': 'user@example.com',
|
||||
'password': generate_password_hash('user'),
|
||||
'created_at': datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'is_active': 1,
|
||||
'role': 'user'
|
||||
}
|
||||
]
|
||||
|
||||
# Benutzer einfügen
|
||||
for user in default_users:
|
||||
cursor.execute("""
|
||||
INSERT INTO user (username, email, password, created_at, is_active, role)
|
||||
VALUES (?, ?, ?, ?, ?, ?);
|
||||
""", (
|
||||
user['username'],
|
||||
user['email'],
|
||||
user['password'],
|
||||
user['created_at'],
|
||||
user['is_active'],
|
||||
user['role']
|
||||
))
|
||||
conn.commit()
|
||||
print(f"{len(default_users)} Standardbenutzer wurden erstellt.")
|
||||
else:
|
||||
print(f"Es sind bereits {user_count} Benutzer in der Datenbank vorhanden.")
|
||||
|
||||
# Überprüfen der eingefügten Benutzer
|
||||
print("\nBenutzer in der Datenbank:")
|
||||
cursor.execute("SELECT id, username, email, role FROM user;")
|
||||
users = cursor.fetchall()
|
||||
for user in users:
|
||||
print(f"ID: {user[0]}, Benutzername: {user[1]}, E-Mail: {user[2]}, Rolle: {user[3]}")
|
||||
|
||||
conn.close()
|
||||
print("\nBenutzeraktualisierung abgeschlossen.")
|
||||
BIN
database/__pycache__/models.cpython-313.pyc
Normal file
BIN
database/__pycache__/models.cpython-313.pyc
Normal file
Binary file not shown.
BIN
database/systades.V2.db.backup
Normal file
BIN
database/systades.V2.db.backup
Normal file
Binary file not shown.
BIN
database/systades.db
Normal file
BIN
database/systades.db
Normal file
Binary file not shown.
BIN
database/systades.db.backup
Normal file
BIN
database/systades.db.backup
Normal file
Binary file not shown.
69
docs/ANLEITUNG.md
Normal file
69
docs/ANLEITUNG.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# Anleitung: Animierter Netzwerk-Hintergrund
|
||||
|
||||
Diese Anleitung erklärt, wie Sie ein Netzwerk-Bild als animierten Hintergrund für die gesamte Website einrichten können.
|
||||
|
||||
## Option 1: Manuelle Installation
|
||||
|
||||
1. Kopieren Sie das gewünschte Netzwerk-Bild (z.B. `d2efd014-1325-471f-b9a7-90d025eb81d6.png`) in die Datei `website/static/network-bg.jpg`.
|
||||
|
||||
Sie können dafür das beiliegende Batch-Skript verwenden:
|
||||
```
|
||||
copy-network-image.bat d2efd014-1325-471f-b9a7-90d025eb81d6.png
|
||||
```
|
||||
|
||||
2. Starten Sie den Flask-Server mit dem folgenden Befehl:
|
||||
```
|
||||
python start-flask-server.py
|
||||
```
|
||||
|
||||
3. Öffnen Sie die Website unter http://127.0.0.1:5000/
|
||||
|
||||
## Anpassung der Animation
|
||||
|
||||
Sie können die Animation des Netzwerk-Hintergrunds anpassen, indem Sie die Datei `website/static/network-background.js` bearbeiten. Hier sind die wichtigsten Parameter:
|
||||
|
||||
```javascript
|
||||
let animationSpeed = 0.0005; // Geschwindigkeit der Rotation
|
||||
let scaleSpeed = 0.0002; // Geschwindigkeit der Skalierung
|
||||
let opacitySpeed = 0.0003; // Geschwindigkeit der Transparenzänderung
|
||||
```
|
||||
|
||||
## Animation der Mindmap-Verbindungen
|
||||
|
||||
Die Verbindungen zwischen den Knoten in der Mindmap werden jetzt mit einer fließenden Animation dargestellt. Diese Animationen verbessern die Sichtbarkeit der Zusammenhänge und machen die Interaktion mit der Karte intuitiver.
|
||||
|
||||
### Funktionen:
|
||||
|
||||
1. **Animierte Linien**: Die Verbindungslinien zwischen den Knoten bewegen sich in einem fließenden Muster.
|
||||
2. **Hervorhebung bei Hover**: Beim Überfahren eines Knotens oder einer Verbindung mit der Maus werden diese hervorgehoben.
|
||||
3. **Kategorien-Beziehungen**: Die visuellen Verbindungen zwischen den Kategorien sind jetzt deutlicher erkennbar.
|
||||
|
||||
## Position des Auswahlfelds
|
||||
|
||||
Das Auswahlfeld auf der Karte wurde weiter nach links verschoben, sodass es vollständig sichtbar ist, wenn keine Auswahl getroffen wurde. Die Größe wurde ebenfalls angepasst, um die Lesbarkeit zu verbessern.
|
||||
|
||||
## Wiederherstellung des ursprünglichen Hintergrunds (optional)
|
||||
|
||||
Wenn Sie zum ursprünglichen Sternenhintergrund zurückkehren möchten, müssen Sie folgende Änderungen vornehmen:
|
||||
|
||||
1. Bearbeiten Sie die Datei `website/templates/base.html` und ersetzen Sie:
|
||||
```html
|
||||
<!-- Network Background Script -->
|
||||
<script src="{{ url_for('static', filename='network-background.js') }}"></script>
|
||||
```
|
||||
|
||||
mit:
|
||||
```html
|
||||
<!-- Three.js für den Sternenhintergrund -->
|
||||
<script src="{{ url_for('static', filename='three.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='background.js') }}"></script>
|
||||
```
|
||||
|
||||
2. Bearbeiten Sie die Datei `website/templates/mindmap.html` und entfernen Sie die Zeile:
|
||||
```html
|
||||
<script src="{{ url_for('static', filename='network-animation.js') }}"></script>
|
||||
```
|
||||
|
||||
3. Entfernen Sie den CSS-Code für `#mindmap-container::before` und die anderen netzwerkspezifischen Stile aus der Datei `website/templates/mindmap.html`.
|
||||
|
||||
4. Starten Sie den Flask-Server neu, um die Änderungen zu übernehmen.
|
||||
BIN
docs/Grundstruktur (funktionales Modell).pdf
Normal file
BIN
docs/Grundstruktur (funktionales Modell).pdf
Normal file
Binary file not shown.
13
example.env
Normal file
13
example.env
Normal file
@@ -0,0 +1,13 @@
|
||||
# MindMap Umgebungsvariablen
|
||||
# Kopiere diese Datei zu .env und passe die Werte an
|
||||
|
||||
# Flask
|
||||
SECRET_KEY=dein-geheimer-schluessel-hier
|
||||
|
||||
# OpenAI API
|
||||
OPENAI_API_KEY=sk-dein-openai-api-schluessel-hier
|
||||
|
||||
# Datenbank
|
||||
# Bei Bedarf kann hier eine andere Datenbank-URL angegeben werden
|
||||
# Der Pfad wird relativ zum Projektverzeichnis angegeben
|
||||
# SQLALCHEMY_DATABASE_URI=sqlite:////absoluter/pfad/zu/database/systades.db
|
||||
70
fix_user_table.py
Normal file
70
fix_user_table.py
Normal file
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import sqlite3
|
||||
import os
|
||||
|
||||
# Prüfen, ob die Datenbank existiert
|
||||
db_path = 'systades.db'
|
||||
if not os.path.exists(db_path):
|
||||
print(f"Datenbank {db_path} existiert nicht.")
|
||||
exit(1)
|
||||
|
||||
# Verbindung zur Datenbank herstellen
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Prüfen, ob die User-Tabelle existiert
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='user';")
|
||||
if not cursor.fetchone():
|
||||
print("Die Tabelle 'user' existiert nicht. Erstelle neue Tabelle...")
|
||||
cursor.execute('''
|
||||
CREATE TABLE user (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username VARCHAR(80) NOT NULL UNIQUE,
|
||||
email VARCHAR(120) NOT NULL UNIQUE,
|
||||
password VARCHAR(512) NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
is_active BOOLEAN DEFAULT 1,
|
||||
role VARCHAR(20) DEFAULT 'user'
|
||||
)
|
||||
''')
|
||||
conn.commit()
|
||||
print("Die Tabelle 'user' wurde erfolgreich erstellt.")
|
||||
else:
|
||||
# Überprüfen, ob die Spalte 'password' existiert
|
||||
cursor.execute("PRAGMA table_info(user);")
|
||||
columns = cursor.fetchall()
|
||||
column_names = [column[1] for column in columns]
|
||||
|
||||
if 'password' not in column_names:
|
||||
print("Die Spalte 'password' fehlt in der Tabelle 'user'. Füge Spalte hinzu...")
|
||||
cursor.execute("ALTER TABLE user ADD COLUMN password VARCHAR(512);")
|
||||
conn.commit()
|
||||
print("Die Spalte 'password' wurde erfolgreich hinzugefügt.")
|
||||
else:
|
||||
print("Die Spalte 'password' existiert bereits in der Tabelle 'user'.")
|
||||
|
||||
# Überprüfen der aktualisierten Spaltenstruktur
|
||||
print("\nAktualisierte Tabellenspalten der 'user'-Tabelle:")
|
||||
cursor.execute("PRAGMA table_info(user);")
|
||||
updated_columns = cursor.fetchall()
|
||||
for column in updated_columns:
|
||||
print(f"Column: {column[1]}, Type: {column[2]}, NOT NULL: {column[3]}, Default: {column[4]}, Primary Key: {column[5]}")
|
||||
|
||||
# Datenbanktabellen anzeigen
|
||||
print("\nAlle Tabellen in der Datenbank:")
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table';")
|
||||
tables = cursor.fetchall()
|
||||
for table in tables:
|
||||
print(f"- {table[0]}")
|
||||
|
||||
# Schemaüberprüfung der user-Tabelle
|
||||
print("\nSchema der 'user'-Tabelle:")
|
||||
cursor.execute("SELECT sql FROM sqlite_master WHERE type='table' AND name='user';")
|
||||
schema = cursor.fetchone()
|
||||
if schema:
|
||||
print(schema[0])
|
||||
|
||||
conn.close()
|
||||
print("\nDatenbankaktualisierung abgeschlossen.")
|
||||
246
init_db.py
Normal file
246
init_db.py
Normal file
@@ -0,0 +1,246 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from app import app, initialize_database, db_path
|
||||
from models import db, User, Thought, Comment, MindMapNode, ThoughtRelation, ThoughtRating, RelationType
|
||||
from models import Category, UserMindmap, UserMindmapNode, MindmapNote
|
||||
import os
|
||||
import sqlite3
|
||||
from flask import Flask
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from datetime import datetime
|
||||
|
||||
# Erstelle eine temporäre Flask-App, um die Datenbank zu initialisieren
|
||||
app = Flask(__name__)
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///systades.db'
|
||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||
db.init_app(app)
|
||||
|
||||
def init_db():
|
||||
with app.app_context():
|
||||
print("Initialisiere Datenbank...")
|
||||
|
||||
# Tabellen erstellen
|
||||
db.create_all()
|
||||
print("Tabellen wurden erstellt.")
|
||||
|
||||
# Standardbenutzer erstellen, falls keine vorhanden sind
|
||||
if User.query.count() == 0:
|
||||
print("Erstelle Standardbenutzer...")
|
||||
create_default_users()
|
||||
|
||||
# Standardkategorien erstellen, falls keine vorhanden sind
|
||||
if Category.query.count() == 0:
|
||||
print("Erstelle Standardkategorien...")
|
||||
create_default_categories()
|
||||
|
||||
# Beispiel-Mindmap erstellen, falls keine Knoten vorhanden sind
|
||||
if MindMapNode.query.count() == 0:
|
||||
print("Erstelle Beispiel-Mindmap...")
|
||||
create_sample_mindmap()
|
||||
|
||||
print("Datenbankinitialisierung abgeschlossen.")
|
||||
|
||||
def create_default_users():
|
||||
"""Erstellt Standardbenutzer für die Anwendung"""
|
||||
users = [
|
||||
{
|
||||
'username': 'admin',
|
||||
'email': 'admin@example.com',
|
||||
'password': 'admin',
|
||||
'role': 'admin'
|
||||
},
|
||||
{
|
||||
'username': 'user',
|
||||
'email': 'user@example.com',
|
||||
'password': 'user',
|
||||
'role': 'user'
|
||||
}
|
||||
]
|
||||
|
||||
for user_data in users:
|
||||
password = user_data.pop('password')
|
||||
user = User(**user_data)
|
||||
user.set_password(password)
|
||||
db.session.add(user)
|
||||
|
||||
db.session.commit()
|
||||
print(f"{len(users)} Benutzer wurden erstellt.")
|
||||
|
||||
def create_default_categories():
|
||||
"""Erstellt die Standardkategorien für die Mindmap"""
|
||||
categories = [
|
||||
{
|
||||
'name': 'Konzept',
|
||||
'description': 'Abstrakte Ideen und theoretische Konzepte',
|
||||
'color_code': '#6366f1',
|
||||
'icon': 'lightbulb'
|
||||
},
|
||||
{
|
||||
'name': 'Technologie',
|
||||
'description': 'Hardware, Software, Tools und Plattformen',
|
||||
'color_code': '#10b981',
|
||||
'icon': 'cpu'
|
||||
},
|
||||
{
|
||||
'name': 'Prozess',
|
||||
'description': 'Workflows, Methodologien und Vorgehensweisen',
|
||||
'color_code': '#f59e0b',
|
||||
'icon': 'git-branch'
|
||||
},
|
||||
{
|
||||
'name': 'Person',
|
||||
'description': 'Personen, Teams und Organisationen',
|
||||
'color_code': '#ec4899',
|
||||
'icon': 'user'
|
||||
},
|
||||
{
|
||||
'name': 'Dokument',
|
||||
'description': 'Dokumentationen, Referenzen und Ressourcen',
|
||||
'color_code': '#3b82f6',
|
||||
'icon': 'file-text'
|
||||
}
|
||||
]
|
||||
|
||||
for cat_data in categories:
|
||||
category = Category(**cat_data)
|
||||
db.session.add(category)
|
||||
|
||||
db.session.commit()
|
||||
print(f"{len(categories)} Kategorien wurden erstellt.")
|
||||
|
||||
def create_sample_mindmap():
|
||||
"""Erstellt eine Beispiel-Mindmap mit Knoten und Beziehungen"""
|
||||
|
||||
# Kategorien für die Zuordnung
|
||||
categories = Category.query.all()
|
||||
category_map = {cat.name: cat for cat in categories}
|
||||
|
||||
# Beispielknoten erstellen
|
||||
nodes = [
|
||||
{
|
||||
'name': 'Wissensmanagement',
|
||||
'description': 'Systematische Erfassung, Speicherung und Nutzung von Wissen in Organisationen.',
|
||||
'color_code': '#6366f1',
|
||||
'icon': 'database',
|
||||
'category': category_map.get('Konzept'),
|
||||
'x': 0,
|
||||
'y': 0
|
||||
},
|
||||
{
|
||||
'name': 'Mind-Mapping',
|
||||
'description': 'Technik zur visuellen Darstellung von Informationen und Zusammenhängen.',
|
||||
'color_code': '#10b981',
|
||||
'icon': 'git-branch',
|
||||
'category': category_map.get('Prozess'),
|
||||
'x': 200,
|
||||
'y': -150
|
||||
},
|
||||
{
|
||||
'name': 'Cytoscape.js',
|
||||
'description': 'JavaScript-Bibliothek für die Visualisierung und Manipulation von Graphen.',
|
||||
'color_code': '#3b82f6',
|
||||
'icon': 'code',
|
||||
'category': category_map.get('Technologie'),
|
||||
'x': 350,
|
||||
'y': -50
|
||||
},
|
||||
{
|
||||
'name': 'Socket.IO',
|
||||
'description': 'Bibliothek für Echtzeit-Kommunikation zwischen Client und Server.',
|
||||
'color_code': '#3b82f6',
|
||||
'icon': 'zap',
|
||||
'category': category_map.get('Technologie'),
|
||||
'x': 350,
|
||||
'y': 100
|
||||
},
|
||||
{
|
||||
'name': 'Kollaboration',
|
||||
'description': 'Zusammenarbeit mehrerer Benutzer an gemeinsamen Inhalten.',
|
||||
'color_code': '#f59e0b',
|
||||
'icon': 'users',
|
||||
'category': category_map.get('Prozess'),
|
||||
'x': 200,
|
||||
'y': 150
|
||||
},
|
||||
{
|
||||
'name': 'SQLite',
|
||||
'description': 'Leichtgewichtige relationale Datenbank, die ohne Server-Prozess auskommt.',
|
||||
'color_code': '#3b82f6',
|
||||
'icon': 'database',
|
||||
'category': category_map.get('Technologie'),
|
||||
'x': 0,
|
||||
'y': 200
|
||||
},
|
||||
{
|
||||
'name': 'Flask',
|
||||
'description': 'Leichtgewichtiges Python-Webframework für die Entwicklung von Webanwendungen.',
|
||||
'color_code': '#3b82f6',
|
||||
'icon': 'server',
|
||||
'category': category_map.get('Technologie'),
|
||||
'x': -200,
|
||||
'y': 150
|
||||
},
|
||||
{
|
||||
'name': 'REST API',
|
||||
'description': 'Architekturstil für verteilte Systeme, insbesondere Webanwendungen.',
|
||||
'color_code': '#10b981',
|
||||
'icon': 'link',
|
||||
'category': category_map.get('Konzept'),
|
||||
'x': -200,
|
||||
'y': -150
|
||||
},
|
||||
{
|
||||
'name': 'Dokumentation',
|
||||
'description': 'Strukturierte Erfassung und Beschreibung von Informationen und Prozessen.',
|
||||
'color_code': '#ec4899',
|
||||
'icon': 'file-text',
|
||||
'category': category_map.get('Dokument'),
|
||||
'x': -350,
|
||||
'y': 0
|
||||
}
|
||||
]
|
||||
|
||||
# Knoten in die Datenbank einfügen
|
||||
node_objects = {}
|
||||
for node_data in nodes:
|
||||
category = node_data.pop('category', None)
|
||||
x = node_data.pop('x', 0)
|
||||
y = node_data.pop('y', 0)
|
||||
node = MindMapNode(**node_data)
|
||||
if category:
|
||||
node.category_id = category.id
|
||||
db.session.add(node)
|
||||
db.session.flush() # Generiert IDs für neue Objekte
|
||||
node_objects[node.name] = node
|
||||
|
||||
# Beziehungen erstellen
|
||||
relationships = [
|
||||
('Wissensmanagement', 'Mind-Mapping'),
|
||||
('Wissensmanagement', 'Kollaboration'),
|
||||
('Wissensmanagement', 'Dokumentation'),
|
||||
('Mind-Mapping', 'Cytoscape.js'),
|
||||
('Kollaboration', 'Socket.IO'),
|
||||
('Wissensmanagement', 'SQLite'),
|
||||
('SQLite', 'Flask'),
|
||||
('Flask', 'REST API'),
|
||||
('REST API', 'Socket.IO'),
|
||||
('REST API', 'Dokumentation')
|
||||
]
|
||||
|
||||
for parent_name, child_name in relationships:
|
||||
parent = node_objects.get(parent_name)
|
||||
child = node_objects.get(child_name)
|
||||
if parent and child:
|
||||
parent.children.append(child)
|
||||
|
||||
db.session.commit()
|
||||
print(f"{len(nodes)} Knoten und {len(relationships)} Beziehungen wurden erstellt.")
|
||||
|
||||
if __name__ == '__main__':
|
||||
init_db()
|
||||
print("Datenbank wurde erfolgreich initialisiert!")
|
||||
print("Sie können die Anwendung jetzt mit 'python app.py' starten")
|
||||
print("Anmelden mit:")
|
||||
print(" Admin: username=admin, password=admin")
|
||||
print(" User: username=user, password=user")
|
||||
1
migrations/README
Normal file
1
migrations/README
Normal file
@@ -0,0 +1 @@
|
||||
Single-database configuration for Flask.
|
||||
BIN
migrations/__pycache__/env.cpython-311.pyc
Normal file
BIN
migrations/__pycache__/env.cpython-311.pyc
Normal file
Binary file not shown.
50
migrations/alembic.ini
Normal file
50
migrations/alembic.ini
Normal file
@@ -0,0 +1,50 @@
|
||||
# A generic, single database configuration.
|
||||
|
||||
[alembic]
|
||||
# template used to generate migration files
|
||||
# file_template = %%(rev)s_%%(slug)s
|
||||
|
||||
# set to 'true' to run the environment during
|
||||
# the 'revision' command, regardless of autogenerate
|
||||
# revision_environment = false
|
||||
|
||||
|
||||
# Logging configuration
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic,flask_migrate
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARN
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARN
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[logger_flask_migrate]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = flask_migrate
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
||||
113
migrations/env.py
Normal file
113
migrations/env.py
Normal file
@@ -0,0 +1,113 @@
|
||||
import logging
|
||||
from logging.config import fileConfig
|
||||
|
||||
from flask import current_app
|
||||
|
||||
from alembic import context
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
config = context.config
|
||||
|
||||
# Interpret the config file for Python logging.
|
||||
# This line sets up loggers basically.
|
||||
fileConfig(config.config_file_name)
|
||||
logger = logging.getLogger('alembic.env')
|
||||
|
||||
|
||||
def get_engine():
|
||||
try:
|
||||
# this works with Flask-SQLAlchemy<3 and Alchemical
|
||||
return current_app.extensions['migrate'].db.get_engine()
|
||||
except (TypeError, AttributeError):
|
||||
# this works with Flask-SQLAlchemy>=3
|
||||
return current_app.extensions['migrate'].db.engine
|
||||
|
||||
|
||||
def get_engine_url():
|
||||
try:
|
||||
return get_engine().url.render_as_string(hide_password=False).replace(
|
||||
'%', '%%')
|
||||
except AttributeError:
|
||||
return str(get_engine().url).replace('%', '%%')
|
||||
|
||||
|
||||
# add your model's MetaData object here
|
||||
# for 'autogenerate' support
|
||||
# from myapp import mymodel
|
||||
# target_metadata = mymodel.Base.metadata
|
||||
config.set_main_option('sqlalchemy.url', get_engine_url())
|
||||
target_db = current_app.extensions['migrate'].db
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
# can be acquired:
|
||||
# my_important_option = config.get_main_option("my_important_option")
|
||||
# ... etc.
|
||||
|
||||
|
||||
def get_metadata():
|
||||
if hasattr(target_db, 'metadatas'):
|
||||
return target_db.metadatas[None]
|
||||
return target_db.metadata
|
||||
|
||||
|
||||
def run_migrations_offline():
|
||||
"""Run migrations in 'offline' mode.
|
||||
|
||||
This configures the context with just a URL
|
||||
and not an Engine, though an Engine is acceptable
|
||||
here as well. By skipping the Engine creation
|
||||
we don't even need a DBAPI to be available.
|
||||
|
||||
Calls to context.execute() here emit the given string to the
|
||||
script output.
|
||||
|
||||
"""
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(
|
||||
url=url, target_metadata=get_metadata(), literal_binds=True
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def run_migrations_online():
|
||||
"""Run migrations in 'online' mode.
|
||||
|
||||
In this scenario we need to create an Engine
|
||||
and associate a connection with the context.
|
||||
|
||||
"""
|
||||
|
||||
# this callback is used to prevent an auto-migration from being generated
|
||||
# when there are no changes to the schema
|
||||
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
|
||||
def process_revision_directives(context, revision, directives):
|
||||
if getattr(config.cmd_opts, 'autogenerate', False):
|
||||
script = directives[0]
|
||||
if script.upgrade_ops.is_empty():
|
||||
directives[:] = []
|
||||
logger.info('No changes in schema detected.')
|
||||
|
||||
conf_args = current_app.extensions['migrate'].configure_args
|
||||
if conf_args.get("process_revision_directives") is None:
|
||||
conf_args["process_revision_directives"] = process_revision_directives
|
||||
|
||||
connectable = get_engine()
|
||||
|
||||
with connectable.connect() as connection:
|
||||
context.configure(
|
||||
connection=connection,
|
||||
target_metadata=get_metadata(),
|
||||
**conf_args
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
||||
24
migrations/script.py.mako
Normal file
24
migrations/script.py.mako
Normal file
@@ -0,0 +1,24 @@
|
||||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = ${repr(up_revision)}
|
||||
down_revision = ${repr(down_revision)}
|
||||
branch_labels = ${repr(branch_labels)}
|
||||
depends_on = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade():
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade():
|
||||
${downgrades if downgrades else "pass"}
|
||||
Binary file not shown.
@@ -0,0 +1,46 @@
|
||||
"""Add password column to user
|
||||
|
||||
Revision ID: d4406f5b12f7
|
||||
Revises:
|
||||
Create Date: 2025-04-28 21:26:37.430823
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'd4406f5b12f7'
|
||||
down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('user', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('password', sa.String(length=512), nullable=False, server_default="changeme"))
|
||||
batch_op.add_column(sa.Column('is_active', sa.Boolean(), nullable=True))
|
||||
batch_op.add_column(sa.Column('role', sa.String(length=20), nullable=True))
|
||||
batch_op.drop_column('last_login')
|
||||
batch_op.drop_column('bio')
|
||||
batch_op.drop_column('password_hash')
|
||||
batch_op.drop_column('is_admin')
|
||||
batch_op.drop_column('avatar')
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('user', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('avatar', sa.VARCHAR(length=200), nullable=True))
|
||||
batch_op.add_column(sa.Column('is_admin', sa.BOOLEAN(), nullable=True))
|
||||
batch_op.add_column(sa.Column('password_hash', sa.VARCHAR(length=128), nullable=True))
|
||||
batch_op.add_column(sa.Column('bio', sa.TEXT(), nullable=True))
|
||||
batch_op.add_column(sa.Column('last_login', sa.DATETIME(), nullable=True))
|
||||
batch_op.drop_column('role')
|
||||
batch_op.drop_column('is_active')
|
||||
batch_op.drop_column('password')
|
||||
|
||||
# ### end Alembic commands ###
|
||||
308
models.py
Normal file
308
models.py
Normal file
@@ -0,0 +1,308 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_login import UserMixin
|
||||
from datetime import datetime
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
from enum import Enum
|
||||
import uuid as uuid_pkg
|
||||
import os
|
||||
|
||||
db = SQLAlchemy()
|
||||
|
||||
# Beziehungstypen für Gedankenverknüpfungen
|
||||
class RelationType(Enum):
|
||||
SUPPORTS = "stützt"
|
||||
CONTRADICTS = "widerspricht"
|
||||
BUILDS_UPON = "baut auf auf"
|
||||
GENERALIZES = "verallgemeinert"
|
||||
SPECIFIES = "spezifiziert"
|
||||
INSPIRES = "inspiriert"
|
||||
|
||||
# Beziehungstabelle für viele-zu-viele Beziehung zwischen MindMapNodes
|
||||
node_relationship = db.Table('node_relationship',
|
||||
db.Column('parent_id', db.Integer, db.ForeignKey('mind_map_node.id'), primary_key=True),
|
||||
db.Column('child_id', db.Integer, db.ForeignKey('mind_map_node.id'), primary_key=True)
|
||||
)
|
||||
|
||||
# Beziehungstabelle für öffentliche Knoten und Gedanken
|
||||
node_thought_association = db.Table('node_thought_association',
|
||||
db.Column('node_id', db.Integer, db.ForeignKey('mind_map_node.id'), primary_key=True),
|
||||
db.Column('thought_id', db.Integer, db.ForeignKey('thought.id'), primary_key=True)
|
||||
)
|
||||
|
||||
# Beziehungstabelle für Benutzer-spezifische Mindmap-Knoten und Gedanken
|
||||
user_mindmap_thought_association = db.Table('user_mindmap_thought_association',
|
||||
db.Column('user_mindmap_id', db.Integer, db.ForeignKey('user_mindmap.id'), primary_key=True),
|
||||
db.Column('thought_id', db.Integer, db.ForeignKey('thought.id'), primary_key=True)
|
||||
)
|
||||
|
||||
# Beziehungstabelle für Benutzer-Bookmarks von Gedanken
|
||||
user_thought_bookmark = db.Table('user_thought_bookmark',
|
||||
db.Column('user_id', db.Integer, db.ForeignKey('user.id'), primary_key=True),
|
||||
db.Column('thought_id', db.Integer, db.ForeignKey('thought.id'), primary_key=True),
|
||||
db.Column('created_at', db.DateTime, default=datetime.utcnow)
|
||||
)
|
||||
|
||||
class User(db.Model, UserMixin):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
username = db.Column(db.String(80), unique=True, nullable=False)
|
||||
email = db.Column(db.String(120), unique=True, nullable=False)
|
||||
password = db.Column(db.String(512), nullable=False)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
is_active = db.Column(db.Boolean, default=True)
|
||||
role = db.Column(db.String(20), default="user") # 'user', 'admin', 'moderator'
|
||||
|
||||
# Relationships
|
||||
threads = db.relationship('Thread', backref='creator', lazy=True)
|
||||
messages = db.relationship('Message', backref='author', lazy=True)
|
||||
projects = db.relationship('Project', backref='owner', lazy=True)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<User {self.username}>'
|
||||
|
||||
def set_password(self, password):
|
||||
self.password = generate_password_hash(password)
|
||||
|
||||
def check_password(self, password):
|
||||
return check_password_hash(self.password, password)
|
||||
|
||||
class Category(db.Model):
|
||||
"""Wissenschaftliche Kategorien für die Gliederung der öffentlichen Mindmap"""
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(100), unique=True, nullable=False)
|
||||
description = db.Column(db.Text)
|
||||
color_code = db.Column(db.String(7)) # Hex color
|
||||
icon = db.Column(db.String(50))
|
||||
parent_id = db.Column(db.Integer, db.ForeignKey('category.id'), nullable=True)
|
||||
|
||||
# Beziehungen
|
||||
children = db.relationship('Category', backref=db.backref('parent', remote_side=[id]))
|
||||
nodes = db.relationship('MindMapNode', backref='category', lazy=True)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Category {self.name}>'
|
||||
|
||||
class MindMapNode(db.Model):
|
||||
"""Öffentliche Mindmap-Knoten, die für alle Benutzer sichtbar sind"""
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(100), nullable=False)
|
||||
description = db.Column(db.Text)
|
||||
color_code = db.Column(db.String(7))
|
||||
icon = db.Column(db.String(50))
|
||||
is_public = db.Column(db.Boolean, default=True)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
created_by_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True)
|
||||
category_id = db.Column(db.Integer, db.ForeignKey('category.id'), nullable=True)
|
||||
|
||||
__table_args__ = {'extend_existing': True}
|
||||
|
||||
# Beziehungen für Baumstruktur (mehrere Eltern möglich)
|
||||
parents = db.relationship(
|
||||
'MindMapNode',
|
||||
secondary=node_relationship,
|
||||
primaryjoin=(node_relationship.c.child_id == id),
|
||||
secondaryjoin=(node_relationship.c.parent_id == id),
|
||||
backref=db.backref('children', lazy='dynamic'),
|
||||
lazy='dynamic'
|
||||
)
|
||||
|
||||
# Beziehungen zu Gedanken
|
||||
thoughts = db.relationship('Thought',
|
||||
secondary=node_thought_association,
|
||||
backref=db.backref('nodes', lazy='dynamic'))
|
||||
|
||||
# Beziehung zum Ersteller
|
||||
created_by = db.relationship('User', backref='created_nodes')
|
||||
|
||||
def __repr__(self):
|
||||
return f'<MindMapNode {self.name}>'
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'name': self.name,
|
||||
'description': self.description,
|
||||
'color_code': self.color_code,
|
||||
'icon': self.icon,
|
||||
'category_id': self.category_id,
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None
|
||||
}
|
||||
|
||||
class UserMindmap(db.Model):
|
||||
"""Benutzerspezifische Mindmap, die vom Benutzer personalisierbar ist"""
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(100), nullable=False)
|
||||
description = db.Column(db.Text)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
last_modified = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
is_private = db.Column(db.Boolean, default=True)
|
||||
|
||||
# Beziehungen zu öffentlichen Knoten
|
||||
public_nodes = db.relationship('MindMapNode',
|
||||
secondary='user_mindmap_node',
|
||||
backref=db.backref('in_user_mindmaps', lazy='dynamic'))
|
||||
|
||||
# Beziehungen zu Gedanken
|
||||
thoughts = db.relationship('Thought',
|
||||
secondary=user_mindmap_thought_association,
|
||||
backref=db.backref('in_user_mindmaps', lazy='dynamic'))
|
||||
|
||||
# Notizen zu dieser Mindmap
|
||||
notes = db.relationship('MindmapNote', backref='mindmap', lazy=True)
|
||||
|
||||
# Beziehungstabelle für benutzerorientierte Mindmaps und öffentliche Knoten
|
||||
class UserMindmapNode(db.Model):
|
||||
"""Speichert die Beziehung zwischen Benutzer-Mindmaps und öffentlichen Knoten inkl. Position"""
|
||||
user_mindmap_id = db.Column(db.Integer, db.ForeignKey('user_mindmap.id'), primary_key=True)
|
||||
node_id = db.Column(db.Integer, db.ForeignKey('mind_map_node.id'), primary_key=True)
|
||||
x_position = db.Column(db.Float, default=0) # Position X auf der Mindmap
|
||||
y_position = db.Column(db.Float, default=0) # Position Y auf der Mindmap
|
||||
scale = db.Column(db.Float, default=1.0) # Größe des Knotens
|
||||
added_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
class MindmapNote(db.Model):
|
||||
"""Private Notizen der Benutzer zu ihrer Mindmap"""
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||
mindmap_id = db.Column(db.Integer, db.ForeignKey('user_mindmap.id'), nullable=False)
|
||||
node_id = db.Column(db.Integer, db.ForeignKey('mind_map_node.id'), nullable=True)
|
||||
thought_id = db.Column(db.Integer, db.ForeignKey('thought.id'), nullable=True)
|
||||
content = db.Column(db.Text, nullable=False)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
last_modified = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
color_code = db.Column(db.String(7), default="#FFF59D") # Farbe der Notiz
|
||||
|
||||
class Thought(db.Model):
|
||||
"""Gedanken und Inhalte, die in der Mindmap verknüpft werden können"""
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
title = db.Column(db.String(200), nullable=False)
|
||||
content = db.Column(db.Text, nullable=False)
|
||||
abstract = db.Column(db.Text)
|
||||
keywords = db.Column(db.String(500))
|
||||
color_code = db.Column(db.String(7)) # Hex color code
|
||||
source_type = db.Column(db.String(50)) # PDF, Markdown, Text etc.
|
||||
branch = db.Column(db.String(100), nullable=False)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
last_modified = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
# Beziehungen
|
||||
comments = db.relationship('Comment', backref='thought', lazy=True, cascade="all, delete-orphan")
|
||||
ratings = db.relationship('ThoughtRating', backref='thought', lazy=True)
|
||||
|
||||
outgoing_relations = db.relationship(
|
||||
'ThoughtRelation',
|
||||
foreign_keys='ThoughtRelation.source_id',
|
||||
backref='source_thought',
|
||||
lazy=True
|
||||
)
|
||||
incoming_relations = db.relationship(
|
||||
'ThoughtRelation',
|
||||
foreign_keys='ThoughtRelation.target_id',
|
||||
backref='target_thought',
|
||||
lazy=True
|
||||
)
|
||||
|
||||
@property
|
||||
def average_rating(self):
|
||||
if not self.ratings:
|
||||
return 0
|
||||
return sum(r.relevance_score for r in self.ratings) / len(self.ratings)
|
||||
|
||||
class ThoughtRelation(db.Model):
|
||||
"""Beziehungen zwischen Gedanken"""
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
source_id = db.Column(db.Integer, db.ForeignKey('thought.id'), nullable=False)
|
||||
target_id = db.Column(db.Integer, db.ForeignKey('thought.id'), nullable=False)
|
||||
relation_type = db.Column(db.Enum(RelationType), nullable=False)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
created_by_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||
|
||||
# Beziehung zum Ersteller
|
||||
created_by = db.relationship('User', backref='created_relations')
|
||||
|
||||
class ThoughtRating(db.Model):
|
||||
"""Bewertungen von Gedanken durch Benutzer"""
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
thought_id = db.Column(db.Integer, db.ForeignKey('thought.id'), nullable=False)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||
relevance_score = db.Column(db.Integer, nullable=False) # 1-5
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
__table_args__ = (
|
||||
db.UniqueConstraint('thought_id', 'user_id', name='unique_thought_rating'),
|
||||
)
|
||||
|
||||
# Beziehung zum Benutzer
|
||||
user = db.relationship('User', backref='ratings')
|
||||
|
||||
class Comment(db.Model):
|
||||
"""Kommentare zu Gedanken"""
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
content = db.Column(db.Text, nullable=False)
|
||||
thought_id = db.Column(db.Integer, db.ForeignKey('thought.id'), nullable=False)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
last_modified = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
# Thread model
|
||||
class Thread(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
title = db.Column(db.String(200), nullable=False)
|
||||
description = db.Column(db.Text)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||
|
||||
# Relationships
|
||||
messages = db.relationship('Message', backref='thread', lazy=True, cascade="all, delete-orphan")
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Thread {self.title}>'
|
||||
|
||||
# Message model
|
||||
class Message(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
content = db.Column(db.Text, nullable=False)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||
thread_id = db.Column(db.Integer, db.ForeignKey('thread.id'), nullable=False)
|
||||
role = db.Column(db.String(20), default="user") # 'user', 'assistant', 'system'
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Message {self.id} by {self.user_id}>'
|
||||
|
||||
# Project model
|
||||
class Project(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
title = db.Column(db.String(150), nullable=False)
|
||||
description = db.Column(db.Text)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||
category_id = db.Column(db.Integer, db.ForeignKey('category.id'))
|
||||
|
||||
# Relationships
|
||||
documents = db.relationship('Document', backref='project', lazy=True, cascade="all, delete-orphan")
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Project {self.title}>'
|
||||
|
||||
# Document model
|
||||
class Document(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
title = db.Column(db.String(150), nullable=False)
|
||||
content = db.Column(db.Text)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
project_id = db.Column(db.Integer, db.ForeignKey('project.id'), nullable=False)
|
||||
filename = db.Column(db.String(150), nullable=True)
|
||||
file_path = db.Column(db.String(300), nullable=True)
|
||||
file_type = db.Column(db.String(50), nullable=True)
|
||||
file_size = db.Column(db.Integer, nullable=True)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Document {self.title}>'
|
||||
17
requirements.txt
Normal file
17
requirements.txt
Normal file
@@ -0,0 +1,17 @@
|
||||
flask==2.2.5
|
||||
flask-login==0.6.2
|
||||
flask-wtf
|
||||
email-validator
|
||||
python-dotenv
|
||||
werkzeug==2.2.3
|
||||
flask-sqlalchemy==3.0.5
|
||||
openai
|
||||
requests==2.31.0
|
||||
gunicorn==21.2.0
|
||||
#pillow==10.0.1
|
||||
pytest==7.4.0
|
||||
pytest-flask==1.2.0
|
||||
Flask-Migrate
|
||||
flask-socketio==5.3.6
|
||||
python-engineio==4.8.2
|
||||
python-socketio==5.11.1
|
||||
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;
|
||||
}
|
||||
203
static/css/assistant.css
Normal file
203
static/css/assistant.css
Normal file
@@ -0,0 +1,203 @@
|
||||
/* ChatGPT Assistent Styles - Verbesserte Version */
|
||||
#chatgpt-assistant {
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
#assistant-chat {
|
||||
transition: max-height 0.4s cubic-bezier(0.25, 0.1, 0.25, 1),
|
||||
opacity 0.3s cubic-bezier(0.25, 0.1, 0.25, 1);
|
||||
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.25);
|
||||
border-radius: 0.75rem;
|
||||
overflow: hidden;
|
||||
max-width: calc(100vw - 2rem);
|
||||
}
|
||||
|
||||
#assistant-toggle {
|
||||
transition: transform 0.3s ease, background-color 0.2s ease;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
z-index: 60;
|
||||
}
|
||||
|
||||
#assistant-toggle:hover {
|
||||
transform: scale(1.1) rotate(10deg);
|
||||
}
|
||||
|
||||
#assistant-history {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(156, 163, 175, 0.5) transparent;
|
||||
}
|
||||
|
||||
#assistant-history::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
#assistant-history::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
#assistant-history::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(156, 163, 175, 0.5);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.dark #assistant-history::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(156, 163, 175, 0.3);
|
||||
}
|
||||
|
||||
/* Verbesserte Message-Bubbles mit Schatten und Animation */
|
||||
#assistant-history .flex > div {
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
|
||||
animation: messageAppear 0.3s ease-out forwards;
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
@keyframes messageAppear {
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Verzögerte Animation für Messages */
|
||||
#assistant-history .flex:nth-child(1) > div { animation-delay: 0.05s; }
|
||||
#assistant-history .flex:nth-child(2) > div { animation-delay: 0.1s; }
|
||||
#assistant-history .flex:nth-child(3) > div { animation-delay: 0.15s; }
|
||||
#assistant-history .flex:nth-child(4) > div { animation-delay: 0.2s; }
|
||||
#assistant-history .flex:nth-child(5) > div { animation-delay: 0.25s; }
|
||||
|
||||
/* Vorschläge styling */
|
||||
#assistant-suggestions {
|
||||
padding: 0.5rem 0.75rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.suggestion-pill {
|
||||
animation: pillAppear 0.4s ease forwards;
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
@keyframes pillAppear {
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Styling für verschiedene Verzögerungen bei Vorschlägen */
|
||||
#assistant-suggestions button:nth-child(1) { animation-delay: 0.1s; }
|
||||
#assistant-suggestions button:nth-child(2) { animation-delay: 0.2s; }
|
||||
#assistant-suggestions button:nth-child(3) { animation-delay: 0.3s; }
|
||||
|
||||
/* Mach Platz für Notifications, damit sie nicht mit dem Assistenten überlappen */
|
||||
.notification-area {
|
||||
bottom: 5rem;
|
||||
}
|
||||
|
||||
/* Verbesserte Glassmorphism-Effekt */
|
||||
.glass-morphism {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.15);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.dark .glass-morphism {
|
||||
background: rgba(15, 23, 42, 0.35);
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
/* Verbesserte Farbpalette für Dark Theme */
|
||||
.dark {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgba(10, 15, 25, var(--tw-bg-opacity)) !important;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.dark .bg-dark-900 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgba(10, 15, 25, var(--tw-bg-opacity)) !important;
|
||||
}
|
||||
|
||||
.dark .bg-dark-800 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgba(15, 23, 42, var(--tw-bg-opacity)) !important;
|
||||
}
|
||||
|
||||
.dark .bg-dark-700 {
|
||||
--tw-bg-opacity: 1;
|
||||
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 */
|
||||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
footer {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
526
static/css/base-styles.css
Normal file
526
static/css/base-styles.css
Normal file
@@ -0,0 +1,526 @@
|
||||
/* Base Styles - Dark, Mystical Theme */
|
||||
|
||||
/* Global Variables */
|
||||
:root {
|
||||
--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;
|
||||
|
||||
/* Light mode optimierte Farben */
|
||||
--light-bg: #f9fafb;
|
||||
--light-text: #1e293b;
|
||||
--light-heading: #0f172a;
|
||||
--light-primary: #3b82f6;
|
||||
--light-primary-hover: #4f46e5;
|
||||
--light-secondary: #6b7280;
|
||||
--light-border: #e5e7eb;
|
||||
--light-card-bg: rgba(255, 255, 255, 0.92);
|
||||
--light-navbar-bg: rgba(255, 255, 255, 0.92);
|
||||
--light-input-bg: #ffffff;
|
||||
--light-input-border: #d1d5db;
|
||||
--light-input-focus: #3b82f6;
|
||||
--light-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
/* Base Styles */
|
||||
html {
|
||||
font-family: var(--font-sans);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
transition: background-color var(--transition-normal), color var(--transition-normal);
|
||||
overflow-x: hidden;
|
||||
min-height: 100vh;
|
||||
font-family: var(--font-sans);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Dark Mode */
|
||||
html.dark body {
|
||||
background-color: var(--bg-primary-dark);
|
||||
color: var(--text-primary-dark);
|
||||
}
|
||||
|
||||
/* Light Mode */
|
||||
body:not(.dark) {
|
||||
background-color: var(--light-bg);
|
||||
color: var(--light-text);
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.hero-heading {
|
||||
font-size: 2.75rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.section-heading {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.gradient-text {
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
color: transparent;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
html.dark .gradient-text {
|
||||
background-image: linear-gradient(135deg, var(--accent-primary-dark), var(--accent-secondary-dark));
|
||||
text-shadow: var(--glow-dark);
|
||||
}
|
||||
|
||||
.gradient-text {
|
||||
background-image: linear-gradient(135deg, var(--accent-primary-light), var(--accent-secondary-light));
|
||||
text-shadow: var(--glow-light);
|
||||
}
|
||||
|
||||
/* Mystical elements */
|
||||
.mystical-border {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.mystical-border::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
border: 1px solid;
|
||||
border-radius: inherit;
|
||||
pointer-events: none;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
html.dark .mystical-border::before {
|
||||
border-color: var(--accent-primary-dark);
|
||||
box-shadow: var(--glow-dark);
|
||||
}
|
||||
|
||||
.mystical-border::before {
|
||||
border-color: var(--accent-primary-light);
|
||||
box-shadow: var(--glow-light);
|
||||
}
|
||||
|
||||
/* Navigation Links */
|
||||
.nav-link {
|
||||
position: relative;
|
||||
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 {
|
||||
color: var(--text-primary-light);
|
||||
background-color: rgba(241, 245, 249, 0.5);
|
||||
}
|
||||
|
||||
html.dark .nav-link-active {
|
||||
color: var(--accent-tertiary-dark);
|
||||
background-color: rgba(109, 40, 217, 0.15);
|
||||
}
|
||||
|
||||
.nav-link-light-active {
|
||||
color: var(--accent-primary-light);
|
||||
background-color: rgba(139, 92, 246, 0.1);
|
||||
}
|
||||
|
||||
/* Glass Morphism Effects */
|
||||
.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);
|
||||
}
|
||||
|
||||
.glass-navbar-light {
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border-color: rgba(226, 232, 240, 0.5);
|
||||
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.glass-morphism {
|
||||
transition: background-color var(--transition-normal), backdrop-filter var(--transition-normal);
|
||||
}
|
||||
|
||||
/* Cards */
|
||||
.mystical-card {
|
||||
border-radius: 0.75rem;
|
||||
overflow: hidden;
|
||||
transition: var(--transition-normal);
|
||||
}
|
||||
|
||||
html.dark .mystical-card {
|
||||
background-color: var(--bg-secondary-dark);
|
||||
border: 1px solid var(--border-dark);
|
||||
box-shadow: var(--shadow-dark);
|
||||
}
|
||||
|
||||
.mystical-card {
|
||||
background-color: var(--bg-secondary-light);
|
||||
border: 1px solid var(--border-light);
|
||||
box-shadow: var(--shadow-light);
|
||||
}
|
||||
|
||||
html.dark .mystical-card:hover {
|
||||
box-shadow: var(--glow-dark), var(--shadow-dark);
|
||||
border-color: var(--accent-primary-dark);
|
||||
}
|
||||
|
||||
.mystical-card:hover {
|
||||
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;
|
||||
transition: var(--transition-normal);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mystical-button::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, transparent, rgba(255, 255, 255, 0.05), transparent);
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.8s ease-in-out;
|
||||
}
|
||||
|
||||
.mystical-button:hover::before {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
html.dark .mystical-button-primary {
|
||||
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); }
|
||||
50% { transform: translateY(-5px); }
|
||||
100% { transform: translateY(0); }
|
||||
}
|
||||
|
||||
.animate-float {
|
||||
animation: floatAnimation 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 0.5s ease-out forwards;
|
||||
}
|
||||
|
||||
/* Scroll Bars */
|
||||
::-webkit-scrollbar {
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
}
|
||||
|
||||
html.dark ::-webkit-scrollbar-track {
|
||||
background: var(--bg-secondary-dark);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--bg-secondary-light);
|
||||
}
|
||||
|
||||
html.dark ::-webkit-scrollbar-thumb {
|
||||
background: var(--accent-primary-dark);
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--accent-primary-light);
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
html.dark ::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--accent-secondary-dark);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--accent-secondary-light);
|
||||
}
|
||||
|
||||
/* Responsive Utilities */
|
||||
@media (max-width: 640px) {
|
||||
.hero-heading {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.section-heading {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Additional background elements */
|
||||
.mystical-dot {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
opacity: 0.15;
|
||||
filter: blur(3px);
|
||||
z-index: -1;
|
||||
transition: opacity var(--transition-normal);
|
||||
}
|
||||
|
||||
html.dark .mystical-dot {
|
||||
background-color: var(--accent-primary-dark);
|
||||
box-shadow: 0 0 15px var(--accent-primary-dark);
|
||||
}
|
||||
|
||||
.mystical-dot {
|
||||
background-color: var(--accent-primary-light);
|
||||
box-shadow: 0 0 15px var(--accent-primary-light);
|
||||
}
|
||||
|
||||
/* Accessibility */
|
||||
.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);
|
||||
}
|
||||
|
||||
/* Light Mode Überschriften */
|
||||
body:not(.dark) h1,
|
||||
body:not(.dark) h2,
|
||||
body:not(.dark) h3,
|
||||
body:not(.dark) h4,
|
||||
body:not(.dark) h5,
|
||||
body:not(.dark) h6 {
|
||||
color: var(--light-heading);
|
||||
}
|
||||
|
||||
/* Light Mode Links */
|
||||
body:not(.dark) a {
|
||||
color: var(--light-primary);
|
||||
}
|
||||
|
||||
body:not(.dark) a:hover {
|
||||
color: var(--light-primary-hover);
|
||||
}
|
||||
|
||||
/* Light Mode Buttons */
|
||||
body:not(.dark) .btn,
|
||||
body:not(.dark) button:not(.toggle) {
|
||||
background-color: var(--light-primary);
|
||||
color: white;
|
||||
border: none;
|
||||
box-shadow: var(--light-shadow);
|
||||
border-radius: 0.375rem;
|
||||
padding: 0.5rem 1rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
body:not(.dark) .btn:hover,
|
||||
body:not(.dark) button:not(.toggle):hover {
|
||||
background-color: var(--light-primary-hover);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Light Mode Cards und Panels */
|
||||
body:not(.dark) .card,
|
||||
body:not(.dark) .panel {
|
||||
background-color: var(--light-card-bg);
|
||||
border: 1px solid var(--light-border);
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: var(--light-shadow);
|
||||
}
|
||||
|
||||
/* Light Mode Tabelle */
|
||||
body:not(.dark) table {
|
||||
background-color: var(--light-card-bg);
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
body:not(.dark) th {
|
||||
background-color: var(--light-bg);
|
||||
color: var(--light-heading);
|
||||
border-bottom: 1px solid var(--light-border);
|
||||
}
|
||||
|
||||
body:not(.dark) td {
|
||||
border-bottom: 1px solid var(--light-border);
|
||||
}
|
||||
|
||||
/* Light Mode Inputs */
|
||||
body:not(.dark) input,
|
||||
body:not(.dark) textarea,
|
||||
body:not(.dark) select {
|
||||
background-color: var(--light-input-bg);
|
||||
border: 1px solid var(--light-input-border);
|
||||
color: var(--light-text);
|
||||
border-radius: 0.375rem;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
body:not(.dark) input:focus,
|
||||
body:not(.dark) textarea:focus,
|
||||
body:not(.dark) select:focus {
|
||||
border-color: var(--light-input-focus);
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Navbar im Light Mode verbessern */
|
||||
body:not(.dark) nav,
|
||||
body:not(.dark) .navbar {
|
||||
background-color: var(--light-navbar-bg);
|
||||
box-shadow: var(--light-shadow);
|
||||
border-bottom: 1px solid var(--light-border);
|
||||
}
|
||||
3884
static/css/main.css
Normal file
3884
static/css/main.css
Normal file
File diff suppressed because it is too large
Load Diff
106
static/css/neural-network-background.css
Normal file
106
static/css/neural-network-background.css
Normal file
@@ -0,0 +1,106 @@
|
||||
/* Neural Network Background CSS */
|
||||
|
||||
/* Make sure the neural network background is always visible */
|
||||
#neural-network-background {
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
z-index: -10 !important; /* Below content but above regular background */
|
||||
pointer-events: none !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
/* Override any solid background colors for the body */
|
||||
body, body.dark {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
/* Make sure any background color is removed */
|
||||
html.dark, html {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
/* Make sure any fixed backgrounds are removed */
|
||||
#app-container {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
/* Ensure content is properly visible over the background */
|
||||
.glass-morphism {
|
||||
background-color: rgba(17, 24, 39, 0.6) !important;
|
||||
backdrop-filter: blur(5px) !important;
|
||||
}
|
||||
|
||||
/* Dark Mode - Navbar */
|
||||
body.dark .glass-navbar-dark {
|
||||
background-color: rgba(10, 14, 25, 0.7) !important;
|
||||
}
|
||||
|
||||
/* Light Mode - Verbesserter Navbar */
|
||||
body .glass-navbar-light {
|
||||
background-color: rgba(255, 255, 255, 0.92) !important;
|
||||
backdrop-filter: blur(10px) !important;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06) !important;
|
||||
border-bottom: 1px solid rgba(220, 220, 220, 0.5) !important;
|
||||
}
|
||||
|
||||
/* Light Mode - Verbesserte Lesbarkeit für Navbar-Elemente */
|
||||
body:not(.dark) .navbar-link,
|
||||
body:not(.dark) .navbar-item {
|
||||
color: #1e3a8a !important; /* Dunkles Blau für bessere Lesbarkeit */
|
||||
}
|
||||
|
||||
body:not(.dark) .navbar-link:hover,
|
||||
body:not(.dark) .navbar-item:hover {
|
||||
color: #4f46e5 !important; /* Helles Lila beim Hover */
|
||||
background-color: rgba(240, 245, 255, 0.9) !important;
|
||||
}
|
||||
|
||||
/* Light Mode - Buttons verbessert */
|
||||
body:not(.dark) .btn,
|
||||
body:not(.dark) button {
|
||||
background-color: #3b82f6 !important; /* Klares Blau statt Grau */
|
||||
color: white !important;
|
||||
border: none !important;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1) !important;
|
||||
}
|
||||
|
||||
body:not(.dark) .btn:hover,
|
||||
body:not(.dark) button:hover {
|
||||
background-color: #4f46e5 !important; /* Lila beim Hover */
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.12) !important;
|
||||
}
|
||||
|
||||
/* Verbesserte Karten im Light Mode */
|
||||
body:not(.dark) .card,
|
||||
body:not(.dark) .panel {
|
||||
background-color: rgba(255, 255, 255, 0.92) !important;
|
||||
border: 1px solid rgba(220, 220, 220, 0.8) !important;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05) !important;
|
||||
}
|
||||
|
||||
/* Verbesserte Lesbarkeit für Text im Light Mode */
|
||||
body:not(.dark) {
|
||||
color: #1e293b !important; /* Dunkles Blau-Grau statt Schwarz */
|
||||
}
|
||||
|
||||
body:not(.dark) h1,
|
||||
body:not(.dark) h2,
|
||||
body:not(.dark) h3,
|
||||
body:not(.dark) h4,
|
||||
body:not(.dark) h5,
|
||||
body:not(.dark) h6 {
|
||||
color: #0f172a !important; /* Fast schwarz für Überschriften */
|
||||
}
|
||||
|
||||
/* Make sure footer has proper transparency and styling */
|
||||
body.dark footer {
|
||||
background-color: rgba(10, 14, 25, 0.7) !important;
|
||||
}
|
||||
|
||||
body:not(.dark) footer {
|
||||
background-color: rgba(249, 250, 251, 0.92) !important;
|
||||
border-top: 1px solid rgba(220, 220, 220, 0.8) !important;
|
||||
}
|
||||
207
static/css/src/input.css
Normal file
207
static/css/src/input.css
Normal file
@@ -0,0 +1,207 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
html {
|
||||
@apply scroll-smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-gray-50 text-gray-800 dark:bg-dark-800 dark:text-gray-100 font-sans;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
@apply font-bold text-gray-900 dark:text-white;
|
||||
}
|
||||
|
||||
h1 {
|
||||
@apply text-4xl md:text-5xl lg:text-6xl;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@apply text-3xl md:text-4xl;
|
||||
}
|
||||
|
||||
h3 {
|
||||
@apply text-2xl md:text-3xl;
|
||||
}
|
||||
|
||||
h4 {
|
||||
@apply text-xl md:text-2xl;
|
||||
}
|
||||
|
||||
a {
|
||||
@apply text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300 transition-colors;
|
||||
}
|
||||
|
||||
input, textarea, select {
|
||||
@apply bg-white border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500 dark:bg-dark-700 dark:border-dark-500 dark:focus:ring-primary-400 dark:focus:border-primary-400;
|
||||
}
|
||||
|
||||
label {
|
||||
@apply block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1;
|
||||
}
|
||||
|
||||
::placeholder {
|
||||
@apply text-gray-400 dark:text-gray-500;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.btn {
|
||||
@apply inline-flex items-center justify-center px-4 py-2 font-medium rounded-md transition-all focus:outline-none focus:ring-2 focus:ring-offset-2 shadow-sm text-base;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
@apply btn bg-primary-600 hover:bg-primary-700 text-white focus:ring-primary-500 hover:shadow-md active:translate-y-0.5 dark:bg-primary-700 dark:hover:bg-primary-600;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
@apply btn bg-secondary-600 hover:bg-secondary-700 text-white focus:ring-secondary-500 hover:shadow-md active:translate-y-0.5 dark:bg-secondary-700 dark:hover:bg-secondary-600;
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
@apply btn border-2 border-gray-300 dark:border-dark-500 bg-white dark:bg-transparent text-gray-700 dark:text-gray-200 hover:bg-gray-50 hover:border-primary-500 hover:text-primary-600 dark:hover:bg-dark-700 dark:hover:border-primary-400 dark:hover:text-primary-400 focus:ring-gray-500 active:translate-y-0.5;
|
||||
}
|
||||
|
||||
.card {
|
||||
@apply bg-white shadow-md dark:bg-dark-700 rounded-lg overflow-hidden border border-gray-200 dark:border-dark-600 hover:shadow-lg transition-shadow duration-300;
|
||||
}
|
||||
|
||||
.gradient-text {
|
||||
@apply text-transparent bg-clip-text bg-gradient-to-r from-primary-600 to-secondary-600 dark:from-primary-500 dark:to-secondary-500 font-bold;
|
||||
}
|
||||
|
||||
.node-tooltip {
|
||||
@apply max-w-xs p-3 bg-white text-gray-800 dark:bg-dark-800 dark:text-white rounded-lg shadow-lg text-sm z-50 border border-gray-200 dark:border-dark-600;
|
||||
}
|
||||
|
||||
.mindmap-node {
|
||||
@apply cursor-pointer transition-all duration-200 hover:shadow-lg border-2 border-gray-200 dark:border-dark-600;
|
||||
}
|
||||
|
||||
/* Mindmap-spezifische Stile */
|
||||
.mindmap-container {
|
||||
@apply bg-gray-50/80 dark:bg-dark-800/80 rounded-lg p-4 shadow-inner;
|
||||
}
|
||||
|
||||
.mindmap-node-root {
|
||||
@apply bg-primary-100 dark:bg-primary-900 text-primary-900 dark:text-primary-100 border-primary-300 dark:border-primary-700;
|
||||
}
|
||||
|
||||
.mindmap-node-branch {
|
||||
@apply bg-secondary-100 dark:bg-secondary-900 text-secondary-900 dark:text-secondary-100 border-secondary-300 dark:border-secondary-700;
|
||||
}
|
||||
|
||||
.mindmap-node-leaf {
|
||||
@apply bg-gray-100 dark:bg-dark-700 text-gray-800 dark:text-gray-200 border-gray-300 dark:border-dark-500;
|
||||
}
|
||||
|
||||
.mindmap-link {
|
||||
@apply stroke-gray-400 dark:stroke-gray-500 stroke-[2];
|
||||
}
|
||||
|
||||
.mindmap-link-active {
|
||||
@apply stroke-primary-500 dark:stroke-primary-400 stroke-[3];
|
||||
}
|
||||
|
||||
.form-group {
|
||||
@apply mb-4;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
@apply w-full;
|
||||
}
|
||||
|
||||
.ascii-art {
|
||||
@apply font-mono text-xs leading-none whitespace-pre tracking-tight select-none text-primary-700 dark:text-primary-400 opacity-80 dark:opacity-60 font-bold;
|
||||
}
|
||||
|
||||
/* Verbesserte Formulareingabefelder */
|
||||
.form-input,
|
||||
.form-textarea,
|
||||
.form-select {
|
||||
@apply w-full rounded-md border border-gray-300 bg-white px-4 py-2 text-gray-800 shadow-sm
|
||||
focus:border-primary-500 focus:ring focus:ring-primary-500 focus:ring-opacity-50
|
||||
dark:border-dark-500 dark:bg-dark-700 dark:text-gray-100 dark:focus:border-primary-400 dark:focus:ring-primary-400;
|
||||
}
|
||||
|
||||
.form-input-lg {
|
||||
@apply py-3 text-lg;
|
||||
}
|
||||
|
||||
.form-input-sm {
|
||||
@apply py-1 text-sm;
|
||||
}
|
||||
|
||||
.form-checkbox,
|
||||
.form-radio {
|
||||
@apply h-5 w-5 rounded border-gray-300 text-primary-600 shadow-sm
|
||||
focus:border-primary-500 focus:ring focus:ring-primary-500 focus:ring-opacity-50
|
||||
dark:border-dark-500 dark:bg-dark-700 dark:focus:border-primary-400 dark:focus:ring-primary-400;
|
||||
}
|
||||
|
||||
.form-checkbox {
|
||||
@apply rounded;
|
||||
}
|
||||
|
||||
.form-radio {
|
||||
@apply rounded-full;
|
||||
}
|
||||
|
||||
.form-error {
|
||||
@apply mt-1 text-sm text-red-600 dark:text-red-400;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.tech-gradient {
|
||||
@apply bg-gradient-to-r from-primary-600 to-secondary-500;
|
||||
}
|
||||
|
||||
.glass-effect {
|
||||
@apply bg-white/95 backdrop-blur-md border border-gray-200 shadow-md dark:bg-dark-800/90 dark:border-dark-700/50 dark:shadow-xl;
|
||||
}
|
||||
|
||||
.focus-ring {
|
||||
@apply focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 dark:focus:ring-primary-400 dark:focus:ring-offset-dark-800;
|
||||
}
|
||||
|
||||
.input-focus {
|
||||
@apply focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500 dark:focus:ring-primary-400 dark:focus:border-primary-400;
|
||||
}
|
||||
}
|
||||
|
||||
/* Form validation styles */
|
||||
.is-valid {
|
||||
@apply border-green-500 dark:border-green-400;
|
||||
}
|
||||
|
||||
.is-invalid {
|
||||
@apply border-red-500 dark:border-red-400;
|
||||
}
|
||||
|
||||
.is-valid:focus {
|
||||
@apply ring-green-500/30 border-green-500 dark:ring-green-400/30 dark:border-green-400;
|
||||
}
|
||||
|
||||
.is-invalid:focus {
|
||||
@apply ring-red-500/30 border-red-500 dark:ring-red-400/30 dark:border-red-400;
|
||||
}
|
||||
|
||||
.form-hint {
|
||||
@apply text-xs text-gray-500 dark:text-gray-400 mt-1;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
@apply block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1;
|
||||
}
|
||||
|
||||
.input-icon {
|
||||
@apply absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-gray-500 dark:text-gray-400;
|
||||
}
|
||||
|
||||
.input-with-icon {
|
||||
@apply pl-10;
|
||||
}
|
||||
1644
static/css/style.css
Normal file
1644
static/css/style.css
Normal file
File diff suppressed because it is too large
Load Diff
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');
|
||||
526
static/d3-extensions.js
vendored
Normal file
526
static/d3-extensions.js
vendored
Normal file
@@ -0,0 +1,526 @@
|
||||
/**
|
||||
* D3.js Erweiterungen für verbesserte Mindmap-Funktionalität
|
||||
* Diese Datei enthält zusätzliche Hilfsfunktionen und Erweiterungen für D3.js
|
||||
*/
|
||||
|
||||
class D3Extensions {
|
||||
/**
|
||||
* Erstellt einen verbesserten radialen Farbverlauf
|
||||
* @param {Object} defs - Das D3 defs Element
|
||||
* @param {string} id - ID für den Gradienten
|
||||
* @param {string} baseColor - Grundfarbe in hexadezimal oder RGB
|
||||
* @returns {Object} - Das erstellte Gradient-Element
|
||||
*/
|
||||
static createEnhancedRadialGradient(defs, id, baseColor) {
|
||||
// Farben berechnen
|
||||
const d3Color = d3.color(baseColor);
|
||||
const lightColor = d3Color.brighter(0.7);
|
||||
const darkColor = d3Color.darker(0.3);
|
||||
const midColor = d3Color;
|
||||
|
||||
// Gradient erstellen
|
||||
const gradient = defs.append('radialGradient')
|
||||
.attr('id', id)
|
||||
.attr('cx', '30%')
|
||||
.attr('cy', '30%')
|
||||
.attr('r', '70%');
|
||||
|
||||
// Farbstops hinzufügen für realistischeren Verlauf
|
||||
gradient.append('stop')
|
||||
.attr('offset', '0%')
|
||||
.attr('stop-color', lightColor.formatHex());
|
||||
|
||||
gradient.append('stop')
|
||||
.attr('offset', '50%')
|
||||
.attr('stop-color', midColor.formatHex());
|
||||
|
||||
gradient.append('stop')
|
||||
.attr('offset', '100%')
|
||||
.attr('stop-color', darkColor.formatHex());
|
||||
|
||||
return gradient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt einen Glüheffekt-Filter
|
||||
* @param {Object} defs - D3-Referenz auf den defs-Bereich
|
||||
* @param {String} id - ID des Filters
|
||||
* @param {String} color - Farbe des Glüheffekts (Hex-Code)
|
||||
* @param {Number} strength - Stärke des Glüheffekts
|
||||
* @returns {Object} D3-Referenz auf den erstellten Filter
|
||||
*/
|
||||
static createGlowFilter(defs, id, color = '#b38fff', strength = 5) {
|
||||
const filter = defs.append('filter')
|
||||
.attr('id', id)
|
||||
.attr('x', '-50%')
|
||||
.attr('y', '-50%')
|
||||
.attr('width', '200%')
|
||||
.attr('height', '200%');
|
||||
|
||||
// Unschärfe-Effekt
|
||||
filter.append('feGaussianBlur')
|
||||
.attr('in', 'SourceGraphic')
|
||||
.attr('stdDeviation', strength)
|
||||
.attr('result', 'blur');
|
||||
|
||||
// Farbverstärkung für den Glüheffekt
|
||||
filter.append('feColorMatrix')
|
||||
.attr('in', 'blur')
|
||||
.attr('type', 'matrix')
|
||||
.attr('values', '0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 18 -7')
|
||||
.attr('result', 'glow');
|
||||
|
||||
// Farbflut mit der angegebenen Farbe
|
||||
filter.append('feFlood')
|
||||
.attr('flood-color', color)
|
||||
.attr('flood-opacity', '0.7')
|
||||
.attr('result', 'color');
|
||||
|
||||
// Zusammensetzen des Glüheffekts mit der Farbe
|
||||
filter.append('feComposite')
|
||||
.attr('in', 'color')
|
||||
.attr('in2', 'glow')
|
||||
.attr('operator', 'in')
|
||||
.attr('result', 'glow-color');
|
||||
|
||||
// Zusammenfügen aller Ebenen
|
||||
const feMerge = filter.append('feMerge');
|
||||
feMerge.append('feMergeNode')
|
||||
.attr('in', 'glow-color');
|
||||
feMerge.append('feMergeNode')
|
||||
.attr('in', 'SourceGraphic');
|
||||
|
||||
return filter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Berechnet eine konsistente Farbe aus einem String
|
||||
* @param {string} str - Eingabestring
|
||||
* @returns {string} - Generierte Farbe als Hex-String
|
||||
*/
|
||||
static stringToColor(str) {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
||||
}
|
||||
|
||||
// Basis-Farbpalette für konsistente Farben
|
||||
const colorPalette = [
|
||||
"#4299E1", // Blau
|
||||
"#9F7AEA", // Lila
|
||||
"#ED64A6", // Pink
|
||||
"#48BB78", // Grün
|
||||
"#ECC94B", // Gelb
|
||||
"#F56565", // Rot
|
||||
"#38B2AC", // Türkis
|
||||
"#ED8936", // Orange
|
||||
"#667EEA", // Indigo
|
||||
];
|
||||
|
||||
// Farbe aus der Palette wählen basierend auf dem Hash
|
||||
const colorIndex = Math.abs(hash) % colorPalette.length;
|
||||
return colorPalette[colorIndex];
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt einen Schatteneffekt-Filter
|
||||
* @param {Object} defs - D3-Referenz auf den defs-Bereich
|
||||
* @param {String} id - ID des Filters
|
||||
* @returns {Object} D3-Referenz auf den erstellten Filter
|
||||
*/
|
||||
static createShadowFilter(defs, id) {
|
||||
const filter = defs.append('filter')
|
||||
.attr('id', id)
|
||||
.attr('x', '-50%')
|
||||
.attr('y', '-50%')
|
||||
.attr('width', '200%')
|
||||
.attr('height', '200%');
|
||||
|
||||
// Einfacher Schlagschatten
|
||||
filter.append('feDropShadow')
|
||||
.attr('dx', 0)
|
||||
.attr('dy', 4)
|
||||
.attr('stdDeviation', 4)
|
||||
.attr('flood-color', 'rgba(0, 0, 0, 0.3)');
|
||||
|
||||
return filter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt einen Glasmorphismus-Effekt-Filter
|
||||
* @param {Object} defs - D3-Referenz auf den defs-Bereich
|
||||
* @param {String} id - ID des Filters
|
||||
* @returns {Object} D3-Referenz auf den erstellten Filter
|
||||
*/
|
||||
static createGlassMorphismFilter(defs, id) {
|
||||
const filter = defs.append('filter')
|
||||
.attr('id', id)
|
||||
.attr('x', '-50%')
|
||||
.attr('y', '-50%')
|
||||
.attr('width', '200%')
|
||||
.attr('height', '200%');
|
||||
|
||||
// Hintergrund-Unschärfe für den Glaseffekt
|
||||
filter.append('feGaussianBlur')
|
||||
.attr('in', 'SourceGraphic')
|
||||
.attr('stdDeviation', 8)
|
||||
.attr('result', 'blur');
|
||||
|
||||
// Hellere Farbe für den Glaseffekt
|
||||
filter.append('feColorMatrix')
|
||||
.attr('in', 'blur')
|
||||
.attr('type', 'matrix')
|
||||
.attr('values', '1 0 0 0 0.1 0 1 0 0 0.1 0 0 1 0 0.1 0 0 0 0.6 0')
|
||||
.attr('result', 'glass');
|
||||
|
||||
// Überlagerung mit dem Original
|
||||
const feMerge = filter.append('feMerge');
|
||||
feMerge.append('feMergeNode')
|
||||
.attr('in', 'glass');
|
||||
feMerge.append('feMergeNode')
|
||||
.attr('in', 'SourceGraphic');
|
||||
|
||||
return filter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt einen verstärkten Glasmorphismus-Effekt mit Farbverlauf
|
||||
* @param {Object} defs - D3-Referenz auf den defs-Bereich
|
||||
* @param {String} id - ID des Filters
|
||||
* @param {String} color1 - Erste Farbe des Verlaufs (Hex-Code)
|
||||
* @param {String} color2 - Zweite Farbe des Verlaufs (Hex-Code)
|
||||
* @returns {Object} D3-Referenz auf den erstellten Filter
|
||||
*/
|
||||
static createEnhancedGlassMorphismFilter(defs, id, color1 = '#b38fff', color2 = '#58a9ff') {
|
||||
// Farbverlauf für den Glaseffekt definieren
|
||||
const gradientId = `gradient-${id}`;
|
||||
const gradient = defs.append('linearGradient')
|
||||
.attr('id', gradientId)
|
||||
.attr('x1', '0%')
|
||||
.attr('y1', '0%')
|
||||
.attr('x2', '100%')
|
||||
.attr('y2', '100%');
|
||||
|
||||
gradient.append('stop')
|
||||
.attr('offset', '0%')
|
||||
.attr('stop-color', color1)
|
||||
.attr('stop-opacity', '0.3');
|
||||
|
||||
gradient.append('stop')
|
||||
.attr('offset', '100%')
|
||||
.attr('stop-color', color2)
|
||||
.attr('stop-opacity', '0.3');
|
||||
|
||||
// Filter erstellen
|
||||
const filter = defs.append('filter')
|
||||
.attr('id', id)
|
||||
.attr('x', '-50%')
|
||||
.attr('y', '-50%')
|
||||
.attr('width', '200%')
|
||||
.attr('height', '200%');
|
||||
|
||||
// Hintergrund-Unschärfe
|
||||
filter.append('feGaussianBlur')
|
||||
.attr('in', 'SourceGraphic')
|
||||
.attr('stdDeviation', 6)
|
||||
.attr('result', 'blur');
|
||||
|
||||
// Farbverlauf einfügen
|
||||
const feImage = filter.append('feImage')
|
||||
.attr('xlink:href', `#${gradientId}`)
|
||||
.attr('result', 'gradient')
|
||||
.attr('x', '0%')
|
||||
.attr('y', '0%')
|
||||
.attr('width', '100%')
|
||||
.attr('height', '100%')
|
||||
.attr('preserveAspectRatio', 'none');
|
||||
|
||||
// Zusammenfügen aller Ebenen
|
||||
const feMerge = filter.append('feMerge');
|
||||
feMerge.append('feMergeNode')
|
||||
.attr('in', 'blur');
|
||||
feMerge.append('feMergeNode')
|
||||
.attr('in', 'gradient');
|
||||
feMerge.append('feMergeNode')
|
||||
.attr('in', 'SourceGraphic');
|
||||
|
||||
return filter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt einen 3D-Glaseffekt mit verbesserter Tiefe und Reflexionen
|
||||
* @param {Object} defs - D3-Referenz auf den defs-Bereich
|
||||
* @param {String} id - ID des Filters
|
||||
* @returns {Object} D3-Referenz auf den erstellten Filter
|
||||
*/
|
||||
static create3DGlassEffect(defs, id) {
|
||||
const filter = defs.append('filter')
|
||||
.attr('id', id)
|
||||
.attr('x', '-50%')
|
||||
.attr('y', '-50%')
|
||||
.attr('width', '200%')
|
||||
.attr('height', '200%');
|
||||
|
||||
// Farbmatrix für Transparenz
|
||||
filter.append('feColorMatrix')
|
||||
.attr('type', 'matrix')
|
||||
.attr('in', 'SourceGraphic')
|
||||
.attr('values', '1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0.7 0')
|
||||
.attr('result', 'transparent');
|
||||
|
||||
// Hintergrund-Unschärfe für Tiefe
|
||||
filter.append('feGaussianBlur')
|
||||
.attr('in', 'transparent')
|
||||
.attr('stdDeviation', '4')
|
||||
.attr('result', 'blurred');
|
||||
|
||||
// Lichtquelle und Schattierung hinzufügen
|
||||
const lightSource = filter.append('feSpecularLighting')
|
||||
.attr('in', 'blurred')
|
||||
.attr('surfaceScale', '6')
|
||||
.attr('specularConstant', '1')
|
||||
.attr('specularExponent', '30')
|
||||
.attr('lighting-color', '#ffffff')
|
||||
.attr('result', 'specular');
|
||||
|
||||
lightSource.append('fePointLight')
|
||||
.attr('x', '100')
|
||||
.attr('y', '100')
|
||||
.attr('z', '200');
|
||||
|
||||
// Lichtreflexion verstärken
|
||||
filter.append('feComposite')
|
||||
.attr('in', 'specular')
|
||||
.attr('in2', 'SourceGraphic')
|
||||
.attr('operator', 'in')
|
||||
.attr('result', 'specularHighlight');
|
||||
|
||||
// Inneren Schatten erzeugen
|
||||
const innerShadow = filter.append('feOffset')
|
||||
.attr('in', 'SourceAlpha')
|
||||
.attr('dx', '0')
|
||||
.attr('dy', '1')
|
||||
.attr('result', 'offsetblur');
|
||||
|
||||
innerShadow.append('feGaussianBlur')
|
||||
.attr('in', 'offsetblur')
|
||||
.attr('stdDeviation', '2')
|
||||
.attr('result', 'innerShadow');
|
||||
|
||||
filter.append('feComposite')
|
||||
.attr('in', 'innerShadow')
|
||||
.attr('in2', 'SourceGraphic')
|
||||
.attr('operator', 'out')
|
||||
.attr('result', 'innerShadowEffect');
|
||||
|
||||
// Schichten kombinieren
|
||||
const feMerge = filter.append('feMerge');
|
||||
feMerge.append('feMergeNode')
|
||||
.attr('in', 'blurred');
|
||||
feMerge.append('feMergeNode')
|
||||
.attr('in', 'innerShadowEffect');
|
||||
feMerge.append('feMergeNode')
|
||||
.attr('in', 'specularHighlight');
|
||||
feMerge.append('feMergeNode')
|
||||
.attr('in', 'SourceGraphic');
|
||||
|
||||
return filter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fügt einen Partikelsystem-Effekt für interaktive Knoten hinzu
|
||||
* @param {Object} parent - Das übergeordnete SVG-Element
|
||||
* @param {number} x - X-Koordinate des Zentrums
|
||||
* @param {number} y - Y-Koordinate des Zentrums
|
||||
* @param {string} color - Partikelfarbe (Hex-Code)
|
||||
* @param {number} count - Anzahl der Partikel
|
||||
*/
|
||||
static createParticleEffect(parent, x, y, color = '#b38fff', count = 5) {
|
||||
const particles = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const particle = parent.append('circle')
|
||||
.attr('cx', x)
|
||||
.attr('cy', y)
|
||||
.attr('r', 0)
|
||||
.attr('fill', color)
|
||||
.style('opacity', 0.8);
|
||||
|
||||
particles.push(particle);
|
||||
|
||||
// Partikel animieren
|
||||
animateParticle(particle);
|
||||
}
|
||||
|
||||
function animateParticle(particle) {
|
||||
// Zufällige Richtung und Geschwindigkeit
|
||||
const angle = Math.random() * Math.PI * 2;
|
||||
const speed = 1 + Math.random() * 2;
|
||||
const distance = 20 + Math.random() * 30;
|
||||
|
||||
// Zielposition berechnen
|
||||
const targetX = x + Math.cos(angle) * distance;
|
||||
const targetY = y + Math.sin(angle) * distance;
|
||||
|
||||
// Animation mit zufälliger Dauer
|
||||
const duration = 1000 + Math.random() * 500;
|
||||
|
||||
particle
|
||||
.attr('r', 0)
|
||||
.style('opacity', 0.8)
|
||||
.transition()
|
||||
.duration(duration)
|
||||
.attr('cx', targetX)
|
||||
.attr('cy', targetY)
|
||||
.attr('r', 2 + Math.random() * 3)
|
||||
.style('opacity', 0)
|
||||
.on('end', function() {
|
||||
// Partikel entfernen
|
||||
particle.remove();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Führt eine Pulsanimation auf einem Knoten durch
|
||||
* @param {Object} node - D3-Knoten-Selektion
|
||||
* @returns {void}
|
||||
*/
|
||||
static pulseAnimation(node) {
|
||||
if (!node) return;
|
||||
|
||||
const circle = node.select('circle');
|
||||
const originalRadius = parseFloat(circle.attr('r'));
|
||||
const originalFill = circle.attr('fill');
|
||||
|
||||
// Pulsanimation
|
||||
circle
|
||||
.transition()
|
||||
.duration(400)
|
||||
.attr('r', originalRadius * 1.3)
|
||||
.attr('fill', '#b38fff')
|
||||
.transition()
|
||||
.duration(400)
|
||||
.attr('r', originalRadius)
|
||||
.attr('fill', originalFill);
|
||||
}
|
||||
|
||||
/**
|
||||
* Berechnet eine adaptive Schriftgröße basierend auf der Textlänge
|
||||
* @param {string} text - Der anzuzeigende Text
|
||||
* @param {number} maxSize - Maximale Schriftgröße in Pixel
|
||||
* @param {number} minSize - Minimale Schriftgröße in Pixel
|
||||
* @returns {number} - Die berechnete Schriftgröße
|
||||
*/
|
||||
static getAdaptiveFontSize(text, maxSize = 14, minSize = 10) {
|
||||
if (!text) return maxSize;
|
||||
|
||||
// Linear die Schriftgröße basierend auf der Textlänge anpassen
|
||||
const length = text.length;
|
||||
if (length <= 6) return maxSize;
|
||||
if (length >= 20) return minSize;
|
||||
|
||||
// Lineare Interpolation
|
||||
const factor = (length - 6) / (20 - 6);
|
||||
return maxSize - factor * (maxSize - minSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fügt einen Pulsierenden Effekt zu einer Selektion hinzu
|
||||
* @param {Object} selection - D3-Selektion
|
||||
* @param {number} duration - Dauer eines Puls-Zyklus in ms
|
||||
* @param {number} minOpacity - Minimale Opazität
|
||||
* @param {number} maxOpacity - Maximale Opazität
|
||||
*/
|
||||
static addPulseEffect(selection, duration = 1500, minOpacity = 0.4, maxOpacity = 0.9) {
|
||||
function pulse() {
|
||||
selection
|
||||
.transition()
|
||||
.duration(duration / 2)
|
||||
.style('opacity', minOpacity)
|
||||
.transition()
|
||||
.duration(duration / 2)
|
||||
.style('opacity', maxOpacity)
|
||||
.on('end', pulse);
|
||||
}
|
||||
|
||||
// Initialen Stil setzen
|
||||
selection.style('opacity', maxOpacity);
|
||||
|
||||
// Pulsanimation starten
|
||||
pulse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verarbeitet Daten aus der Datenbank für die Mindmap-Visualisierung
|
||||
* @param {Array} databaseNodes - Knotendaten aus der Datenbank
|
||||
* @param {Array} links - Verbindungsdaten oder null für automatische Extraktion
|
||||
* @returns {Object} Aufbereitete Daten für D3.js
|
||||
*/
|
||||
static processDbNodesForVisualization(databaseNodes, links = null) {
|
||||
// Überprüfe, ob Daten vorhanden sind
|
||||
if (!databaseNodes || databaseNodes.length === 0) {
|
||||
console.warn('Keine Knotendaten zum Verarbeiten vorhanden');
|
||||
return { nodes: [], links: [] };
|
||||
}
|
||||
|
||||
// Knoten mit D3-Kompatiblem Format erstellen
|
||||
const nodes = databaseNodes.map(node => {
|
||||
// Farbgenerierung, falls keine vorhanden
|
||||
const nodeColor = node.color_code ||
|
||||
node.color ||
|
||||
D3Extensions.stringToColor(node.name || 'default');
|
||||
|
||||
return {
|
||||
id: node.id,
|
||||
name: node.name,
|
||||
description: node.description || '',
|
||||
thought_count: node.thought_count || 0,
|
||||
color: nodeColor,
|
||||
// Zusätzliche Attribute
|
||||
category_id: node.category_id,
|
||||
is_public: node.is_public !== undefined ? node.is_public : true,
|
||||
// Position, falls vorhanden
|
||||
x: node.x_position,
|
||||
y: node.y_position,
|
||||
// Größe, falls vorhanden
|
||||
scale: node.scale || 1.0
|
||||
};
|
||||
});
|
||||
|
||||
// Verbindungen verarbeiten
|
||||
let processedLinks = [];
|
||||
|
||||
if (links && Array.isArray(links)) {
|
||||
// Verwende übergebene Verbindungen
|
||||
processedLinks = links.map(link => {
|
||||
return {
|
||||
source: link.source,
|
||||
target: link.target,
|
||||
// Zusätzliche Attribute
|
||||
type: link.type || 'default',
|
||||
strength: link.strength || 1
|
||||
};
|
||||
});
|
||||
} else {
|
||||
// Extrahiere Verbindungen aus den Knoten
|
||||
databaseNodes.forEach(node => {
|
||||
if (node.connections && Array.isArray(node.connections)) {
|
||||
node.connections.forEach(conn => {
|
||||
processedLinks.push({
|
||||
source: node.id,
|
||||
target: conn.target,
|
||||
type: conn.type || 'default',
|
||||
strength: conn.strength || 1
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return { nodes, links: processedLinks };
|
||||
}
|
||||
}
|
||||
|
||||
// Globale Verfügbarkeit sicherstellen
|
||||
window.D3Extensions = D3Extensions;
|
||||
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');
|
||||
}
|
||||
25
static/img/favicon-gen.py
Normal file
25
static/img/favicon-gen.py
Normal file
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
from PIL import Image
|
||||
import cairosvg
|
||||
|
||||
# Pfad zum SVG-Favicon
|
||||
svg_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'favicon.svg')
|
||||
# Ausgabepfad für das PNG
|
||||
png_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'favicon.png')
|
||||
# Ausgabepfad für das ICO
|
||||
ico_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'favicon.ico')
|
||||
|
||||
# SVG zu PNG konvertieren
|
||||
cairosvg.svg2png(url=svg_path, write_to=png_path, output_width=512, output_height=512)
|
||||
|
||||
# PNG zu ICO konvertieren
|
||||
img = Image.open(png_path)
|
||||
img.save(ico_path, sizes=[(16, 16), (32, 32), (48, 48), (64, 64), (128, 128)])
|
||||
|
||||
print(f"Favicon erfolgreich erstellt: {ico_path}")
|
||||
|
||||
# Optional: PNG-Datei löschen, wenn nur ICO benötigt wird
|
||||
# os.remove(png_path)
|
||||
21
static/img/favicon.svg
Normal file
21
static/img/favicon.svg
Normal file
@@ -0,0 +1,21 @@
|
||||
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="512" height="512" rx="128" fill="url(#paint0_linear)" />
|
||||
<path d="M143.5 384V128H180.5L256.5 270L332.5 128H369.5V384H328.5V196L256.5 332H256L183.5 196V384H143.5Z" fill="white"/>
|
||||
<circle cx="143.5" cy="128" r="20" fill="#a040ff" />
|
||||
<circle cx="256.5" cy="270" r="25" fill="#a040ff" />
|
||||
<circle cx="369.5" cy="128" r="20" fill="#a040ff" />
|
||||
<circle cx="143.5" cy="384" r="20" fill="#4080ff" />
|
||||
<circle cx="369.5" cy="384" r="20" fill="#4080ff" />
|
||||
<path d="M143.5 128L183.5 196" stroke="white" stroke-width="4" stroke-linecap="round" />
|
||||
<path d="M256.5 270L183.5 196" stroke="white" stroke-width="4" stroke-linecap="round" />
|
||||
<path d="M256.5 270L328.5 196" stroke="white" stroke-width="4" stroke-linecap="round" />
|
||||
<path d="M369.5 128L328.5 196" stroke="white" stroke-width="4" stroke-linecap="round" />
|
||||
<path d="M183.5 196L143.5 384" stroke="white" stroke-width="4" stroke-linecap="round" />
|
||||
<path d="M328.5 196L369.5 384" stroke="white" stroke-width="4" stroke-linecap="round" />
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear" x1="0" y1="0" x2="512" y2="512" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#205cf5" />
|
||||
<stop offset="1" stop-color="#8020f5" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
5
static/js/alpine.min.js
vendored
Normal file
5
static/js/alpine.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
235
static/js/main.js
Normal file
235
static/js/main.js
Normal file
@@ -0,0 +1,235 @@
|
||||
/**
|
||||
* MindMap - Hauptdatei für globale JavaScript-Funktionen
|
||||
*/
|
||||
|
||||
/**
|
||||
* Hauptmodul für die MindMap-Anwendung
|
||||
* Verwaltet die globale Anwendungslogik
|
||||
*/
|
||||
|
||||
/**
|
||||
* Hauptobjekt der MindMap-Anwendung
|
||||
*/
|
||||
const MindMap = {
|
||||
// App-Status
|
||||
initialized: false,
|
||||
darkMode: document.documentElement.classList.contains('dark'),
|
||||
pageInitializers: {},
|
||||
currentPage: null,
|
||||
|
||||
/**
|
||||
* Initialisiert die MindMap-Anwendung
|
||||
*/
|
||||
init() {
|
||||
if (this.initialized) return;
|
||||
|
||||
// Setze currentPage erst jetzt, wenn DOM garantiert geladen ist
|
||||
this.currentPage = document.body && document.body.dataset ? document.body.dataset.page : null;
|
||||
|
||||
console.log('MindMap-Anwendung wird initialisiert...');
|
||||
|
||||
// Initialisiere den ChatGPT-Assistenten
|
||||
if (typeof ChatGPTAssistant !== 'undefined') {
|
||||
const assistant = new ChatGPTAssistant();
|
||||
assistant.init();
|
||||
// Speichere als Teil von MindMap
|
||||
this.assistant = assistant;
|
||||
}
|
||||
|
||||
// Seiten-spezifische Initialisierer aufrufen
|
||||
if (this.currentPage && this.pageInitializers[this.currentPage]) {
|
||||
this.pageInitializers[this.currentPage]();
|
||||
}
|
||||
|
||||
// Event-Listener einrichten
|
||||
this.setupEventListeners();
|
||||
|
||||
// Dunkel-/Hellmodus aus LocalStorage wiederherstellen
|
||||
if (localStorage.getItem('darkMode') === 'dark') {
|
||||
document.documentElement.classList.add('dark');
|
||||
this.darkMode = true;
|
||||
}
|
||||
|
||||
// Mindmap initialisieren, falls auf der richtigen Seite
|
||||
this.initializeMindmap();
|
||||
|
||||
this.initialized = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialisiert die D3.js Mindmap-Visualisierung
|
||||
*/
|
||||
initializeMindmap() {
|
||||
// Prüfe, ob wir auf der Mindmap-Seite sind
|
||||
const mindmapContainer = document.getElementById('mindmap-container');
|
||||
if (!mindmapContainer) return;
|
||||
|
||||
try {
|
||||
console.log('Initialisiere Mindmap...');
|
||||
|
||||
// Prüfe, ob MindMapVisualization geladen ist
|
||||
if (typeof MindMapVisualization === 'undefined') {
|
||||
console.error('MindMapVisualization-Klasse ist nicht definiert!');
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialisiere die Mindmap
|
||||
const mindmap = new MindMapVisualization('#mindmap-container', {
|
||||
height: mindmapContainer.clientHeight || 600,
|
||||
nodeRadius: 18,
|
||||
selectedNodeRadius: 24,
|
||||
linkDistance: 150,
|
||||
onNodeClick: this.handleNodeClick.bind(this)
|
||||
});
|
||||
|
||||
// Globale Referenz für andere Module
|
||||
window.mindmapInstance = mindmap;
|
||||
|
||||
// Event-Listener für Zoom-Buttons
|
||||
const zoomInBtn = document.getElementById('zoom-in-btn');
|
||||
if (zoomInBtn) {
|
||||
zoomInBtn.addEventListener('click', () => {
|
||||
const svg = d3.select('#mindmap-container svg');
|
||||
const currentZoom = d3.zoomTransform(svg.node());
|
||||
const newScale = currentZoom.k * 1.3;
|
||||
svg.transition().duration(300).call(
|
||||
d3.zoom().transform,
|
||||
d3.zoomIdentity.translate(currentZoom.x, currentZoom.y).scale(newScale)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const zoomOutBtn = document.getElementById('zoom-out-btn');
|
||||
if (zoomOutBtn) {
|
||||
zoomOutBtn.addEventListener('click', () => {
|
||||
const svg = d3.select('#mindmap-container svg');
|
||||
const currentZoom = d3.zoomTransform(svg.node());
|
||||
const newScale = currentZoom.k / 1.3;
|
||||
svg.transition().duration(300).call(
|
||||
d3.zoom().transform,
|
||||
d3.zoomIdentity.translate(currentZoom.x, currentZoom.y).scale(newScale)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const centerBtn = document.getElementById('center-btn');
|
||||
if (centerBtn) {
|
||||
centerBtn.addEventListener('click', () => {
|
||||
const svg = d3.select('#mindmap-container svg');
|
||||
svg.transition().duration(500).call(
|
||||
d3.zoom().transform,
|
||||
d3.zoomIdentity.scale(1)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Event-Listener für Add-Thought-Button
|
||||
const addThoughtBtn = document.getElementById('add-thought-btn');
|
||||
if (addThoughtBtn) {
|
||||
addThoughtBtn.addEventListener('click', () => {
|
||||
this.showAddThoughtDialog();
|
||||
});
|
||||
}
|
||||
|
||||
// Event-Listener für Connect-Button
|
||||
const connectBtn = document.getElementById('connect-btn');
|
||||
if (connectBtn) {
|
||||
connectBtn.addEventListener('click', () => {
|
||||
this.showConnectDialog();
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler bei der Initialisierung der Mindmap:', error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handler für Klick auf einen Knoten in der Mindmap
|
||||
* @param {Object} node - Der angeklickte Knoten
|
||||
*/
|
||||
handleNodeClick(node) {
|
||||
console.log('Knoten wurde angeklickt:', node);
|
||||
|
||||
// Hier könnte man Logik hinzufügen, um Detailinformationen anzuzeigen
|
||||
// oder den ausgewählten Knoten hervorzuheben
|
||||
const detailsContainer = document.getElementById('node-details');
|
||||
if (detailsContainer) {
|
||||
detailsContainer.innerHTML = `
|
||||
<div class="p-4">
|
||||
<h3 class="text-xl font-bold mb-2">${node.name}</h3>
|
||||
<p class="text-gray-300 mb-4">${node.description || 'Keine Beschreibung verfügbar.'}</p>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm">
|
||||
<i class="fas fa-brain mr-1"></i> ${node.thought_count || 0} Gedanken
|
||||
</span>
|
||||
<button class="px-3 py-1 bg-purple-600 bg-opacity-30 rounded-lg text-sm">
|
||||
<i class="fas fa-plus mr-1"></i> Gedanke hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Button zum Hinzufügen eines Gedankens
|
||||
const addThoughtBtn = detailsContainer.querySelector('button');
|
||||
addThoughtBtn.addEventListener('click', () => {
|
||||
this.showAddThoughtDialog(node);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Dialog zum Hinzufügen eines neuen Knotens
|
||||
*/
|
||||
showAddNodeDialog() {
|
||||
// Diese Funktionalität würde in einer vollständigen Implementierung eingebunden werden
|
||||
alert('Diese Funktion steht bald zur Verfügung!');
|
||||
},
|
||||
|
||||
/**
|
||||
* Dialog zum Hinzufügen eines neuen Gedankens zu einem Knoten
|
||||
*/
|
||||
showAddThoughtDialog(node) {
|
||||
// Diese Funktionalität würde in einer vollständigen Implementierung eingebunden werden
|
||||
alert('Diese Funktion steht bald zur Verfügung!');
|
||||
},
|
||||
|
||||
/**
|
||||
* Dialog zum Verbinden von Knoten
|
||||
*/
|
||||
showConnectDialog() {
|
||||
// Diese Funktionalität würde in einer vollständigen Implementierung eingebunden werden
|
||||
alert('Diese Funktion steht bald zur Verfügung!');
|
||||
},
|
||||
|
||||
/**
|
||||
* Richtet Event-Listener für die Benutzeroberfläche ein
|
||||
*/
|
||||
setupEventListeners() {
|
||||
// Event-Listener für Dark Mode-Wechsel
|
||||
document.addEventListener('darkModeToggled', (event) => {
|
||||
this.darkMode = event.detail.isDark;
|
||||
});
|
||||
|
||||
// Responsive Anpassungen bei Fenstergröße
|
||||
window.addEventListener('resize', () => {
|
||||
if (window.mindmapInstance) {
|
||||
const container = document.getElementById('mindmap-container');
|
||||
if (container) {
|
||||
window.mindmapInstance.width = container.clientWidth;
|
||||
window.mindmapInstance.height = container.clientHeight;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
window.MindMap = MindMap;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialisiere die Anwendung
|
||||
MindMap.init();
|
||||
|
||||
// Wende Dunkel-/Hellmodus an
|
||||
const isDarkMode = localStorage.getItem('darkMode') === 'dark';
|
||||
document.documentElement.classList.toggle('dark', isDarkMode);
|
||||
});
|
||||
234
static/js/mindmap.html
Normal file
234
static/js/mindmap.html
Normal file
@@ -0,0 +1,234 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Interaktive Mindmap</title>
|
||||
|
||||
<!-- Cytoscape.js -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.26.0/cytoscape.min.js"></script>
|
||||
|
||||
<!-- Socket.IO -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.7.2/socket.io.min.js"></script>
|
||||
|
||||
<!-- Feather Icons (optional) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
|
||||
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background-color: #f9fafb;
|
||||
color: #111827;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.header {
|
||||
background-color: #1f2937;
|
||||
color: white;
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
background-color: #f3f4f6;
|
||||
padding: 0.75rem;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background-color: #3b82f6;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background-color: #2563eb;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: #6b7280;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: #4b5563;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: #ef4444;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background-color: #dc2626;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
#cy {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.category-filters {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
padding: 0.75rem;
|
||||
background-color: #ffffff;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.category-filter {
|
||||
border: none;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.25rem 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.category-filter:not(.active) {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.category-filter:hover:not(.active) {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.footer {
|
||||
background-color: #f3f4f6;
|
||||
padding: 0.75rem;
|
||||
text-align: center;
|
||||
font-size: 0.75rem;
|
||||
color: #6b7280;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
/* Kontextmenü Styling */
|
||||
#context-menu {
|
||||
position: absolute;
|
||||
background-color: white;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 0.25rem;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#context-menu .menu-item {
|
||||
padding: 0.5rem 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#context-menu .menu-item:hover {
|
||||
background-color: #f3f4f6;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header class="header">
|
||||
<h1>Interaktive Mindmap</h1>
|
||||
<div class="search-container">
|
||||
<input type="text" id="search-mindmap" class="search-input" placeholder="Suchen...">
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="toolbar">
|
||||
<button id="addNode" class="btn">
|
||||
<i data-feather="plus-circle"></i>
|
||||
Knoten hinzufügen
|
||||
</button>
|
||||
<button id="addEdge" class="btn">
|
||||
<i data-feather="git-branch"></i>
|
||||
Verbindung erstellen
|
||||
</button>
|
||||
<button id="editNode" class="btn btn-secondary">
|
||||
<i data-feather="edit-2"></i>
|
||||
Knoten bearbeiten
|
||||
</button>
|
||||
<button id="deleteNode" class="btn btn-danger">
|
||||
<i data-feather="trash-2"></i>
|
||||
Knoten löschen
|
||||
</button>
|
||||
<button id="deleteEdge" class="btn btn-danger">
|
||||
<i data-feather="scissors"></i>
|
||||
Verbindung löschen
|
||||
</button>
|
||||
<button id="reLayout" class="btn btn-secondary">
|
||||
<i data-feather="refresh-cw"></i>
|
||||
Layout neu anordnen
|
||||
</button>
|
||||
<button id="exportMindmap" class="btn btn-secondary">
|
||||
<i data-feather="download"></i>
|
||||
Exportieren
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="category-filters" class="category-filters">
|
||||
<!-- Wird dynamisch befüllt -->
|
||||
</div>
|
||||
|
||||
<div id="cy"></div>
|
||||
|
||||
<footer class="footer">
|
||||
Mindmap-Anwendung © 2023
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- Unsere Mindmap JS -->
|
||||
<script src="../js/mindmap.js"></script>
|
||||
|
||||
<!-- Icons initialisieren -->
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (typeof feather !== 'undefined') {
|
||||
feather.replace();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
749
static/js/mindmap.js
Normal file
749
static/js/mindmap.js
Normal file
@@ -0,0 +1,749 @@
|
||||
/**
|
||||
* Mindmap.js - Interaktive Mind-Map Implementierung
|
||||
* - Cytoscape.js für Graph-Rendering
|
||||
* - Fetch API für REST-Zugriffe
|
||||
* - Socket.IO für Echtzeit-Synchronisation
|
||||
*/
|
||||
|
||||
(async () => {
|
||||
/* 1. Initialisierung und Grundkonfiguration */
|
||||
const cy = cytoscape({
|
||||
container: document.getElementById('cy'),
|
||||
style: [
|
||||
{
|
||||
selector: 'node',
|
||||
style: {
|
||||
'label': 'data(name)',
|
||||
'text-valign': 'center',
|
||||
'color': '#fff',
|
||||
'background-color': 'data(color)',
|
||||
'width': 45,
|
||||
'height': 45,
|
||||
'font-size': 11,
|
||||
'text-outline-width': 1,
|
||||
'text-outline-color': '#000',
|
||||
'text-outline-opacity': 0.5,
|
||||
'text-wrap': 'wrap',
|
||||
'text-max-width': 80
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: 'node[icon]',
|
||||
style: {
|
||||
'background-image': function(ele) {
|
||||
return `static/img/icons/${ele.data('icon')}.svg`;
|
||||
},
|
||||
'background-width': '60%',
|
||||
'background-height': '60%',
|
||||
'background-position-x': '50%',
|
||||
'background-position-y': '40%',
|
||||
'text-margin-y': 10
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: 'edge',
|
||||
style: {
|
||||
'width': 2,
|
||||
'line-color': '#888',
|
||||
'target-arrow-shape': 'triangle',
|
||||
'curve-style': 'bezier',
|
||||
'target-arrow-color': '#888'
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: ':selected',
|
||||
style: {
|
||||
'border-width': 3,
|
||||
'border-color': '#f8f32b'
|
||||
}
|
||||
}
|
||||
],
|
||||
layout: {
|
||||
name: 'breadthfirst',
|
||||
directed: true,
|
||||
padding: 30,
|
||||
spacingFactor: 1.2
|
||||
}
|
||||
});
|
||||
|
||||
/* 2. Hilfs-Funktionen für API-Zugriffe */
|
||||
const get = async endpoint => {
|
||||
try {
|
||||
const response = await fetch(endpoint);
|
||||
if (!response.ok) {
|
||||
console.error(`API-Fehler: ${endpoint} antwortet mit Status ${response.status}`);
|
||||
return []; // Leeres Array zurückgeben bei Fehlern
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error(`Fehler beim Abrufen von ${endpoint}:`, error);
|
||||
return []; // Leeres Array zurückgeben bei Netzwerkfehlern
|
||||
}
|
||||
};
|
||||
|
||||
const post = async (endpoint, body) => {
|
||||
try {
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
if (!response.ok) {
|
||||
console.error(`API-Fehler: ${endpoint} antwortet mit Status ${response.status}`);
|
||||
return {}; // Leeres Objekt zurückgeben bei Fehlern
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error(`Fehler beim POST zu ${endpoint}:`, error);
|
||||
return {}; // Leeres Objekt zurückgeben bei Netzwerkfehlern
|
||||
}
|
||||
};
|
||||
|
||||
const del = async endpoint => {
|
||||
try {
|
||||
const response = await fetch(endpoint, { method: 'DELETE' });
|
||||
if (!response.ok) {
|
||||
console.error(`API-Fehler: ${endpoint} antwortet mit Status ${response.status}`);
|
||||
return {}; // Leeres Objekt zurückgeben bei Fehlern
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error(`Fehler beim DELETE zu ${endpoint}:`, error);
|
||||
return {}; // Leeres Objekt zurückgeben bei Netzwerkfehlern
|
||||
}
|
||||
};
|
||||
|
||||
/* 3. Kategorien laden für Style-Informationen */
|
||||
let categories = await get('/api/categories');
|
||||
|
||||
/* 4. Daten laden und Rendering */
|
||||
const loadMindmap = async () => {
|
||||
try {
|
||||
// Nodes und Beziehungen parallel laden
|
||||
const [nodes, relationships] = await Promise.all([
|
||||
get('/api/mind_map_nodes'),
|
||||
get('/api/node_relationships')
|
||||
]);
|
||||
|
||||
// Graph leeren (für Reload-Fälle)
|
||||
cy.elements().remove();
|
||||
|
||||
// Überprüfen, ob nodes ein Array ist, wenn nicht, setze es auf ein leeres Array
|
||||
const nodesArray = Array.isArray(nodes) ? nodes : [];
|
||||
|
||||
// Knoten zum Graph hinzufügen
|
||||
cy.add(
|
||||
nodesArray.map(node => {
|
||||
// Kategorie-Informationen für Styling abrufen
|
||||
const category = categories.find(c => c.id === node.category_id) || {};
|
||||
|
||||
return {
|
||||
data: {
|
||||
id: node.id.toString(),
|
||||
name: node.name,
|
||||
description: node.description,
|
||||
color: node.color_code || category.color_code || '#6b7280',
|
||||
icon: node.icon || category.icon,
|
||||
category_id: node.category_id
|
||||
},
|
||||
position: node.x && node.y ? { x: node.x, y: node.y } : undefined
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
// Überprüfen, ob relationships ein Array ist, wenn nicht, setze es auf ein leeres Array
|
||||
const relationshipsArray = Array.isArray(relationships) ? relationships : [];
|
||||
|
||||
// Kanten zum Graph hinzufügen
|
||||
cy.add(
|
||||
relationshipsArray.map(rel => ({
|
||||
data: {
|
||||
id: `${rel.parent_id}_${rel.child_id}`,
|
||||
source: rel.parent_id.toString(),
|
||||
target: rel.child_id.toString()
|
||||
}
|
||||
}))
|
||||
);
|
||||
|
||||
// Wenn keine Knoten geladen wurden, Fallback-Knoten erstellen
|
||||
if (nodesArray.length === 0) {
|
||||
// Mindestens einen Standardknoten hinzufügen
|
||||
cy.add({
|
||||
data: {
|
||||
id: 'fallback-1',
|
||||
name: 'Mindmap',
|
||||
description: 'Erstellen Sie hier Ihre eigene Mindmap',
|
||||
color: '#3b82f6',
|
||||
icon: 'help-circle'
|
||||
},
|
||||
position: { x: 300, y: 200 }
|
||||
});
|
||||
|
||||
// Erfolgsmeldung anzeigen
|
||||
console.log('Mindmap erfolgreich initialisiert mit Fallback-Knoten');
|
||||
|
||||
// Info-Meldung für Benutzer anzeigen
|
||||
const infoBox = document.createElement('div');
|
||||
infoBox.classList.add('info-message');
|
||||
infoBox.style.position = 'absolute';
|
||||
infoBox.style.top = '50%';
|
||||
infoBox.style.left = '50%';
|
||||
infoBox.style.transform = 'translate(-50%, -50%)';
|
||||
infoBox.style.padding = '15px 20px';
|
||||
infoBox.style.backgroundColor = 'rgba(59, 130, 246, 0.9)';
|
||||
infoBox.style.color = 'white';
|
||||
infoBox.style.borderRadius = '8px';
|
||||
infoBox.style.zIndex = '5';
|
||||
infoBox.style.maxWidth = '80%';
|
||||
infoBox.style.textAlign = 'center';
|
||||
infoBox.style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)';
|
||||
infoBox.innerHTML = 'Mindmap erfolgreich initialisiert.<br>Verwenden Sie die Werkzeugleiste, um Knoten hinzuzufügen.';
|
||||
|
||||
document.getElementById('cy').appendChild(infoBox);
|
||||
|
||||
// Meldung nach 5 Sekunden ausblenden
|
||||
setTimeout(() => {
|
||||
infoBox.style.opacity = '0';
|
||||
infoBox.style.transition = 'opacity 0.5s ease';
|
||||
setTimeout(() => {
|
||||
if (infoBox.parentNode) {
|
||||
infoBox.parentNode.removeChild(infoBox);
|
||||
}
|
||||
}, 500);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Layout anwenden wenn keine Positionsdaten vorhanden
|
||||
const nodesWithoutPosition = cy.nodes().filter(node =>
|
||||
!node.position() || (node.position().x === 0 && node.position().y === 0)
|
||||
);
|
||||
|
||||
if (nodesWithoutPosition.length > 0) {
|
||||
cy.layout({
|
||||
name: 'breadthfirst',
|
||||
directed: true,
|
||||
padding: 30,
|
||||
spacingFactor: 1.2
|
||||
}).run();
|
||||
}
|
||||
|
||||
// Tooltip-Funktionalität
|
||||
cy.nodes().unbind('mouseover').bind('mouseover', (event) => {
|
||||
const node = event.target;
|
||||
const description = node.data('description');
|
||||
|
||||
if (description) {
|
||||
const tooltip = document.getElementById('node-tooltip') ||
|
||||
document.createElement('div');
|
||||
|
||||
if (!tooltip.id) {
|
||||
tooltip.id = 'node-tooltip';
|
||||
tooltip.style.position = 'absolute';
|
||||
tooltip.style.backgroundColor = '#333';
|
||||
tooltip.style.color = '#fff';
|
||||
tooltip.style.padding = '8px';
|
||||
tooltip.style.borderRadius = '4px';
|
||||
tooltip.style.maxWidth = '250px';
|
||||
tooltip.style.zIndex = 10;
|
||||
tooltip.style.pointerEvents = 'none';
|
||||
tooltip.style.transition = 'opacity 0.2s';
|
||||
tooltip.style.boxShadow = '0 2px 10px rgba(0,0,0,0.3)';
|
||||
document.body.appendChild(tooltip);
|
||||
}
|
||||
|
||||
const renderedPosition = node.renderedPosition();
|
||||
const containerRect = cy.container().getBoundingClientRect();
|
||||
|
||||
tooltip.innerHTML = description;
|
||||
tooltip.style.left = (containerRect.left + renderedPosition.x + 25) + 'px';
|
||||
tooltip.style.top = (containerRect.top + renderedPosition.y - 15) + 'px';
|
||||
tooltip.style.opacity = '1';
|
||||
}
|
||||
});
|
||||
|
||||
cy.nodes().unbind('mouseout').bind('mouseout', () => {
|
||||
const tooltip = document.getElementById('node-tooltip');
|
||||
if (tooltip) {
|
||||
tooltip.style.opacity = '0';
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Mindmap:', error);
|
||||
alert('Die Mindmap konnte nicht geladen werden. Bitte prüfen Sie die Konsole für Details.');
|
||||
}
|
||||
};
|
||||
|
||||
// Initial laden
|
||||
await loadMindmap();
|
||||
|
||||
/* 5. Socket.IO für Echtzeit-Synchronisation */
|
||||
const socket = io();
|
||||
|
||||
socket.on('node_added', async (node) => {
|
||||
// Kategorie-Informationen für Styling abrufen
|
||||
const category = categories.find(c => c.id === node.category_id) || {};
|
||||
|
||||
cy.add({
|
||||
data: {
|
||||
id: node.id.toString(),
|
||||
name: node.name,
|
||||
description: node.description,
|
||||
color: node.color_code || category.color_code || '#6b7280',
|
||||
icon: node.icon || category.icon,
|
||||
category_id: node.category_id
|
||||
}
|
||||
});
|
||||
|
||||
// Layout neu anwenden, wenn nötig
|
||||
if (!node.x || !node.y) {
|
||||
cy.layout({ name: 'breadthfirst', directed: true, padding: 30 }).run();
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('node_updated', (node) => {
|
||||
const cyNode = cy.$id(node.id.toString());
|
||||
if (cyNode.length > 0) {
|
||||
// Kategorie-Informationen für Styling abrufen
|
||||
const category = categories.find(c => c.id === node.category_id) || {};
|
||||
|
||||
cyNode.data({
|
||||
name: node.name,
|
||||
description: node.description,
|
||||
color: node.color_code || category.color_code || '#6b7280',
|
||||
icon: node.icon || category.icon,
|
||||
category_id: node.category_id
|
||||
});
|
||||
|
||||
if (node.x && node.y) {
|
||||
cyNode.position({ x: node.x, y: node.y });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('node_deleted', (nodeId) => {
|
||||
const cyNode = cy.$id(nodeId.toString());
|
||||
if (cyNode.length > 0) {
|
||||
cy.remove(cyNode);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('relationship_added', (rel) => {
|
||||
cy.add({
|
||||
data: {
|
||||
id: `${rel.parent_id}_${rel.child_id}`,
|
||||
source: rel.parent_id.toString(),
|
||||
target: rel.child_id.toString()
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('relationship_deleted', (rel) => {
|
||||
const edgeId = `${rel.parent_id}_${rel.child_id}`;
|
||||
const cyEdge = cy.$id(edgeId);
|
||||
if (cyEdge.length > 0) {
|
||||
cy.remove(cyEdge);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('category_updated', async () => {
|
||||
// Kategorien neu laden
|
||||
categories = await get('/api/categories');
|
||||
// Nodes aktualisieren, die diese Kategorie verwenden
|
||||
cy.nodes().forEach(node => {
|
||||
const categoryId = node.data('category_id');
|
||||
if (categoryId) {
|
||||
const category = categories.find(c => c.id === categoryId);
|
||||
if (category) {
|
||||
node.data('color', node.data('color_code') || category.color_code);
|
||||
node.data('icon', node.data('icon') || category.icon);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/* 6. UI-Interaktionen */
|
||||
// Knoten hinzufügen
|
||||
const btnAddNode = document.getElementById('addNode');
|
||||
if (btnAddNode) {
|
||||
btnAddNode.addEventListener('click', async () => {
|
||||
const name = prompt('Knotenname eingeben:');
|
||||
if (!name) return;
|
||||
|
||||
const description = prompt('Beschreibung (optional):');
|
||||
|
||||
// Kategorie auswählen
|
||||
let categoryId = null;
|
||||
if (categories.length > 0) {
|
||||
const categoryOptions = categories.map((c, i) => `${i}: ${c.name}`).join('\n');
|
||||
const categoryChoice = prompt(
|
||||
`Kategorie auswählen (Nummer eingeben):\n${categoryOptions}`,
|
||||
'0'
|
||||
);
|
||||
|
||||
if (categoryChoice !== null) {
|
||||
const index = parseInt(categoryChoice, 10);
|
||||
if (!isNaN(index) && index >= 0 && index < categories.length) {
|
||||
categoryId = categories[index].id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Knoten erstellen
|
||||
await post('/api/mind_map_node', {
|
||||
name,
|
||||
description,
|
||||
category_id: categoryId
|
||||
});
|
||||
// Darstellung wird durch Socket.IO Event übernommen
|
||||
});
|
||||
}
|
||||
|
||||
// Verbindung hinzufügen
|
||||
const btnAddEdge = document.getElementById('addEdge');
|
||||
if (btnAddEdge) {
|
||||
btnAddEdge.addEventListener('click', async () => {
|
||||
const sel = cy.$('node:selected');
|
||||
if (sel.length !== 2) {
|
||||
alert('Bitte genau zwei Knoten auswählen (Parent → Child)');
|
||||
return;
|
||||
}
|
||||
|
||||
const [parent, child] = sel.map(node => node.id());
|
||||
await post('/api/node_relationship', {
|
||||
parent_id: parent,
|
||||
child_id: child
|
||||
});
|
||||
// Darstellung wird durch Socket.IO Event übernommen
|
||||
});
|
||||
}
|
||||
|
||||
// Knoten bearbeiten
|
||||
const btnEditNode = document.getElementById('editNode');
|
||||
if (btnEditNode) {
|
||||
btnEditNode.addEventListener('click', async () => {
|
||||
const sel = cy.$('node:selected');
|
||||
if (sel.length !== 1) {
|
||||
alert('Bitte genau einen Knoten auswählen');
|
||||
return;
|
||||
}
|
||||
|
||||
const node = sel[0];
|
||||
const nodeData = node.data();
|
||||
|
||||
const name = prompt('Knotenname:', nodeData.name);
|
||||
if (!name) return;
|
||||
|
||||
const description = prompt('Beschreibung:', nodeData.description || '');
|
||||
|
||||
// Kategorie auswählen
|
||||
let categoryId = nodeData.category_id;
|
||||
if (categories.length > 0) {
|
||||
const categoryOptions = categories.map((c, i) => `${i}: ${c.name}`).join('\n');
|
||||
const categoryChoice = prompt(
|
||||
`Kategorie auswählen (Nummer eingeben):\n${categoryOptions}`,
|
||||
categories.findIndex(c => c.id === categoryId).toString()
|
||||
);
|
||||
|
||||
if (categoryChoice !== null) {
|
||||
const index = parseInt(categoryChoice, 10);
|
||||
if (!isNaN(index) && index >= 0 && index < categories.length) {
|
||||
categoryId = categories[index].id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Knoten aktualisieren
|
||||
await post(`/api/mind_map_node/${nodeData.id}`, {
|
||||
name,
|
||||
description,
|
||||
category_id: categoryId
|
||||
});
|
||||
// Darstellung wird durch Socket.IO Event übernommen
|
||||
});
|
||||
}
|
||||
|
||||
// Knoten löschen
|
||||
const btnDeleteNode = document.getElementById('deleteNode');
|
||||
if (btnDeleteNode) {
|
||||
btnDeleteNode.addEventListener('click', async () => {
|
||||
const sel = cy.$('node:selected');
|
||||
if (sel.length !== 1) {
|
||||
alert('Bitte genau einen Knoten auswählen');
|
||||
return;
|
||||
}
|
||||
|
||||
if (confirm('Sind Sie sicher, dass Sie diesen Knoten löschen möchten?')) {
|
||||
const nodeId = sel[0].id();
|
||||
await del(`/api/mind_map_node/${nodeId}`);
|
||||
// Darstellung wird durch Socket.IO Event übernommen
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Verbindung löschen
|
||||
const btnDeleteEdge = document.getElementById('deleteEdge');
|
||||
if (btnDeleteEdge) {
|
||||
btnDeleteEdge.addEventListener('click', async () => {
|
||||
const sel = cy.$('edge:selected');
|
||||
if (sel.length !== 1) {
|
||||
alert('Bitte genau eine Verbindung auswählen');
|
||||
return;
|
||||
}
|
||||
|
||||
if (confirm('Sind Sie sicher, dass Sie diese Verbindung löschen möchten?')) {
|
||||
const edge = sel[0];
|
||||
const parentId = edge.source().id();
|
||||
const childId = edge.target().id();
|
||||
|
||||
await del(`/api/node_relationship/${parentId}/${childId}`);
|
||||
// Darstellung wird durch Socket.IO Event übernommen
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Layout aktualisieren
|
||||
const btnReLayout = document.getElementById('reLayout');
|
||||
if (btnReLayout) {
|
||||
btnReLayout.addEventListener('click', () => {
|
||||
cy.layout({
|
||||
name: 'breadthfirst',
|
||||
directed: true,
|
||||
padding: 30,
|
||||
spacingFactor: 1.2
|
||||
}).run();
|
||||
});
|
||||
}
|
||||
|
||||
/* 7. Position speichern bei Drag & Drop */
|
||||
cy.on('dragfree', 'node', async (e) => {
|
||||
const node = e.target;
|
||||
const position = node.position();
|
||||
|
||||
await post(`/api/mind_map_node/${node.id()}/position`, {
|
||||
x: Math.round(position.x),
|
||||
y: Math.round(position.y)
|
||||
});
|
||||
|
||||
// Andere Benutzer erhalten die Position über den node_updated Event
|
||||
});
|
||||
|
||||
/* 8. Kontextmenü (optional) */
|
||||
const setupContextMenu = () => {
|
||||
cy.on('cxttap', 'node', function(e) {
|
||||
const node = e.target;
|
||||
const nodeData = node.data();
|
||||
|
||||
// Position des Kontextmenüs berechnen
|
||||
const renderedPosition = node.renderedPosition();
|
||||
const containerRect = cy.container().getBoundingClientRect();
|
||||
const menuX = containerRect.left + renderedPosition.x;
|
||||
const menuY = containerRect.top + renderedPosition.y;
|
||||
|
||||
// Kontextmenü erstellen oder aktualisieren
|
||||
let contextMenu = document.getElementById('context-menu');
|
||||
if (!contextMenu) {
|
||||
contextMenu = document.createElement('div');
|
||||
contextMenu.id = 'context-menu';
|
||||
contextMenu.style.position = 'absolute';
|
||||
contextMenu.style.backgroundColor = '#fff';
|
||||
contextMenu.style.border = '1px solid #ccc';
|
||||
contextMenu.style.borderRadius = '4px';
|
||||
contextMenu.style.padding = '5px 0';
|
||||
contextMenu.style.boxShadow = '0 2px 10px rgba(0,0,0,0.2)';
|
||||
contextMenu.style.zIndex = 1000;
|
||||
document.body.appendChild(contextMenu);
|
||||
}
|
||||
|
||||
// Menüinhalte
|
||||
contextMenu.innerHTML = `
|
||||
<div class="menu-item" data-action="edit">Knoten bearbeiten</div>
|
||||
<div class="menu-item" data-action="connect">Verbindung erstellen</div>
|
||||
<div class="menu-item" data-action="delete">Knoten löschen</div>
|
||||
`;
|
||||
|
||||
// Styling für Menüpunkte
|
||||
const menuItems = contextMenu.querySelectorAll('.menu-item');
|
||||
menuItems.forEach(item => {
|
||||
item.style.padding = '8px 20px';
|
||||
item.style.cursor = 'pointer';
|
||||
item.style.fontSize = '14px';
|
||||
|
||||
item.addEventListener('mouseover', function() {
|
||||
this.style.backgroundColor = '#f0f0f0';
|
||||
});
|
||||
|
||||
item.addEventListener('mouseout', function() {
|
||||
this.style.backgroundColor = 'transparent';
|
||||
});
|
||||
|
||||
// Event-Handler
|
||||
item.addEventListener('click', async function() {
|
||||
const action = this.getAttribute('data-action');
|
||||
|
||||
switch(action) {
|
||||
case 'edit':
|
||||
// Knoten bearbeiten (gleiche Logik wie beim Edit-Button)
|
||||
const name = prompt('Knotenname:', nodeData.name);
|
||||
if (name) {
|
||||
const description = prompt('Beschreibung:', nodeData.description || '');
|
||||
await post(`/api/mind_map_node/${nodeData.id}`, { name, description });
|
||||
}
|
||||
break;
|
||||
|
||||
case 'connect':
|
||||
// Modus zum Verbinden aktivieren
|
||||
cy.nodes().unselect();
|
||||
node.select();
|
||||
alert('Wählen Sie nun einen zweiten Knoten aus, um eine Verbindung zu erstellen');
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
if (confirm('Sind Sie sicher, dass Sie diesen Knoten löschen möchten?')) {
|
||||
await del(`/api/mind_map_node/${nodeData.id}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Menü schließen
|
||||
contextMenu.style.display = 'none';
|
||||
});
|
||||
});
|
||||
|
||||
// Menü positionieren und anzeigen
|
||||
contextMenu.style.left = menuX + 'px';
|
||||
contextMenu.style.top = menuY + 'px';
|
||||
contextMenu.style.display = 'block';
|
||||
|
||||
// Event-Listener zum Schließen des Menüs
|
||||
const closeMenu = function() {
|
||||
if (contextMenu) {
|
||||
contextMenu.style.display = 'none';
|
||||
}
|
||||
document.removeEventListener('click', closeMenu);
|
||||
};
|
||||
|
||||
// Verzögerung, um den aktuellen Click nicht zu erfassen
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', closeMenu);
|
||||
}, 0);
|
||||
|
||||
e.preventDefault();
|
||||
});
|
||||
};
|
||||
|
||||
// Kontextmenü aktivieren (optional)
|
||||
// setupContextMenu();
|
||||
|
||||
/* 9. Export-Funktion (optional) */
|
||||
const btnExport = document.getElementById('exportMindmap');
|
||||
if (btnExport) {
|
||||
btnExport.addEventListener('click', () => {
|
||||
const elements = cy.json().elements;
|
||||
const exportData = {
|
||||
nodes: elements.nodes.map(n => ({
|
||||
id: n.data.id,
|
||||
name: n.data.name,
|
||||
description: n.data.description,
|
||||
category_id: n.data.category_id,
|
||||
x: Math.round(n.position?.x || 0),
|
||||
y: Math.round(n.position?.y || 0)
|
||||
})),
|
||||
relationships: elements.edges.map(e => ({
|
||||
parent_id: e.data.source,
|
||||
child_id: e.data.target
|
||||
}))
|
||||
};
|
||||
|
||||
const blob = new Blob([JSON.stringify(exportData, null, 2)], {type: 'application/json'});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'mindmap_export.json';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
});
|
||||
}
|
||||
|
||||
/* 10. Filter-Funktion nach Kategorien (optional) */
|
||||
const setupCategoryFilters = () => {
|
||||
const filterContainer = document.getElementById('category-filters');
|
||||
if (!filterContainer || !categories.length) return;
|
||||
|
||||
filterContainer.innerHTML = '';
|
||||
|
||||
// "Alle anzeigen" Option
|
||||
const allBtn = document.createElement('button');
|
||||
allBtn.innerText = 'Alle Kategorien';
|
||||
allBtn.className = 'category-filter active';
|
||||
allBtn.onclick = () => {
|
||||
document.querySelectorAll('.category-filter').forEach(btn => btn.classList.remove('active'));
|
||||
allBtn.classList.add('active');
|
||||
cy.nodes().removeClass('filtered').show();
|
||||
cy.edges().show();
|
||||
};
|
||||
filterContainer.appendChild(allBtn);
|
||||
|
||||
// Filter-Button pro Kategorie
|
||||
categories.forEach(category => {
|
||||
const btn = document.createElement('button');
|
||||
btn.innerText = category.name;
|
||||
btn.className = 'category-filter';
|
||||
btn.style.backgroundColor = category.color_code;
|
||||
btn.style.color = '#fff';
|
||||
btn.onclick = () => {
|
||||
document.querySelectorAll('.category-filter').forEach(btn => btn.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
|
||||
const matchingNodes = cy.nodes().filter(node => node.data('category_id') === category.id);
|
||||
cy.nodes().addClass('filtered').hide();
|
||||
matchingNodes.removeClass('filtered').show();
|
||||
|
||||
// Verbindungen zu/von diesen Knoten anzeigen
|
||||
cy.edges().hide();
|
||||
matchingNodes.connectedEdges().show();
|
||||
};
|
||||
filterContainer.appendChild(btn);
|
||||
});
|
||||
};
|
||||
|
||||
// Filter-Funktionalität aktivieren (optional)
|
||||
// setupCategoryFilters();
|
||||
|
||||
/* 11. Suchfunktion (optional) */
|
||||
const searchInput = document.getElementById('search-mindmap');
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('input', (e) => {
|
||||
const searchTerm = e.target.value.toLowerCase();
|
||||
|
||||
if (!searchTerm) {
|
||||
cy.nodes().removeClass('search-hidden').show();
|
||||
cy.edges().show();
|
||||
return;
|
||||
}
|
||||
|
||||
cy.nodes().forEach(node => {
|
||||
const name = node.data('name').toLowerCase();
|
||||
const description = (node.data('description') || '').toLowerCase();
|
||||
|
||||
if (name.includes(searchTerm) || description.includes(searchTerm)) {
|
||||
node.removeClass('search-hidden').show();
|
||||
node.connectedEdges().show();
|
||||
} else {
|
||||
node.addClass('search-hidden').hide();
|
||||
// Kanten nur verstecken, wenn beide verbundenen Knoten versteckt sind
|
||||
node.connectedEdges().forEach(edge => {
|
||||
const otherNode = edge.source().id() === node.id() ? edge.target() : edge.source();
|
||||
if (otherNode.hasClass('search-hidden')) {
|
||||
edge.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Mindmap erfolgreich initialisiert');
|
||||
})();
|
||||
572
static/js/modules/chatgpt-assistant.js
Normal file
572
static/js/modules/chatgpt-assistant.js
Normal file
@@ -0,0 +1,572 @@
|
||||
/**
|
||||
* ChatGPT Assistent Modul
|
||||
* Verwaltet die Interaktion mit der OpenAI API und die Benutzeroberfläche des Assistenten
|
||||
*/
|
||||
|
||||
class ChatGPTAssistant {
|
||||
constructor() {
|
||||
this.messages = [];
|
||||
this.isOpen = false;
|
||||
this.isLoading = false;
|
||||
this.container = null;
|
||||
this.chatHistory = null;
|
||||
this.inputField = null;
|
||||
this.suggestionArea = null;
|
||||
this.maxRetries = 2;
|
||||
this.retryCount = 0;
|
||||
this.markdownParser = null;
|
||||
this.initializeMarkdownParser();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialisiert den Markdown-Parser
|
||||
*/
|
||||
async initializeMarkdownParser() {
|
||||
// Dynamisch marked.js laden, wenn noch nicht vorhanden
|
||||
if (!window.marked) {
|
||||
try {
|
||||
// Prüfen, ob marked.js bereits im Dokument geladen ist
|
||||
if (!document.querySelector('script[src*="marked"]')) {
|
||||
// Falls nicht, Script-Tag erstellen und einfügen
|
||||
const script = document.createElement('script');
|
||||
script.src = 'https://cdn.jsdelivr.net/npm/marked/marked.min.js';
|
||||
script.async = true;
|
||||
|
||||
// Promise erstellen, das resolved wird, wenn das Script geladen wurde
|
||||
await new Promise((resolve, reject) => {
|
||||
script.onload = resolve;
|
||||
script.onerror = reject;
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
|
||||
console.log('Marked.js erfolgreich geladen');
|
||||
}
|
||||
|
||||
// Marked konfigurieren
|
||||
this.markdownParser = window.marked;
|
||||
this.markdownParser.setOptions({
|
||||
gfm: true,
|
||||
breaks: true,
|
||||
sanitize: true,
|
||||
smartLists: true,
|
||||
smartypants: true
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden von marked.js:', error);
|
||||
// Fallback-Parser, der nur einfache Absätze erkennt
|
||||
this.markdownParser = {
|
||||
parse: (text) => {
|
||||
return text.split('\n').map(line => {
|
||||
if (line.trim() === '') return '<br>';
|
||||
return `<p>${line}</p>`;
|
||||
}).join('');
|
||||
}
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// Marked ist bereits geladen
|
||||
this.markdownParser = window.marked;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialisiert den Assistenten und fügt die UI zum DOM hinzu
|
||||
*/
|
||||
init() {
|
||||
// Assistent-Container erstellen
|
||||
this.createAssistantUI();
|
||||
|
||||
// Event-Listener hinzufügen
|
||||
this.setupEventListeners();
|
||||
|
||||
// Ersten Willkommensnachricht anzeigen
|
||||
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!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt die UI-Elemente für den Assistenten
|
||||
*/
|
||||
createAssistantUI() {
|
||||
// Hauptcontainer erstellen
|
||||
this.container = document.createElement('div');
|
||||
this.container.id = 'chatgpt-assistant';
|
||||
this.container.className = 'fixed bottom-4 right-4 z-50 flex flex-col';
|
||||
|
||||
// Button zum Öffnen/Schließen des Assistenten
|
||||
const toggleButton = document.createElement('button');
|
||||
toggleButton.id = 'assistant-toggle';
|
||||
toggleButton.className = 'ml-auto bg-primary-600 hover:bg-primary-700 text-white rounded-full p-3 shadow-lg transition-all duration-300 mb-2';
|
||||
toggleButton.innerHTML = '<i class="fas fa-robot text-xl"></i>';
|
||||
|
||||
// Chat-Container
|
||||
const chatContainer = document.createElement('div');
|
||||
chatContainer.id = 'assistant-chat';
|
||||
chatContainer.className = 'bg-white dark:bg-dark-800 rounded-lg shadow-xl overflow-hidden transition-all duration-300 w-80 md:w-96 max-h-0 opacity-0';
|
||||
|
||||
// Chat-Header
|
||||
const header = document.createElement('div');
|
||||
header.className = 'bg-primary-600 text-white p-3 flex items-center justify-between';
|
||||
header.innerHTML = `
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-robot mr-2"></i>
|
||||
<span>KI-Assistent (4o-mini)</span>
|
||||
</div>
|
||||
<button id="assistant-close" class="text-white hover:text-gray-200">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
`;
|
||||
|
||||
// Chat-Verlauf
|
||||
this.chatHistory = document.createElement('div');
|
||||
this.chatHistory.id = 'assistant-history';
|
||||
this.chatHistory.className = 'p-3 overflow-y-auto max-h-96 space-y-3';
|
||||
|
||||
// Vorschlagsbereich
|
||||
this.suggestionArea = document.createElement('div');
|
||||
this.suggestionArea.id = 'assistant-suggestions';
|
||||
this.suggestionArea.className = 'px-3 pb-2 flex flex-wrap gap-2 overflow-x-auto hidden';
|
||||
|
||||
// Chat-Eingabe
|
||||
const inputContainer = document.createElement('div');
|
||||
inputContainer.className = 'border-t border-gray-200 dark:border-dark-600 p-3 flex items-center';
|
||||
|
||||
this.inputField = document.createElement('input');
|
||||
this.inputField.type = 'text';
|
||||
this.inputField.placeholder = 'Stelle eine Frage zur Wissensdatenbank...';
|
||||
this.inputField.className = 'flex-1 border border-gray-300 dark:border-dark-600 dark:bg-dark-700 dark:text-white rounded-l-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary-500';
|
||||
|
||||
const sendButton = document.createElement('button');
|
||||
sendButton.id = 'assistant-send';
|
||||
sendButton.className = 'bg-primary-600 hover:bg-primary-700 text-white px-4 py-2 rounded-r-lg';
|
||||
sendButton.innerHTML = '<i class="fas fa-paper-plane"></i>';
|
||||
|
||||
// Elemente zusammenfügen
|
||||
inputContainer.appendChild(this.inputField);
|
||||
inputContainer.appendChild(sendButton);
|
||||
|
||||
chatContainer.appendChild(header);
|
||||
chatContainer.appendChild(this.chatHistory);
|
||||
chatContainer.appendChild(this.suggestionArea);
|
||||
chatContainer.appendChild(inputContainer);
|
||||
|
||||
this.container.appendChild(toggleButton);
|
||||
this.container.appendChild(chatContainer);
|
||||
|
||||
// Zum DOM hinzufügen
|
||||
document.body.appendChild(this.container);
|
||||
}
|
||||
|
||||
/**
|
||||
* Richtet Event-Listener für die Benutzeroberfläche ein
|
||||
*/
|
||||
setupEventListeners() {
|
||||
// Toggle-Button
|
||||
const toggleButton = document.getElementById('assistant-toggle');
|
||||
if (toggleButton) {
|
||||
toggleButton.addEventListener('click', () => this.toggleAssistant());
|
||||
}
|
||||
|
||||
// Schließen-Button
|
||||
const closeButton = document.getElementById('assistant-close');
|
||||
if (closeButton) {
|
||||
closeButton.addEventListener('click', () => this.toggleAssistant(false));
|
||||
}
|
||||
|
||||
// Senden-Button
|
||||
const sendButton = document.getElementById('assistant-send');
|
||||
if (sendButton) {
|
||||
sendButton.addEventListener('click', () => this.sendMessage());
|
||||
}
|
||||
|
||||
// Enter-Taste im Eingabefeld
|
||||
if (this.inputField) {
|
||||
this.inputField.addEventListener('keyup', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
this.sendMessage();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Vorschläge klickbar machen
|
||||
if (this.suggestionArea) {
|
||||
this.suggestionArea.addEventListener('click', (e) => {
|
||||
if (e.target.classList.contains('suggestion-pill')) {
|
||||
this.inputField.value = e.target.textContent;
|
||||
this.sendMessage();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Öffnet oder schließt den Assistenten
|
||||
* @param {boolean} state - Optional: erzwingt einen bestimmten Zustand
|
||||
*/
|
||||
toggleAssistant(state = null) {
|
||||
const chatContainer = document.getElementById('assistant-chat');
|
||||
if (!chatContainer) return;
|
||||
|
||||
this.isOpen = state !== null ? state : !this.isOpen;
|
||||
|
||||
if (this.isOpen) {
|
||||
chatContainer.classList.remove('max-h-0', 'opacity-0');
|
||||
chatContainer.classList.add('max-h-[32rem]', 'opacity-100');
|
||||
if (this.inputField) this.inputField.focus();
|
||||
|
||||
// Zeige Vorschläge wenn verfügbar
|
||||
if (this.suggestionArea && this.suggestionArea.children.length > 0) {
|
||||
this.suggestionArea.classList.remove('hidden');
|
||||
}
|
||||
} else {
|
||||
chatContainer.classList.remove('max-h-[32rem]', 'opacity-100');
|
||||
chatContainer.classList.add('max-h-0', 'opacity-0');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fügt eine Nachricht zum Chat-Verlauf hinzu
|
||||
* @param {string} sender - 'user' oder 'assistant'
|
||||
* @param {string} text - Nachrichtentext
|
||||
*/
|
||||
addMessage(sender, text) {
|
||||
// Nachricht zum Verlauf hinzufügen
|
||||
this.messages.push({ role: sender, content: text });
|
||||
|
||||
// DOM-Element erstellen
|
||||
const messageEl = document.createElement('div');
|
||||
messageEl.className = `flex ${sender === 'user' ? 'justify-end' : 'justify-start'}`;
|
||||
|
||||
const bubble = document.createElement('div');
|
||||
bubble.className = sender === 'user'
|
||||
? 'bg-primary-100 dark:bg-primary-900 text-gray-800 dark:text-white rounded-lg py-2 px-3 max-w-[85%]'
|
||||
: 'bg-gray-100 dark:bg-dark-700 text-gray-800 dark:text-white rounded-lg py-2 px-3 max-w-[85%]';
|
||||
|
||||
// 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);
|
||||
|
||||
if (this.chatHistory) {
|
||||
this.chatHistory.appendChild(messageEl);
|
||||
|
||||
// Scroll zum Ende des Verlaufs
|
||||
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
|
||||
*/
|
||||
async sendMessage() {
|
||||
if (!this.inputField) return;
|
||||
|
||||
const userInput = this.inputField.value.trim();
|
||||
if (!userInput || this.isLoading) return;
|
||||
|
||||
// Vorschläge ausblenden
|
||||
if (this.suggestionArea) {
|
||||
this.suggestionArea.classList.add('hidden');
|
||||
}
|
||||
|
||||
// Benutzernachricht anzeigen
|
||||
this.addMessage('user', userInput);
|
||||
|
||||
// Eingabefeld zurücksetzen
|
||||
this.inputField.value = '';
|
||||
|
||||
// Ladeindikator anzeigen
|
||||
this.isLoading = true;
|
||||
this.showLoadingIndicator();
|
||||
|
||||
try {
|
||||
console.log('Sende Anfrage an KI-Assistent API...');
|
||||
// Anfrage an den Server senden
|
||||
const response = await fetch('/api/assistant', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
messages: this.messages
|
||||
}),
|
||||
cache: 'no-cache', // Kein Cache verwenden
|
||||
credentials: 'same-origin' // Cookies senden
|
||||
});
|
||||
|
||||
// Ladeindikator entfernen
|
||||
this.removeLoadingIndicator();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Serverfehler: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('Antwort erhalten:', data);
|
||||
|
||||
// Antwort anzeigen
|
||||
if (data.response) {
|
||||
this.addMessage('assistant', data.response);
|
||||
|
||||
// Neue Vorschläge basierend auf dem aktuellen Kontext anzeigen
|
||||
this.generateContextualSuggestions();
|
||||
|
||||
// Erfolgreiche Anfrage zurücksetzen
|
||||
this.retryCount = 0;
|
||||
} else if (data.error) {
|
||||
this.addMessage('assistant', `Fehler: ${data.error}`);
|
||||
} else {
|
||||
throw new Error('Unerwartetes Antwortformat vom Server');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler bei der Kommunikation mit dem Assistenten:', error);
|
||||
|
||||
// Ladeindikator entfernen, falls noch vorhanden
|
||||
this.removeLoadingIndicator();
|
||||
|
||||
// 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.retryCount = 0; // Zurücksetzen für die nächste Anfrage
|
||||
}
|
||||
} finally {
|
||||
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
|
||||
*/
|
||||
showLoadingIndicator() {
|
||||
if (!this.chatHistory) return;
|
||||
|
||||
// Entferne vorhandenen Ladeindikator (falls vorhanden)
|
||||
this.removeLoadingIndicator();
|
||||
|
||||
const loadingEl = document.createElement('div');
|
||||
loadingEl.id = 'assistant-loading';
|
||||
loadingEl.className = 'flex justify-start';
|
||||
|
||||
const bubble = document.createElement('div');
|
||||
bubble.className = 'bg-gray-100 dark:bg-dark-700 text-gray-800 dark:text-white rounded-lg py-2 px-3';
|
||||
bubble.innerHTML = '<div class="typing-indicator"><span></span><span></span><span></span></div>';
|
||||
|
||||
loadingEl.appendChild(bubble);
|
||||
this.chatHistory.appendChild(loadingEl);
|
||||
|
||||
// Scroll zum Ende des Verlaufs
|
||||
this.chatHistory.scrollTop = this.chatHistory.scrollHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Entfernt den Ladeindikator aus dem Chat
|
||||
*/
|
||||
removeLoadingIndicator() {
|
||||
const loadingIndicator = document.getElementById('assistant-loading');
|
||||
if (loadingIndicator) {
|
||||
loadingIndicator.remove();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Öffnet den Assistenten und sendet eine vorgegebene Frage
|
||||
* @param {string} question - Die zu stellende Frage
|
||||
*/
|
||||
async sendQuestion(question) {
|
||||
if (!question || this.isLoading) return;
|
||||
|
||||
// Assistenten öffnen
|
||||
this.toggleAssistant(true);
|
||||
|
||||
// Kurze Verzögerung, um sicherzustellen, dass der UI vollständig geöffnet ist
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
|
||||
// Frage in Eingabefeld setzen
|
||||
if (this.inputField) {
|
||||
this.inputField.value = question;
|
||||
|
||||
// Sende die Frage
|
||||
this.sendMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mache die Klasse global verfügbar
|
||||
window.ChatGPTAssistant = ChatGPTAssistant;
|
||||
777
static/js/modules/mindmap-page.js
Normal file
777
static/js/modules/mindmap-page.js
Normal file
@@ -0,0 +1,777 @@
|
||||
/**
|
||||
* Mindmap-Seite JavaScript
|
||||
* Spezifische Funktionen für die Mindmap-Seite
|
||||
*/
|
||||
|
||||
// Füge das Modul zum globalen MindMap-Objekt hinzu
|
||||
if (!window.MindMap) {
|
||||
window.MindMap = {};
|
||||
}
|
||||
|
||||
// Registriere den Initialisierer im MindMap-Objekt
|
||||
if (window.MindMap) {
|
||||
window.MindMap.pageInitializers = window.MindMap.pageInitializers || {};
|
||||
window.MindMap.pageInitializers.mindmap = initMindmapPage;
|
||||
}
|
||||
|
||||
// Initialisiere die Mindmap-Seite nur, wenn alle Abhängigkeiten vorhanden sind
|
||||
if (window.MindMap && typeof MindMapVisualization !== 'undefined') {
|
||||
if (document.body && document.body.dataset && document.body.dataset.page === 'mindmap') {
|
||||
window.MindMap.pageInitializers = window.MindMap.pageInitializers || {};
|
||||
window.MindMap.pageInitializers.mindmap = initMindmapPage;
|
||||
initMindmapPage();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Prüfe, ob wir auf der Mindmap-Seite sind und initialisiere
|
||||
if (document.body && document.body.dataset && document.body.dataset.page === 'mindmap') {
|
||||
initMindmapPage();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Initialisiert die Mindmap-Seite
|
||||
*/
|
||||
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 thoughtsContainer = document.getElementById('thoughts-container');
|
||||
|
||||
if (!mindmapContainer) {
|
||||
console.error('Mindmap-Container nicht gefunden!');
|
||||
return;
|
||||
}
|
||||
console.log('Mindmap-Container gefunden:', mindmapContainer);
|
||||
|
||||
// Prüfe, ob D3.js geladen ist
|
||||
if (typeof d3 === 'undefined') {
|
||||
console.error('D3.js ist nicht geladen!');
|
||||
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">D3.js konnte nicht geladen werden. Bitte laden Sie die Seite neu.</p>
|
||||
</div>
|
||||
`;
|
||||
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
|
||||
try {
|
||||
console.log('Versuche, MindMapVisualization zu erstellen...');
|
||||
const mindmap = new MindMapVisualization('#mindmap-container', {
|
||||
height: 600,
|
||||
onNodeClick: handleNodeClick
|
||||
});
|
||||
|
||||
// Globale Referenz für die Zoom-Buttons erstellen
|
||||
window.mindmapInstance = mindmap;
|
||||
|
||||
// Lade die Mindmap-Daten
|
||||
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
|
||||
const searchInput = document.getElementById('mindmap-search');
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('input', function(e) {
|
||||
mindmap.filterBySearchTerm(e.target.value);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Behandelt Klicks auf Mindmap-Knoten
|
||||
*/
|
||||
async function handleNodeClick(node) {
|
||||
if (!thoughtsContainer) return;
|
||||
|
||||
// Zeige Lade-Animation
|
||||
thoughtsContainer.innerHTML = `
|
||||
<div class="flex justify-center items-center p-12">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-primary-400"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
try {
|
||||
// Lade Gedanken für den ausgewählten Knoten
|
||||
const response = await fetch(`/api/nodes/${node.id}/thoughts`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const thoughts = await response.json();
|
||||
|
||||
// Gedanken anzeigen
|
||||
renderThoughts(thoughts, node.name);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Gedanken:', error);
|
||||
thoughtsContainer.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 Laden der Gedanken.</p>
|
||||
<p class="text-gray-300">Bitte versuchen Sie es später erneut.</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendert die Gedanken in den Container
|
||||
*/
|
||||
function renderThoughts(thoughts, nodeName) {
|
||||
// Wenn keine Gedanken vorhanden sind
|
||||
if (thoughts.length === 0) {
|
||||
thoughtsContainer.innerHTML = `
|
||||
<div class="glass-effect p-6 text-center">
|
||||
<div class="text-blue-400 mb-4">
|
||||
<i class="fa-solid fa-info-circle text-4xl"></i>
|
||||
</div>
|
||||
<p class="text-xl">Keine Gedanken für "${nodeName}" vorhanden.</p>
|
||||
<button id="add-thought-btn" class="btn-primary mt-4">
|
||||
<i class="fa-solid fa-plus mr-2"></i> Gedanke hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Event-Listener für den Button
|
||||
document.getElementById('add-thought-btn').addEventListener('click', () => {
|
||||
openAddThoughtModal(nodeName);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Gedanken anzeigen
|
||||
thoughtsContainer.innerHTML = `
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h2 class="text-xl font-bold text-white">Gedanken zu "${nodeName}"</h2>
|
||||
<button id="add-thought-btn" class="btn-primary">
|
||||
<i class="fa-solid fa-plus mr-2"></i> Neuer Gedanke
|
||||
</button>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-4" id="thoughts-grid"></div>
|
||||
`;
|
||||
|
||||
// Button-Event-Listener
|
||||
document.getElementById('add-thought-btn').addEventListener('click', () => {
|
||||
openAddThoughtModal(nodeName);
|
||||
});
|
||||
|
||||
// Gedanken-Karten rendern
|
||||
const thoughtsGrid = document.getElementById('thoughts-grid');
|
||||
thoughts.forEach((thought, index) => {
|
||||
const card = createThoughtCard(thought);
|
||||
|
||||
// Animation verzögern für gestaffeltes Erscheinen
|
||||
setTimeout(() => {
|
||||
card.classList.add('opacity-100');
|
||||
card.classList.remove('opacity-0', 'translate-y-4');
|
||||
}, index * 100);
|
||||
|
||||
thoughtsGrid.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt eine Gedanken-Karte
|
||||
*/
|
||||
function createThoughtCard(thought) {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card transition-all duration-300 opacity-0 translate-y-4 transform hover:shadow-lg border-l-4';
|
||||
card.style.borderLeftColor = thought.color_code || '#4080ff';
|
||||
|
||||
// Karten-Inhalt
|
||||
card.innerHTML = `
|
||||
<div class="p-4">
|
||||
<div class="flex justify-between items-start">
|
||||
<h3 class="text-lg font-bold text-white">${thought.title}</h3>
|
||||
<div class="text-sm text-gray-400">${thought.timestamp}</div>
|
||||
</div>
|
||||
<div class="prose dark:prose-invert mt-2">
|
||||
<p>${thought.content}</p>
|
||||
</div>
|
||||
${thought.keywords ? `
|
||||
<div class="flex flex-wrap gap-1 mt-3">
|
||||
${thought.keywords.split(',').map(keyword =>
|
||||
`<span class="px-2 py-1 text-xs rounded-full bg-secondary-700 text-white">${keyword.trim()}</span>`
|
||||
).join('')}
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="mt-4 flex justify-between items-center">
|
||||
<div class="text-sm text-gray-400">
|
||||
<i class="fa-solid fa-user mr-1"></i> ${thought.author}
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<button class="text-sm px-2 py-1 rounded hover:bg-white/10 transition-colors"
|
||||
onclick="showComments(${thought.id})">
|
||||
<i class="fa-solid fa-comments mr-1"></i> Kommentare
|
||||
</button>
|
||||
<button class="text-sm px-2 py-1 rounded hover:bg-white/10 transition-colors"
|
||||
onclick="showRelations(${thought.id})">
|
||||
<i class="fa-solid fa-diagram-project mr-1"></i> Beziehungen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
/**
|
||||
* Öffnet das Modal zum Hinzufügen eines neuen Gedankens
|
||||
*/
|
||||
function openAddThoughtModal(nodeName) {
|
||||
// Node-Information extrahieren
|
||||
let nodeId, nodeTitle;
|
||||
|
||||
if (typeof nodeName === 'string') {
|
||||
// Wenn nur ein String übergeben wurde
|
||||
nodeTitle = nodeName;
|
||||
// Versuche nodeId aus der Mindmap zu finden
|
||||
const nodeElement = d3.selectAll('.node-group').filter(d => d.name === nodeName);
|
||||
if (nodeElement.size() > 0) {
|
||||
nodeId = nodeElement.datum().id;
|
||||
}
|
||||
} else if (typeof nodeName === 'object') {
|
||||
// Wenn ein Node-Objekt übergeben wurde
|
||||
nodeId = nodeName.id;
|
||||
nodeTitle = nodeName.name;
|
||||
} else {
|
||||
console.error('Ungültiger Node-Parameter', nodeName);
|
||||
return;
|
||||
}
|
||||
|
||||
// Modal-Struktur erstellen
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'fixed inset-0 z-50 flex items-center justify-center p-4';
|
||||
modal.innerHTML = `
|
||||
<div class="absolute inset-0 bg-black/50 backdrop-blur-sm" id="modal-backdrop"></div>
|
||||
<div class="glass-effect relative rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto z-10 transform transition-all">
|
||||
<div class="p-6">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-xl font-bold text-white flex items-center">
|
||||
<span class="w-3 h-3 rounded-full bg-primary-400 mr-2"></span>
|
||||
Neuer Gedanke zu "${nodeTitle}"
|
||||
</h3>
|
||||
<button id="close-modal-btn" class="text-gray-400 hover:text-white transition-colors">
|
||||
<i class="fa-solid fa-xmark text-xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
<form id="add-thought-form" class="space-y-4">
|
||||
<input type="hidden" id="node_id" name="node_id" value="${nodeId || ''}">
|
||||
<div>
|
||||
<label for="title" class="block text-sm font-medium text-gray-300">Titel</label>
|
||||
<input type="text" id="title" name="title" required
|
||||
class="mt-1 block w-full rounded-md bg-dark-700 border border-dark-500 text-white p-2.5 focus:ring-2 focus:ring-primary-500 focus:border-transparent">
|
||||
</div>
|
||||
<div>
|
||||
<label for="content" class="block text-sm font-medium text-gray-300">Inhalt</label>
|
||||
<textarea id="content" name="content" rows="5" required
|
||||
class="mt-1 block w-full rounded-md bg-dark-700 border border-dark-500 text-white p-2.5 focus:ring-2 focus:ring-primary-500 focus:border-transparent"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label for="keywords" class="block text-sm font-medium text-gray-300">Schlüsselwörter (kommagetrennt)</label>
|
||||
<input type="text" id="keywords" name="keywords"
|
||||
class="mt-1 block w-full rounded-md bg-dark-700 border border-dark-500 text-white p-2.5 focus:ring-2 focus:ring-primary-500 focus:border-transparent">
|
||||
</div>
|
||||
<div>
|
||||
<label for="abstract" class="block text-sm font-medium text-gray-300">Zusammenfassung (optional)</label>
|
||||
<textarea id="abstract" name="abstract" rows="2"
|
||||
class="mt-1 block w-full rounded-md bg-dark-700 border border-dark-500 text-white p-2.5 focus:ring-2 focus:ring-primary-500 focus:border-transparent"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label for="color_code" class="block text-sm font-medium text-gray-300">Farbcode</label>
|
||||
<div class="flex space-x-2 mt-1">
|
||||
<input type="color" id="color_code" name="color_code" value="#4080ff"
|
||||
class="h-10 w-10 rounded bg-dark-700 border border-dark-500">
|
||||
<select id="predefined_colors"
|
||||
class="block flex-grow rounded-md bg-dark-700 border border-dark-500 text-white p-2.5">
|
||||
<option value="#4080ff">Blau</option>
|
||||
<option value="#a040ff">Lila</option>
|
||||
<option value="#40bf80">Grün</option>
|
||||
<option value="#ff4080">Rot</option>
|
||||
<option value="#ffaa00">Orange</option>
|
||||
<option value="#00ccff">Türkis</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between pt-4">
|
||||
<div class="flex items-center">
|
||||
<div class="relative">
|
||||
<button type="button" id="open-relation-btn" class="btn-outline text-sm pl-3 pr-9">
|
||||
<i class="fa-solid fa-diagram-project mr-2"></i> Verbindung
|
||||
<i class="fa-solid fa-chevron-down absolute right-3 top-1/2 transform -translate-y-1/2"></i>
|
||||
</button>
|
||||
<div id="relation-menu" class="absolute left-0 mt-2 w-60 rounded-md shadow-lg bg-dark-800 ring-1 ring-black ring-opacity-5 z-10 hidden">
|
||||
<div class="py-1">
|
||||
<div class="px-3 py-2 text-xs font-semibold text-gray-400 border-b border-dark-600">BEZIEHUNGSTYPEN</div>
|
||||
<div class="max-h-48 overflow-y-auto">
|
||||
<button type="button" class="relation-type-btn w-full text-left px-4 py-2 text-sm text-white hover:bg-dark-600" data-type="supports">
|
||||
<i class="fa-solid fa-circle-arrow-up text-green-400 mr-2"></i> Stützt
|
||||
</button>
|
||||
<button type="button" class="relation-type-btn w-full text-left px-4 py-2 text-sm text-white hover:bg-dark-600" data-type="contradicts">
|
||||
<i class="fa-solid fa-circle-arrow-down text-red-400 mr-2"></i> Widerspricht
|
||||
</button>
|
||||
<button type="button" class="relation-type-btn w-full text-left px-4 py-2 text-sm text-white hover:bg-dark-600" data-type="builds_upon">
|
||||
<i class="fa-solid fa-arrow-right text-blue-400 mr-2"></i> Baut auf auf
|
||||
</button>
|
||||
<button type="button" class="relation-type-btn w-full text-left px-4 py-2 text-sm text-white hover:bg-dark-600" data-type="generalizes">
|
||||
<i class="fa-solid fa-arrow-up-wide-short text-purple-400 mr-2"></i> Verallgemeinert
|
||||
</button>
|
||||
<button type="button" class="relation-type-btn w-full text-left px-4 py-2 text-sm text-white hover:bg-dark-600" data-type="specifies">
|
||||
<i class="fa-solid fa-arrow-down-wide-short text-yellow-400 mr-2"></i> Spezifiziert
|
||||
</button>
|
||||
<button type="button" class="relation-type-btn w-full text-left px-4 py-2 text-sm text-white hover:bg-dark-600" data-type="inspires">
|
||||
<i class="fa-solid fa-lightbulb text-amber-400 mr-2"></i> Inspiriert
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" id="relation_type" name="relation_type" value="">
|
||||
<input type="hidden" id="relation_target" name="relation_target" value="">
|
||||
</div>
|
||||
<div class="flex space-x-3">
|
||||
<button type="button" id="cancel-btn" class="btn-outline">Abbrechen</button>
|
||||
<button type="submit" class="btn-primary">
|
||||
<i class="fa-solid fa-save mr-2"></i> Speichern
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(modal);
|
||||
|
||||
// Focus auf das erste Feld setzen
|
||||
setTimeout(() => {
|
||||
modal.querySelector('#title').focus();
|
||||
}, 100);
|
||||
|
||||
// Event-Listener hinzufügen
|
||||
modal.querySelector('#modal-backdrop').addEventListener('click', closeModal);
|
||||
modal.querySelector('#close-modal-btn').addEventListener('click', closeModal);
|
||||
modal.querySelector('#cancel-btn').addEventListener('click', closeModal);
|
||||
|
||||
// Farbauswahl-Event-Listener
|
||||
const colorInput = modal.querySelector('#color_code');
|
||||
const predefinedColors = modal.querySelector('#predefined_colors');
|
||||
|
||||
predefinedColors.addEventListener('change', function() {
|
||||
colorInput.value = this.value;
|
||||
});
|
||||
|
||||
// Beziehungsmenü-Funktionalität
|
||||
const relationBtn = modal.querySelector('#open-relation-btn');
|
||||
const relationMenu = modal.querySelector('#relation-menu');
|
||||
|
||||
relationBtn.addEventListener('click', function() {
|
||||
relationMenu.classList.toggle('hidden');
|
||||
});
|
||||
|
||||
// Klick außerhalb des Menüs schließt es
|
||||
document.addEventListener('click', function(event) {
|
||||
if (!relationBtn.contains(event.target) && !relationMenu.contains(event.target)) {
|
||||
relationMenu.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
// Beziehungstyp-Auswahl
|
||||
const relationTypeBtns = modal.querySelectorAll('.relation-type-btn');
|
||||
const relationTypeInput = modal.querySelector('#relation_type');
|
||||
|
||||
relationTypeBtns.forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
const relationType = this.dataset.type;
|
||||
relationTypeInput.value = relationType;
|
||||
|
||||
// Sichtbare Anzeige aktualisieren
|
||||
relationBtn.innerHTML = `
|
||||
<i class="fa-solid fa-diagram-project mr-2"></i>
|
||||
${this.innerText.trim()}
|
||||
<i class="fa-solid fa-chevron-down absolute right-3 top-1/2 transform -translate-y-1/2"></i>
|
||||
`;
|
||||
|
||||
// Menü schließen
|
||||
relationMenu.classList.add('hidden');
|
||||
});
|
||||
});
|
||||
|
||||
// Form-Submit-Handler
|
||||
const form = modal.querySelector('#add-thought-form');
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(form);
|
||||
const thoughtData = {
|
||||
node_id: formData.get('node_id'),
|
||||
title: formData.get('title'),
|
||||
content: formData.get('content'),
|
||||
keywords: formData.get('keywords'),
|
||||
abstract: formData.get('abstract'),
|
||||
color_code: formData.get('color_code'),
|
||||
relation_type: formData.get('relation_type'),
|
||||
relation_target: formData.get('relation_target')
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/thoughts', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(thoughtData)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Fehler beim Speichern des Gedankens.');
|
||||
}
|
||||
|
||||
// Modal schließen
|
||||
closeModal();
|
||||
|
||||
// Gedanken neu laden
|
||||
if (nodeId) {
|
||||
handleNodeClick({ id: nodeId, name: nodeTitle });
|
||||
}
|
||||
|
||||
// Erfolgsbenachrichtigung
|
||||
if (window.MindMap && window.MindMap.showNotification) {
|
||||
window.MindMap.showNotification('Gedanke erfolgreich gespeichert.', 'success');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Speichern:', error);
|
||||
if (window.MindMap && window.MindMap.showNotification) {
|
||||
window.MindMap.showNotification('Fehler beim Speichern des Gedankens.', 'error');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Modal schließen
|
||||
function closeModal() {
|
||||
modal.classList.add('opacity-0');
|
||||
setTimeout(() => {
|
||||
modal.remove();
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Füge globale Funktionen für das Mindmap-Objekt hinzu
|
||||
*/
|
||||
window.showComments = async function(thoughtId) {
|
||||
try {
|
||||
// Lade-Animation erstellen
|
||||
const modal = createModalWithLoading('Kommentare werden geladen...');
|
||||
document.body.appendChild(modal);
|
||||
|
||||
// Kommentare laden
|
||||
const response = await fetch(`/api/comments/${thoughtId}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const comments = await response.json();
|
||||
|
||||
// Modal mit Kommentaren aktualisieren
|
||||
updateModalWithComments(modal, comments, thoughtId);
|
||||
|
||||
} catch (error) {
|
||||
console.error('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.');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Zeigt die Beziehungen eines Gedankens an
|
||||
*/
|
||||
window.showRelations = async function(thoughtId) {
|
||||
try {
|
||||
// Lade-Animation erstellen
|
||||
const modal = createModalWithLoading('Beziehungen werden geladen...');
|
||||
document.body.appendChild(modal);
|
||||
|
||||
// Beziehungen laden
|
||||
const response = await fetch(`/api/thoughts/${thoughtId}/relations`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const relations = await response.json();
|
||||
|
||||
// Modal mit Beziehungen aktualisieren
|
||||
updateModalWithRelations(modal, relations, thoughtId);
|
||||
|
||||
} catch (error) {
|
||||
console.error('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.');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Erstellt ein Modal mit Lade-Animation
|
||||
*/
|
||||
function createModalWithLoading(loadingText) {
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'fixed inset-0 z-50 flex items-center justify-center p-4';
|
||||
modal.innerHTML = `
|
||||
<div class="absolute inset-0 bg-black/50" id="modal-backdrop"></div>
|
||||
<div class="glass-effect relative rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto z-10">
|
||||
<div class="p-6 text-center">
|
||||
<div class="flex justify-center mb-4">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-primary-400"></div>
|
||||
</div>
|
||||
<p class="text-lg text-white">${loadingText}</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Event-Listener zum Schließen
|
||||
modal.querySelector('#modal-backdrop').addEventListener('click', () => {
|
||||
modal.remove();
|
||||
});
|
||||
|
||||
return modal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiert das Modal mit Kommentaren
|
||||
*/
|
||||
function updateModalWithComments(modal, comments, thoughtId) {
|
||||
const modalContent = modal.querySelector('.glass-effect');
|
||||
|
||||
modalContent.innerHTML = `
|
||||
<div class="p-6">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-xl font-bold text-white">Kommentare</h3>
|
||||
<button id="close-modal-btn" class="text-gray-400 hover:text-white">
|
||||
<i class="fa-solid fa-xmark text-xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="comments-list mb-6 space-y-4">
|
||||
${comments.length === 0 ?
|
||||
'<div class="text-center text-gray-400 py-4">Keine Kommentare vorhanden.</div>' :
|
||||
comments.map(comment => `
|
||||
<div class="glass-effect p-3 rounded">
|
||||
<div class="flex justify-between items-start">
|
||||
<div class="font-medium text-white">${comment.author}</div>
|
||||
<div class="text-xs text-gray-400">${comment.timestamp}</div>
|
||||
</div>
|
||||
<p class="mt-2 text-gray-200">${comment.content}</p>
|
||||
</div>
|
||||
`).join('')
|
||||
}
|
||||
</div>
|
||||
|
||||
<form id="comment-form" class="space-y-3">
|
||||
<div>
|
||||
<label for="comment-content" class="block text-sm font-medium text-gray-300">Neuer Kommentar</label>
|
||||
<textarea id="comment-content" name="content" rows="3" required
|
||||
class="mt-1 block w-full rounded-md bg-dark-700 border-dark-500 text-white"></textarea>
|
||||
</div>
|
||||
<div class="flex justify-end pt-2">
|
||||
<button type="submit" class="btn-primary">
|
||||
<i class="fa-solid fa-paper-plane mr-2"></i> Senden
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Event-Listener hinzufügen
|
||||
modalContent.querySelector('#close-modal-btn').addEventListener('click', () => {
|
||||
modal.remove();
|
||||
});
|
||||
|
||||
// Kommentar-Formular
|
||||
const form = modalContent.querySelector('#comment-form');
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const content = form.elements.content.value;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/comments', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
thought_id: thoughtId,
|
||||
content: content
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Fehler beim Speichern des Kommentars.');
|
||||
}
|
||||
|
||||
// Modal schließen
|
||||
modal.remove();
|
||||
|
||||
// Erfolgsbenachrichtigung
|
||||
MindMap.showNotification('Kommentar erfolgreich gespeichert.', 'success');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Speichern des Kommentars:', error);
|
||||
MindMap.showNotification('Fehler beim Speichern des Kommentars.', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiert das Modal mit Beziehungen
|
||||
*/
|
||||
function updateModalWithRelations(modal, relations, thoughtId) {
|
||||
const modalContent = modal.querySelector('.glass-effect');
|
||||
|
||||
modalContent.innerHTML = `
|
||||
<div class="p-6">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-xl font-bold text-white">Beziehungen</h3>
|
||||
<button id="close-modal-btn" class="text-gray-400 hover:text-white">
|
||||
<i class="fa-solid fa-xmark text-xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="relations-list mb-6 space-y-4">
|
||||
${relations.length === 0 ?
|
||||
'<div class="text-center text-gray-400 py-4">Keine Beziehungen vorhanden.</div>' :
|
||||
relations.map(relation => `
|
||||
<div class="glass-effect p-3 rounded">
|
||||
<div class="flex items-center">
|
||||
<span class="inline-block px-2 py-1 rounded-full text-xs font-medium bg-primary-600 text-white">
|
||||
${relation.relation_type}
|
||||
</span>
|
||||
<div class="ml-3">
|
||||
<div class="text-white">Ziel: Gedanke #${relation.target_id}</div>
|
||||
<div class="text-xs text-gray-400">Erstellt von ${relation.created_by} am ${relation.created_at}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('')
|
||||
}
|
||||
</div>
|
||||
|
||||
<form id="relation-form" class="space-y-3">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="target_id" class="block text-sm font-medium text-gray-300">Ziel-Gedanke ID</label>
|
||||
<input type="number" id="target_id" name="target_id" required
|
||||
class="mt-1 block w-full rounded-md bg-dark-700 border-dark-500 text-white">
|
||||
</div>
|
||||
<div>
|
||||
<label for="relation_type" class="block text-sm font-medium text-gray-300">Beziehungstyp</label>
|
||||
<select id="relation_type" name="relation_type" required
|
||||
class="mt-1 block w-full rounded-md bg-dark-700 border-dark-500 text-white">
|
||||
<option value="SUPPORTS">Stützt</option>
|
||||
<option value="CONTRADICTS">Widerspricht</option>
|
||||
<option value="BUILDS_UPON">Baut auf auf</option>
|
||||
<option value="GENERALIZES">Verallgemeinert</option>
|
||||
<option value="SPECIFIES">Spezifiziert</option>
|
||||
<option value="INSPIRES">Inspiriert</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end pt-2">
|
||||
<button type="submit" class="btn-primary">
|
||||
<i class="fa-solid fa-plus mr-2"></i> Beziehung erstellen
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Event-Listener hinzufügen
|
||||
modalContent.querySelector('#close-modal-btn').addEventListener('click', () => {
|
||||
modal.remove();
|
||||
});
|
||||
|
||||
// Beziehungs-Formular
|
||||
const form = modalContent.querySelector('#relation-form');
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = {
|
||||
source_id: thoughtId,
|
||||
target_id: parseInt(form.elements.target_id.value),
|
||||
relation_type: form.elements.relation_type.value
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/relations', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Fehler beim Erstellen der Beziehung.');
|
||||
}
|
||||
|
||||
// Modal schließen
|
||||
modal.remove();
|
||||
|
||||
// Erfolgsbenachrichtigung
|
||||
MindMap.showNotification('Beziehung erfolgreich erstellt.', 'success');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Erstellen der Beziehung:', error);
|
||||
MindMap.showNotification('Fehler beim Erstellen der Beziehung.', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
844
static/js/modules/mindmap.js
Normal file
844
static/js/modules/mindmap.js
Normal file
@@ -0,0 +1,844 @@
|
||||
/**
|
||||
* MindMap D3.js Modul
|
||||
* Visualisiert die Mindmap mit D3.js
|
||||
*/
|
||||
|
||||
class MindMapVisualization {
|
||||
constructor(containerSelector, options = {}) {
|
||||
this.containerSelector = containerSelector;
|
||||
this.container = d3.select(containerSelector);
|
||||
this.width = options.width || this.container.node().clientWidth || 800;
|
||||
this.height = options.height || 600;
|
||||
this.nodeRadius = options.nodeRadius || 14;
|
||||
this.selectedNodeRadius = options.selectedNodeRadius || 20;
|
||||
this.linkDistance = options.linkDistance || 150;
|
||||
this.chargeStrength = options.chargeStrength || -900;
|
||||
this.centerForce = options.centerForce || 0.15;
|
||||
this.onNodeClick = options.onNodeClick || ((node) => console.log('Node clicked:', node));
|
||||
|
||||
this.nodes = [];
|
||||
this.links = [];
|
||||
this.simulation = null;
|
||||
this.svg = null;
|
||||
this.linkElements = null;
|
||||
this.nodeElements = null;
|
||||
this.textElements = null;
|
||||
this.tooltipEnabled = options.tooltipEnabled !== undefined ? options.tooltipEnabled : true;
|
||||
|
||||
this.mouseoverNode = null;
|
||||
this.selectedNode = null;
|
||||
|
||||
this.zoomFactor = 1;
|
||||
this.tooltipDiv = null;
|
||||
this.isLoading = true;
|
||||
|
||||
// Lade die gemerkten Knoten
|
||||
this.bookmarkedNodes = this.loadBookmarkedNodes();
|
||||
|
||||
// Sicherstellen, dass der Container bereit ist
|
||||
if (this.container.node()) {
|
||||
this.init();
|
||||
this.setupDefaultNodes();
|
||||
|
||||
// Sofortige Datenladung
|
||||
window.setTimeout(() => {
|
||||
this.loadData();
|
||||
}, 100);
|
||||
} else {
|
||||
console.error('Mindmap-Container nicht gefunden:', containerSelector);
|
||||
}
|
||||
}
|
||||
|
||||
// Standardknoten als Fallback einrichten, falls die API nicht reagiert
|
||||
setupDefaultNodes() {
|
||||
// Basis-Mindmap mit Hauptthemen
|
||||
const defaultNodes = [
|
||||
{ id: "root", name: "Wissen", description: "Zentrale Wissensbasis", thought_count: 0 },
|
||||
{ id: "philosophy", name: "Philosophie", description: "Philosophisches Denken", thought_count: 0 },
|
||||
{ id: "science", name: "Wissenschaft", description: "Wissenschaftliche Erkenntnisse", thought_count: 0 },
|
||||
{ id: "technology", name: "Technologie", description: "Technologische Entwicklungen", thought_count: 0 },
|
||||
{ id: "arts", name: "Künste", description: "Künstlerische Ausdrucksformen", thought_count: 0 }
|
||||
];
|
||||
|
||||
const defaultLinks = [
|
||||
{ source: "root", target: "philosophy" },
|
||||
{ source: "root", target: "science" },
|
||||
{ source: "root", target: "technology" },
|
||||
{ source: "root", target: "arts" }
|
||||
];
|
||||
|
||||
// Als Fallback verwenden, falls die API fehlschlägt
|
||||
this.defaultNodes = defaultNodes;
|
||||
this.defaultLinks = defaultLinks;
|
||||
}
|
||||
|
||||
init() {
|
||||
// SVG erstellen, wenn noch nicht vorhanden
|
||||
if (!this.svg) {
|
||||
// Container zuerst leeren
|
||||
this.container.html('');
|
||||
|
||||
this.svg = this.container
|
||||
.append('svg')
|
||||
.attr('width', '100%')
|
||||
.attr('height', this.height)
|
||||
.attr('viewBox', `0 0 ${this.width} ${this.height}`)
|
||||
.attr('class', 'mindmap-svg')
|
||||
.call(
|
||||
d3.zoom()
|
||||
.scaleExtent([0.1, 5])
|
||||
.on('zoom', (event) => {
|
||||
this.handleZoom(event.transform);
|
||||
})
|
||||
);
|
||||
|
||||
// Hauptgruppe für alles, was zoom-transformierbar ist
|
||||
this.g = this.svg.append('g');
|
||||
|
||||
// Tooltip initialisieren
|
||||
if (!d3.select('body').select('.node-tooltip').size()) {
|
||||
this.tooltipDiv = d3.select('body')
|
||||
.append('div')
|
||||
.attr('class', 'node-tooltip')
|
||||
.style('opacity', 0)
|
||||
.style('position', 'absolute')
|
||||
.style('pointer-events', 'none')
|
||||
.style('background', 'rgba(20, 20, 40, 0.9)')
|
||||
.style('color', '#ffffff')
|
||||
.style('border', '1px solid rgba(160, 80, 255, 0.2)')
|
||||
.style('border-radius', '6px')
|
||||
.style('padding', '8px 12px')
|
||||
.style('font-size', '14px')
|
||||
.style('max-width', '250px')
|
||||
.style('box-shadow', '0 10px 25px rgba(0, 0, 0, 0.5), 0 0 10px rgba(160, 80, 255, 0.2)');
|
||||
} else {
|
||||
this.tooltipDiv = d3.select('body').select('.node-tooltip');
|
||||
}
|
||||
}
|
||||
|
||||
// Force-Simulation initialisieren
|
||||
this.simulation = d3.forceSimulation()
|
||||
.force('link', d3.forceLink().id(d => d.id).distance(this.linkDistance))
|
||||
.force('charge', d3.forceManyBody().strength(this.chargeStrength))
|
||||
.force('center', d3.forceCenter(this.width / 2, this.height / 2).strength(this.centerForce))
|
||||
.force('collision', d3.forceCollide().radius(this.nodeRadius * 2));
|
||||
|
||||
// Globale Mindmap-Instanz für externe Zugriffe setzen
|
||||
window.mindmapInstance = this;
|
||||
}
|
||||
|
||||
handleZoom(transform) {
|
||||
this.g.attr('transform', transform);
|
||||
this.zoomFactor = transform.k;
|
||||
|
||||
// Knotengröße anpassen, um bei Zoom lesbar zu bleiben
|
||||
if (this.nodeElements) {
|
||||
this.nodeElements
|
||||
.attr('r', d => (d === this.selectedNode ? this.selectedNodeRadius : this.nodeRadius) / Math.sqrt(transform.k));
|
||||
}
|
||||
|
||||
// Textgröße anpassen
|
||||
if (this.textElements) {
|
||||
this.textElements
|
||||
.style('font-size', `${12 / Math.sqrt(transform.k)}px`);
|
||||
}
|
||||
}
|
||||
|
||||
async loadData() {
|
||||
try {
|
||||
// Ladeindikator anzeigen
|
||||
this.showLoading();
|
||||
|
||||
// Verwende sofort die Standarddaten für eine schnelle erste Anzeige
|
||||
this.nodes = [...this.defaultNodes];
|
||||
this.links = [...this.defaultLinks];
|
||||
|
||||
// Visualisierung sofort aktualisieren
|
||||
this.isLoading = false;
|
||||
this.updateVisualization();
|
||||
|
||||
// Status auf bereit setzen - don't wait for API
|
||||
this.container.attr('data-status', 'ready');
|
||||
|
||||
// API-Aufruf mit kürzerem Timeout im Hintergrund durchführen
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 Sekunden Timeout
|
||||
|
||||
const response = await fetch('/api/mindmap', {
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache',
|
||||
'Pragma': 'no-cache'
|
||||
}
|
||||
});
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn(`HTTP Fehler: ${response.status}, 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
|
||||
}
|
||||
|
||||
// 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();
|
||||
|
||||
if (!data || !data.nodes || data.nodes.length === 0) {
|
||||
console.warn('Keine Mindmap-Daten vorhanden, verwende weiterhin Standard-Daten.');
|
||||
return; // Keep using default data
|
||||
}
|
||||
|
||||
// Flache Liste von Knoten und Verbindungen erstellen
|
||||
this.nodes = [];
|
||||
this.links = [];
|
||||
this.processHierarchicalData(data.nodes);
|
||||
|
||||
// Visualisierung aktualisieren mit den tatsächlichen Daten
|
||||
this.updateVisualization();
|
||||
|
||||
} catch (error) {
|
||||
console.warn('Fehler beim Laden der Mindmap-Daten, verwende Standarddaten:', error);
|
||||
// Already using default data, no action needed
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Kritischer Fehler bei der Mindmap-Darstellung:', error);
|
||||
this.showError('Fehler beim Laden der Mindmap-Daten. Bitte laden Sie die Seite neu.');
|
||||
this.container.attr('data-status', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
showLoading() {
|
||||
// Element nur leeren, wenn es noch kein SVG enthält
|
||||
if (!this.container.select('svg').size()) {
|
||||
this.container.html(`
|
||||
<div class="flex justify-center items-center h-full">
|
||||
<div class="text-center">
|
||||
<div class="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-primary-400 mx-auto mb-4"></div>
|
||||
<p class="text-lg text-white">Mindmap wird geladen...</p>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
processHierarchicalData(hierarchicalNodes, parentId = null) {
|
||||
hierarchicalNodes.forEach(node => {
|
||||
// Knoten hinzufügen, wenn noch nicht vorhanden
|
||||
if (!this.nodes.find(n => n.id === node.id)) {
|
||||
this.nodes.push({
|
||||
id: node.id,
|
||||
name: node.name,
|
||||
description: node.description || '',
|
||||
thought_count: node.thought_count || 0,
|
||||
color: this.generateColorFromString(node.name),
|
||||
});
|
||||
}
|
||||
|
||||
// Verbindung zum Elternknoten hinzufügen
|
||||
if (parentId !== null) {
|
||||
this.links.push({
|
||||
source: parentId,
|
||||
target: node.id
|
||||
});
|
||||
}
|
||||
|
||||
// Rekursiv für Kindknoten aufrufen
|
||||
if (node.children && node.children.length > 0) {
|
||||
this.processHierarchicalData(node.children, node.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
generateColorFromString(str) {
|
||||
// Erzeugt eine deterministische Farbe basierend auf dem String
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
||||
}
|
||||
|
||||
// Verwende deterministische Farbe aus unserem Farbschema
|
||||
const colors = [
|
||||
'#4080ff', // primary-400
|
||||
'#a040ff', // secondary-400
|
||||
'#205cf5', // primary-500
|
||||
'#8020f5', // secondary-500
|
||||
'#1040e0', // primary-600
|
||||
'#6010e0', // secondary-600
|
||||
];
|
||||
|
||||
return colors[Math.abs(hash) % colors.length];
|
||||
}
|
||||
|
||||
updateVisualization() {
|
||||
// Starte die Visualisierung nur, wenn nicht mehr im Ladezustand
|
||||
if (this.isLoading) return;
|
||||
|
||||
// Container leeren, wenn Diagramm neu erstellt wird
|
||||
if (!this.svg) {
|
||||
this.container.html('');
|
||||
this.init();
|
||||
}
|
||||
|
||||
// Performance-Optimierung: Deaktiviere Transition während des Datenladens
|
||||
const useTransitions = false;
|
||||
|
||||
// Links (Edges) erstellen
|
||||
this.linkElements = this.g.selectAll('.link')
|
||||
.data(this.links)
|
||||
.join(
|
||||
enter => enter.append('line')
|
||||
.attr('class', 'link')
|
||||
.attr('stroke', '#ffffff30')
|
||||
.attr('stroke-width', 2)
|
||||
.attr('stroke-dasharray', d => d.relation_type === 'contradicts' ? '5,5' : null)
|
||||
.attr('marker-end', d => d.relation_type === 'builds_upon' ? 'url(#arrowhead)' : null),
|
||||
update => update
|
||||
.attr('stroke', '#ffffff30')
|
||||
.attr('stroke-dasharray', d => d.relation_type === 'contradicts' ? '5,5' : null)
|
||||
.attr('marker-end', d => d.relation_type === 'builds_upon' ? 'url(#arrowhead)' : null),
|
||||
exit => exit.remove()
|
||||
);
|
||||
|
||||
// Pfeilspitze für gerichtete Beziehungen hinzufügen (falls noch nicht vorhanden)
|
||||
if (!this.svg.select('defs').node()) {
|
||||
const defs = this.svg.append('defs');
|
||||
defs.append('marker')
|
||||
.attr('id', 'arrowhead')
|
||||
.attr('viewBox', '0 -5 10 10')
|
||||
.attr('refX', 20)
|
||||
.attr('refY', 0)
|
||||
.attr('orient', 'auto')
|
||||
.attr('markerWidth', 6)
|
||||
.attr('markerHeight', 6)
|
||||
.append('path')
|
||||
.attr('d', 'M0,-5L10,0L0,5')
|
||||
.attr('fill', '#ffffff50');
|
||||
}
|
||||
|
||||
// Simplified Effekte definieren, falls noch nicht vorhanden
|
||||
if (!this.svg.select('#glow').node()) {
|
||||
const defs = this.svg.select('defs').size() ? this.svg.select('defs') : this.svg.append('defs');
|
||||
|
||||
// Glow-Effekt für Knoten
|
||||
const filter = defs.append('filter')
|
||||
.attr('id', 'glow')
|
||||
.attr('x', '-50%')
|
||||
.attr('y', '-50%')
|
||||
.attr('width', '200%')
|
||||
.attr('height', '200%');
|
||||
|
||||
filter.append('feGaussianBlur')
|
||||
.attr('stdDeviation', '1')
|
||||
.attr('result', 'blur');
|
||||
|
||||
filter.append('feComposite')
|
||||
.attr('in', 'SourceGraphic')
|
||||
.attr('in2', 'blur')
|
||||
.attr('operator', 'over');
|
||||
|
||||
// Blur-Effekt für Schatten
|
||||
const blurFilter = defs.append('filter')
|
||||
.attr('id', 'blur')
|
||||
.attr('x', '-50%')
|
||||
.attr('y', '-50%')
|
||||
.attr('width', '200%')
|
||||
.attr('height', '200%');
|
||||
|
||||
blurFilter.append('feGaussianBlur')
|
||||
.attr('stdDeviation', '1');
|
||||
}
|
||||
|
||||
// Knoten-Gruppe erstellen/aktualisieren
|
||||
const nodeGroups = this.g.selectAll('.node-group')
|
||||
.data(this.nodes)
|
||||
.join(
|
||||
enter => {
|
||||
const group = enter.append('g')
|
||||
.attr('class', 'node-group')
|
||||
.call(d3.drag()
|
||||
.on('start', (event, d) => this.dragStarted(event, d))
|
||||
.on('drag', (event, d) => this.dragged(event, d))
|
||||
.on('end', (event, d) => this.dragEnded(event, d)));
|
||||
|
||||
// Hintergrundschatten für besseren Kontrast
|
||||
group.append('circle')
|
||||
.attr('class', 'node-shadow')
|
||||
.attr('r', d => this.nodeRadius * 1.2)
|
||||
.attr('fill', 'rgba(0, 0, 0, 0.3)')
|
||||
.attr('filter', 'url(#blur)');
|
||||
|
||||
// Kreis für jeden Knoten
|
||||
group.append('circle')
|
||||
.attr('class', d => `node ${this.isNodeBookmarked(d.id) ? 'bookmarked' : ''}`)
|
||||
.attr('r', this.nodeRadius)
|
||||
.attr('fill', d => d.color || this.generateColorFromString(d.name))
|
||||
.attr('stroke', d => this.isNodeBookmarked(d.id) ? '#FFD700' : '#ffffff50')
|
||||
.attr('stroke-width', d => this.isNodeBookmarked(d.id) ? 3 : 2)
|
||||
.attr('filter', 'url(#glow)');
|
||||
|
||||
// Text-Label mit besserem Kontrast
|
||||
group.append('text')
|
||||
.attr('class', 'node-label')
|
||||
.attr('dy', '0.35em')
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('fill', '#ffffff')
|
||||
.attr('stroke', 'rgba(0, 0, 0, 0.4)')
|
||||
.attr('stroke-width', '0.7px')
|
||||
.attr('paint-order', 'stroke')
|
||||
.style('font-size', '12px')
|
||||
.style('font-weight', '500')
|
||||
.style('pointer-events', 'none')
|
||||
.text(d => d.name.length > 12 ? d.name.slice(0, 10) + '...' : d.name);
|
||||
|
||||
// Interaktivität hinzufügen
|
||||
group
|
||||
.on('mouseover', (event, d) => this.nodeMouseover(event, d))
|
||||
.on('mouseout', (event, d) => this.nodeMouseout(event, d))
|
||||
.on('click', (event, d) => this.nodeClicked(event, d));
|
||||
|
||||
return group;
|
||||
},
|
||||
update => {
|
||||
// Knoten aktualisieren
|
||||
update.select('.node')
|
||||
.attr('class', d => `node ${this.isNodeBookmarked(d.id) ? 'bookmarked' : ''}`)
|
||||
.attr('fill', d => d.color || this.generateColorFromString(d.name))
|
||||
.attr('stroke', d => this.isNodeBookmarked(d.id) ? '#FFD700' : '#ffffff50')
|
||||
.attr('stroke-width', d => this.isNodeBookmarked(d.id) ? 3 : 2);
|
||||
|
||||
// Text aktualisieren
|
||||
update.select('.node-label')
|
||||
.text(d => d.name.length > 12 ? d.name.slice(0, 10) + '...' : d.name);
|
||||
|
||||
return update;
|
||||
},
|
||||
exit => exit.remove()
|
||||
);
|
||||
|
||||
// Einzelne Elemente für direkten Zugriff speichern
|
||||
this.nodeElements = this.g.selectAll('.node');
|
||||
this.textElements = this.g.selectAll('.node-label');
|
||||
|
||||
// Performance-Optimierung: Weniger Simulationsschritte für schnellere Stabilisierung
|
||||
this.simulation
|
||||
.nodes(this.nodes)
|
||||
.on('tick', () => this.ticked())
|
||||
.alpha(0.3) // Reduzierter Wert für schnellere Stabilisierung
|
||||
.alphaDecay(0.05); // Erhöhter Wert für schnellere Stabilisierung
|
||||
|
||||
this.simulation.force('link')
|
||||
.links(this.links);
|
||||
|
||||
// Simulation neu starten
|
||||
this.simulation.restart();
|
||||
|
||||
// Update connection counts
|
||||
this.updateConnectionCounts();
|
||||
}
|
||||
|
||||
ticked() {
|
||||
// Linienpositionen aktualisieren
|
||||
this.linkElements
|
||||
.attr('x1', d => d.source.x)
|
||||
.attr('y1', d => d.source.y)
|
||||
.attr('x2', d => d.target.x)
|
||||
.attr('y2', d => d.target.y);
|
||||
|
||||
// Knotenpositionen aktualisieren
|
||||
this.g.selectAll('.node-group')
|
||||
.attr('transform', d => `translate(${d.x}, ${d.y})`);
|
||||
}
|
||||
|
||||
dragStarted(event, d) {
|
||||
if (!event.active) this.simulation.alphaTarget(0.3).restart();
|
||||
d.fx = d.x;
|
||||
d.fy = d.y;
|
||||
}
|
||||
|
||||
dragged(event, d) {
|
||||
d.fx = event.x;
|
||||
d.fy = event.y;
|
||||
}
|
||||
|
||||
dragEnded(event, d) {
|
||||
if (!event.active) this.simulation.alphaTarget(0);
|
||||
d.fx = null;
|
||||
d.fy = null;
|
||||
}
|
||||
|
||||
nodeMouseover(event, d) {
|
||||
this.mouseoverNode = d;
|
||||
|
||||
// Tooltip anzeigen
|
||||
if (this.tooltipEnabled) {
|
||||
const isBookmarked = this.isNodeBookmarked(d.id);
|
||||
const tooltipContent = `
|
||||
<div class="p-2">
|
||||
<strong>${d.name}</strong>
|
||||
${d.description ? `<p class="text-sm text-gray-200 mt-1">${d.description}</p>` : ''}
|
||||
<div class="text-xs text-gray-300 mt-1">
|
||||
Gedanken: ${d.thought_count}
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<button id="bookmark-button" class="px-2 py-1 text-xs rounded bg-gray-700 hover:bg-gray-600 text-white"
|
||||
data-nodeid="${d.id}">
|
||||
${isBookmarked ? '<i class="fas fa-bookmark mr-1"></i> Gemerkt' : '<i class="far fa-bookmark mr-1"></i> Merken'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.tooltipDiv
|
||||
.html(tooltipContent)
|
||||
.style('left', (event.pageX + 10) + 'px')
|
||||
.style('top', (event.pageY - 10) + 'px')
|
||||
.transition()
|
||||
.duration(200)
|
||||
.style('opacity', 1);
|
||||
|
||||
// Event-Listener für den Bookmark-Button hinzufügen
|
||||
document.getElementById('bookmark-button').addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const nodeId = e.currentTarget.getAttribute('data-nodeid');
|
||||
const isNowBookmarked = this.toggleBookmark(nodeId);
|
||||
|
||||
// Button-Text aktualisieren
|
||||
if (isNowBookmarked) {
|
||||
e.currentTarget.innerHTML = '<i class="fas fa-bookmark mr-1"></i> Gemerkt';
|
||||
} else {
|
||||
e.currentTarget.innerHTML = '<i class="far fa-bookmark mr-1"></i> Merken';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Knoten visuell hervorheben
|
||||
d3.select(event.currentTarget).select('circle')
|
||||
.transition()
|
||||
.duration(200)
|
||||
.attr('r', this.nodeRadius * 1.2)
|
||||
.attr('stroke', this.isNodeBookmarked(d.id) ? '#FFD700' : '#ffffff');
|
||||
}
|
||||
|
||||
nodeMouseout(event, d) {
|
||||
this.mouseoverNode = null;
|
||||
|
||||
// Tooltip ausblenden
|
||||
if (this.tooltipEnabled) {
|
||||
this.tooltipDiv
|
||||
.transition()
|
||||
.duration(200)
|
||||
.style('opacity', 0);
|
||||
}
|
||||
|
||||
// Knoten-Stil zurücksetzen, wenn nicht ausgewählt
|
||||
const nodeElement = d3.select(event.currentTarget).select('circle');
|
||||
if (d !== this.selectedNode) {
|
||||
const isBookmarked = this.isNodeBookmarked(d.id);
|
||||
nodeElement
|
||||
.transition()
|
||||
.duration(200)
|
||||
.attr('r', this.nodeRadius)
|
||||
.attr('stroke', isBookmarked ? '#FFD700' : '#ffffff50')
|
||||
.attr('stroke-width', isBookmarked ? 3 : 2);
|
||||
}
|
||||
}
|
||||
|
||||
nodeClicked(event, d) {
|
||||
// Frühere Auswahl zurücksetzen
|
||||
if (this.selectedNode && this.selectedNode !== d) {
|
||||
this.g.selectAll('.node')
|
||||
.filter(n => n === this.selectedNode)
|
||||
.transition()
|
||||
.duration(200)
|
||||
.attr('r', this.nodeRadius)
|
||||
.attr('stroke', '#ffffff50');
|
||||
}
|
||||
|
||||
// Neue Auswahl hervorheben
|
||||
if (this.selectedNode !== d) {
|
||||
this.selectedNode = d;
|
||||
d3.select(event.currentTarget).select('circle')
|
||||
.transition()
|
||||
.duration(200)
|
||||
.attr('r', this.selectedNodeRadius)
|
||||
.attr('stroke', '#ffffff');
|
||||
}
|
||||
|
||||
// Callback mit Node-Daten aufrufen
|
||||
this.onNodeClick(d);
|
||||
}
|
||||
|
||||
showError(message) {
|
||||
this.container.html(`
|
||||
<div class="w-full text-center p-6">
|
||||
<div class="mb-4 text-red-500">
|
||||
<i class="fas fa-exclamation-triangle text-4xl"></i>
|
||||
</div>
|
||||
<p class="text-lg text-gray-200">${message}</p>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
// Fokussiert die Ansicht auf einen bestimmten Knoten
|
||||
focusNode(nodeId) {
|
||||
const node = this.nodes.find(n => n.id === nodeId);
|
||||
if (!node) return;
|
||||
|
||||
// Simuliere einen Klick auf den Knoten
|
||||
const nodeElement = this.g.selectAll('.node-group')
|
||||
.filter(d => d.id === nodeId);
|
||||
|
||||
nodeElement.dispatch('click');
|
||||
|
||||
// Zentriere den Knoten in der Ansicht
|
||||
const transform = d3.zoomIdentity
|
||||
.translate(this.width / 2, this.height / 2)
|
||||
.scale(1.2)
|
||||
.translate(-node.x, -node.y);
|
||||
|
||||
this.svg.transition()
|
||||
.duration(750)
|
||||
.call(
|
||||
d3.zoom().transform,
|
||||
transform
|
||||
);
|
||||
}
|
||||
|
||||
// Filtert die Mindmap basierend auf einem Suchbegriff
|
||||
filterBySearchTerm(searchTerm) {
|
||||
if (!searchTerm || searchTerm.trim() === '') {
|
||||
// Alle Knoten anzeigen
|
||||
this.g.selectAll('.node-group')
|
||||
.style('opacity', 1)
|
||||
.style('pointer-events', 'all');
|
||||
|
||||
this.g.selectAll('.link')
|
||||
.style('opacity', 1);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const searchLower = searchTerm.toLowerCase();
|
||||
const matchingNodes = this.nodes.filter(node =>
|
||||
node.name.toLowerCase().includes(searchLower) ||
|
||||
(node.description && node.description.toLowerCase().includes(searchLower))
|
||||
);
|
||||
|
||||
const matchingNodeIds = new Set(matchingNodes.map(n => n.id));
|
||||
|
||||
// Passende Knoten hervorheben, andere ausblenden
|
||||
this.g.selectAll('.node-group')
|
||||
.style('opacity', d => matchingNodeIds.has(d.id) ? 1 : 0.2)
|
||||
.style('pointer-events', d => matchingNodeIds.has(d.id) ? 'all' : 'none');
|
||||
|
||||
// Verbindungen zwischen passenden Knoten hervorheben
|
||||
this.g.selectAll('.link')
|
||||
.style('opacity', d =>
|
||||
matchingNodeIds.has(d.source.id) && matchingNodeIds.has(d.target.id) ? 1 : 0.1
|
||||
);
|
||||
|
||||
// Auf den ersten passenden Knoten fokussieren, wenn vorhanden
|
||||
if (matchingNodes.length > 0) {
|
||||
this.focusNode(matchingNodes[0].id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the thought_count property for each node based on existing connections
|
||||
*/
|
||||
updateConnectionCounts() {
|
||||
// Reset all counts first
|
||||
this.nodes.forEach(node => {
|
||||
// Initialize thought_count if it doesn't exist
|
||||
if (typeof node.thought_count !== 'number') {
|
||||
node.thought_count = 0;
|
||||
}
|
||||
|
||||
// Count connections for this node
|
||||
const connectedNodes = this.getConnectedNodes(node);
|
||||
node.thought_count = connectedNodes.length;
|
||||
});
|
||||
|
||||
// Update UI to show counts
|
||||
this.updateNodeLabels();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the visual representation of node labels to include connection counts
|
||||
*/
|
||||
updateNodeLabels() {
|
||||
if (!this.textElements) return;
|
||||
|
||||
this.textElements.text(d => {
|
||||
if (d.thought_count > 0) {
|
||||
return `${d.name} (${d.thought_count})`;
|
||||
}
|
||||
return d.name;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new connection between nodes and updates the counts
|
||||
*/
|
||||
addConnection(sourceNode, targetNode) {
|
||||
if (!sourceNode || !targetNode) return false;
|
||||
|
||||
// Check if connection already exists
|
||||
if (this.isConnected(sourceNode, targetNode)) return false;
|
||||
|
||||
// Add new connection
|
||||
this.links.push({
|
||||
source: sourceNode.id,
|
||||
target: targetNode.id
|
||||
});
|
||||
|
||||
// Update counts
|
||||
this.updateConnectionCounts();
|
||||
|
||||
// Update visualization
|
||||
this.updateVisualization();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Lädt gemerkete Knoten aus dem LocalStorage
|
||||
loadBookmarkedNodes() {
|
||||
try {
|
||||
const bookmarked = localStorage.getItem('bookmarkedNodes');
|
||||
return bookmarked ? JSON.parse(bookmarked) : [];
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der gemerkten Knoten:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Speichert gemerkete Knoten im LocalStorage
|
||||
saveBookmarkedNodes() {
|
||||
try {
|
||||
localStorage.setItem('bookmarkedNodes', JSON.stringify(this.bookmarkedNodes));
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Speichern der gemerkten Knoten:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Prüft, ob ein Knoten gemerkt ist
|
||||
isNodeBookmarked(nodeId) {
|
||||
return this.bookmarkedNodes.includes(nodeId);
|
||||
}
|
||||
|
||||
// Merkt einen Knoten oder hebt die Markierung auf
|
||||
toggleBookmark(nodeId) {
|
||||
const index = this.bookmarkedNodes.indexOf(nodeId);
|
||||
if (index === -1) {
|
||||
// Node hinzufügen
|
||||
this.bookmarkedNodes.push(nodeId);
|
||||
this.updateNodeAppearance(nodeId, true);
|
||||
} else {
|
||||
// Node entfernen
|
||||
this.bookmarkedNodes.splice(index, 1);
|
||||
this.updateNodeAppearance(nodeId, false);
|
||||
}
|
||||
|
||||
// Änderungen speichern
|
||||
this.saveBookmarkedNodes();
|
||||
|
||||
// Event auslösen für andere Komponenten
|
||||
const event = new CustomEvent('nodeBookmarkToggled', {
|
||||
detail: {
|
||||
nodeId: nodeId,
|
||||
isBookmarked: index === -1
|
||||
}
|
||||
});
|
||||
document.dispatchEvent(event);
|
||||
|
||||
return index === -1; // true wenn jetzt gemerkt, false wenn Markierung aufgehoben
|
||||
}
|
||||
|
||||
// Aktualisiert das Aussehen eines Knotens basierend auf Bookmark-Status
|
||||
updateNodeAppearance(nodeId, isBookmarked) {
|
||||
this.g.selectAll('.node-group')
|
||||
.filter(d => d.id === nodeId)
|
||||
.select('.node')
|
||||
.classed('bookmarked', isBookmarked)
|
||||
.attr('stroke', isBookmarked ? '#FFD700' : '#ffffff50')
|
||||
.attr('stroke-width', isBookmarked ? 3 : 2);
|
||||
}
|
||||
|
||||
// Aktualisiert das Aussehen aller gemerkten Knoten
|
||||
updateAllBookmarkedNodes() {
|
||||
this.g.selectAll('.node-group')
|
||||
.each((d) => {
|
||||
const isBookmarked = this.isNodeBookmarked(d.id);
|
||||
this.updateNodeAppearance(d.id, isBookmarked);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt alle direkt verbundenen Knoten eines Knotens zurück
|
||||
* @param {Object} node - Der Knoten, für den die Verbindungen gesucht werden
|
||||
* @returns {Array} Array der verbundenen Knotenobjekte
|
||||
*/
|
||||
getConnectedNodes(node) {
|
||||
if (!node || !this.links || !this.nodes) return [];
|
||||
const nodeId = node.id;
|
||||
const connectedIds = new Set();
|
||||
this.links.forEach(link => {
|
||||
if (link.source === nodeId || (link.source && link.source.id === nodeId)) {
|
||||
connectedIds.add(link.target.id ? link.target.id : link.target);
|
||||
}
|
||||
if (link.target === nodeId || (link.target && link.target.id === nodeId)) {
|
||||
connectedIds.add(link.source.id ? link.source.id : link.source);
|
||||
}
|
||||
});
|
||||
return this.nodes.filter(n => connectedIds.has(n.id));
|
||||
}
|
||||
}
|
||||
|
||||
// Exportiere die Klasse für die Verwendung in anderen Modulen
|
||||
window.MindMapVisualization = MindMapVisualization;
|
||||
1904
static/mindmap.js
Normal file
1904
static/mindmap.js
Normal file
File diff suppressed because it is too large
Load Diff
1109
static/neural-network-background-full.js
Normal file
1109
static/neural-network-background-full.js
Normal file
File diff suppressed because it is too large
Load Diff
2096
static/neural-network-background.js
Normal file
2096
static/neural-network-background.js
Normal file
File diff suppressed because it is too large
Load Diff
508
static/style.css
Normal file
508
static/style.css
Normal file
@@ -0,0 +1,508 @@
|
||||
/* Main Systades Styles - Dark Mystical Theme */
|
||||
|
||||
/* Import Fonts */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap');
|
||||
|
||||
/* Root Variables */
|
||||
:root {
|
||||
/* Light Theme Colors */
|
||||
--light-bg-primary: #f8fafc;
|
||||
--light-bg-secondary: #f1f5f9;
|
||||
--light-text-primary: #1e293b;
|
||||
--light-text-secondary: #475569;
|
||||
--light-accent-primary: #7c3aed;
|
||||
--light-accent-secondary: #8b5cf6;
|
||||
--light-border: #e2e8f0;
|
||||
|
||||
/* Dark Theme Colors */
|
||||
--dark-bg-primary: #0a0e19;
|
||||
--dark-bg-secondary: #111827;
|
||||
--dark-text-primary: #f9fafb;
|
||||
--dark-text-secondary: #e5e7eb;
|
||||
--dark-accent-primary: #6d28d9;
|
||||
--dark-accent-secondary: #8b5cf6;
|
||||
--dark-border: #1f2937;
|
||||
|
||||
/* Common */
|
||||
--font-sans: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
--font-mono: 'JetBrains Mono', monospace;
|
||||
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
--shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
--shadow-md: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
--shadow-lg: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
|
||||
/* Transitions */
|
||||
--transition-fast: 150ms ease-in-out;
|
||||
--transition-normal: 300ms ease-in-out;
|
||||
--transition-slow: 500ms ease-in-out;
|
||||
}
|
||||
|
||||
/* Base Elements */
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
transition: background-color var(--transition-normal), color var(--transition-normal);
|
||||
background-color: transparent !important; /* Ensure background is transparent */
|
||||
}
|
||||
|
||||
/* HTML root element should also be transparent */
|
||||
html {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
html.dark {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
/* Theme Specific - keep the color but remove background */
|
||||
body {
|
||||
color: var(--light-text-primary);
|
||||
}
|
||||
|
||||
body.dark {
|
||||
color: var(--dark-text-primary);
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* Ensure proper contrast in both modes */
|
||||
body:not(.dark) {
|
||||
--text-primary: var(--light-text-primary);
|
||||
--text-secondary: var(--light-text-secondary);
|
||||
--bg-primary: var(--light-bg-primary);
|
||||
--bg-secondary: var(--light-bg-secondary);
|
||||
}
|
||||
|
||||
body.dark {
|
||||
--text-primary: var(--dark-text-primary);
|
||||
--text-secondary: var(--dark-text-secondary);
|
||||
--bg-primary: var(--dark-bg-primary);
|
||||
--bg-secondary: var(--dark-bg-secondary);
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
p {
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.gradient-text {
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
color: transparent;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
body .gradient-text {
|
||||
background-image: linear-gradient(135deg, var(--light-accent-primary), var(--light-accent-secondary));
|
||||
}
|
||||
|
||||
body.dark .gradient-text {
|
||||
background-image: linear-gradient(135deg, var(--dark-accent-primary), var(--dark-accent-secondary));
|
||||
}
|
||||
|
||||
/* Subtle glow for dark mode gradient text */
|
||||
body.dark .gradient-text::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
filter: blur(8px);
|
||||
opacity: 0.3;
|
||||
background-image: inherit;
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Containers and Layout */
|
||||
.container {
|
||||
width: 100%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.container {
|
||||
max-width: 640px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.container {
|
||||
max-width: 768px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.container {
|
||||
max-width: 1024px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.container {
|
||||
max-width: 1280px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Glass Morphism */
|
||||
.glass-morphism {
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
body .glass-navbar-light {
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
border-color: rgba(226, 232, 240, 0.5);
|
||||
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
body.dark .glass-navbar-dark {
|
||||
background-color: rgba(10, 14, 25, 0.8);
|
||||
border-color: rgba(31, 41, 55, 0.5);
|
||||
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* Navigation */
|
||||
.nav-link {
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 0.5rem;
|
||||
transition: all var(--transition-normal);
|
||||
}
|
||||
|
||||
body .nav-link {
|
||||
color: var(--light-text-secondary);
|
||||
}
|
||||
|
||||
body.dark .nav-link {
|
||||
color: var(--dark-text-secondary);
|
||||
}
|
||||
|
||||
body .nav-link:hover {
|
||||
color: var(--light-text-primary);
|
||||
background-color: rgba(241, 245, 249, 0.5);
|
||||
}
|
||||
|
||||
body.dark .nav-link:hover {
|
||||
color: var(--dark-text-primary);
|
||||
background-color: rgba(31, 41, 55, 0.5);
|
||||
}
|
||||
|
||||
body .nav-link-light-active {
|
||||
color: var(--light-accent-primary);
|
||||
background-color: rgba(139, 92, 246, 0.1);
|
||||
}
|
||||
|
||||
body.dark .nav-link-active {
|
||||
color: var(--dark-accent-secondary);
|
||||
background-color: rgba(109, 40, 217, 0.15);
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
font-weight: 500;
|
||||
transition: all var(--transition-normal);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
body .btn-primary {
|
||||
background-color: var(--light-accent-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
body.dark .btn-primary {
|
||||
background-color: var(--dark-accent-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
body .btn-primary:hover {
|
||||
background-color: var(--light-accent-secondary);
|
||||
box-shadow: 0 0 15px rgba(124, 58, 237, 0.3);
|
||||
}
|
||||
|
||||
body.dark .btn-primary:hover {
|
||||
background-color: var(--dark-accent-secondary);
|
||||
box-shadow: 0 0 15px rgba(109, 40, 217, 0.5);
|
||||
}
|
||||
|
||||
body .btn-secondary {
|
||||
background-color: transparent;
|
||||
border: 1px solid var(--light-border);
|
||||
color: var(--light-text-primary);
|
||||
}
|
||||
|
||||
body.dark .btn-secondary {
|
||||
background-color: transparent;
|
||||
border: 1px solid var(--dark-border);
|
||||
color: var(--dark-text-primary);
|
||||
}
|
||||
|
||||
body .btn-secondary:hover {
|
||||
background-color: var(--light-bg-secondary);
|
||||
border-color: var(--light-accent-secondary);
|
||||
}
|
||||
|
||||
body.dark .btn-secondary:hover {
|
||||
background-color: var(--dark-bg-secondary);
|
||||
border-color: var(--dark-accent-secondary);
|
||||
}
|
||||
|
||||
/* Cards */
|
||||
.card {
|
||||
border-radius: 0.75rem;
|
||||
overflow: hidden;
|
||||
transition: all var(--transition-normal);
|
||||
}
|
||||
|
||||
body .card {
|
||||
background-color: white;
|
||||
border: 1px solid var(--light-border);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
body.dark .card {
|
||||
background-color: var(--dark-bg-secondary);
|
||||
border: 1px solid var(--dark-border);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
body .card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
body.dark .card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* Form Elements */
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 0.5rem;
|
||||
transition: all var(--transition-normal);
|
||||
}
|
||||
|
||||
body .form-input {
|
||||
background-color: white;
|
||||
border: 1px solid var(--light-border);
|
||||
color: var(--light-text-primary);
|
||||
}
|
||||
|
||||
body.dark .form-input {
|
||||
background-color: var(--dark-bg-secondary);
|
||||
border: 1px solid var(--dark-border);
|
||||
color: var(--dark-text-primary);
|
||||
}
|
||||
|
||||
body .form-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--light-accent-secondary);
|
||||
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.15);
|
||||
}
|
||||
|
||||
body.dark .form-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--dark-accent-secondary);
|
||||
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.25);
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 0.5s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-10px); }
|
||||
}
|
||||
|
||||
.animate-float {
|
||||
animation: float 5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 0.7; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
|
||||
.animate-pulse {
|
||||
animation: pulse 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Utilities */
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
.shadow-elevation {
|
||||
transition: box-shadow var(--transition-normal), transform var(--transition-normal);
|
||||
}
|
||||
|
||||
body .shadow-elevation {
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
body.dark .shadow-elevation {
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
body .shadow-elevation:hover {
|
||||
box-shadow: var(--shadow-md);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
body.dark .shadow-elevation:hover {
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.4), 0 4px 6px -2px rgba(0, 0, 0, 0.3);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Tooltips */
|
||||
.tooltip {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tooltip:hover::before {
|
||||
content: attr(data-tooltip);
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
white-space: nowrap;
|
||||
z-index: 10;
|
||||
opacity: 0;
|
||||
animation: fadeIn 0.3s ease-out forwards;
|
||||
}
|
||||
|
||||
body .tooltip:hover::before {
|
||||
background-color: var(--light-text-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
body.dark .tooltip:hover::before {
|
||||
background-color: var(--dark-text-primary);
|
||||
color: var(--dark-bg-primary);
|
||||
}
|
||||
|
||||
/* Mystical elements */
|
||||
.mystical-border {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mystical-border::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
border: 1px solid;
|
||||
border-radius: inherit;
|
||||
pointer-events: none;
|
||||
opacity: 0.3;
|
||||
transition: opacity var(--transition-normal);
|
||||
}
|
||||
|
||||
body .mystical-border::after {
|
||||
border-color: var(--light-accent-primary);
|
||||
}
|
||||
|
||||
body.dark .mystical-border::after {
|
||||
border-color: var(--dark-accent-primary);
|
||||
}
|
||||
|
||||
.mystical-border:hover::after {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* Responsive Design Helpers */
|
||||
@media (max-width: 640px) {
|
||||
.container {
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
.hero-heading {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.section-heading {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Accessibility */
|
||||
:focus-visible {
|
||||
outline: 2px solid;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
body :focus-visible {
|
||||
outline-color: var(--light-accent-primary);
|
||||
}
|
||||
|
||||
body.dark :focus-visible {
|
||||
outline-color: var(--dark-accent-primary);
|
||||
}
|
||||
|
||||
/* Scrollbar styling */
|
||||
::-webkit-scrollbar {
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
}
|
||||
|
||||
body ::-webkit-scrollbar-track {
|
||||
background: var(--light-bg-secondary);
|
||||
}
|
||||
|
||||
body.dark ::-webkit-scrollbar-track {
|
||||
background: var(--dark-bg-secondary);
|
||||
}
|
||||
|
||||
body ::-webkit-scrollbar-thumb {
|
||||
background: var(--light-accent-primary);
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
body.dark ::-webkit-scrollbar-thumb {
|
||||
background: var(--dark-accent-primary);
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
body ::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--light-accent-secondary);
|
||||
}
|
||||
|
||||
body.dark ::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--dark-accent-secondary);
|
||||
}
|
||||
6
static/three.min.js
vendored
Normal file
6
static/three.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
308
templates/admin.html
Normal file
308
templates/admin.html
Normal file
@@ -0,0 +1,308 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Admin-Bereich{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<h1 class="text-3xl font-bold mb-8 text-gray-800 dark:text-white">Admin-Bereich</h1>
|
||||
|
||||
<!-- Tabs für verschiedene Bereiche -->
|
||||
<div x-data="{ activeTab: 'users' }" class="mb-8">
|
||||
<div class="flex space-x-2 mb-6 overflow-x-auto">
|
||||
<button
|
||||
@click="activeTab = 'users'"
|
||||
:class="activeTab === 'users' ? 'bg-primary-600 text-white' : 'bg-white/10 text-gray-700 dark:text-gray-200'"
|
||||
class="px-4 py-2 rounded-lg font-medium transition-all">
|
||||
<i class="fas fa-users mr-2"></i> Benutzer
|
||||
</button>
|
||||
<button
|
||||
@click="activeTab = 'nodes'"
|
||||
:class="activeTab === 'nodes' ? 'bg-primary-600 text-white' : 'bg-white/10 text-gray-700 dark:text-gray-200'"
|
||||
class="px-4 py-2 rounded-lg font-medium transition-all">
|
||||
<i class="fas fa-project-diagram mr-2"></i> Mindmap-Knoten
|
||||
</button>
|
||||
<button
|
||||
@click="activeTab = 'thoughts'"
|
||||
:class="activeTab === 'thoughts' ? 'bg-primary-600 text-white' : 'bg-white/10 text-gray-700 dark:text-gray-200'"
|
||||
class="px-4 py-2 rounded-lg font-medium transition-all">
|
||||
<i class="fas fa-lightbulb mr-2"></i> Gedanken
|
||||
</button>
|
||||
<button
|
||||
@click="activeTab = 'stats'"
|
||||
:class="activeTab === 'stats' ? 'bg-primary-600 text-white' : 'bg-white/10 text-gray-700 dark:text-gray-200'"
|
||||
class="px-4 py-2 rounded-lg font-medium transition-all">
|
||||
<i class="fas fa-chart-bar mr-2"></i> Statistiken
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Benutzer-Tab -->
|
||||
<div x-show="activeTab === 'users'" class="glass-morphism rounded-lg p-6">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h2 class="text-xl font-bold text-gray-800 dark:text-white">Benutzerverwaltung</h2>
|
||||
<button class="btn-outline">
|
||||
<i class="fas fa-plus mr-2"></i> Neuer Benutzer
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full">
|
||||
<thead>
|
||||
<tr class="text-left border-b border-gray-200 dark:border-gray-700">
|
||||
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">ID</th>
|
||||
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">Benutzername</th>
|
||||
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">E-Mail</th>
|
||||
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">Admin</th>
|
||||
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">Gedanken</th>
|
||||
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for user in users %}
|
||||
<tr class="border-b border-gray-100 dark:border-gray-800 hover:bg-gray-50 dark:hover:bg-dark-700/30">
|
||||
<td class="px-4 py-3 text-gray-700 dark:text-gray-300">{{ user.id }}</td>
|
||||
<td class="px-4 py-3 text-gray-700 dark:text-gray-300 font-medium">{{ user.username }}</td>
|
||||
<td class="px-4 py-3 text-gray-700 dark:text-gray-300">{{ user.email }}</td>
|
||||
<td class="px-4 py-3 text-gray-700 dark:text-gray-300">
|
||||
{% if user.is_admin %}
|
||||
<span class="bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300 px-2 py-1 rounded text-xs">Admin</span>
|
||||
{% else %}
|
||||
<span class="bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300 px-2 py-1 rounded text-xs">User</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-gray-700 dark:text-gray-300">{{ user.thoughts|length }}</td>
|
||||
<td class="px-4 py-3 flex space-x-2">
|
||||
<button class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<button class="text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mindmap-Knoten-Tab -->
|
||||
<div x-show="activeTab === 'nodes'" class="glass-morphism rounded-lg p-6">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h2 class="text-xl font-bold text-gray-800 dark:text-white">Mindmap-Knoten Verwaltung</h2>
|
||||
<button class="btn-outline">
|
||||
<i class="fas fa-plus mr-2"></i> Neuer Knoten
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full">
|
||||
<thead>
|
||||
<tr class="text-left border-b border-gray-200 dark:border-gray-700">
|
||||
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">ID</th>
|
||||
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">Name</th>
|
||||
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">Elternknoten</th>
|
||||
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">Gedanken</th>
|
||||
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for node in nodes %}
|
||||
<tr class="border-b border-gray-100 dark:border-gray-800 hover:bg-gray-50 dark:hover:bg-dark-700/30">
|
||||
<td class="px-4 py-3 text-gray-700 dark:text-gray-300">{{ node.id }}</td>
|
||||
<td class="px-4 py-3 text-gray-700 dark:text-gray-300 font-medium">{{ node.name }}</td>
|
||||
<td class="px-4 py-3 text-gray-700 dark:text-gray-300">
|
||||
{% if node.parent %}
|
||||
{{ node.parent.name }}
|
||||
{% else %}
|
||||
<span class="text-gray-400 dark:text-gray-500">Wurzelknoten</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-gray-700 dark:text-gray-300">{{ node.thoughts|length }}</td>
|
||||
<td class="px-4 py-3 flex space-x-2">
|
||||
<button class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<button class="text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gedanken-Tab -->
|
||||
<div x-show="activeTab === 'thoughts'" class="glass-morphism rounded-lg p-6">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h2 class="text-xl font-bold text-gray-800 dark:text-white">Gedanken-Verwaltung</h2>
|
||||
<div class="flex space-x-2">
|
||||
<div class="relative">
|
||||
<input type="text" placeholder="Suchen..." class="form-input pl-10 pr-4 py-2 rounded-lg bg-white/10 border border-gray-200/20 dark:border-gray-700/20 focus:outline-none focus:ring-2 focus:ring-primary-500 text-gray-700 dark:text-gray-200">
|
||||
<div class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500">
|
||||
<i class="fas fa-search"></i>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn-outline">
|
||||
<i class="fas fa-filter mr-2"></i> Filter
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full">
|
||||
<thead>
|
||||
<tr class="text-left border-b border-gray-200 dark:border-gray-700">
|
||||
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">ID</th>
|
||||
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">Titel</th>
|
||||
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">Autor</th>
|
||||
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">Datum</th>
|
||||
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">Bewertung</th>
|
||||
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for thought in thoughts %}
|
||||
<tr class="border-b border-gray-100 dark:border-gray-800 hover:bg-gray-50 dark:hover:bg-dark-700/30">
|
||||
<td class="px-4 py-3 text-gray-700 dark:text-gray-300">{{ thought.id }}</td>
|
||||
<td class="px-4 py-3 text-gray-700 dark:text-gray-300 font-medium">{{ thought.title }}</td>
|
||||
<td class="px-4 py-3 text-gray-700 dark:text-gray-300">{{ thought.author.username }}</td>
|
||||
<td class="px-4 py-3 text-gray-700 dark:text-gray-300">{{ thought.timestamp.strftime('%d.%m.%Y') }}</td>
|
||||
<td class="px-4 py-3 text-gray-700 dark:text-gray-300">
|
||||
<div class="flex items-center">
|
||||
<span class="mr-2">{{ "%.1f"|format(thought.average_rating) }}</span>
|
||||
<div class="flex">
|
||||
{% for i in range(5) %}
|
||||
{% if i < thought.average_rating|int %}
|
||||
<i class="fas fa-star text-yellow-400"></i>
|
||||
{% elif i < (thought.average_rating|int + 0.5) %}
|
||||
<i class="fas fa-star-half-alt text-yellow-400"></i>
|
||||
{% else %}
|
||||
<i class="far fa-star text-yellow-400"></i>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-3 flex space-x-2">
|
||||
<button class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
<button class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<button class="text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Statistiken-Tab -->
|
||||
<div x-show="activeTab === 'stats'" class="glass-morphism rounded-lg p-6">
|
||||
<h2 class="text-xl font-bold mb-6 text-gray-800 dark:text-white">Systemstatistiken</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
<div class="glass-effect p-4 rounded-lg">
|
||||
<div class="flex items-center mb-2">
|
||||
<div class="bg-blue-500/20 p-3 rounded-lg mr-3">
|
||||
<i class="fas fa-users text-blue-500"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-gray-600 dark:text-gray-300">Benutzer</h3>
|
||||
<p class="text-2xl font-bold text-gray-800 dark:text-white">{{ users|length }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="glass-effect p-4 rounded-lg">
|
||||
<div class="flex items-center mb-2">
|
||||
<div class="bg-purple-500/20 p-3 rounded-lg mr-3">
|
||||
<i class="fas fa-project-diagram text-purple-500"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-gray-600 dark:text-gray-300">Knoten</h3>
|
||||
<p class="text-2xl font-bold text-gray-800 dark:text-white">{{ nodes|length }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="glass-effect p-4 rounded-lg">
|
||||
<div class="flex items-center mb-2">
|
||||
<div class="bg-green-500/20 p-3 rounded-lg mr-3">
|
||||
<i class="fas fa-lightbulb text-green-500"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-gray-600 dark:text-gray-300">Gedanken</h3>
|
||||
<p class="text-2xl font-bold text-gray-800 dark:text-white">{{ thoughts|length }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="glass-effect p-4 rounded-lg">
|
||||
<div class="flex items-center mb-2">
|
||||
<div class="bg-red-500/20 p-3 rounded-lg mr-3">
|
||||
<i class="fas fa-comments text-red-500"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-gray-600 dark:text-gray-300">Kommentare</h3>
|
||||
<p class="text-2xl font-bold text-gray-800 dark:text-white">
|
||||
{% set comment_count = 0 %}
|
||||
{% for thought in thoughts %}
|
||||
{% set comment_count = comment_count + thought.comments|length %}
|
||||
{% endfor %}
|
||||
{{ comment_count }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="glass-effect p-4 rounded-lg">
|
||||
<h3 class="text-lg font-bold mb-4 text-gray-800 dark:text-white">Aktive Benutzer</h3>
|
||||
<div class="h-64 flex items-center justify-center bg-gray-100/20 dark:bg-dark-700/20 rounded">
|
||||
<p class="text-gray-500 dark:text-gray-400">Hier würde ein Aktivitätsdiagramm angezeigt werden</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="glass-effect p-4 rounded-lg">
|
||||
<h3 class="text-lg font-bold mb-4 text-gray-800 dark:text-white">Gedanken pro Kategorie</h3>
|
||||
<div class="h-64 flex items-center justify-center bg-gray-100/20 dark:bg-dark-700/20 rounded">
|
||||
<p class="text-gray-500 dark:text-gray-400">Hier würde eine Verteilungsstatistik angezeigt werden</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- System-Log (immer sichtbar) -->
|
||||
<div class="mt-8">
|
||||
<h2 class="text-xl font-bold mb-4 text-gray-800 dark:text-white">System-Log</h2>
|
||||
<div class="glass-morphism rounded-lg p-4 h-32 overflow-y-auto font-mono text-sm text-gray-700 dark:text-gray-300">
|
||||
<div class="text-green-500">[INFO] [{{ now.strftime('%Y-%m-%d %H:%M:%S') }}] System gestartet</div>
|
||||
<div class="text-blue-500">[INFO] [{{ now.strftime('%Y-%m-%d %H:%M:%S') }}] Admin-Bereich aufgerufen von {{ current_user.username }}</div>
|
||||
<div class="text-yellow-500">[WARN] [{{ now.strftime('%Y-%m-%d %H:%M:%S') }}] Hohe Serverauslastung erkannt</div>
|
||||
<div class="text-gray-500">[INFO] [{{ now.strftime('%Y-%m-%d %H:%M:%S') }}] Backup erfolgreich erstellt</div>
|
||||
<div class="text-red-500">[ERROR] [{{ now.strftime('%Y-%m-%d %H:%M:%S') }}] API-Zugriffsfehler (Timeout) bei externer Anfrage</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
// Admin-spezifische JavaScript-Funktionen
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('Admin-Bereich geladen');
|
||||
|
||||
// Beispiel für AJAX-Ladeverhalten von Daten
|
||||
// Kann später durch echte API-Calls ersetzt werden
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
71
templates/agb.html
Normal file
71
templates/agb.html
Normal file
@@ -0,0 +1,71 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}AGB{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-3xl mx-auto">
|
||||
<div class="card p-6 md:p-8">
|
||||
<h1 class="text-3xl font-bold mb-6 gradient-text">Allgemeine Geschäftsbedingungen</h1>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-bold mb-4">1. Geltungsbereich</h2>
|
||||
<p class="mb-4">Diese Allgemeinen Geschäftsbedingungen (nachfolgend "AGB") gelten für die Nutzung der MindMap-Plattform (nachfolgend "Plattform"), die von der MindMap GmbH, Musterstraße 123, 12345 Musterstadt (nachfolgend "Anbieter") betrieben wird.</p>
|
||||
<p class="mb-4">Mit der Registrierung und/oder Nutzung der Plattform erkennt der Nutzer diese AGB an. Die Nutzung der Plattform ist nur zulässig, wenn der Nutzer diese AGB akzeptiert.</p>
|
||||
</section>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-bold mb-4">2. Leistungsbeschreibung</h2>
|
||||
<p class="mb-4">Die Plattform bietet dem Nutzer die Möglichkeit, komplexe Informationen in Form von Mindmaps zu visualisieren, zu organisieren und zu teilen. Der genaue Funktionsumfang ergibt sich aus der jeweiligen Leistungsbeschreibung auf der Website des Anbieters.</p>
|
||||
<p class="mb-4">Der Anbieter ist berechtigt, die angebotenen Dienste zu ändern, neue Dienste unentgeltlich oder entgeltlich verfügbar zu machen und die Bereitstellung unentgeltlicher Dienste einzustellen. Der Anbieter wird hierbei jeweils auf die berechtigten Interessen des Nutzers Rücksicht nehmen.</p>
|
||||
</section>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-bold mb-4">3. Registrierung und Nutzerkonto</h2>
|
||||
<p class="mb-4">Die Nutzung bestimmter Funktionen der Plattform setzt die Registrierung eines Nutzerkontos voraus. Die Registrierung ist nur volljährigen und voll geschäftsfähigen natürlichen Personen erlaubt.</p>
|
||||
<p class="mb-4">Der Nutzer verpflichtet sich, bei der Registrierung wahrheitsgemäße und vollständige Angaben zu machen und diese Daten stets aktuell zu halten. Es ist nicht gestattet, mehrere Nutzerkonten zu erstellen.</p>
|
||||
<p class="mb-4">Der Nutzer ist verpflichtet, seine Zugangsdaten geheim zu halten und vor dem Zugriff durch unbefugte Dritte zu schützen. Der Anbieter wird den Nutzer niemals nach seinem Passwort fragen.</p>
|
||||
</section>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-bold mb-4">4. Nutzungsrechte</h2>
|
||||
<p class="mb-4">Der Anbieter gewährt dem Nutzer für die Dauer der Vertragslaufzeit ein einfaches, nicht übertragbares Recht zur Nutzung der Plattform im vertraglich vereinbarten Umfang.</p>
|
||||
<p class="mb-4">Der Nutzer räumt dem Anbieter an den von ihm auf der Plattform eingestellten Inhalten ein einfaches, übertragbares, unterlizenzierbares, räumlich und zeitlich unbeschränktes Nutzungsrecht ein, soweit dies für den Betrieb der Plattform erforderlich ist.</p>
|
||||
<p class="mb-4">Der Nutzer garantiert, dass er über alle Rechte an den von ihm eingestellten Inhalten verfügt und durch diese keine Rechte Dritter verletzt werden.</p>
|
||||
</section>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-bold mb-4">5. Pflichten des Nutzers</h2>
|
||||
<p class="mb-4">Der Nutzer verpflichtet sich, die Plattform nur im Einklang mit diesen AGB und den geltenden Gesetzen zu nutzen. Insbesondere ist es dem Nutzer untersagt:</p>
|
||||
<ul class="list-disc pl-6 mb-4 space-y-2">
|
||||
<li>die Plattform für rechtswidrige oder betrügerische Zwecke zu nutzen</li>
|
||||
<li>rechtswidrige, beleidigende, diskriminierende oder anderweitig anstößige Inhalte zu verbreiten</li>
|
||||
<li>Schadsoftware, Viren oder andere schädliche Computercodes zu verbreiten</li>
|
||||
<li>die normale Funktion der Plattform zu stören oder übermäßig zu belasten</li>
|
||||
<li>auf die Plattform mit automatisierten Mitteln zuzugreifen (wie z.B. Bots, Scraper)</li>
|
||||
<li>die Plattform zu reverse-engineeren, zu dekompilieren oder zu disassemblieren</li>
|
||||
</ul>
|
||||
<p class="mb-4">Der Anbieter behält sich das Recht vor, bei Verstößen gegen diese Pflichten entsprechende Maßnahmen zu ergreifen, einschließlich der Sperrung des Nutzerkontos.</p>
|
||||
</section>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-bold mb-4">6. Verfügbarkeit und Wartung</h2>
|
||||
<p class="mb-4">Der Anbieter ist bemüht, eine hohe Verfügbarkeit der Plattform zu gewährleisten, kann jedoch keine unterbrechungsfreie Verfügbarkeit garantieren. Insbesondere können Wartungsarbeiten, Sicherheits- oder Kapazitätsprobleme sowie Ereignisse, die außerhalb des Einflussbereichs des Anbieters liegen, zu vorübergehenden Unterbrechungen führen.</p>
|
||||
<p class="mb-4">Der Anbieter wird planmäßige Wartungsarbeiten, sofern möglich, vorher ankündigen und zu Zeiten durchführen, in denen die Nutzung der Plattform typischerweise gering ist.</p>
|
||||
</section>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-bold mb-4">7. Haftung</h2>
|
||||
<p class="mb-4">Der Anbieter haftet unbeschränkt für Vorsatz und grobe Fahrlässigkeit sowie nach dem Produkthaftungsgesetz. Für leichte Fahrlässigkeit haftet der Anbieter nur bei Verletzung einer wesentlichen Vertragspflicht und der Höhe nach beschränkt auf die bei Vertragsschluss vorhersehbaren und vertragstypischen Schäden. Wesentliche Vertragspflichten sind solche, deren Erfüllung die ordnungsgemäße Durchführung des Vertrags überhaupt erst ermöglicht und auf deren Einhaltung der Nutzer regelmäßig vertrauen darf.</p>
|
||||
<p class="mb-4">Diese Haftungsbeschränkung gilt nicht für Schäden aus der Verletzung des Lebens, des Körpers oder der Gesundheit.</p>
|
||||
</section>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-bold mb-4">8. Schlussbestimmungen</h2>
|
||||
<p class="mb-4">Es gilt das Recht der Bundesrepublik Deutschland unter Ausschluss des UN-Kaufrechts.</p>
|
||||
<p class="mb-4">Sollten einzelne Bestimmungen dieser AGB unwirksam sein oder werden, bleibt die Wirksamkeit der übrigen Bestimmungen unberührt.</p>
|
||||
<p class="mb-4">Der Anbieter behält sich vor, diese AGB jederzeit zu ändern. Änderungen werden dem Nutzer rechtzeitig vor ihrem Inkrafttreten mitgeteilt. Die Änderungen gelten als akzeptiert, wenn der Nutzer ihnen nicht innerhalb von vier Wochen nach Erhalt der Mitteilung widerspricht.</p>
|
||||
<p class="mb-4">Stand: Mai 2023</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
614
templates/base.html
Normal file
614
templates/base.html
Normal file
@@ -0,0 +1,614 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de" class="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Systades - {% block title %}{% endblock %}</title>
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" href="{{ url_for('static', filename='img/favicon.svg') }}" type="image/svg+xml">
|
||||
<link rel="icon" href="{{ url_for('static', filename='img/favicon.ico') }}" sizes="any">
|
||||
|
||||
<!-- Meta Tags -->
|
||||
<meta name="description" content="Eine interaktive Plattform zum Visualisieren, Erforschen und Teilen von Wissen">
|
||||
<meta name="keywords" content="systades, wissen, visualisierung, lernen, gedanken, theorie">
|
||||
<meta name="author" content="Systades-Team">
|
||||
|
||||
<!-- Tailwind CSS - CDN für Entwicklung und Produktion (laut Vorgabe) -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<!-- Alternative lokale Version, falls die CDN-Version blockiert wird -->
|
||||
<script>
|
||||
tailwind = window.tailwind || {};
|
||||
tailwind.config = {
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ['Inter', 'ui-sans-serif', 'system-ui', '-apple-system', 'sans-serif'],
|
||||
mono: ['JetBrains Mono', 'ui-monospace', 'monospace']
|
||||
},
|
||||
colors: {
|
||||
primary: {
|
||||
50: '#f5f3ff',
|
||||
100: '#ede9fe',
|
||||
200: '#ddd6fe',
|
||||
300: '#c4b5fd',
|
||||
400: '#a78bfa',
|
||||
500: '#8b5cf6',
|
||||
600: '#7c3aed',
|
||||
700: '#6d28d9',
|
||||
800: '#5b21b6',
|
||||
900: '#4c1d95'
|
||||
},
|
||||
secondary: {
|
||||
50: '#ecfdf5',
|
||||
100: '#d1fae5',
|
||||
200: '#a7f3d0',
|
||||
300: '#6ee7b7',
|
||||
400: '#34d399',
|
||||
500: '#10b981',
|
||||
600: '#059669',
|
||||
700: '#047857',
|
||||
800: '#065f46',
|
||||
900: '#064e3b'
|
||||
},
|
||||
dark: {
|
||||
500: '#374151',
|
||||
600: '#1f2937',
|
||||
700: '#111827',
|
||||
800: '#0e1220',
|
||||
900: '#0a0e19'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Local Font Files -->
|
||||
<link href="{{ url_for('static', filename='fonts/inter.css') }}" rel="stylesheet">
|
||||
<link href="{{ url_for('static', filename='fonts/jetbrains-mono.css') }}" rel="stylesheet">
|
||||
|
||||
<!-- Icons - Self-hosted Font Awesome -->
|
||||
<link href="{{ url_for('static', filename='css/all.min.css') }}" rel="stylesheet">
|
||||
|
||||
<!-- Assistent CSS -->
|
||||
<link href="{{ url_for('static', filename='css/assistant.css') }}" rel="stylesheet">
|
||||
|
||||
<!-- Basis-Stylesheet -->
|
||||
<link href="{{ url_for('static', filename='style.css') }}" rel="stylesheet">
|
||||
|
||||
<!-- Base-Styles ausgelagert in eigene Datei -->
|
||||
<link href="{{ url_for('static', filename='css/base-styles.css') }}" rel="stylesheet">
|
||||
|
||||
<!-- Alpine.js - CDN Version -->
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.12.3/dist/cdn.min.js"></script>
|
||||
|
||||
<!-- Neural Network Background CSS -->
|
||||
<link href="{{ url_for('static', filename='css/neural-network-background.css') }}" rel="stylesheet">
|
||||
|
||||
<!-- D3.js für Visualisierungen -->
|
||||
<script src="https://d3js.org/d3.v7.min.js"></script>
|
||||
|
||||
<!-- Marked.js für Markdown-Parsing -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
|
||||
<!-- 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 -->
|
||||
<!-- ► ► Farb‑Token strikt getrennt ◄ ◄ -->
|
||||
<style>
|
||||
/* Light‑Mode */
|
||||
:root {
|
||||
--bg-primary:#f4f6fa;
|
||||
--bg-secondary:#e9ecf3;
|
||||
--text-primary:#232837;
|
||||
--text-secondary:#475569;
|
||||
--accent-primary:#7c3aed;
|
||||
--accent-secondary:#8b5cf6;
|
||||
--glow-effect:0 0 8px rgba(139,92,246,.08);
|
||||
}
|
||||
/* Dark‑Mode */
|
||||
.dark {
|
||||
--bg-primary:#181c24;
|
||||
--bg-secondary:#232837;
|
||||
--text-primary:#f9fafb;
|
||||
--text-secondary:#e5e7eb;
|
||||
--accent-primary:#6d28d9;
|
||||
--accent-secondary:#8b5cf6;
|
||||
--glow-effect:0 0 8px rgba(124,58,237,.15);
|
||||
}
|
||||
|
||||
body {
|
||||
@apply min-h-screen bg-[color:var(--bg-primary)] text-[color:var(--text-primary)] transition-colors duration-300;
|
||||
}
|
||||
|
||||
/* Utilities */
|
||||
.mystical-glow { text-shadow: var(--glow-effect); }
|
||||
.gradient-text {
|
||||
background:linear-gradient(135deg,var(--accent-primary),var(--accent-secondary));
|
||||
-webkit-background-clip:text; background-clip:text; color:transparent; text-shadow:none;
|
||||
}
|
||||
.glass-morphism { backdrop-filter: blur(10px); }
|
||||
.glass-navbar { @apply glass-morphism border backdrop-blur-xl; }
|
||||
.light .glass-navbar { background-color:rgba(255,255,255,.8); border-color:rgba(0,0,0,.05); }
|
||||
.dark .glass-navbar { background-color:rgba(10,14,25,.8); border-color:rgba(255,255,255,.05); }
|
||||
</style>
|
||||
</head>
|
||||
<body data-page="{{ request.endpoint }}" class="relative overflow-x-hidden dark bg-gray-900 text-white" x-data="{
|
||||
darkMode: true,
|
||||
mobileMenuOpen: false,
|
||||
userMenuOpen: false,
|
||||
showSettingsModal: false,
|
||||
|
||||
init() {
|
||||
this.fetchDarkModeFromSession();
|
||||
},
|
||||
|
||||
fetchDarkModeFromSession() {
|
||||
fetch('/api/get_dark_mode')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
this.darkMode = data.darkMode === 'true';
|
||||
document.querySelector('html').classList.toggle('dark', this.darkMode);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Fehler beim Laden der Dark Mode-Einstellung:', error);
|
||||
});
|
||||
},
|
||||
|
||||
toggleDarkMode() {
|
||||
this.darkMode = !this.darkMode;
|
||||
document.querySelector('html').classList.toggle('dark', this.darkMode);
|
||||
|
||||
fetch('/api/set_dark_mode', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ darkMode: this.darkMode })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
localStorage.setItem('darkMode', this.darkMode ? 'dark' : 'light');
|
||||
document.dispatchEvent(new CustomEvent('darkModeToggled', {
|
||||
detail: { isDark: this.darkMode }
|
||||
}));
|
||||
} else {
|
||||
console.error('Fehler beim Speichern der Dark Mode-Einstellung:', data.error);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Fehler beim Speichern der Dark Mode-Einstellung:', error);
|
||||
});
|
||||
}
|
||||
}">
|
||||
<!-- App-Container -->
|
||||
<div id="app-container" class="flex flex-col min-h-screen">
|
||||
<!-- Hauptnavigation -->
|
||||
<nav class="sticky top-0 left-0 right-0 z-50 transition-all duration-300 py-4 px-5 border-b glass-morphism"
|
||||
x-bind:class="darkMode ? 'glass-navbar-dark border-white/10' : 'glass-navbar-light border-gray-200/50'">
|
||||
<div class="container mx-auto flex justify-between items-center">
|
||||
<!-- Logo -->
|
||||
<a href="{{ url_for('index') }}" class="flex items-center group">
|
||||
<span class="text-2xl font-bold gradient-text transform transition-transform group-hover:scale-105">Systades</span>
|
||||
</a>
|
||||
|
||||
<!-- Hauptnavigation - Desktop -->
|
||||
<div class="hidden md:flex items-center space-x-5">
|
||||
<a href="{{ url_for('index') }}"
|
||||
class="nav-link flex items-center"
|
||||
x-bind:class="darkMode
|
||||
? '{{ 'nav-link-active' if request.endpoint == 'index' else '' }}'
|
||||
: '{{ 'nav-link-light-active' if request.endpoint == 'index' else 'nav-link-light' }}'">
|
||||
<i class="fa-solid fa-home mr-2"></i>Start
|
||||
</a>
|
||||
<a href="{{ url_for('mindmap') }}"
|
||||
class="nav-link flex items-center"
|
||||
x-bind:class="darkMode
|
||||
? '{{ 'nav-link-active' if request.endpoint == 'mindmap' else '' }}'
|
||||
: '{{ 'nav-link-light-active' if request.endpoint == 'mindmap' else 'nav-link-light' }}'">
|
||||
<i class="fa-solid fa-diagram-project mr-2"></i>Mindmap
|
||||
</a>
|
||||
<a href="{{ url_for('search_thoughts_page') }}"
|
||||
class="nav-link flex items-center"
|
||||
x-bind:class="darkMode
|
||||
? '{{ 'nav-link-active' if request.endpoint == 'search_thoughts_page' else '' }}'
|
||||
: '{{ 'nav-link-light-active' if request.endpoint == 'search_thoughts_page' else 'nav-link-light' }}'">
|
||||
<i class="fa-solid fa-search mr-2"></i>Suche
|
||||
</a>
|
||||
<!-- KI-Assistent Button -->
|
||||
<button onclick="window.MindMap && window.MindMap.assistant && window.MindMap.assistant.toggleAssistant(true)"
|
||||
class="nav-link flex items-center"
|
||||
x-bind:class="darkMode
|
||||
? 'bg-gradient-to-r from-purple-900/90 to-indigo-800/90 text-white font-medium px-4 py-2 rounded-xl hover:shadow-lg hover:shadow-purple-800/30 transition-all duration-300'
|
||||
: 'bg-gradient-to-r from-purple-600/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
|
||||
</button>
|
||||
{% if current_user.is_authenticated %}
|
||||
<a href="{{ url_for('profile') }}"
|
||||
class="nav-link flex items-center"
|
||||
x-bind:class="darkMode
|
||||
? '{{ 'nav-link-active' if request.endpoint == 'profile' else '' }}'
|
||||
: '{{ 'nav-link-light-active' if request.endpoint == 'profile' else 'nav-link-light' }}'">
|
||||
<i class="fa-solid fa-user mr-2"></i>Profil
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Rechte Seite -->
|
||||
<div class="flex items-center space-x-4">
|
||||
<!-- Dark Mode Toggle Switch -->
|
||||
<div class="flex items-center cursor-pointer" @click="toggleDarkMode">
|
||||
<div class="relative w-12 h-6">
|
||||
<input type="checkbox" id="darkModeToggle" class="sr-only" x-model="darkMode">
|
||||
<div class="block w-12 h-6 rounded-full transition-colors duration-300"
|
||||
x-bind:class="darkMode ? 'bg-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"
|
||||
x-bind:class="darkMode ? 'bg-purple-600 transform translate-x-6' : 'bg-white'"></div>
|
||||
</div>
|
||||
<div class="ml-3 hidden sm:block"
|
||||
x-bind:class="darkMode ? 'text-white/90' : 'text-gray-700'">
|
||||
<span x-text="darkMode ? 'Dunkel' : 'Hell'"></span>
|
||||
</div>
|
||||
<div class="ml-2 sm:hidden"
|
||||
x-bind:class="darkMode ? 'text-white/90' : 'text-gray-700'">
|
||||
<i class="fa-solid" :class="darkMode ? 'fa-sun' : 'fa-moon'"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Profil-Link oder Login -->
|
||||
{% if current_user.is_authenticated %}
|
||||
<div class="relative" x-data="{ open: false }">
|
||||
<button @click="open = !open"
|
||||
class="flex items-center space-x-2 p-2 rounded-full transition-all duration-300 cursor-pointer"
|
||||
x-bind:class="darkMode
|
||||
? 'bg-gray-800/80 text-white/90 hover:bg-gray-700/80'
|
||||
: 'bg-gray-200/80 text-gray-700 hover:bg-gray-300/80'">
|
||||
<div class="w-9 h-9 rounded-full flex items-center justify-center text-white font-medium text-sm overflow-hidden"
|
||||
style="background: linear-gradient(135deg, #8b5cf6, #6366f1);">
|
||||
{% if current_user.avatar %}
|
||||
<img src="{{ current_user.avatar }}" alt="{{ current_user.username }}" class="w-full h-full object-cover">
|
||||
{% else %}
|
||||
{{ current_user.username[0].upper() }}
|
||||
{% endif %}
|
||||
</div>
|
||||
<span class="text-sm hidden lg:block">{{ current_user.username }}</span>
|
||||
<i class="fa-solid fa-chevron-down text-xs hidden lg:block transition-transform duration-200"
|
||||
x-bind:class="open ? 'transform rotate-180' : ''"></i>
|
||||
</button>
|
||||
|
||||
<!-- Dropdown-Menü -->
|
||||
<div x-show="open"
|
||||
@click.away="open = false"
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 scale-95"
|
||||
x-transition:enter-end="opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100 scale-100"
|
||||
x-transition:leave-end="opacity-0 scale-95"
|
||||
class="absolute right-0 mt-2 w-52 rounded-2xl overflow-hidden shadow-lg transform origin-top-right z-50"
|
||||
x-bind:class="darkMode
|
||||
? 'bg-gray-800/95 backdrop-blur-md border border-white/10'
|
||||
: 'bg-white/95 backdrop-blur-md border border-gray-200/50'">
|
||||
<a href="{{ url_for('profile') }}"
|
||||
class="block px-4 py-3 transition-colors duration-200 flex items-center"
|
||||
x-bind:class="darkMode
|
||||
? 'text-white/90 hover:bg-purple-500/20'
|
||||
: 'text-gray-700 hover:bg-purple-500/10'">
|
||||
<i class="fa-solid fa-user mr-2 text-purple-400"></i>Profil
|
||||
</a>
|
||||
<a href="{{ url_for('my_account') }}"
|
||||
class="block px-4 py-3 transition-colors duration-200 flex items-center"
|
||||
x-bind:class="darkMode
|
||||
? 'text-white/90 hover:bg-purple-500/20'
|
||||
: 'text-gray-700 hover:bg-purple-500/10'">
|
||||
<i class="fa-solid fa-bookmark mr-2 text-purple-400"></i>Meine Merkliste
|
||||
</a>
|
||||
<a href="{{ url_for('settings') }}"
|
||||
class="block px-4 py-3 transition-colors duration-200 flex items-center"
|
||||
x-bind:class="darkMode
|
||||
? 'text-white/90 hover:bg-purple-500/20'
|
||||
: 'text-gray-700 hover:bg-purple-500/10'">
|
||||
<i class="fa-solid fa-gear mr-2 text-purple-400"></i>Einstellungen
|
||||
</a>
|
||||
<div class="my-2 h-px" x-bind:class="darkMode ? 'bg-white/10' : 'bg-gray-200'"></div>
|
||||
<a href="{{ url_for('logout') }}"
|
||||
class="block px-4 py-3 transition-colors duration-200 flex items-center"
|
||||
x-bind:class="darkMode
|
||||
? 'text-white/90 hover:bg-red-500/20'
|
||||
: 'text-gray-700 hover:bg-red-500/10'">
|
||||
<i class="fa-solid fa-right-from-bracket mr-2 text-red-400"></i>Abmelden
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="flex items-center space-x-2">
|
||||
<a href="{{ url_for('login') }}"
|
||||
class="py-2 px-4 rounded-lg transition-all duration-300"
|
||||
x-bind:class="darkMode
|
||||
? 'text-white/90 hover:bg-dark-700/80'
|
||||
: 'text-gray-700 hover:bg-gray-100/80'">
|
||||
<i class="fa-solid fa-sign-in-alt mr-2"></i>Login
|
||||
</a>
|
||||
<a href="{{ url_for('register') }}"
|
||||
class="py-2 px-4 rounded-lg transition-all duration-300 font-medium"
|
||||
x-bind:class="darkMode
|
||||
? 'bg-purple-800/80 text-white hover:bg-purple-700/80'
|
||||
: 'bg-purple-600/20 text-gray-700 hover:bg-purple-600/30'">
|
||||
Registrieren
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Mobilmenü-Button -->
|
||||
<button @click="mobileMenuOpen = !mobileMenuOpen"
|
||||
class="md:hidden rounded-xl p-2.5 transition-colors duration-200 focus:outline-none"
|
||||
x-bind:class="darkMode
|
||||
? 'text-white/90 hover:bg-gray-700/50'
|
||||
: 'text-gray-700 hover:bg-gray-200/80'">
|
||||
<i class="fa-solid" :class="mobileMenuOpen ? 'fa-times' : 'fa-bars'"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Mobile Menü -->
|
||||
<div x-show="mobileMenuOpen"
|
||||
x-cloak
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 -translate-y-4"
|
||||
x-transition:enter-end="opacity-100 translate-y-0"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100 translate-y-0"
|
||||
x-transition:leave-end="opacity-0 -translate-y-4"
|
||||
class="md:hidden w-full z-40 border-b"
|
||||
x-bind:class="darkMode
|
||||
? 'bg-gray-900/90 backdrop-blur-lg border-white/10'
|
||||
: 'bg-white/90 backdrop-blur-lg border-gray-200'">
|
||||
<div class="px-4 py-4 space-y-3">
|
||||
<a href="{{ url_for('index') }}"
|
||||
class="block py-3.5 px-4 rounded-xl transition-all duration-200 flex items-center"
|
||||
x-bind:class="darkMode
|
||||
? '{{ 'bg-purple-500/20 text-white' if request.endpoint == 'index' else 'text-white/80 hover:bg-gray-800/80 hover:text-white' }}'
|
||||
: '{{ 'bg-purple-500/10 text-gray-900' if request.endpoint == 'index' else 'text-gray-700 hover:bg-gray-100 hover:text-gray-900' }}'">
|
||||
<i class="fa-solid fa-home w-5 mr-3"></i>Start
|
||||
</a>
|
||||
<a href="{{ url_for('mindmap') }}"
|
||||
class="block py-3.5 px-4 rounded-xl transition-all duration-200 flex items-center"
|
||||
x-bind:class="darkMode
|
||||
? '{{ 'bg-purple-500/20 text-white' if request.endpoint == 'mindmap' else 'text-white/80 hover:bg-gray-800/80 hover:text-white' }}'
|
||||
: '{{ 'bg-purple-500/10 text-gray-900' if request.endpoint == 'mindmap' else 'text-gray-700 hover:bg-gray-100 hover:text-gray-900' }}'">
|
||||
<i class="fa-solid fa-diagram-project w-5 mr-3"></i>Mindmap
|
||||
</a>
|
||||
<a href="{{ url_for('search_thoughts_page') }}"
|
||||
class="block py-3.5 px-4 rounded-xl transition-all duration-200 flex items-center"
|
||||
x-bind:class="darkMode
|
||||
? '{{ 'bg-purple-500/20 text-white' if request.endpoint == 'search_thoughts_page' else 'text-white/80 hover:bg-gray-800/80 hover:text-white' }}'
|
||||
: '{{ 'bg-purple-500/10 text-gray-900' if request.endpoint == 'search_thoughts_page' else 'text-gray-700 hover:bg-gray-100 hover:text-gray-900' }}'">
|
||||
<i class="fa-solid fa-search w-5 mr-3"></i>Suche
|
||||
</a>
|
||||
<!-- KI-Button für Mobilmenü -->
|
||||
<button onclick="window.MindMap && window.MindMap.assistant && window.MindMap.assistant.toggleAssistant(true); mobileMenuOpen = false;"
|
||||
class="block w-full text-left py-3.5 px-4 rounded-xl transition-all duration-200 flex items-center"
|
||||
x-bind:class="darkMode
|
||||
? 'bg-gradient-to-r from-purple-600/30 to-blue-500/30 text-white hover:from-purple-600/40 hover:to-blue-500/40'
|
||||
: 'bg-gradient-to-r from-purple-500/10 to-blue-400/10 text-gray-900 hover:from-purple-500/20 hover:to-blue-400/20'">
|
||||
<i class="fa-solid fa-robot w-5 mr-3"></i>KI-Chat
|
||||
</button>
|
||||
{% if current_user.is_authenticated %}
|
||||
<a href="{{ url_for('profile') }}"
|
||||
class="block py-3.5 px-4 rounded-xl transition-all duration-200 flex items-center"
|
||||
x-bind:class="darkMode
|
||||
? '{{ 'bg-purple-500/20 text-white' if request.endpoint == 'profile' else 'text-white/80 hover:bg-gray-800/80 hover:text-white' }}'
|
||||
: '{{ 'bg-purple-500/10 text-gray-900' if request.endpoint == 'profile' else 'text-gray-700 hover:bg-gray-100 hover:text-gray-900' }}'">
|
||||
<i class="fa-solid fa-user w-5 mr-3"></i>Profil
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hauptinhalt -->
|
||||
<main class="flex-grow pt-6">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="mt-12 py-10 transition-colors duration-300 rounded-t-3xl mx-4 sm:mx-6 md:mx-8"
|
||||
:class="darkMode ? 'bg-gray-900/60 backdrop-blur-xl border-t border-white/10' : 'bg-white/60 backdrop-blur-xl border-t border-gray-200/50'">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-10">
|
||||
<!-- Logo und Beschreibung -->
|
||||
<div class="text-center md:text-left flex flex-col">
|
||||
<a href="{{ url_for('index') }}" class="text-2xl font-bold mb-4 gradient-text inline-block transform transition-transform hover:scale-105">Systades</a>
|
||||
<p class="mt-2 text-sm max-w-md"
|
||||
:class="darkMode ? 'text-gray-300' : 'text-gray-600'">
|
||||
Eine interaktive Plattform zum Visualisieren, Erforschen und Teilen von Wissen und Gedanken in einem strukturierten Format.
|
||||
</p>
|
||||
<!-- Social Media Icons -->
|
||||
<div class="flex items-center space-x-4 mt-6 justify-center md:justify-start">
|
||||
<a href="#" class="transition-all duration-200 transform hover:scale-110 hover:-translate-y-1"
|
||||
:class="darkMode ? 'text-purple-400 hover:text-purple-300' : 'text-purple-600 hover:text-purple-500'">
|
||||
<i class="fab fa-twitter text-xl"></i>
|
||||
</a>
|
||||
<a href="#" class="transition-all duration-200 transform hover:scale-110 hover:-translate-y-1"
|
||||
:class="darkMode ? 'text-purple-400 hover:text-purple-300' : 'text-purple-600 hover:text-purple-500'">
|
||||
<i class="fab fa-linkedin text-xl"></i>
|
||||
</a>
|
||||
<a href="#" class="transition-all duration-200 transform hover:scale-110 hover:-translate-y-1"
|
||||
:class="darkMode ? 'text-purple-400 hover:text-purple-300' : 'text-purple-600 hover:text-purple-500'">
|
||||
<i class="fab fa-github text-xl"></i>
|
||||
</a>
|
||||
<a href="#" class="transition-all duration-200 transform hover:scale-110 hover:-translate-y-1"
|
||||
:class="darkMode ? 'text-purple-400 hover:text-purple-300' : 'text-purple-600 hover:text-purple-500'">
|
||||
<i class="fab fa-discord text-xl"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Links -->
|
||||
<div class="grid grid-cols-2 gap-8">
|
||||
<div class="flex flex-col space-y-3">
|
||||
<h3 class="font-semibold text-lg mb-2"
|
||||
:class="darkMode ? 'text-white' : 'text-gray-800'">Navigation</h3>
|
||||
<a href="{{ url_for('index') }}" class="text-sm transition-all duration-200"
|
||||
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
||||
Startseite
|
||||
</a>
|
||||
<a href="{{ url_for('mindmap') }}" class="text-sm transition-all duration-200"
|
||||
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
||||
Mindmap
|
||||
</a>
|
||||
{% if current_user.is_authenticated %}
|
||||
<a href="{{ url_for('profile') }}" class="text-sm transition-all duration-200"
|
||||
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
||||
Profil
|
||||
</a>
|
||||
<a href="{{ url_for('my_account') }}" class="text-sm transition-all duration-200"
|
||||
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
||||
Meine Merkliste
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{{ url_for('login') }}" class="text-sm transition-all duration-200"
|
||||
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
||||
Anmelden
|
||||
</a>
|
||||
<a href="{{ url_for('register') }}" class="text-sm transition-all duration-200"
|
||||
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
||||
Registrieren
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col space-y-3">
|
||||
<h3 class="font-semibold text-lg mb-2"
|
||||
:class="darkMode ? 'text-white' : 'text-gray-800'">Rechtliches</h3>
|
||||
<a href="{{ url_for('impressum') }}" class="text-sm transition-all duration-200"
|
||||
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
||||
Impressum
|
||||
</a>
|
||||
<a href="{{ url_for('ueber_uns') }}" class="text-sm transition-all duration-200"
|
||||
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
||||
Über uns
|
||||
</a>
|
||||
<a href="{{ url_for('datenschutz') }}" class="text-sm transition-all duration-200"
|
||||
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
||||
Datenschutz
|
||||
</a>
|
||||
<a href="{{ url_for('agb') }}" class="text-sm transition-all duration-200"
|
||||
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
||||
AGB
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Newsletter Anmeldung -->
|
||||
<div class="flex flex-col">
|
||||
<h3 class="font-semibold text-lg mb-4"
|
||||
:class="darkMode ? 'text-white' : 'text-gray-800'">Newsletter</h3>
|
||||
<p class="text-sm mb-4"
|
||||
:class="darkMode ? 'text-gray-300' : 'text-gray-600'">
|
||||
Bleibe auf dem Laufenden mit unseren neuesten Funktionen und Updates.
|
||||
</p>
|
||||
<form class="flex flex-col space-y-3">
|
||||
<input type="email" placeholder="Deine E-Mail Adresse"
|
||||
class="px-4 py-2.5 rounded-xl transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-purple-500"
|
||||
:class="darkMode ? 'bg-gray-800/80 text-white border border-gray-700 focus:bg-gray-800' : 'bg-white/80 text-gray-800 border border-gray-300 focus:bg-white'" />
|
||||
<button type="submit"
|
||||
class="px-4 py-2.5 rounded-xl font-medium transition-all duration-300 bg-gradient-to-r from-purple-600 to-indigo-600 text-white shadow-md hover:shadow-lg hover:-translate-y-0.5">
|
||||
Abonnieren
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Untere Linie -->
|
||||
<div class="mt-10 pt-6 border-t flex flex-col md:flex-row justify-between items-center"
|
||||
:class="darkMode ? 'border-gray-800/50 text-gray-400' : 'border-gray-300/50 text-gray-600'">
|
||||
<div class="text-xs md:text-sm mb-3 md:mb-0">
|
||||
© {{ current_year }} Systades. Alle Rechte vorbehalten.
|
||||
</div>
|
||||
<div class="text-xs md:text-sm">
|
||||
Designed with <i class="fas fa-heart text-pink-500"></i> in Deutschland
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- Hilfsscripts -->
|
||||
{% block scripts %}{% endblock %}
|
||||
|
||||
<!-- KI-Chat Initialisierung -->
|
||||
<script>
|
||||
// Initialisiere den ChatGPT-Assistenten direkt, um sicherzustellen,
|
||||
// dass er auf jeder Seite verfügbar ist, selbst wenn MindMap nicht geladen ist
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Prüfen, ob der Assistent bereits durch MindMap initialisiert wurde
|
||||
if (!window.MindMap || !window.MindMap.assistant) {
|
||||
console.log('KI-Assistent wird direkt initialisiert...');
|
||||
const assistant = new ChatGPTAssistant();
|
||||
assistant.init();
|
||||
|
||||
// Speichere in window.MindMap, falls es existiert, oder erstelle es
|
||||
if (!window.MindMap) {
|
||||
window.MindMap = {};
|
||||
}
|
||||
window.MindMap.assistant = assistant;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Dark/Light-Mode persistent und robust -->
|
||||
<script>
|
||||
(function() {
|
||||
function applyMode(mode) {
|
||||
if (mode === 'dark') {
|
||||
document.documentElement.classList.add('dark');
|
||||
localStorage.setItem('colorMode', 'dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
localStorage.setItem('colorMode', 'light');
|
||||
}
|
||||
}
|
||||
// Beim Laden: Präferenz aus localStorage oder System übernehmen
|
||||
const stored = localStorage.getItem('colorMode');
|
||||
if (stored === 'dark' || stored === 'light') {
|
||||
applyMode(stored);
|
||||
} else {
|
||||
// Systempräferenz als Fallback
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
applyMode(prefersDark ? 'dark' : 'light');
|
||||
}
|
||||
// Umschalter für alle Mode-Toggles
|
||||
window.toggleColorMode = function() {
|
||||
const isDark = document.documentElement.classList.contains('dark');
|
||||
applyMode(isDark ? 'light' : 'dark');
|
||||
};
|
||||
// Optional: globales Event für andere Skripte
|
||||
window.addEventListener('storage', function(e) {
|
||||
if (e.key === 'colorMode') applyMode(e.newValue);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
64
templates/datenschutz.html
Normal file
64
templates/datenschutz.html
Normal file
@@ -0,0 +1,64 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Datenschutz{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-3xl mx-auto">
|
||||
<div class="card p-6 md:p-8">
|
||||
<h1 class="text-3xl font-bold mb-6 gradient-text">Datenschutzerklärung</h1>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-bold mb-4">1. Datenschutz auf einen Blick</h2>
|
||||
|
||||
<h3 class="text-lg font-bold mb-2">Allgemeine Hinweise</h3>
|
||||
<p class="mb-4">Die folgenden Hinweise geben einen einfachen Überblick darüber, was mit Ihren personenbezogenen Daten passiert, wenn Sie diese Website besuchen. Personenbezogene Daten sind alle Daten, mit denen Sie persönlich identifiziert werden können. Ausführliche Informationen zum Thema Datenschutz entnehmen Sie unserer unter diesem Text aufgeführten Datenschutzerklärung.</p>
|
||||
|
||||
<h3 class="text-lg font-bold mb-2">Datenerfassung auf dieser Website</h3>
|
||||
<p class="mb-4"><strong>Wer ist verantwortlich für die Datenerfassung auf dieser Website?</strong></p>
|
||||
<p class="mb-4">Die Datenverarbeitung auf dieser Website erfolgt durch den Websitebetreiber. Dessen Kontaktdaten können Sie dem Impressum dieser Website entnehmen.</p>
|
||||
|
||||
<p class="mb-4"><strong>Wie erfassen wir Ihre Daten?</strong></p>
|
||||
<p class="mb-4">Ihre Daten werden zum einen dadurch erhoben, dass Sie uns diese mitteilen. Hierbei kann es sich z. B. um Daten handeln, die Sie in ein Kontaktformular eingeben.</p>
|
||||
<p class="mb-4">Andere Daten werden automatisch oder nach Ihrer Einwilligung beim Besuch der Website durch unsere IT-Systeme erfasst. Das sind vor allem technische Daten (z. B. Internetbrowser, Betriebssystem oder Uhrzeit des Seitenaufrufs). Die Erfassung dieser Daten erfolgt automatisch, sobald Sie diese Website betreten.</p>
|
||||
|
||||
<p class="mb-4"><strong>Wofür nutzen wir Ihre Daten?</strong></p>
|
||||
<p class="mb-4">Ein Teil der Daten wird erhoben, um eine fehlerfreie Bereitstellung der Website zu gewährleisten. Andere Daten können zur Analyse Ihres Nutzerverhaltens verwendet werden.</p>
|
||||
|
||||
<p class="mb-4"><strong>Welche Rechte haben Sie bezüglich Ihrer Daten?</strong></p>
|
||||
<p class="mb-4">Sie haben jederzeit das Recht, unentgeltlich Auskunft über Herkunft, Empfänger und Zweck Ihrer gespeicherten personenbezogenen Daten zu erhalten. Sie haben außerdem ein Recht, die Berichtigung oder Löschung dieser Daten zu verlangen. Wenn Sie eine Einwilligung zur Datenverarbeitung erteilt haben, können Sie diese Einwilligung jederzeit für die Zukunft widerrufen. Außerdem haben Sie das Recht, unter bestimmten Umständen die Einschränkung der Verarbeitung Ihrer personenbezogenen Daten zu verlangen.</p>
|
||||
</section>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-bold mb-4">2. Allgemeine Hinweise und Pflichtinformationen</h2>
|
||||
|
||||
<h3 class="text-lg font-bold mb-2">Datenschutz</h3>
|
||||
<p class="mb-4">Die Betreiber dieser Seiten nehmen den Schutz Ihrer persönlichen Daten sehr ernst. Wir behandeln Ihre personenbezogenen Daten vertraulich und entsprechend der gesetzlichen Datenschutzvorschriften sowie dieser Datenschutzerklärung.</p>
|
||||
<p class="mb-4">Wenn Sie diese Website benutzen, werden verschiedene personenbezogene Daten erhoben. Personenbezogene Daten sind Daten, mit denen Sie persönlich identifiziert werden können. Die vorliegende Datenschutzerklärung erläutert, welche Daten wir erheben und wofür wir sie nutzen. Sie erläutert auch, wie und zu welchem Zweck das geschieht.</p>
|
||||
<p class="mb-4">Wir weisen darauf hin, dass die Datenübertragung im Internet (z. B. bei der Kommunikation per E-Mail) Sicherheitslücken aufweisen kann. Ein lückenloser Schutz der Daten vor dem Zugriff durch Dritte ist nicht möglich.</p>
|
||||
|
||||
<h3 class="text-lg font-bold mb-2">Hinweis zur verantwortlichen Stelle</h3>
|
||||
<p class="mb-4">Die verantwortliche Stelle für die Datenverarbeitung auf dieser Website ist:</p>
|
||||
<p class="mb-4">
|
||||
MindMap GmbH<br>
|
||||
Musterstraße 123<br>
|
||||
12345 Musterstadt<br>
|
||||
Deutschland
|
||||
</p>
|
||||
<p class="mb-4">
|
||||
Telefon: +49 (0) 123 456789<br>
|
||||
E-Mail: info@mindmap-example.com
|
||||
</p>
|
||||
<p class="mb-4">Verantwortliche Stelle ist die natürliche oder juristische Person, die allein oder gemeinsam mit anderen über die Zwecke und Mittel der Verarbeitung von personenbezogenen Daten (z. B. Namen, E-Mail-Adressen o. Ä.) entscheidet.</p>
|
||||
</section>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-bold mb-4">3. Datenerfassung auf dieser Website</h2>
|
||||
|
||||
<h3 class="text-lg font-bold mb-2">Cookies</h3>
|
||||
<p class="mb-4">Unsere Internetseiten verwenden so genannte "Cookies". Cookies sind kleine Datenpakete und richten auf Ihrem Endgerät keinen Schaden an. Sie werden entweder vorübergehend für die Dauer einer Sitzung (Session-Cookies) oder dauerhaft (permanente Cookies) auf Ihrem Endgerät gespeichert. Session-Cookies werden nach Ende Ihres Besuchs automatisch gelöscht. Permanente Cookies bleiben auf Ihrem Endgerät gespeichert, bis Sie diese selbst löschen oder eine automatische Löschung durch Ihren Webbrowser erfolgt.</p>
|
||||
<p class="mb-4">Cookies können von uns (First-Party-Cookies) oder von Drittunternehmen stammen (sog. Third-Party-Cookies). Third-Party-Cookies ermöglichen die Einbindung bestimmter Dienstleistungen von Drittunternehmen innerhalb von Webseiten (z. B. Cookies zur Abwicklung von Zahlungsdienstleistungen).</p>
|
||||
<p class="mb-4">Die meisten Browser bieten Ihnen die Möglichkeit, das Setzen von Cookies für bestimmte Webseiten zu verbieten oder Cookies jedes Mal vor dem Akzeptieren anzuzeigen. Ebenso können Sie jederzeit mitgeteilt bekommen, sobald Ihr Browser ein neues Cookie empfängt.</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
33
templates/errors/403.html
Normal file
33
templates/errors/403.html
Normal file
@@ -0,0 +1,33 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}403 - Zugriff verweigert{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<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-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="flex justify-center mb-6">
|
||||
<div class="relative">
|
||||
<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">
|
||||
<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
|
||||
</a>
|
||||
<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
|
||||
</a>
|
||||
</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>
|
||||
{% endblock %}
|
||||
33
templates/errors/404.html
Normal file
33
templates/errors/404.html
Normal file
@@ -0,0 +1,33 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}404 - Seite nicht gefunden{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<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-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="flex justify-center mb-6">
|
||||
<div class="relative">
|
||||
<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">
|
||||
<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
|
||||
</a>
|
||||
<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
|
||||
</a>
|
||||
</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>
|
||||
{% endblock %}
|
||||
33
templates/errors/429.html
Normal file
33
templates/errors/429.html
Normal file
@@ -0,0 +1,33 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}429 - Zu viele Anfragen{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<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-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="flex justify-center mb-6">
|
||||
<div class="relative">
|
||||
<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">
|
||||
<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
|
||||
</a>
|
||||
<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
|
||||
</a>
|
||||
</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>
|
||||
{% endblock %}
|
||||
33
templates/errors/500.html
Normal file
33
templates/errors/500.html
Normal file
@@ -0,0 +1,33 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}500 - Serverfehler{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<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-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="flex justify-center mb-6">
|
||||
<div class="relative">
|
||||
<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">
|
||||
<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
|
||||
</a>
|
||||
<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
|
||||
</a>
|
||||
</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>
|
||||
{% endblock %}
|
||||
64
templates/impressum.html
Normal file
64
templates/impressum.html
Normal file
@@ -0,0 +1,64 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Impressum{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-3xl mx-auto">
|
||||
<div class="card p-6 md:p-8">
|
||||
<h1 class="text-3xl font-bold mb-6 gradient-text">Impressum</h1>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-bold mb-4">Angaben gemäß § 5 TMG und § 55 RStV</h2>
|
||||
<p class="mb-4">
|
||||
Diese Website wird privat betrieben von:<br>
|
||||
Marwin Medczinski<br>
|
||||
Fasanenstraße 30<br>
|
||||
16761 Hennigsdorf<br>
|
||||
Deutschland
|
||||
</p>
|
||||
|
||||
|
||||
</p>
|
||||
|
||||
<p class="mb-4">
|
||||
<strong>Kontakt:</strong><br>
|
||||
Telefon: +49 (0) 173 8041824<br>
|
||||
E-Mail: medczinski.marwin@gmx.de
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-bold mb-4">Inhaltlich Verantwortlicher gemäß § 55 Abs. 2 RStV</h2>
|
||||
<p class="mb-4">
|
||||
Marwin Medczinski<br>
|
||||
Fasanenstraße 30<br>
|
||||
16761 Hennigsdorf
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-bold mb-4">Hinweis zur Streitbeilegung</h2>
|
||||
<p class="mb-4">Die Europäische Kommission stellt eine Plattform zur Online-Streitbeilegung (OS) bereit: <a href="https://ec.europa.eu/consumers/odr/" target="_blank" class="text-purple-600 hover:text-purple-800">https://ec.europa.eu/consumers/odr/</a></p>
|
||||
<p class="mb-4">Ich bin nicht bereit oder verpflichtet, an Streitbeilegungsverfahren vor einer Verbraucherschlichtungsstelle teilzunehmen.</p>
|
||||
</section>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-bold mb-4">Haftungsausschluss</h2>
|
||||
|
||||
<h3 class="text-lg font-bold mb-2">Haftung für Inhalte</h3>
|
||||
<p class="mb-4">Die Inhalte dieser Seiten wurden mit größter Sorgfalt erstellt. Für die Richtigkeit, Vollständigkeit und Aktualität der Inhalte kann ich jedoch keine Gewähr übernehmen. Als Diensteanbieter bin ich gemäß § 7 Abs.1 TMG für eigene Inhalte auf diesen Seiten nach den allgemeinen Gesetzen verantwortlich. Nach §§ 8 bis 10 TMG bin ich als Diensteanbieter jedoch nicht verpflichtet, übermittelte oder gespeicherte fremde Informationen zu überwachen oder nach Umständen zu forschen, die auf eine rechtswidrige Tätigkeit hinweisen.</p>
|
||||
<p class="mb-4">Verpflichtungen zur Entfernung oder Sperrung der Nutzung von Informationen nach den allgemeinen Gesetzen bleiben hiervon unberührt. Eine diesbezügliche Haftung ist jedoch erst ab dem Zeitpunkt der Kenntnis einer konkreten Rechtsverletzung möglich. Bei Bekanntwerden von entsprechenden Rechtsverletzungen werde ich diese Inhalte umgehend entfernen.</p>
|
||||
|
||||
<h3 class="text-lg font-bold mb-2">Haftung für Links</h3>
|
||||
<p class="mb-4">Diese Website enthält Links zu externen Webseiten Dritter, auf deren Inhalte ich keinen Einfluss habe. Deshalb kann ich für diese fremden Inhalte auch keine Gewähr übernehmen. Für die Inhalte der verlinkten Seiten ist stets der jeweilige Anbieter oder Betreiber der Seiten verantwortlich. Die verlinkten Seiten wurden zum Zeitpunkt der Verlinkung auf mögliche Rechtsverstöße überprüft. Rechtswidrige Inhalte waren zum Zeitpunkt der Verlinkung nicht erkennbar.</p>
|
||||
<p class="mb-4">Eine permanente inhaltliche Kontrolle der verlinkten Seiten ist jedoch ohne konkrete Anhaltspunkte einer Rechtsverletzung nicht zumutbar. Bei Bekanntwerden von Rechtsverletzungen werde ich derartige Links umgehend entfernen.</p>
|
||||
</section>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-bold mb-4">Urheberrecht</h2>
|
||||
<p class="mb-4">Die durch mich erstellten Inhalte und Werke auf diesen Seiten unterliegen dem deutschen Urheberrecht. Die Vervielfältigung, Bearbeitung, Verbreitung und jede Art der Verwertung außerhalb der Grenzen des Urheberrechtes bedürfen meiner schriftlichen Zustimmung. Downloads und Kopien dieser Seite sind nur für den privaten, nicht kommerziellen Gebrauch gestattet.</p>
|
||||
<p>Soweit die Inhalte auf dieser Seite nicht von mir erstellt wurden, werden die Urheberrechte Dritter beachtet. Insbesondere werden Inhalte Dritter als solche gekennzeichnet. Sollten Sie trotzdem auf eine Urheberrechtsverletzung aufmerksam werden, bitte ich um einen entsprechenden Hinweis. Bei Bekanntwerden von Rechtsverletzungen werde ich derartige Inhalte umgehend entfernen.</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
520
templates/index.html
Normal file
520
templates/index.html
Normal file
@@ -0,0 +1,520 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Wissensnetzwerk{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
/* Full height and width for the page */
|
||||
html, body {
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Remove gradient backgrounds */
|
||||
.hero-gradient, .bg-fade {
|
||||
background: none !important;
|
||||
clip-path: none !important;
|
||||
}
|
||||
|
||||
/* Style elements */
|
||||
.mystical-line {
|
||||
height: 1px;
|
||||
background: linear-gradient(to right, transparent, rgba(109, 40, 217, 0.2), transparent);
|
||||
}
|
||||
|
||||
.dark .mystical-line {
|
||||
background: linear-gradient(to right, transparent, rgba(139, 92, 246, 0.2), transparent);
|
||||
}
|
||||
|
||||
/* Text reveal animation */
|
||||
@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%); }
|
||||
}
|
||||
|
||||
.text-reveal {
|
||||
animation: textReveal 1s cubic-bezier(0.77, 0, 0.18, 1) forwards;
|
||||
}
|
||||
|
||||
.delay-1 { animation-delay: 0.2s; }
|
||||
.delay-2 { animation-delay: 0.4s; }
|
||||
.delay-3 { animation-delay: 0.6s; }
|
||||
|
||||
/* 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);
|
||||
}
|
||||
|
||||
.dark .featured-card {
|
||||
border-color: rgba(109, 40, 217, 0.2);
|
||||
background-color: rgba(17, 24, 39, 0.7);
|
||||
}
|
||||
|
||||
.featured-card:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.dark .featured-card:hover {
|
||||
box-shadow: 0 5px 15px rgba(109, 40, 217, 0.2);
|
||||
border-color: rgba(109, 40, 217, 0.3);
|
||||
}
|
||||
|
||||
.featured-card:hover {
|
||||
box-shadow: 0 5px 15px rgba(139, 92, 246, 0.1);
|
||||
border-color: rgba(139, 92, 246, 0.2);
|
||||
}
|
||||
|
||||
/* Chat section styles */
|
||||
.embedded-chat {
|
||||
height: 350px;
|
||||
border-radius: 1rem;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid;
|
||||
}
|
||||
|
||||
.dark .embedded-chat {
|
||||
background-color: rgba(17, 24, 39, 0.7);
|
||||
border-color: rgba(109, 40, 217, 0.2);
|
||||
}
|
||||
|
||||
.embedded-chat {
|
||||
background-color: rgba(255, 255, 255, 0.7);
|
||||
border-color: rgba(139, 92, 246, 0.1);
|
||||
}
|
||||
|
||||
#embedded-chat-messages {
|
||||
height: 250px;
|
||||
overflow-y: auto;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
/* Chat typing indicator */
|
||||
.typing-dots span {
|
||||
display: inline-block;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 50%;
|
||||
margin-right: 3px;
|
||||
background-color: currentColor;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.typing-dots span:nth-child(1) {
|
||||
animation: dot-pulse 1.2s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.typing-dots span:nth-child(2) {
|
||||
animation: dot-pulse 1.2s infinite ease-in-out 0.2s;
|
||||
}
|
||||
|
||||
.typing-dots span:nth-child(3) {
|
||||
animation: dot-pulse 1.2s infinite ease-in-out 0.4s;
|
||||
}
|
||||
|
||||
@keyframes dot-pulse {
|
||||
0%, 100% { transform: scale(1); opacity: 0.5; }
|
||||
50% { transform: scale(1.3); opacity: 1; }
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Hero Section -->
|
||||
<section class="relative pt-20 pb-24">
|
||||
<!-- Hero Content -->
|
||||
<div class="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||
<div class="text-center mb-16">
|
||||
<h1 class="hero-heading mb-8 text-gray-900 dark:text-white">
|
||||
<div class="overflow-hidden">
|
||||
<span class="gradient-text inline-block text-reveal">Wissen</span>
|
||||
</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>
|
||||
</span>
|
||||
</div>
|
||||
</h1>
|
||||
<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
|
||||
in einem interaktiven Wissensnetzwerk.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-col sm:flex-row gap-5 justify-center">
|
||||
<a href="{{ url_for('mindmap') }}" class="mystical-button mystical-button-primary group transition-all duration-300">
|
||||
<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"></i>
|
||||
<span class="relative">
|
||||
Mindmap erkunden
|
||||
<span class="absolute -bottom-1 left-0 w-0 h-0.5 bg-white group-hover:w-full transition-all duration-300"></span>
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
{% if not current_user.is_authenticated %}
|
||||
<a href="{{ url_for('register') }}" class="mystical-button mystical-button-secondary group transition-all duration-300">
|
||||
<span class="flex items-center justify-center">
|
||||
<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">
|
||||
Konto erstellen
|
||||
<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>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Central logo and name -->
|
||||
<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="text-center">
|
||||
<div class="text-3xl font-bold gradient-text mb-2 animate-float">Systades</div>
|
||||
<div class="text-lg text-gray-700 dark:text-gray-300">WISSEN VERNETZEN</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Features Section -->
|
||||
<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-screen-xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="text-center mb-16">
|
||||
<h2 class="section-heading mb-4 text-gray-900 dark:text-white">Was ist <span class="gradient-text">Systades?</span></h2>
|
||||
<p class="text-lg text-gray-700 dark:text-gray-300 max-w-3xl mx-auto">
|
||||
Ein modernes Werkzeug zum Visualisieren, Erforschen und Teilen von Wissen in einem interaktiven
|
||||
Netzwerk, das dir hilft, Verbindungen zu entdecken und dein Wissen zu organisieren.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Feature Cards -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-8 mb-12">
|
||||
<!-- Feature 1: Visualize -->
|
||||
<div class="featured-card rounded-2xl p-6 bg-white/80 dark:bg-gray-800/30 backdrop-blur-sm">
|
||||
<div class="mb-4 text-purple-600 dark:text-purple-400">
|
||||
<i class="fa-solid fa-diagram-project text-3xl"></i>
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold mb-2 text-gray-900 dark:text-white">Visualisiere Wissen</h3>
|
||||
<p class="text-gray-700 dark:text-gray-300">
|
||||
Organisiere Gedanken und Ideen in einem interaktiven Netzwerk, das komplexe Beziehungen
|
||||
visuell darstellt und neue Verbindungen offenbart.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Feature 2: Connect -->
|
||||
<div class="featured-card rounded-2xl p-6 bg-white/80 dark:bg-gray-800/30 backdrop-blur-sm">
|
||||
<div class="mb-4 text-indigo-600 dark:text-indigo-400">
|
||||
<i class="fa-solid fa-network-wired text-3xl"></i>
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold mb-2 text-gray-900 dark:text-white">Verknüpfe Gedanken</h3>
|
||||
<p class="text-gray-700 dark:text-gray-300">
|
||||
Entdecke Zusammenhänge zwischen scheinbar unzusammenhängenden Ideen und schaffe dein
|
||||
persönliches Wissensnetzwerk über verschiedene Bereiche hinweg.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Feature 3: Share -->
|
||||
<div class="featured-card rounded-2xl p-6 bg-white/80 dark:bg-gray-800/30 backdrop-blur-sm">
|
||||
<div class="mb-4 text-purple-600 dark:text-purple-400">
|
||||
<i class="fa-solid fa-share-nodes text-3xl"></i>
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold mb-2 text-gray-900 dark:text-white">Teile und Entdecke</h3>
|
||||
<p class="text-gray-700 dark:text-gray-300">
|
||||
Tausche Erkenntnisse mit anderen aus und erweitere dein Wissen durch die
|
||||
Perspektiven und Gedanken der Community.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- AI Assistant Preview -->
|
||||
<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-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">Dein <span class="gradient-text">KI-Assistent</span></h2>
|
||||
<p class="text-lg text-gray-700 dark:text-gray-300 max-w-3xl mx-auto">
|
||||
Unser integrierter KI-Assistent hilft dir, Wissen zu organisieren, Verbindungen zu finden und
|
||||
Einsichten zu gewinnen.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Chat Interface Preview -->
|
||||
<div class="max-w-3xl mx-auto">
|
||||
<div class="embedded-chat" id="demo-chat">
|
||||
<!-- Chat Header -->
|
||||
<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>
|
||||
|
||||
<!-- Chat Messages -->
|
||||
<div id="embedded-chat-messages" class="border-b border-gray-200 dark:border-gray-700">
|
||||
<!-- Assistant Message -->
|
||||
<div class="mb-4 flex">
|
||||
<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>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>
|
||||
|
||||
<!-- User Message -->
|
||||
<div class="mb-4 flex justify-end">
|
||||
<div class="bg-purple-100 dark:bg-purple-900/30 rounded-lg p-3 max-w-[80%]">
|
||||
<p class="text-gray-800 dark:text-gray-200">
|
||||
Kann ich mit deiner Hilfe eine Mindmap zum Thema Künstliche Intelligenz erstellen?
|
||||
</p>
|
||||
</div>
|
||||
<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>
|
||||
</button>
|
||||
</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>
|
||||
|
||||
<!-- Try it Button -->
|
||||
<div class="text-center mt-10">
|
||||
<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?')"
|
||||
class="mystical-button mystical-button-primary inline-flex items-center">
|
||||
<i class="fa-solid fa-robot mr-2"></i>
|
||||
KI-Assistenten ausprobieren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Getting Started Section -->
|
||||
<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-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>
|
||||
</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 %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Expand-Button mit dem echten Assistenten verknüpfen
|
||||
const openRealAssistantBtn = document.getElementById('open-real-assistant');
|
||||
if (openRealAssistantBtn) {
|
||||
openRealAssistantBtn.addEventListener('click', function() {
|
||||
if (window.MindMap && window.MindMap.assistant) {
|
||||
window.MindMap.assistant.toggleAssistant(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Auch die Beispiel-Buttons im Demo-Chat klickbar machen
|
||||
const quickQueryButtons = document.querySelectorAll('.quick-query-btn');
|
||||
quickQueryButtons.forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
if (window.MindMap && window.MindMap.assistant) {
|
||||
const question = button.getAttribute('data-question');
|
||||
if (question) {
|
||||
window.MindMap.assistant.sendQuestion(question);
|
||||
} else {
|
||||
// Fallback auf den Button-Text, falls kein data-question Attribut gesetzt ist
|
||||
const queryText = button.textContent.trim();
|
||||
window.MindMap.assistant.sendQuestion(queryText);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Styling für die markdown-content hinzufügen
|
||||
const style = document.createElement('style');
|
||||
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; }
|
||||
.markdown-content h2 { font-size: 1.3rem; }
|
||||
.markdown-content h3 { font-size: 1.2rem; }
|
||||
.markdown-content h4 { font-size: 1.1rem; }
|
||||
.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>
|
||||
{% endblock %}
|
||||
11
templates/layout.html
Normal file
11
templates/layout.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<!-- Navigation -->
|
||||
<header class="w-full">
|
||||
<nav class="fixed top-0 z-50 w-full bg-dark-900 border-b border-gray-700">
|
||||
<!-- ... existing code ... -->
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<!-- Main Content Container -->
|
||||
<div class="container mx-auto px-4 pt-20 pb-10">
|
||||
<!-- ... existing code ... -->
|
||||
</div>
|
||||
55
templates/login.html
Normal file
55
templates/login.html
Normal file
@@ -0,0 +1,55 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Anmelden{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="flex justify-center items-center min-h-screen px-4 py-12">
|
||||
<div class="w-full max-w-md">
|
||||
<div class="bg-white bg-opacity-20 backdrop-blur-lg rounded-xl shadow-xl overflow-hidden transition-all duration-300 hover:shadow-2xl">
|
||||
<div class="p-6 sm:p-8">
|
||||
<h2 class="text-center text-2xl font-bold text-gray-800 mb-6">
|
||||
<i class="fas fa-sign-in-alt mr-2"></i>
|
||||
Anmelden
|
||||
</h2>
|
||||
|
||||
<form method="POST" action="{{ url_for('login') }}" class="space-y-6">
|
||||
<div class="space-y-2">
|
||||
<label for="username" class="block text-sm font-medium text-gray-700">Benutzername</label>
|
||||
<div class="relative rounded-md shadow-sm">
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<i class="fas fa-user text-gray-400"></i>
|
||||
</div>
|
||||
<input type="text" id="username" name="username" required
|
||||
class="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
|
||||
placeholder="Benutzername eingeben">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<label for="password" class="block text-sm font-medium text-gray-700">Passwort</label>
|
||||
<div class="relative rounded-md shadow-sm">
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<i class="fas fa-lock text-gray-400"></i>
|
||||
</div>
|
||||
<input type="password" id="password" name="password" required
|
||||
class="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
|
||||
placeholder="Passwort eingeben">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button type="submit"
|
||||
class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors duration-200">
|
||||
<i class="fas fa-sign-in-alt mr-2"></i> Anmelden
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="text-center text-sm text-gray-600">
|
||||
<p>Noch kein Konto? <a href="{{ url_for('register') }}" class="font-medium text-blue-600 hover:text-blue-500 transition-colors">Registrieren</a></p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
234
templates/mindmap.html
Normal file
234
templates/mindmap.html
Normal file
@@ -0,0 +1,234 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Interaktive Mindmap</title>
|
||||
|
||||
<!-- Cytoscape.js -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.26.0/cytoscape.min.js"></script>
|
||||
|
||||
<!-- Socket.IO -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.7.2/socket.io.min.js"></script>
|
||||
|
||||
<!-- Feather Icons (optional) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
|
||||
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background-color: #f9fafb;
|
||||
color: #111827;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.header {
|
||||
background-color: #1f2937;
|
||||
color: white;
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
background-color: #f3f4f6;
|
||||
padding: 0.75rem;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background-color: #3b82f6;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background-color: #2563eb;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: #6b7280;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: #4b5563;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: #ef4444;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background-color: #dc2626;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
#cy {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.category-filters {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
padding: 0.75rem;
|
||||
background-color: #ffffff;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.category-filter {
|
||||
border: none;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.25rem 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.category-filter:not(.active) {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.category-filter:hover:not(.active) {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.footer {
|
||||
background-color: #f3f4f6;
|
||||
padding: 0.75rem;
|
||||
text-align: center;
|
||||
font-size: 0.75rem;
|
||||
color: #6b7280;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
/* Kontextmenü Styling */
|
||||
#context-menu {
|
||||
position: absolute;
|
||||
background-color: white;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 0.25rem;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#context-menu .menu-item {
|
||||
padding: 0.5rem 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#context-menu .menu-item:hover {
|
||||
background-color: #f3f4f6;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header class="header">
|
||||
<h1>Interaktive Mindmap</h1>
|
||||
<div class="search-container">
|
||||
<input type="text" id="search-mindmap" class="search-input" placeholder="Suchen...">
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="toolbar">
|
||||
<button id="addNode" class="btn">
|
||||
<i data-feather="plus-circle"></i>
|
||||
Knoten hinzufügen
|
||||
</button>
|
||||
<button id="addEdge" class="btn">
|
||||
<i data-feather="git-branch"></i>
|
||||
Verbindung erstellen
|
||||
</button>
|
||||
<button id="editNode" class="btn btn-secondary">
|
||||
<i data-feather="edit-2"></i>
|
||||
Knoten bearbeiten
|
||||
</button>
|
||||
<button id="deleteNode" class="btn btn-danger">
|
||||
<i data-feather="trash-2"></i>
|
||||
Knoten löschen
|
||||
</button>
|
||||
<button id="deleteEdge" class="btn btn-danger">
|
||||
<i data-feather="scissors"></i>
|
||||
Verbindung löschen
|
||||
</button>
|
||||
<button id="reLayout" class="btn btn-secondary">
|
||||
<i data-feather="refresh-cw"></i>
|
||||
Layout neu anordnen
|
||||
</button>
|
||||
<button id="exportMindmap" class="btn btn-secondary">
|
||||
<i data-feather="download"></i>
|
||||
Exportieren
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="category-filters" class="category-filters">
|
||||
<!-- Wird dynamisch befüllt -->
|
||||
</div>
|
||||
|
||||
<div id="cy"></div>
|
||||
|
||||
<footer class="footer">
|
||||
Mindmap-Anwendung © 2023
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- Unsere Mindmap JS -->
|
||||
<script src="{{ url_for('static', filename='js/mindmap.js') }}"></script>
|
||||
|
||||
<!-- Icons initialisieren -->
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (typeof feather !== 'undefined') {
|
||||
feather.replace();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
320
templates/my_account.html
Normal file
320
templates/my_account.html
Normal file
@@ -0,0 +1,320 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Meine Merkliste{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
/* Hintergrund über die gesamte Seite erstrecken */
|
||||
html, body {
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.personal-mindmap-container {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 1rem;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dark .personal-mindmap-container {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.bookmark-item {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.bookmark-item:hover {
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
.dark .empty-state {
|
||||
background-color: rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Hauptbereich -->
|
||||
<div class="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8 pt-8 pb-12">
|
||||
<!-- Header -->
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white mb-2">
|
||||
<span class="gradient-text">Meine Merkliste</span>
|
||||
</h1>
|
||||
<p class="text-lg text-gray-700 dark:text-gray-300">
|
||||
Deine persönliche Sammlung gemerkter Wissensbereiche und Beiträge
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Persönliche Mindmap -->
|
||||
<div class="mb-12">
|
||||
<h2 class="text-xl font-bold text-gray-800 dark:text-white mb-4 flex items-center">
|
||||
<i class="fas fa-project-diagram mr-3 text-purple-500"></i>
|
||||
Meine Mindmap
|
||||
</h2>
|
||||
|
||||
<div id="personal-mindmap" class="personal-mindmap-container glass-morphism">
|
||||
<div id="empty-mindmap-message" class="absolute inset-0 flex items-center justify-center">
|
||||
<div class="text-center p-6">
|
||||
<div class="text-6xl mb-4 opacity-20">
|
||||
<i class="fas fa-bookmark"></i>
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold mb-2 text-gray-700 dark:text-gray-300">Deine persönliche Mindmap ist leer</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400 max-w-md mx-auto">
|
||||
Merke dir Wissensbereiche und Gedanken in der Hauptmindmap, um deine persönliche Mindmap zu erstellen.
|
||||
</p>
|
||||
<a href="{{ url_for('mindmap') }}" class="mt-4 inline-block px-4 py-2 bg-purple-500 text-white rounded-lg hover:bg-purple-600 transition-colors">
|
||||
Zur Mindmap
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gemerkte Inhalte -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 mb-8">
|
||||
<!-- Wissensbereiche -->
|
||||
<div>
|
||||
<h2 class="text-xl font-bold text-gray-800 dark:text-white mb-4 flex items-center">
|
||||
<i class="fas fa-folder-open mr-3 text-blue-500"></i>
|
||||
Gemerkte Wissensbereiche
|
||||
</h2>
|
||||
|
||||
<div id="bookmarked-areas-container" class="space-y-4">
|
||||
<!-- Ladezustand -->
|
||||
<div class="animate-pulse space-y-4">
|
||||
<div class="h-16 bg-gray-200 dark:bg-gray-700 rounded-lg"></div>
|
||||
<div class="h-16 bg-gray-200 dark:bg-gray-700 rounded-lg"></div>
|
||||
<div class="h-16 bg-gray-200 dark:bg-gray-700 rounded-lg"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gedanken -->
|
||||
<div>
|
||||
<h2 class="text-xl font-bold text-gray-800 dark:text-white mb-4 flex items-center">
|
||||
<i class="fas fa-lightbulb mr-3 text-amber-500"></i>
|
||||
Gemerkte Gedanken
|
||||
</h2>
|
||||
|
||||
<div id="bookmarked-thoughts-container" class="space-y-4">
|
||||
<!-- Ladezustand -->
|
||||
<div class="animate-pulse space-y-4">
|
||||
<div class="h-16 bg-gray-200 dark:bg-gray-700 rounded-lg"></div>
|
||||
<div class="h-16 bg-gray-200 dark:bg-gray-700 rounded-lg"></div>
|
||||
<div class="h-16 bg-gray-200 dark:bg-gray-700 rounded-lg"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- JavaScript für persönliche Mindmap -->
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Lade gespeicherte Bookmarks aus dem LocalStorage
|
||||
function loadBookmarkedNodes() {
|
||||
try {
|
||||
const bookmarked = localStorage.getItem('bookmarkedNodes');
|
||||
return bookmarked ? JSON.parse(bookmarked) : [];
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der gemerkten Knoten:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
const bookmarkedNodeIds = loadBookmarkedNodes();
|
||||
|
||||
// Prüfe, ob es gemerkte Knoten gibt
|
||||
if (bookmarkedNodeIds && bookmarkedNodeIds.length > 0) {
|
||||
// Verstecke die Leer-Nachricht
|
||||
document.getElementById('empty-mindmap-message').style.display = 'none';
|
||||
|
||||
// Initialisiere die persönliche Mindmap
|
||||
const personalMindmap = new MindMapVisualization('#personal-mindmap', {
|
||||
width: document.getElementById('personal-mindmap').clientWidth,
|
||||
height: 400,
|
||||
nodeRadius: 18,
|
||||
selectedNodeRadius: 22,
|
||||
linkDistance: 120,
|
||||
chargeStrength: -800,
|
||||
centerForce: 0.1,
|
||||
tooltipEnabled: true
|
||||
});
|
||||
|
||||
// Lade Daten für die Mindmap - hier müssten wir normalerweise die API nutzen,
|
||||
// aber als Fallback laden wir die Standarddaten und filtern sie
|
||||
window.setTimeout(() => {
|
||||
// Wenn die Hauptmindmap geladen ist, können wir deren Daten verwenden
|
||||
if (window.mindmapInstance) {
|
||||
// Filtere nur gemerkte Knoten und ihre Verbindungen
|
||||
const nodes = window.mindmapInstance.nodes.filter(node =>
|
||||
bookmarkedNodeIds.includes(node.id)
|
||||
);
|
||||
|
||||
// Finde Verbindungen zwischen den gemerkten Knoten
|
||||
const links = window.mindmapInstance.links.filter(link =>
|
||||
bookmarkedNodeIds.includes(link.source.id || link.source) &&
|
||||
bookmarkedNodeIds.includes(link.target.id || link.target)
|
||||
);
|
||||
|
||||
// Setze Daten und aktualisiere die Visualisierung
|
||||
personalMindmap.nodes = nodes;
|
||||
personalMindmap.links = links;
|
||||
personalMindmap.isLoading = false;
|
||||
personalMindmap.updateVisualization();
|
||||
} else {
|
||||
// Fallback: Leere Mindmap anzeigen
|
||||
document.getElementById('empty-mindmap-message').style.display = 'flex';
|
||||
}
|
||||
}, 800);
|
||||
|
||||
// Lade die gemerkten Inhalte
|
||||
loadBookmarkedContent(bookmarkedNodeIds);
|
||||
} else {
|
||||
// Zeige Leerzustand an
|
||||
const areasContainer = document.getElementById('bookmarked-areas-container');
|
||||
const thoughtsContainer = document.getElementById('bookmarked-thoughts-container');
|
||||
|
||||
areasContainer.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<div class="text-4xl mb-2 opacity-20">
|
||||
<i class="fas fa-folder-open"></i>
|
||||
</div>
|
||||
<p class="text-gray-600 dark:text-gray-400">Keine gemerkten Wissensbereiche</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
thoughtsContainer.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<div class="text-4xl mb-2 opacity-20">
|
||||
<i class="fas fa-lightbulb"></i>
|
||||
</div>
|
||||
<p class="text-gray-600 dark:text-gray-400">Keine gemerkten Gedanken</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
});
|
||||
|
||||
// Funktion zum Laden der gemerkten Inhalte
|
||||
function loadBookmarkedContent(nodeIds) {
|
||||
if (!nodeIds || nodeIds.length === 0) return;
|
||||
|
||||
// In einer vollständigen Implementierung würden wir hier einen API-Aufruf machen
|
||||
// Für diese Demo erstellen wir Beispielinhalte
|
||||
|
||||
const areasContainer = document.getElementById('bookmarked-areas-container');
|
||||
const thoughtsContainer = document.getElementById('bookmarked-thoughts-container');
|
||||
|
||||
// Verschiedene Beispiel-Farben
|
||||
const colors = ['purple', 'blue', 'green', 'indigo', 'amber'];
|
||||
|
||||
// Leere die Container
|
||||
areasContainer.innerHTML = '';
|
||||
thoughtsContainer.innerHTML = '';
|
||||
|
||||
// Beispielinhalte für Wissensbereiche
|
||||
const areaTemplates = [
|
||||
{ name: 'Philosophie', description: 'Grundlagen philosophischen Denkens', count: 24 },
|
||||
{ name: 'Wissenschaft', description: 'Wissenschaftliche Methoden und Erkenntnisse', count: 42 },
|
||||
{ name: 'Technologie', description: 'Zukunftsweisende Technologien', count: 36 },
|
||||
{ name: 'Kunst', description: 'Künstlerische Ausdrucksformen', count: 18 },
|
||||
{ name: 'Psychologie', description: 'Menschliches Verhalten verstehen', count: 30 }
|
||||
];
|
||||
|
||||
// Beispielinhalte für Gedanken
|
||||
const thoughtTemplates = [
|
||||
{ title: 'Quantenphysik und Bewusstsein', author: 'Maria Schmidt', date: '12.04.2023' },
|
||||
{ title: 'Ethik in der künstlichen Intelligenz', author: 'Thomas Weber', date: '23.02.2023' },
|
||||
{ title: 'Die Rolle der Kunst in der Gesellschaft', author: 'Lena Müller', date: '05.06.2023' },
|
||||
{ title: 'Nachhaltige Entwicklung im 21. Jahrhundert', author: 'Michael Bauer', date: '18.08.2023' },
|
||||
{ title: 'Kognitive Verzerrungen im Alltag', author: 'Sophie Klein', date: '30.09.2023' }
|
||||
];
|
||||
|
||||
// Zeige nur so viele Elemente wie wir Knoten haben (bis zu 5)
|
||||
const areaCount = Math.min(nodeIds.length, 5);
|
||||
|
||||
if (areaCount > 0) {
|
||||
// Wissensbereiche hinzufügen
|
||||
for (let i = 0; i < areaCount; i++) {
|
||||
const area = areaTemplates[i];
|
||||
const colorClass = colors[i % colors.length];
|
||||
|
||||
areasContainer.innerHTML += `
|
||||
<a href="{{ url_for('mindmap') }}" class="bookmark-item block p-4 rounded-xl bg-white/80 dark:bg-gray-800/80 shadow-sm hover:shadow-md transition-all">
|
||||
<div class="flex items-center">
|
||||
<div class="w-10 h-10 rounded-lg bg-${colorClass}-100 dark:bg-${colorClass}-900/30 flex items-center justify-center mr-3">
|
||||
<i class="fas fa-bookmark text-${colorClass}-500"></i>
|
||||
</div>
|
||||
<div class="flex-grow">
|
||||
<h3 class="font-semibold text-gray-900 dark:text-white">${area.name}</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">${area.description}</p>
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">
|
||||
${area.count} Einträge
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
} else {
|
||||
areasContainer.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<div class="text-4xl mb-2 opacity-20">
|
||||
<i class="fas fa-folder-open"></i>
|
||||
</div>
|
||||
<p class="text-gray-600 dark:text-gray-400">Keine gemerkten Wissensbereiche</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Zeige bis zu 5 Gedanken an (oder weniger, falls weniger gemerkt wurden)
|
||||
const thoughtCount = Math.min(nodeIds.length, 5);
|
||||
|
||||
if (thoughtCount > 0) {
|
||||
// Gedanken hinzufügen
|
||||
for (let i = 0; i < thoughtCount; i++) {
|
||||
const thought = thoughtTemplates[i];
|
||||
const colorClass = colors[(i + 2) % colors.length]; // Andere Farben als für Bereiche
|
||||
|
||||
thoughtsContainer.innerHTML += `
|
||||
<a href="{{ url_for('mindmap') }}" class="bookmark-item block p-4 rounded-xl bg-white/80 dark:bg-gray-800/80 shadow-sm hover:shadow-md transition-all">
|
||||
<div class="flex items-center">
|
||||
<div class="w-10 h-10 rounded-lg bg-${colorClass}-100 dark:bg-${colorClass}-900/30 flex items-center justify-center mr-3">
|
||||
<i class="fas fa-lightbulb text-${colorClass}-500"></i>
|
||||
</div>
|
||||
<div class="flex-grow">
|
||||
<h3 class="font-semibold text-gray-900 dark:text-white">${thought.title}</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">Von ${thought.author} • ${thought.date}</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
} else {
|
||||
thoughtsContainer.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<div class="text-4xl mb-2 opacity-20">
|
||||
<i class="fas fa-lightbulb"></i>
|
||||
</div>
|
||||
<p class="text-gray-600 dark:text-gray-400">Keine gemerkten Gedanken</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
800
templates/profile.html
Normal file
800
templates/profile.html
Normal file
@@ -0,0 +1,800 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Profil{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
/* Grundstile für das Profil mit verbessertem Glasmorphismus */
|
||||
.profile-container {
|
||||
background: rgba(24, 28, 45, 0.75);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-radius: 24px;
|
||||
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.35);
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.profile-container:hover {
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.45);
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
|
||||
/* Verbesserte Profile-Header mit dynamischer User-Anzeige */
|
||||
.profile-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 2rem;
|
||||
gap: 2.5rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.avatar-container {
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
border-radius: 50%;
|
||||
background: rgba(32, 36, 55, 0.9);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border: 3px solid rgba(179, 143, 255, 0.3);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25), 0 0 15px rgba(179, 143, 255, 0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
transition: all 0.3s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.avatar-container:hover {
|
||||
transform: scale(1.05);
|
||||
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);
|
||||
}
|
||||
|
||||
.avatar-container img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: filter 0.3s ease;
|
||||
}
|
||||
|
||||
.avatar-container:hover img {
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
|
||||
.avatar-edit {
|
||||
position: absolute;
|
||||
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;
|
||||
}
|
||||
|
||||
.avatar-edit:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.user-info {
|
||||
flex: 1;
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
.user-info h1 {
|
||||
font-size: 2.75rem;
|
||||
font-weight: 800;
|
||||
margin-bottom: 0.75rem;
|
||||
background: linear-gradient(135deg, #b38fff, #58a9ff);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
text-shadow: 0 2px 10px rgba(179, 143, 255, 0.3);
|
||||
letter-spacing: 0.3px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.user-bio {
|
||||
font-size: 1.15rem;
|
||||
line-height: 1.7;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
max-width: 650px;
|
||||
margin-bottom: 1.5rem;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.user-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1.5rem;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-size: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.user-meta span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
|
||||
.user-meta span:hover {
|
||||
color: rgba(255, 255, 255, 1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.user-meta i {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Benutzer-Aktionsbereich */
|
||||
.profile-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.profile-action-btn {
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 16px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
transition: all 0.3s ease;
|
||||
background: rgba(32, 36, 55, 0.75);
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
backdrop-filter: blur(15px);
|
||||
-webkit-backdrop-filter: blur(15px);
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.profile-action-btn:hover {
|
||||
transform: translateY(-3px);
|
||||
background: rgba(179, 143, 255, 0.25);
|
||||
border: 1px solid rgba(179, 143, 255, 0.3);
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2), 0 0 15px rgba(179, 143, 255, 0.2);
|
||||
text-shadow: 0 2px 3px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.profile-action-btn.primary {
|
||||
background: rgba(179, 143, 255, 0.25);
|
||||
color: #ffffff;
|
||||
border: 1px solid rgba(179, 143, 255, 0.3);
|
||||
}
|
||||
|
||||
.profile-action-btn.primary:hover {
|
||||
background: rgba(179, 143, 255, 0.35);
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.25), 0 0 20px rgba(179, 143, 255, 0.35);
|
||||
}
|
||||
|
||||
/* Verbesserte Profil-Tabs */
|
||||
.profile-tabs {
|
||||
display: flex;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
margin-bottom: 1.5rem;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(179, 143, 255, 0.5) rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.profile-tab {
|
||||
padding: 1rem 1.5rem;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
border-bottom: 3px solid transparent;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.profile-tab:hover {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.profile-tab.active {
|
||||
color: #b38fff;
|
||||
border-bottom: 3px solid #b38fff;
|
||||
background: rgba(179, 143, 255, 0.1);
|
||||
}
|
||||
|
||||
/* Aktivitäten und Beiträge */
|
||||
.activity-feed {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.activity-card {
|
||||
background: rgba(32, 36, 55, 0.7);
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
backdrop-filter: blur(15px);
|
||||
-webkit-backdrop-filter: blur(15px);
|
||||
}
|
||||
|
||||
.activity-card:hover {
|
||||
transform: translateY(-3px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.25), 0 0 15px rgba(179, 143, 255, 0.15);
|
||||
}
|
||||
|
||||
.activity-header {
|
||||
padding: 1.25rem 1.5rem;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.activity-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
}
|
||||
|
||||
.activity-date {
|
||||
font-size: 0.85rem;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.activity-content {
|
||||
padding: 1.5rem;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
font-size: 1.05rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.activity-footer {
|
||||
padding: 1rem 1.5rem;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.activity-reactions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.reaction-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 12px;
|
||||
font-size: 0.9rem;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: none;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.reaction-button:hover {
|
||||
background: rgba(179, 143, 255, 0.15);
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.reaction-button.active {
|
||||
background: rgba(179, 143, 255, 0.2);
|
||||
color: #b38fff;
|
||||
}
|
||||
|
||||
.activity-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 12px;
|
||||
font-size: 0.9rem;
|
||||
background: rgba(179, 143, 255, 0.1);
|
||||
color: #b38fff;
|
||||
border: 1px solid rgba(179, 143, 255, 0.25);
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.action-button:hover {
|
||||
background: rgba(179, 143, 255, 0.2);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1), 0 0 10px rgba(179, 143, 255, 0.2);
|
||||
}
|
||||
|
||||
/* Verbesserte Einstellungskarten */
|
||||
.settings-card {
|
||||
background: rgba(32, 36, 55, 0.7);
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
margin-bottom: 1.5rem;
|
||||
backdrop-filter: blur(15px);
|
||||
-webkit-backdrop-filter: blur(15px);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.settings-card:hover {
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.2), 0 0 15px rgba(179, 143, 255, 0.15);
|
||||
}
|
||||
|
||||
.settings-card-header {
|
||||
padding: 1.25rem 1.5rem;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
}
|
||||
|
||||
.settings-card-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.settings-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.settings-label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
|
||||
.settings-input {
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
background: rgba(24, 28, 45, 0.6);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
font-size: 1rem;
|
||||
transition: all 0.3s ease;
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.settings-input:focus {
|
||||
border-color: rgba(179, 143, 255, 0.4);
|
||||
box-shadow: 0 0 0 3px rgba(179, 143, 255, 0.15);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Light Mode Anpassungen */
|
||||
html.light .profile-container,
|
||||
html.light .profile-tabs,
|
||||
html.light .activity-card,
|
||||
html.light .settings-card {
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
html.light .avatar-container {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
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);
|
||||
}
|
||||
|
||||
html.light .user-info h1 {
|
||||
background: linear-gradient(135deg, #7e3ff2, #3282f6);
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
html.light .user-bio,
|
||||
html.light .activity-content {
|
||||
color: #1a202c;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
html.light .user-meta span {
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
html.light .stat-item,
|
||||
html.light .settings-input {
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
html.light .stat-value {
|
||||
background: linear-gradient(135deg, #7e3ff2, #3282f6);
|
||||
}
|
||||
|
||||
html.light .stat-label {
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
html.light .profile-tab {
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
html.light .profile-tab:hover {
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
color: #1a202c;
|
||||
}
|
||||
|
||||
html.light .profile-tab.active {
|
||||
color: #7e3ff2;
|
||||
border-bottom: 3px solid #7e3ff2;
|
||||
background: rgba(126, 63, 242, 0.08);
|
||||
}
|
||||
|
||||
html.light .activity-title,
|
||||
html.light .settings-card-header,
|
||||
html.light .settings-label {
|
||||
color: #1a202c;
|
||||
}
|
||||
|
||||
html.light .activity-date {
|
||||
color: #718096;
|
||||
}
|
||||
|
||||
html.light .activity-footer {
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
html.light .reaction-button {
|
||||
color: #4a5568;
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
html.light .reaction-button:hover {
|
||||
background: rgba(126, 63, 242, 0.1);
|
||||
color: #7e3ff2;
|
||||
}
|
||||
|
||||
html.light .reaction-button.active {
|
||||
background: rgba(126, 63, 242, 0.15);
|
||||
color: #7e3ff2;
|
||||
}
|
||||
|
||||
html.light .action-button {
|
||||
background: rgba(126, 63, 242, 0.1);
|
||||
color: #7e3ff2;
|
||||
border: 1px solid rgba(126, 63, 242, 0.2);
|
||||
}
|
||||
|
||||
html.light .action-button:hover {
|
||||
background: rgba(126, 63, 242, 0.2);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4 py-10">
|
||||
<!-- Profil-Container -->
|
||||
<div class="profile-container">
|
||||
<!-- User Info Section -->
|
||||
<div class="profile-header">
|
||||
<div class="avatar-container">
|
||||
<img src="{{ user.avatar if user.avatar else url_for('static', filename='img/default-avatar.png') }}" alt="Profilbild" class="avatar">
|
||||
<div class="avatar-edit">
|
||||
<i class="fas fa-camera"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<h1>{{ user.username }}</h1>
|
||||
<p class="user-bio">{{ user.bio if user.bio else 'Keine Bio vorhanden. Klicke auf bearbeiten, um eine hinzuzufügen.' }}</p>
|
||||
<div class="user-meta">
|
||||
<span><i class="fas fa-map-marker-alt"></i> {{ user.location if user.location else 'Kein Standort angegeben' }}</span>
|
||||
<span><i class="fas fa-calendar-alt"></i> Mitglied seit {{ user.created_at.strftime('%d.%m.%Y') }}</span>
|
||||
</div>
|
||||
<button class="edit-profile-btn">Profil bearbeiten</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Statistiken -->
|
||||
<div class="profile-stats">
|
||||
<!-- Gedanken -->
|
||||
<div class="stat-item">
|
||||
<div class="stat-icon">
|
||||
<i class="fas fa-lightbulb"></i>
|
||||
</div>
|
||||
<div class="stat-value">{{ stats.thought_count if stats and stats.thought_count else 0 }}</div>
|
||||
<div class="stat-label">Gedanken</div>
|
||||
</div>
|
||||
|
||||
<!-- Verbindungen -->
|
||||
<div class="stat-item">
|
||||
<div class="stat-icon">
|
||||
<i class="fas fa-project-diagram"></i>
|
||||
</div>
|
||||
<div class="stat-value">{{ stats.connections_count if stats and stats.connections_count else 0 }}</div>
|
||||
<div class="stat-label">Verbindungen</div>
|
||||
</div>
|
||||
|
||||
<!-- Follower -->
|
||||
<div class="stat-item">
|
||||
<div class="stat-icon">
|
||||
<i class="fas fa-users"></i>
|
||||
</div>
|
||||
<div class="stat-value">{{ stats.followers_count if stats and stats.followers_count else 0 }}</div>
|
||||
<div class="stat-label">Follower</div>
|
||||
</div>
|
||||
|
||||
<!-- Beiträge -->
|
||||
<div class="stat-item">
|
||||
<div class="stat-icon">
|
||||
<i class="fas fa-comment-dots"></i>
|
||||
</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>
|
||||
|
||||
<!-- Bewertung -->
|
||||
<div class="stat-item">
|
||||
<div class="stat-icon">
|
||||
<i class="fas fa-star"></i>
|
||||
</div>
|
||||
<div class="stat-value">{{ stats.rating if stats and stats.rating else '0.0' }}</div>
|
||||
<div class="stat-label">Bewertung</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabs und Inhaltsbereich -->
|
||||
<div class="glass-card">
|
||||
<!-- Profil-Tabs -->
|
||||
<div class="profile-tabs">
|
||||
<div class="profile-tab active" data-tab="activity">Aktivitäten</div>
|
||||
<div class="profile-tab" data-tab="thoughts">Gedanken</div>
|
||||
<div class="profile-tab" data-tab="collections">Sammlungen</div>
|
||||
<div class="profile-tab" data-tab="connections">Verbindungen</div>
|
||||
<div class="profile-tab" data-tab="settings">Einstellungen</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab-Inhalte -->
|
||||
<div class="p-6">
|
||||
<!-- Aktivitäts-Tab (Standardmäßig angezeigt) -->
|
||||
<div class="tab-content" id="activity-tab">
|
||||
<div class="activity-feed">
|
||||
{% if activities %}
|
||||
{% for activity in activities %}
|
||||
<div class="activity-card">
|
||||
<div class="activity-header">
|
||||
<div class="activity-title">{{ activity.title }}</div>
|
||||
<div class="activity-date">{{ activity.date }}</div>
|
||||
</div>
|
||||
<div class="activity-content">
|
||||
<p>{{ activity.content }}</p>
|
||||
</div>
|
||||
<div class="activity-footer">
|
||||
<div class="activity-reactions">
|
||||
<button class="reaction-button {% if activity.user_liked %}active{% endif %}">
|
||||
<i class="fas fa-thumbs-up"></i> <span>{{ activity.likes }}</span>
|
||||
</button>
|
||||
<button class="reaction-button">
|
||||
<i class="fas fa-comment"></i> <span>{{ activity.comments }}</span>
|
||||
</button>
|
||||
<button class="reaction-button">
|
||||
<i class="fas fa-share"></i> <span>{{ activity.shares }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="activity-actions">
|
||||
<button class="action-button">
|
||||
Ansehen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="text-center py-12">
|
||||
<i class="fas fa-history text-5xl text-gray-400 mb-4"></i>
|
||||
<p class="text-gray-500">Noch keine Aktivitäten vorhanden</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Weitere Tab-Inhalte (werden per JavaScript angezeigt) -->
|
||||
<div class="tab-content hidden" id="thoughts-tab">
|
||||
<div id="thoughts-container">
|
||||
{% if thoughts %}
|
||||
{% for thought in thoughts %}
|
||||
<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('get_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 class="tab-content hidden" id="collections-tab">
|
||||
<div id="collections-container">
|
||||
{% if collections %}
|
||||
{% for collection in collections %}
|
||||
<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 class="tab-content hidden" id="connections-tab">
|
||||
<div id="connections-container">
|
||||
{% if connections %}
|
||||
{% for connection in connections %}
|
||||
<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 class="tab-content hidden" id="settings-tab">
|
||||
<!-- Einstellungs-Tab-Inhalt -->
|
||||
<div class="settings-card">
|
||||
<div class="settings-card-header">Profilinformationen</div>
|
||||
<div class="settings-card-body">
|
||||
<div class="settings-group">
|
||||
<label class="settings-label" for="name">Name</label>
|
||||
<input type="text" id="name" class="settings-input" value="{{ user.name }}" />
|
||||
</div>
|
||||
|
||||
<div class="settings-group">
|
||||
<label class="settings-label" for="bio">Über mich</label>
|
||||
<textarea id="bio" class="settings-input" rows="4">{{ user.bio }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="settings-group">
|
||||
<label class="settings-label" for="location">Standort</label>
|
||||
<input type="text" id="location" class="settings-input" value="{{ user.location }}" />
|
||||
</div>
|
||||
|
||||
<div class="settings-group">
|
||||
<label class="settings-label" for="website">Website</label>
|
||||
<input type="url" id="website" class="settings-input" value="{{ user.website }}" />
|
||||
</div>
|
||||
|
||||
<button class="profile-action-btn primary mt-4">
|
||||
<i class="fas fa-save mr-1"></i> Speichern
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-card">
|
||||
<div class="settings-card-header">Datenschutz und Sicherheit</div>
|
||||
<div class="settings-card-body">
|
||||
<div class="settings-group">
|
||||
<label class="settings-label" for="email">E-Mail-Adresse</label>
|
||||
<input type="email" id="email" class="settings-input" value="{{ user.email }}" />
|
||||
</div>
|
||||
|
||||
<div class="settings-group">
|
||||
<label class="settings-label" for="password">Neues Passwort</label>
|
||||
<input type="password" id="password" class="settings-input" placeholder="Neues Passwort eingeben" />
|
||||
</div>
|
||||
|
||||
<div class="settings-group">
|
||||
<label class="settings-label" for="password_confirm">Passwort bestätigen</label>
|
||||
<input type="password" id="password_confirm" class="settings-input" placeholder="Passwort wiederholen" />
|
||||
</div>
|
||||
|
||||
<button class="profile-action-btn primary mt-4">
|
||||
<i class="fas fa-lock mr-1"></i> Passwort aktualisieren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script nonce="{{ csp_nonce }}">
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Profil-Tab-Funktionalität
|
||||
const tabs = document.querySelectorAll('.profile-tab');
|
||||
const tabContents = document.querySelectorAll('.tab-content');
|
||||
|
||||
tabs.forEach(tab => {
|
||||
tab.addEventListener('click', function() {
|
||||
// Alle Tabs deaktivieren
|
||||
tabs.forEach(t => t.classList.remove('active'));
|
||||
|
||||
// Aktuellen Tab aktivieren
|
||||
this.classList.add('active');
|
||||
|
||||
// Alle Tab-Inhalte ausblenden
|
||||
tabContents.forEach(content => content.classList.add('hidden'));
|
||||
|
||||
// Entsprechenden Tab-Inhalt anzeigen
|
||||
const tabId = this.getAttribute('data-tab');
|
||||
document.getElementById(`${tabId}-tab`).classList.remove('hidden');
|
||||
});
|
||||
});
|
||||
|
||||
// Like-Button-Funktionalität
|
||||
const reactionButtons = document.querySelectorAll('.reaction-button');
|
||||
|
||||
reactionButtons.forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
// Toggle active-Klasse
|
||||
this.classList.toggle('active');
|
||||
|
||||
// Aktueller Zählerstand
|
||||
const countElement = this.querySelector('span');
|
||||
let count = parseInt(countElement.textContent);
|
||||
|
||||
// Zähler aktualisieren
|
||||
if (this.classList.contains('active')) {
|
||||
count += 1;
|
||||
} else {
|
||||
count -= 1;
|
||||
}
|
||||
|
||||
countElement.textContent = count;
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
115
templates/register.html
Normal file
115
templates/register.html
Normal file
@@ -0,0 +1,115 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Registrieren{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="flex justify-center items-center mt-10 px-4">
|
||||
<div class="w-full max-w-md">
|
||||
<div class="bg-white bg-opacity-80 backdrop-blur-lg rounded-lg shadow-md border border-white border-opacity-30 p-6 md:p-8 transition-all duration-300 transform hover:shadow-lg">
|
||||
<h2 class="text-center mb-6 text-gray-800 font-bold text-2xl flex items-center justify-center">
|
||||
<i class="fas fa-user-plus mr-2 text-blue-600"></i>
|
||||
Registrieren
|
||||
</h2>
|
||||
|
||||
<form method="POST" action="{{ url_for('register') }}" class="needs-validation space-y-6" novalidate>
|
||||
<div class="space-y-2">
|
||||
<label for="username" class="block text-gray-700 font-medium text-sm">Benutzername</label>
|
||||
<div class="relative flex items-center">
|
||||
<span class="absolute left-3 text-blue-600">
|
||||
<i class="fas fa-user"></i>
|
||||
</span>
|
||||
<input type="text" class="pl-10 w-full rounded-md border border-gray-300 py-2 px-4 focus:border-blue-500 focus:ring focus:ring-blue-200 focus:ring-opacity-50 transition-all duration-200"
|
||||
id="username" name="username" placeholder="Dein Benutzername" required>
|
||||
</div>
|
||||
<div class="invalid-feedback text-red-600 text-sm hidden">
|
||||
Bitte gib einen Benutzernamen ein.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<label for="email" class="block text-gray-700 font-medium text-sm">E-Mail</label>
|
||||
<div class="relative flex items-center">
|
||||
<span class="absolute left-3 text-blue-600">
|
||||
<i class="fas fa-envelope"></i>
|
||||
</span>
|
||||
<input type="email" class="pl-10 w-full rounded-md border border-gray-300 py-2 px-4 focus:border-blue-500 focus:ring focus:ring-blue-200 focus:ring-opacity-50 transition-all duration-200"
|
||||
id="email" name="email" placeholder="name@beispiel.de" required>
|
||||
</div>
|
||||
<div class="invalid-feedback text-red-600 text-sm hidden">
|
||||
Bitte gib eine gültige E-Mail-Adresse ein.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<label for="password" class="block text-gray-700 font-medium text-sm">Passwort</label>
|
||||
<div class="relative flex items-center">
|
||||
<span class="absolute left-3 text-blue-600">
|
||||
<i class="fas fa-lock"></i>
|
||||
</span>
|
||||
<input type="password" class="pl-10 w-full rounded-md border border-gray-300 py-2 px-4 focus:border-blue-500 focus:ring focus:ring-blue-200 focus:ring-opacity-50 transition-all duration-200"
|
||||
id="password" name="password" placeholder="Mindestens 8 Zeichen" required>
|
||||
<button class="absolute right-2 text-gray-500 hover:text-gray-700 focus:outline-none" type="button" id="togglePassword">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="invalid-feedback text-red-600 text-sm hidden">
|
||||
Bitte gib ein sicheres Passwort ein.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-2">
|
||||
<button type="submit" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-md shadow-sm transition-all duration-200 transform hover:scale-[1.02] focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50">
|
||||
<i class="fas fa-user-plus mr-2"></i> Konto erstellen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="text-center mt-4 text-gray-700 text-sm">
|
||||
<p>Bereits registriert? <a href="{{ url_for('login') }}" class="text-blue-600 hover:text-blue-800 font-medium transition-colors duration-200">Anmelden</a></p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Formularvalidierung aktivieren
|
||||
(function() {
|
||||
'use strict';
|
||||
var forms = document.querySelectorAll('.needs-validation');
|
||||
Array.prototype.slice.call(forms).forEach(function(form) {
|
||||
form.addEventListener('submit', function(event) {
|
||||
if (!form.checkValidity()) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
// Zeige Fehlermeldungen an
|
||||
form.querySelectorAll(':invalid').forEach(function(input) {
|
||||
input.parentNode.nextElementSibling.classList.remove('hidden');
|
||||
});
|
||||
}
|
||||
form.classList.add('was-validated');
|
||||
}, false);
|
||||
|
||||
// Verstecke Fehlermeldungen bei Eingabe
|
||||
form.querySelectorAll('input').forEach(function(input) {
|
||||
input.addEventListener('input', function() {
|
||||
if (this.checkValidity()) {
|
||||
this.parentNode.nextElementSibling.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Passwort-Sichtbarkeit umschalten
|
||||
const togglePassword = document.querySelector('#togglePassword');
|
||||
const password = document.querySelector('#password');
|
||||
|
||||
togglePassword.addEventListener('click', function() {
|
||||
const type = password.getAttribute('type') === 'password' ? 'text' : 'password';
|
||||
password.setAttribute('type', type);
|
||||
this.querySelector('i').classList.toggle('fa-eye');
|
||||
this.querySelector('i').classList.toggle('fa-eye-slash');
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
105
templates/search.html
Normal file
105
templates/search.html
Normal file
@@ -0,0 +1,105 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Suche{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="flex flex-col md:flex-row gap-6">
|
||||
<!-- Filter-Sidebar -->
|
||||
<div class="w-full md:w-1/3 lg:w-1/4 mb-6">
|
||||
<div class="bg-white/10 backdrop-blur-md rounded-xl p-6 shadow-lg">
|
||||
<h4 class="text-xl font-semibold mb-4">Erweiterte Suche</h4>
|
||||
<form id="search-form">
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium mb-1">Suchbegriff</label>
|
||||
<div class="flex items-center border rounded-lg overflow-hidden bg-white/5">
|
||||
<span class="px-3 py-2 text-gray-400">
|
||||
<i class="fas fa-search"></i>
|
||||
</span>
|
||||
<input type="text" class="w-full bg-transparent border-0 focus:ring-0 py-2 px-1" name="q" placeholder="Suche...">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium mb-1">Schlagworte</label>
|
||||
<div class="flex items-center border rounded-lg overflow-hidden bg-white/5">
|
||||
<span class="px-3 py-2 text-gray-400">
|
||||
<i class="fas fa-tags"></i>
|
||||
</span>
|
||||
<input type="text" class="w-full bg-transparent border-0 focus:ring-0 py-2 px-1" name="keywords" placeholder="Schlagworte (kommagetrennt)">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium mb-1">Minimale Bewertung</label>
|
||||
<div class="flex items-center border rounded-lg overflow-hidden bg-white/5">
|
||||
<span class="px-3 py-2 text-gray-400">
|
||||
<i class="fas fa-star"></i>
|
||||
</span>
|
||||
<select class="w-full bg-transparent border-0 focus:ring-0 py-2 px-1" name="min_rating">
|
||||
<option value="">Alle</option>
|
||||
<option value="4">4+ Sterne</option>
|
||||
<option value="3">3+ Sterne</option>
|
||||
<option value="2">2+ Sterne</option>
|
||||
<option value="1">1+ Stern</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium mb-1">Quellentyp</label>
|
||||
<div class="flex items-center border rounded-lg overflow-hidden bg-white/5">
|
||||
<span class="px-3 py-2 text-gray-400">
|
||||
<i class="fas fa-file-alt"></i>
|
||||
</span>
|
||||
<select class="w-full bg-transparent border-0 focus:ring-0 py-2 px-1" name="source_type">
|
||||
<option value="">Alle</option>
|
||||
<option value="PDF">PDF</option>
|
||||
<option value="Markdown">Markdown</option>
|
||||
<option value="Text">Text</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-5">
|
||||
<label class="block text-sm font-medium mb-1">Beziehungstyp</label>
|
||||
<div class="flex items-center border rounded-lg overflow-hidden bg-white/5">
|
||||
<span class="px-3 py-2 text-gray-400">
|
||||
<i class="fas fa-project-diagram"></i>
|
||||
</span>
|
||||
<select class="w-full bg-transparent border-0 focus:ring-0 py-2 px-1" name="relation_type">
|
||||
<option value="">Alle</option>
|
||||
<option value="SUPPORTS">Stützt</option>
|
||||
<option value="CONTRADICTS">Widerspricht</option>
|
||||
<option value="BUILDS_UPON">Baut auf auf</option>
|
||||
<option value="GENERALIZES">Verallgemeinert</option>
|
||||
<option value="SPECIFIES">Spezifiziert</option>
|
||||
<option value="INSPIRES">Inspiriert</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="w-full bg-gradient-to-r from-blue-500 to-indigo-600 text-white py-2 px-4 rounded-lg hover:opacity-90 transition-all flex items-center justify-center">
|
||||
<i class="fas fa-search mr-2"></i> Suchen
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Suchergebnisse -->
|
||||
<div class="w-full md:w-2/3 lg:w-3/4">
|
||||
<div class="bg-white/10 backdrop-blur-md rounded-xl p-6 shadow-lg mb-6">
|
||||
<h3 class="text-2xl font-semibold mb-2">Suchergebnisse</h3>
|
||||
<p class="text-gray-300 text-sm">Nutze die Filter links, um deine Suche zu präzisieren.</p>
|
||||
</div>
|
||||
|
||||
<div id="search-results" class="mb-6">
|
||||
<!-- Suchergebnisse werden hier dynamisch eingefügt -->
|
||||
<div class="bg-white/10 backdrop-blur-md rounded-xl p-8 shadow-lg text-center">
|
||||
<i class="fas fa-search text-5xl mb-6 text-blue-400"></i>
|
||||
<h5 class="text-xl font-medium mb-3">Wissen entdecken</h5>
|
||||
<p class="text-gray-300">Gib einen Suchbegriff ein, um in der wissenschaftlichen Wissensdatenbank zu suchen.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
269
templates/settings.html
Normal file
269
templates/settings.html
Normal file
@@ -0,0 +1,269 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Einstellungen{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="relative mb-6" data-page="settings">
|
||||
<!-- Header Bereich -->
|
||||
<div class="mb-8 p-6 bg-gradient-to-br from-slate-900/80 to-slate-800/60 rounded-lg border border-slate-700/20 shadow-xl">
|
||||
<h1 class="text-4xl md:text-5xl font-bold mb-3">
|
||||
<span class="gradient-text">Einstellungen</span>
|
||||
</h1>
|
||||
<p class="text-lg text-gray-300 max-w-3xl">
|
||||
Verwalten Sie Ihr Konto und passen Sie Ihre Benutzererfahrung an.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Einstellungen Grid Layout -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<!-- Seitennavigation (Desktop) -->
|
||||
<div class="lg:col-span-1 order-2 lg:order-1">
|
||||
<div class="sticky top-6 bg-gradient-to-br from-slate-900/80 to-slate-800/60 rounded-lg border border-slate-700/20 shadow-xl overflow-hidden p-4">
|
||||
<nav>
|
||||
<div class="text-xs uppercase text-gray-400 font-semibold tracking-wider mb-3 px-3">Navigation</div>
|
||||
<ul class="space-y-1">
|
||||
<li>
|
||||
<button id="nav-account" class="w-full text-left px-3 py-2 rounded-md flex items-center text-purple-300 bg-purple-900/20 border-l-2 border-purple-500">
|
||||
<i class="fa-solid fa-user-circle mr-3"></i>
|
||||
<span>Konto</span>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button id="nav-appearance" class="w-full text-left px-3 py-2 rounded-md flex items-center text-gray-300 hover:bg-white/5">
|
||||
<i class="fa-solid fa-palette mr-3"></i>
|
||||
<span>Erscheinungsbild</span>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button id="nav-notifications" class="w-full text-left px-3 py-2 rounded-md flex items-center text-gray-300 hover:bg-white/5">
|
||||
<i class="fa-solid fa-bell mr-3"></i>
|
||||
<span>Benachrichtigungen</span>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button id="nav-privacy" class="w-full text-left px-3 py-2 rounded-md flex items-center text-gray-300 hover:bg-white/5">
|
||||
<i class="fa-solid fa-shield-alt mr-3"></i>
|
||||
<span>Datenschutz</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hauptinhalt der Einstellungen -->
|
||||
<div class="lg:col-span-2 order-1 lg:order-2">
|
||||
<!-- Konto-Einstellungen -->
|
||||
<div id="section-account" class="bg-gradient-to-br from-slate-900/80 to-slate-800/60 rounded-lg border border-slate-700/20 shadow-xl p-6 mb-6">
|
||||
<h2 class="text-2xl font-bold text-white mb-4 flex items-center">
|
||||
<i class="fa-solid fa-user-circle text-purple-400 mr-3"></i>
|
||||
Konto-Einstellungen
|
||||
</h2>
|
||||
|
||||
<div class="mb-6 p-4 bg-slate-900/50 rounded-lg">
|
||||
<div class="flex items-center">
|
||||
<div class="h-16 w-16 rounded-full bg-gradient-to-r from-purple-500 to-blue-500 flex items-center justify-center text-white text-2xl font-bold">
|
||||
{{ current_user.username[0] | upper }}
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<h3 class="text-lg font-medium text-white">{{ current_user.username }}</h3>
|
||||
<p class="text-gray-400">{{ current_user.email }}</p>
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
Mitglied seit {{ current_user.id | string }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="POST" class="space-y-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label for="username" class="block text-sm font-medium text-gray-400 mb-1">Benutzername</label>
|
||||
<input type="text" name="username" id="username" value="{{ current_user.username }}" disabled
|
||||
class="w-full bg-slate-800/80 border border-slate-700 rounded-md py-2 px-3 text-white focus:ring-1 focus:ring-purple-500">
|
||||
<p class="mt-1 text-xs text-gray-500">Der Benutzername kann derzeit nicht geändert werden.</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium text-gray-400 mb-1">E-Mail-Adresse</label>
|
||||
<input type="email" name="email" id="email" value="{{ current_user.email }}" disabled
|
||||
class="w-full bg-slate-800/80 border border-slate-700 rounded-md py-2 px-3 text-white focus:ring-1 focus:ring-purple-500">
|
||||
<p class="mt-1 text-xs text-gray-500">Die E-Mail-Adresse kann derzeit nicht geändert werden.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-slate-700 pt-6">
|
||||
<h3 class="text-lg font-medium text-white mb-4">Passwort ändern</h3>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="current_password" class="block text-sm font-medium text-gray-400 mb-1">Aktuelles Passwort</label>
|
||||
<input type="password" name="current_password" id="current_password"
|
||||
class="w-full bg-slate-800/80 border border-slate-700 rounded-md py-2 px-3 text-white focus:ring-1 focus:ring-purple-500">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="new_password" class="block text-sm font-medium text-gray-400 mb-1">Neues Passwort</label>
|
||||
<input type="password" name="new_password" id="new_password"
|
||||
class="w-full bg-slate-800/80 border border-slate-700 rounded-md py-2 px-3 text-white focus:ring-1 focus:ring-purple-500">
|
||||
<p class="mt-1 text-xs text-gray-500">Mindestens 6 Zeichen</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="confirm_password" class="block text-sm font-medium text-gray-400 mb-1">Passwort bestätigen</label>
|
||||
<input type="password" name="confirm_password" id="confirm_password"
|
||||
class="w-full bg-slate-800/80 border border-slate-700 rounded-md py-2 px-3 text-white focus:ring-1 focus:ring-purple-500">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<button type="submit" class="py-2 px-4 rounded-md bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-700 hover:to-blue-700 text-white font-medium transition duration-300 transform hover:-translate-y-0.5">
|
||||
Änderungen speichern
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Erscheinungsbild -->
|
||||
<div id="section-appearance" class="bg-gradient-to-br from-slate-900/80 to-slate-800/60 rounded-lg border border-slate-700/20 shadow-xl p-6 mb-6 hidden">
|
||||
<h2 class="text-2xl font-bold text-white mb-4 flex items-center">
|
||||
<i class="fa-solid fa-palette text-purple-400 mr-3"></i>
|
||||
Erscheinungsbild
|
||||
</h2>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="p-4 bg-slate-900/50 rounded-lg">
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<h3 class="text-lg font-medium text-white">Dunkles Design</h3>
|
||||
<p class="text-gray-400">Wechsle zwischen hellem und dunklem Modus</p>
|
||||
</div>
|
||||
<div>
|
||||
<button id="toggle-dark-mode" class="w-14 h-7 flex items-center bg-purple-900/30 rounded-full px-1 transition-all" onclick="toggleDarkMode()">
|
||||
<div id="dark-mode-indicator" class="bg-purple-500 w-5 h-5 rounded-full shadow-md transform transition-transform duration-300 translate-x-7"></div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Benachrichtigungen -->
|
||||
<div id="section-notifications" class="bg-gradient-to-br from-slate-900/80 to-slate-800/60 rounded-lg border border-slate-700/20 shadow-xl p-6 mb-6 hidden">
|
||||
<h2 class="text-2xl font-bold text-white mb-4 flex items-center">
|
||||
<i class="fa-solid fa-bell text-purple-400 mr-3"></i>
|
||||
Benachrichtigungen
|
||||
</h2>
|
||||
|
||||
<div class="p-4 bg-slate-900/50 rounded-lg">
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<h3 class="text-lg font-medium text-white">E-Mail-Benachrichtigungen</h3>
|
||||
<p class="text-gray-400">Diese Funktion wird in einer zukünftigen Version verfügbar sein.</p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="px-2 py-1 text-xs rounded-full bg-slate-700 text-gray-300">
|
||||
Demnächst
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Datenschutz -->
|
||||
<div id="section-privacy" class="bg-gradient-to-br from-slate-900/80 to-slate-800/60 rounded-lg border border-slate-700/20 shadow-xl p-6 mb-6 hidden">
|
||||
<h2 class="text-2xl font-bold text-white mb-4 flex items-center">
|
||||
<i class="fa-solid fa-shield-alt text-purple-400 mr-3"></i>
|
||||
Datenschutz und Sicherheit
|
||||
</h2>
|
||||
|
||||
<div class="p-4 bg-slate-900/50 rounded-lg">
|
||||
<h3 class="text-lg font-medium text-white mb-2">Datenverarbeitung</h3>
|
||||
<p class="text-gray-400 mb-3">
|
||||
Wir verarbeiten Ihre Daten gemäß unserer Datenschutzrichtlinie.
|
||||
</p>
|
||||
<a href="{{ url_for('datenschutz') }}" class="text-purple-400 hover:text-purple-300 inline-flex items-center">
|
||||
<span>Datenschutzerklärung lesen</span>
|
||||
<i class="fa-solid fa-arrow-right ml-1 text-sm"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Navigation-Buttons
|
||||
const navButtons = {
|
||||
'nav-account': 'section-account',
|
||||
'nav-appearance': 'section-appearance',
|
||||
'nav-notifications': 'section-notifications',
|
||||
'nav-privacy': 'section-privacy'
|
||||
};
|
||||
|
||||
// Alle Abschnitte ausblenden außer dem ersten
|
||||
Object.values(navButtons).forEach(sectionId => {
|
||||
if (sectionId !== 'section-account') {
|
||||
document.getElementById(sectionId).classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
// Klick-Handler für Navigation
|
||||
for (const [navId, sectionId] of Object.entries(navButtons)) {
|
||||
document.getElementById(navId).addEventListener('click', function() {
|
||||
// Alle Abschnitte ausblenden
|
||||
Object.values(navButtons).forEach(id => {
|
||||
document.getElementById(id).classList.add('hidden');
|
||||
});
|
||||
|
||||
// Ausgewählten Abschnitt anzeigen
|
||||
document.getElementById(sectionId).classList.remove('hidden');
|
||||
|
||||
// Aktiven Navigations-Button hervorheben, andere zurücksetzen
|
||||
for (const btnId of Object.keys(navButtons)) {
|
||||
const btn = document.getElementById(btnId);
|
||||
if (btnId === navId) {
|
||||
btn.classList.add('text-purple-300', 'bg-purple-900/20', 'border-l-2', 'border-purple-500');
|
||||
btn.classList.remove('text-gray-300', 'hover:bg-white/5');
|
||||
} else {
|
||||
btn.classList.remove('text-purple-300', 'bg-purple-900/20', 'border-l-2', 'border-purple-500');
|
||||
btn.classList.add('text-gray-300', 'hover:bg-white/5');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Dark Mode Toggle aktualisieren
|
||||
function updateDarkModeToggle() {
|
||||
const isDarkMode = document.documentElement.classList.contains('dark');
|
||||
const indicator = document.getElementById('dark-mode-indicator');
|
||||
|
||||
if (indicator) {
|
||||
if (isDarkMode) {
|
||||
indicator.classList.add('translate-x-7');
|
||||
indicator.classList.remove('translate-x-0');
|
||||
} else {
|
||||
indicator.classList.add('translate-x-0');
|
||||
indicator.classList.remove('translate-x-7');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateDarkModeToggle();
|
||||
|
||||
// Globale Funktion für Toggle-Button
|
||||
window.toggleDarkMode = function() {
|
||||
if (typeof MindMap !== 'undefined' && typeof MindMap.toggleDarkMode === 'function') {
|
||||
MindMap.toggleDarkMode();
|
||||
} else {
|
||||
document.documentElement.classList.toggle('dark');
|
||||
}
|
||||
updateDarkModeToggle();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
79
templates/ueber_uns.html
Normal file
79
templates/ueber_uns.html
Normal file
@@ -0,0 +1,79 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Über uns{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-3xl mx-auto">
|
||||
<div class="card p-6 md:p-8">
|
||||
<h1 class="text-3xl font-bold mb-6 gradient-text">Über uns</h1>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-bold mb-4">Unsere Vision</h2>
|
||||
<p class="mb-4">
|
||||
Systades ist ein innovatives Projekt, das darauf abzielt, das Teilen und Vernetzen von Wissen und Gedanken zu revolutionieren. Unsere Plattform ermöglicht es Nutzern, ihre Ideen in interaktiven Mindmaps zu organisieren und mit anderen zu teilen, wodurch ein kollaboratives Netzwerk des Wissens entsteht.
|
||||
</p>
|
||||
<p class="mb-4">
|
||||
Wir glauben daran, dass Wissen am wertvollsten ist, wenn es geteilt und vernetzt wird. Durch die Verbindung verschiedener Perspektiven und Denkansätze entstehen neue Erkenntnisse und Innovationen.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-bold mb-4">Das Team</h2>
|
||||
<p class="mb-4">
|
||||
Till Tomczak und Marwin Medczinski arbeiten gemeinsam daran, Systades kontinuierlich zu verbessern und weiterzuentwickeln.
|
||||
</p>
|
||||
|
||||
<!-- Platz für Team-Mitglieder -->
|
||||
<div class="team-members space-y-6">
|
||||
<!-- Beispiel für ein Team-Mitglied (kann als Vorlage verwendet werden) -->
|
||||
<!--
|
||||
<div class="team-member p-4 border border-gray-200 dark:border-gray-700 rounded-lg">
|
||||
<h3 class="text-lg font-bold mb-2">[Name]</h3>
|
||||
<p class="text-gray-600 dark:text-gray-300 mb-2">[Position/Rolle]</p>
|
||||
<p class="text-sm">[Kurze Beschreibung oder Verantwortlichkeiten]</p>
|
||||
</div>
|
||||
-->
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-bold mb-4">Unsere Mission</h2>
|
||||
<p class="mb-4">
|
||||
Wir setzen uns dafür ein, eine Plattform zu schaffen, die:
|
||||
</p>
|
||||
<ul class="list-disc list-inside space-y-2 mb-4">
|
||||
<li>Intuitive Werkzeuge für die Organisation und Visualisierung von Wissen bereitstellt</li>
|
||||
<li>Die Zusammenarbeit und den Austausch zwischen Nutzern fördert</li>
|
||||
<li>Kreativität und innovative Denkansätze unterstützt</li>
|
||||
<li>Einen sicheren und respektvollen Raum für intellektuellen Austausch bietet</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-bold mb-4">Technologie & Innovation</h2>
|
||||
<p class="mb-4">
|
||||
Systades nutzt modernste Technologien und innovative Ansätze, um eine optimale Nutzererfahrung zu gewährleisten. Unsere Plattform wird kontinuierlich weiterentwickelt, um neue Funktionen und Verbesserungen zu integrieren.
|
||||
</p>
|
||||
<p>
|
||||
Wir legen besonderen Wert auf:
|
||||
</p>
|
||||
<ul class="list-disc list-inside space-y-2 mt-2">
|
||||
<li>Intuitive Benutzeroberfläche</li>
|
||||
<li>Hohe Performance und Zuverlässigkeit</li>
|
||||
<li>Datensicherheit und Privatsphäre</li>
|
||||
<li>Barrierefreiheit und Inklusivität</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="text-xl font-bold mb-4">Kontakt & Feedback</h2>
|
||||
<p class="mb-4">
|
||||
Wir freuen uns über Ihr Feedback und Ihre Ideen zur Verbesserung von Systades. Gemeinsam können wir die Plattform weiter optimieren und an die Bedürfnisse unserer Nutzer anpassen.
|
||||
</p>
|
||||
<p>
|
||||
Kontaktieren Sie uns gerne für Fragen, Anregungen oder Kooperationsmöglichkeiten.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
60
test_app.py
Normal file
60
test_app.py
Normal file
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import sqlite3
|
||||
import os
|
||||
import requests
|
||||
import time
|
||||
|
||||
print("Systades Anwendungstest")
|
||||
print("=======================")
|
||||
|
||||
# Prüfen, ob die Datenbank existiert
|
||||
db_path = 'systades.db'
|
||||
if not os.path.exists(db_path):
|
||||
print(f"Datenbank {db_path} existiert nicht.")
|
||||
exit(1)
|
||||
|
||||
# Datenbankprüfung
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Prüfen, ob die User-Tabelle existiert und Benutzer enthält
|
||||
cursor.execute("SELECT COUNT(*) FROM user;")
|
||||
user_count = cursor.fetchone()[0]
|
||||
print(f"Anzahl der Benutzer in der Datenbank: {user_count}")
|
||||
|
||||
if user_count == 0:
|
||||
print("WARNUNG: Keine Benutzer in der Datenbank gefunden!")
|
||||
print("Bitte führen Sie das Skript 'create_default_users.py' aus, um Standardbenutzer zu erstellen.")
|
||||
conn.close()
|
||||
exit(1)
|
||||
|
||||
# Tabellenschema prüfen
|
||||
cursor.execute("PRAGMA table_info(user);")
|
||||
columns = cursor.fetchall()
|
||||
column_names = [column[1] for column in columns]
|
||||
print(f"Spalten in der User-Tabelle: {', '.join(column_names)}")
|
||||
|
||||
# Überprüfen, ob die password-Spalte existiert
|
||||
if 'password' not in column_names:
|
||||
print("FEHLER: Die Spalte 'password' fehlt in der Tabelle 'user'!")
|
||||
print("Bitte führen Sie das Skript 'fix_user_table.py' aus, um die Datenbank zu reparieren.")
|
||||
conn.close()
|
||||
exit(1)
|
||||
|
||||
# Benutzer für Testlogin abrufen
|
||||
cursor.execute("SELECT username, email FROM user LIMIT 1;")
|
||||
test_user = cursor.fetchone()
|
||||
if test_user:
|
||||
print(f"Testbenutzer für Login: {test_user[0]} (E-Mail: {test_user[1]})")
|
||||
else:
|
||||
print("FEHLER: Konnte keinen Testbenutzer abrufen.")
|
||||
|
||||
conn.close()
|
||||
|
||||
print("\nDie Datenbank scheint korrekt konfiguriert zu sein.")
|
||||
print("Sie können nun die Anwendung starten und sich mit den folgenden Zugangsdaten anmelden:")
|
||||
print(" Admin: username=admin, password=admin")
|
||||
print(" User: username=user, password=user")
|
||||
print("\nTest abgeschlossen.")
|
||||
41
utils/__init__.py
Normal file
41
utils/__init__.py
Normal file
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Utility functions for the website application.
|
||||
This package contains various utilities for database management,
|
||||
user management, and server administration.
|
||||
"""
|
||||
|
||||
# Import utilities that don't depend on app.py first
|
||||
from .db_check import check_db_connection, initialize_db_if_needed
|
||||
|
||||
# Define the list of all available utilities
|
||||
__all__ = [
|
||||
# Database utilities
|
||||
'check_db_connection',
|
||||
'initialize_db_if_needed',
|
||||
'fix_database_schema',
|
||||
'rebuild_database',
|
||||
'test_database_connection',
|
||||
'test_models',
|
||||
'print_database_stats',
|
||||
'run_all_tests',
|
||||
|
||||
# User management
|
||||
'list_users',
|
||||
'create_user',
|
||||
'reset_password',
|
||||
'delete_user',
|
||||
'create_admin_user',
|
||||
|
||||
# Server management
|
||||
'run_development_server',
|
||||
]
|
||||
|
||||
# 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
|
||||
BIN
utils/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
utils/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/__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.
BIN
utils/__pycache__/db_fix.cpython-311.pyc
Normal file
BIN
utils/__pycache__/db_fix.cpython-311.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/db_fix.cpython-313.pyc
Normal file
BIN
utils/__pycache__/db_fix.cpython-313.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/db_rebuild.cpython-311.pyc
Normal file
BIN
utils/__pycache__/db_rebuild.cpython-311.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/db_rebuild.cpython-313.pyc
Normal file
BIN
utils/__pycache__/db_rebuild.cpython-313.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/db_test.cpython-311.pyc
Normal file
BIN
utils/__pycache__/db_test.cpython-311.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/db_test.cpython-313.pyc
Normal file
BIN
utils/__pycache__/db_test.cpython-313.pyc
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user