Compare commits
50 Commits
till-v2
...
693e542d5f
| Author | SHA1 | Date | |
|---|---|---|---|
| 693e542d5f | |||
| 4c3e476338 | |||
| 613c38ccb2 | |||
| 91fdd43fe0 | |||
| f36dd5ffaa | |||
| 2e1c3ce8b0 | |||
| d80c4c9aec | |||
| 3b0bea959c | |||
| cb3bfe0e6a | |||
| fd63810845 | |||
| 883973fe7b | |||
| 027e632856 | |||
| 406289e54f | |||
| 71b33e6cec | |||
| c74d3164bb | |||
| 4982cddeef | |||
| 631619ccb4 | |||
| f9881b678d | |||
| 259ce3cf69 | |||
| 9f4743eaea | |||
| de0f837cfd | |||
| 1c49ddfb19 | |||
| 46c16e5f01 | |||
| 84667bca00 | |||
| 779449559d | |||
| 721a10e861 | |||
| a431873ca2 | |||
| e4ab1e1bb5 | |||
| f69356473b | |||
| 38ac13e87c | |||
| 0afb8cb6e2 | |||
| 5d282d2108 | |||
| 4aba72efa2 | |||
| 89476d5353 | |||
| 0f7a33340a | |||
| 73501e7cda | |||
| 9f8eba6736 | |||
| b6bf9f387d | |||
| d9fe1f8efc | |||
| fd7bc59851 | |||
| 55f1f87509 | |||
| 03f8761312 | |||
| 506748fda7 | |||
| 6d069f68cd | |||
| 4310239a7a | |||
| e9fe907af0 | |||
| 0c69d9aba3 | |||
| 6da85cdece | |||
| a073b09115 | |||
| f1f4870989 |
34
.cursor/rules/ai-integration.mdc
Normal file
34
.cursor/rules/ai-integration.mdc
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
description:
|
||||||
|
globs:
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
# KI-Integration
|
||||||
|
|
||||||
|
Die Anwendung integriert OpenAI für KI-Funktionalitäten:
|
||||||
|
|
||||||
|
## Konfiguration
|
||||||
|
- [app.py](mdc:app.py): OpenAI-Client-Initialisierung
|
||||||
|
- [requirements.txt](mdc:requirements.txt): OpenAI SDK als Abhängigkeit
|
||||||
|
|
||||||
|
## Endpunkte
|
||||||
|
- `/api/assistant`: Hauptendpunkt für KI-Anfragen
|
||||||
|
|
||||||
|
## Funktionalitäten
|
||||||
|
- Chatbot-Integration: Benutzer können mit einem KI-Assistenten kommunizieren
|
||||||
|
- Inhaltsanalyse: KI kann Gedanken und Konzepte analysieren
|
||||||
|
- Vorschläge: Kontextbezogene Vorschläge basierend auf dem Benutzerkontext
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
- Verwendet den OpenAI SDK für API-Aufrufe
|
||||||
|
- Kontextübergabe für konsistente Konversationen
|
||||||
|
- Streaming-Antworten für bessere Benutzererfahrung
|
||||||
|
|
||||||
|
## Konfigurationsparameter
|
||||||
|
- `OPENAI_API_KEY`: API-Schlüssel (in .env-Datei)
|
||||||
|
- Das System verwendet vorwiegend das Chat-Completion-API
|
||||||
|
|
||||||
|
## Sicherheitsmaßnahmen
|
||||||
|
- API-Schlüssel werden sicher über Umgebungsvariablen geladen
|
||||||
|
- Ratenbegrenzung und Fehlerbehandlung für API-Aufrufe
|
||||||
|
- Eingabevalidierung vor API-Anfragen
|
||||||
36
.cursor/rules/authentication.mdc
Normal file
36
.cursor/rules/authentication.mdc
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
description:
|
||||||
|
globs:
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
# Authentifizierung und Benutzerrollen
|
||||||
|
|
||||||
|
Die Anwendung nutzt Flask-Login für das Authentifizierungssystem:
|
||||||
|
|
||||||
|
## Hauptkomponenten
|
||||||
|
- [LoginManager](mdc:app.py): Konfiguration im app.py
|
||||||
|
- [User Model](mdc:models.py): Die User-Klasse implementiert UserMixin für Flask-Login
|
||||||
|
- Passwort-Hashing: Verwendet Werkzeug Security für sichere Passwort-Speicherung
|
||||||
|
|
||||||
|
## Authentifizierungsrouten
|
||||||
|
- `/login`: Benutzeranmeldung (GET/POST)
|
||||||
|
- `/register`: Benutzerregistrierung (GET/POST)
|
||||||
|
- `/logout`: Benutzerabmeldung
|
||||||
|
|
||||||
|
## Benutzerrollen
|
||||||
|
- Reguläre Benutzer: Grundlegende Funktionen
|
||||||
|
- Administratoren (`is_admin=True`): Erweiterte Privilegien
|
||||||
|
|
||||||
|
## Zugriffskontrollen
|
||||||
|
- `@login_required`: Decorator für routenspezifischen Authentifizierungsschutz
|
||||||
|
- `@admin_required`: Benutzerdefinierter Decorator für Admin-Zugriffskontrolle
|
||||||
|
|
||||||
|
## Sitzungsverwaltung
|
||||||
|
- Tracking von Anmeldezeit (`last_login`)
|
||||||
|
- Langlebige Sitzungen für Präferenzen (z.B. Dark Mode)
|
||||||
|
- Angepasste Flash-Nachrichten
|
||||||
|
|
||||||
|
## Profilmanagement
|
||||||
|
- `/settings`: Benutzereinstellungen aktualisieren
|
||||||
|
- Passwortänderung
|
||||||
|
- Profildetails (Biografie, Avatar, etc.)
|
||||||
31
.cursor/rules/configuration.mdc
Normal file
31
.cursor/rules/configuration.mdc
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
description:
|
||||||
|
globs:
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
# Konfiguration und Umgebungsvariablen
|
||||||
|
|
||||||
|
Die Anwendung verwendet Umgebungsvariablen für die Konfiguration:
|
||||||
|
|
||||||
|
## Konfigurationsdateien
|
||||||
|
- [.env](mdc:.env): Haupt-Umgebungsvariablen (nicht in Git)
|
||||||
|
- [example.env](mdc:example.env): Beispiel-Konfiguration als Vorlage
|
||||||
|
|
||||||
|
## Wichtige Konfigurationsparameter
|
||||||
|
- `SECRET_KEY`: Geheimer Schlüssel für Flask-Sitzungen
|
||||||
|
- `SQLALCHEMY_DATABASE_URI`: Datenbankverbindung
|
||||||
|
- `OPENAI_API_KEY`: API-Schlüssel für OpenAI-Integration
|
||||||
|
|
||||||
|
## Anwendungsinitialisierung
|
||||||
|
- [run.py](mdc:run.py): Lädt Umgebungsvariablen und startet die Anwendung
|
||||||
|
- [app.py](mdc:app.py): Konfiguriert Flask mit den geladenen Umgebungsvariablen
|
||||||
|
- [init_db.py](mdc:init_db.py): Initialisiert die Datenbank mit Beispieldaten
|
||||||
|
|
||||||
|
## Datenbank-Konfiguration
|
||||||
|
- SQLite-Datenbank im `/database`-Verzeichnis
|
||||||
|
- Automatische Erstellung der Datenbankstruktur bei Anwendungsstart
|
||||||
|
- Beispieldaten werden mit `init_database()` erstellt
|
||||||
|
|
||||||
|
## Ausführung der Anwendung
|
||||||
|
- Entwicklungsserver: `python run.py`
|
||||||
|
- In Produktion: Nutzung von Gunicorn (siehe requirements.txt)
|
||||||
31
.cursor/rules/data-models.mdc
Normal file
31
.cursor/rules/data-models.mdc
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
description:
|
||||||
|
globs:
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
# Datenmodelle
|
||||||
|
|
||||||
|
Die Anwendung verwendet SQLAlchemy als ORM mit folgenden Hauptmodellen:
|
||||||
|
|
||||||
|
## Benutzer und Authentifizierung
|
||||||
|
- [User](mdc:models.py): Benutzermodell mit Authentifizierung und Profildaten
|
||||||
|
|
||||||
|
## Mind-Mapping und Wissensorganisation
|
||||||
|
- [Category](mdc:models.py): Wissenschaftliche Kategorien zur Organisation der Mindmap
|
||||||
|
- [MindMapNode](mdc:models.py): Knoten in der öffentlichen Mindmap
|
||||||
|
- [UserMindmap](mdc:models.py): Benutzerspezifische Mindmaps
|
||||||
|
- [UserMindmapNode](mdc:models.py): Speichert Positionen von Knoten in Benutzer-Mindmaps
|
||||||
|
- [MindmapNote](mdc:models.py): Private Notizen zu Mindmap-Elementen
|
||||||
|
|
||||||
|
## Gedanken und Inhalte
|
||||||
|
- [Thought](mdc:models.py): Gedanken und Konzepte, die in Mindmaps verknüpft werden
|
||||||
|
- [ThoughtRelation](mdc:models.py): Verknüpfungen zwischen verschiedenen Gedanken
|
||||||
|
- [ThoughtRating](mdc:models.py): Bewertungen von Gedanken durch Benutzer
|
||||||
|
- [Comment](mdc:models.py): Kommentare zu Gedanken
|
||||||
|
|
||||||
|
## Hauptbeziehungen
|
||||||
|
- Benutzer → Gedanken: 1-zu-n (Autor)
|
||||||
|
- Benutzer → MindMaps: 1-zu-n
|
||||||
|
- Gedanken ↔ MindMapNodes: n-zu-m
|
||||||
|
- Kategorien → MindMapNodes: 1-zu-n
|
||||||
|
- Gedanken ↔ Gedanken: über ThoughtRelation (gerichtete Beziehungen)
|
||||||
32
.cursor/rules/development-workflow.mdc
Normal file
32
.cursor/rules/development-workflow.mdc
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
description:
|
||||||
|
globs:
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
# Entwicklungs-Workflow
|
||||||
|
|
||||||
|
## Grundlegende Entwicklungsschritte
|
||||||
|
1. Umgebung einrichten: Python 3.11 und Abhängigkeiten installieren
|
||||||
|
2. `.env`-Datei basierend auf `example.env` erstellen
|
||||||
|
3. Datenbank initialisieren: `python init_db.py`
|
||||||
|
4. Entwicklungsserver starten: `python run.py`
|
||||||
|
|
||||||
|
## Datenbankentwicklung
|
||||||
|
- Models in [models.py](mdc:models.py) definieren
|
||||||
|
- Migrationen bei Schemaänderungen durchführen
|
||||||
|
- Testdaten über [init_db.py](mdc:init_db.py) bereitstellen
|
||||||
|
|
||||||
|
## Anwendungsentwicklung
|
||||||
|
- Neue Routen in [app.py](mdc:app.py) hinzufügen
|
||||||
|
- Frontend-Templates in `/templates` erstellen/anpassen
|
||||||
|
- API-Endpoints für AJAX/Frontend-Integration implementieren
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
- Tests mit pytest schreiben (siehe requirements.txt)
|
||||||
|
- Flask-Testumgebung für Integrationstest verwenden
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
- Immer auf Datenbankmodelle zurückgreifen (kein Raw-SQL)
|
||||||
|
- API-Endpunkte mit Authentifizierung schützen
|
||||||
|
- Flash-Nachrichten für Benutzerrückmeldungen verwenden
|
||||||
|
- Code-Dokumentation in deutscher Sprache halten
|
||||||
41
.cursor/rules/frontend-structure.mdc
Normal file
41
.cursor/rules/frontend-structure.mdc
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
---
|
||||||
|
description:
|
||||||
|
globs:
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
# Frontend-Struktur
|
||||||
|
|
||||||
|
Die Anwendung verwendet ein Flask-Jinja2-Template-System mit JavaScript-Erweiterungen:
|
||||||
|
|
||||||
|
## Template-Struktur
|
||||||
|
- `/templates`: Hauptverzeichnis für Jinja2-Templates
|
||||||
|
- `/templates/errors`: Fehlerseiten (404, 500, etc.)
|
||||||
|
- Layout-Templates für einheitliches Design
|
||||||
|
|
||||||
|
## Frontend-Assets
|
||||||
|
- `/static/css`: CSS-Dateien (mit Tailwind)
|
||||||
|
- `/static/css/src`: Quell-CSS-Dateien
|
||||||
|
- `/static/js`: JavaScript-Dateien
|
||||||
|
- `/static/js/modules`: Modulare JS-Komponenten
|
||||||
|
- `/static/img`: Bilder und grafische Elemente
|
||||||
|
|
||||||
|
## JavaScript-Funktionalität
|
||||||
|
- API-Integration: Asynchrone Kommunikation mit Backend
|
||||||
|
- Mindmap-Visualisierung: Interaktive Darstellung von Konzepten
|
||||||
|
- Benutzeroberflächen-Interaktivität: Drag & Drop, Tooltips, Modals
|
||||||
|
|
||||||
|
## CSS-Framework
|
||||||
|
- Tailwind CSS für responsive Design-Elemente
|
||||||
|
- TAILWIND CDN verwenden, nicht manuell build!
|
||||||
|
|
||||||
|
## Responsive Design
|
||||||
|
- Mobile-first Ansatz für verschiedene Gerätetypen
|
||||||
|
- Anpassungsfähiges Layout für verschiedene Bildschirmgrößen
|
||||||
|
|
||||||
|
## Zugänglichkeit
|
||||||
|
- Semantisches HTML für bessere Zugänglichkeit
|
||||||
|
- ARIA-Attribute für Screenreader-Unterstützung
|
||||||
|
|
||||||
|
## Internationalisierung
|
||||||
|
- Deutsche Benutzeroberfläche als Standard
|
||||||
|
- Vorbereitet für mehrsprachige Unterstützung
|
||||||
27
.cursor/rules/project-structure.mdc
Normal file
27
.cursor/rules/project-structure.mdc
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
description:
|
||||||
|
globs:
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
# Projekt-Struktur (Systades)
|
||||||
|
|
||||||
|
## Hauptkomponenten
|
||||||
|
Diese Python-Flask-Webanwendung implementiert ein Mind-Mapping und Gedanken-Management System:
|
||||||
|
|
||||||
|
- [app.py](mdc:app.py): Hauptanwendungsdatei mit allen Routen und Endpunkten
|
||||||
|
- [models.py](mdc:models.py): Datenbankmodelle und Beziehungen
|
||||||
|
- [run.py](mdc:run.py): Startpunkt der Anwendung
|
||||||
|
- [init_db.py](mdc:init_db.py): Initialisiert die Datenbank mit Beispieldaten
|
||||||
|
|
||||||
|
## Projektstruktur
|
||||||
|
- `/database`: Enthält SQLite-Datenbank
|
||||||
|
- `/docs`: Dokumentation
|
||||||
|
- `/static`: Frontend-Ressourcen (CSS, JS, Bilder)
|
||||||
|
- `/templates`: Jinja2-Templates für die Webseiten
|
||||||
|
- `/utils`: Hilfsfunktionen und -klassen
|
||||||
|
|
||||||
|
## Hauptfunktionalität
|
||||||
|
- Mind-Mapping: Visualisierung von Wissen und Beziehungen
|
||||||
|
- Gedanken-Management: Erfassung und Organisation von Ideen und Konzepten
|
||||||
|
- Benutzer-Management: Registrierung, Login, Profile
|
||||||
|
- API-Endpunkte: RESTful-Schnittstellen für Frontend-Integration
|
||||||
43
.cursor/rules/routing.mdc
Normal file
43
.cursor/rules/routing.mdc
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
---
|
||||||
|
description:
|
||||||
|
globs:
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
# Routing und API-Endpunkte
|
||||||
|
|
||||||
|
## Hauptrouten (Webseiten)
|
||||||
|
- `/`: Startseite
|
||||||
|
- `/login`, `/register`, `/logout`: Authentifizierung
|
||||||
|
- `/mindmap`: Öffentliche Mindmap-Ansicht
|
||||||
|
- `/profile`: Benutzerprofil
|
||||||
|
- `/settings`: Benutzereinstellungen
|
||||||
|
- `/search`: Suchfunktion
|
||||||
|
- `/my_account`: Kontoübersicht
|
||||||
|
|
||||||
|
## API-Endpunkte
|
||||||
|
### Mindmap-Verwaltung
|
||||||
|
- `/api/mindmap`: Öffentliche Mindmap-Daten abrufen
|
||||||
|
- `/api/mindmap/public`: Öffentliche Mindmap abrufen
|
||||||
|
- `/api/mindmap/user/<id>`: Benutzer-Mindmap abrufen
|
||||||
|
- `/api/mindmap/<id>/add_node`: Knoten hinzufügen
|
||||||
|
- `/api/mindmap/<id>/remove_node/<node_id>`: Knoten entfernen
|
||||||
|
- `/api/mindmap/<id>/update_node_position`: Knotenposition aktualisieren
|
||||||
|
- `/api/mindmap/<id>/notes`: Notizen verwalten
|
||||||
|
|
||||||
|
### Gedanken und Inhalte
|
||||||
|
- `/api/thoughts`: Gedanken erstellen
|
||||||
|
- `/api/thoughts/<id>`: Gedanken abrufen, aktualisieren, löschen
|
||||||
|
- `/api/thoughts/<id>/bookmark`: Lesezeichen setzen/entfernen
|
||||||
|
- `/api/nodes/<id>/thoughts`: Gedanken zu einem Knoten abrufen/hinzufügen
|
||||||
|
|
||||||
|
### System und Benutzereinstellungen
|
||||||
|
- `/api/set_dark_mode`, `/api/get_dark_mode`: Erscheinungsbild-Einstellungen
|
||||||
|
- `/api/assistant`: KI-Assistent-Kommunikation
|
||||||
|
- `/api/categories`: Kategorien abrufen
|
||||||
|
- `/api/get_flash_messages`: Flash-Nachrichten für AJAX-Anfragen
|
||||||
|
|
||||||
|
## Fehlerbehandlung
|
||||||
|
- 404: Page Not Found
|
||||||
|
- 403: Forbidden
|
||||||
|
- 500: Internal Server Error
|
||||||
|
- 429: Too Many Requests
|
||||||
8
.env
8
.env
@@ -2,12 +2,14 @@
|
|||||||
# Kopiere diese Datei zu .env und passe die Werte an
|
# Kopiere diese Datei zu .env und passe die Werte an
|
||||||
|
|
||||||
# Flask
|
# Flask
|
||||||
SECRET_KEY=dein-geheimer-schluessel-hier
|
FLASK_APP=app.py
|
||||||
|
FLASK_DEBUG=1
|
||||||
|
SECRET_KEY=your-secret-key-replace-in-production
|
||||||
|
|
||||||
# OpenAI API
|
# OpenAI API
|
||||||
OPENAI_API_KEY=sk-dein-openai-api-schluessel-hier
|
OPENAI_API_KEY=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
|
||||||
|
|
||||||
# Datenbank
|
# Datenbank
|
||||||
# Bei Bedarf kann hier eine andere Datenbank-URL angegeben werden
|
# Bei Bedarf kann hier eine andere Datenbank-URL angegeben werden
|
||||||
# Der Pfad wird relativ zum Projektverzeichnis angegeben
|
# Der Pfad wird relativ zum Projektverzeichnis angegeben
|
||||||
# SQLALCHEMY_DATABASE_URI=sqlite:////absoluter/pfad/zu/database/systades.db OPENAI_API_KEY=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
|
# SQLALCHEMY_DATABASE_URI=sqlite:////absoluter/pfad/zu/database/systades.db
|
||||||
8
.vscode/jsconfig.json
vendored
Normal file
8
.vscode/jsconfig.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "esnext",
|
||||||
|
"lib": [
|
||||||
|
"esnext"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
68
.vscode/main.js
vendored
Normal file
68
.vscode/main.js
vendored
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
/// <reference types="vscode" />
|
||||||
|
// @ts-check
|
||||||
|
// API: https://code.visualstudio.com/api/references/vscode-api
|
||||||
|
// @ts-ignore
|
||||||
|
const vscode = require('vscode');
|
||||||
|
* @typedef {import('vscode').ExtensionContext} ExtensionContext
|
||||||
|
* @typedef {import('vscode').commands} commands
|
||||||
|
* @typedef {import('vscode').window} window
|
||||||
|
* @typedef {import('vscode').TextEditor} TextEditor
|
||||||
|
* @typedef {import('vscode').TextDocument} TextDocument
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aktiviert die Erweiterung und registriert den Auto-Resume-Befehl
|
||||||
|
* @param {vscode.ExtensionContext} context - Der Erweiterungskontext
|
||||||
|
*/
|
||||||
|
function activate(context) {
|
||||||
|
const disposable = vscode.commands.registerCommand('extension.autoResume', () => {
|
||||||
|
const editor = vscode.window.activeTextEditor;
|
||||||
|
if (!editor) return;
|
||||||
|
|
||||||
|
const document = editor.document;
|
||||||
|
const text = document.getText();
|
||||||
|
|
||||||
|
// Track last click time to avoid multiple clicks
|
||||||
|
let lastClickTime = 0;
|
||||||
|
|
||||||
|
// Main function that looks for and clicks the resume link
|
||||||
|
function clickResumeLink() {
|
||||||
|
// Prevent clicking too frequently (3 second cooldown)
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - lastClickTime < 3000) return;
|
||||||
|
|
||||||
|
// Check if text contains rate limit text
|
||||||
|
if (text.includes('stop the agent after 25 tool calls') ||
|
||||||
|
text.includes('Note: we default stop')) {
|
||||||
|
|
||||||
|
// Find the resume link position
|
||||||
|
const resumePos = text.indexOf('resume the conversation');
|
||||||
|
if (resumePos !== -1) {
|
||||||
|
vscode.window.showInformationMessage('Auto-resuming conversation...');
|
||||||
|
lastClickTime = now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Führe periodisch aus
|
||||||
|
const interval = global.setInterval(clickResumeLink, 1000);
|
||||||
|
|
||||||
|
// Speichere das Intervall in den Subscriptions
|
||||||
|
context.subscriptions.push({
|
||||||
|
dispose: () => global.clearInterval(interval)
|
||||||
|
});
|
||||||
|
// Führe die Funktion sofort aus
|
||||||
|
clickResumeLink();
|
||||||
|
});
|
||||||
|
|
||||||
|
context.subscriptions.push(disposable);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deactivate() {
|
||||||
|
// Cleanup if needed
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
activate,
|
||||||
|
deactivate
|
||||||
|
}
|
||||||
239
COMMON_ERRORS.md
Normal file
239
COMMON_ERRORS.md
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
# ABSOLUTE DON'TS:
|
||||||
|
- Verwendung von npm anstelle der Tailwind CDN
|
||||||
|
- Implementierung von Content Security Policy (CSP) - UNTER KEINEN UMSTÄNDEN!
|
||||||
|
- Implementierung von Cross-Site Request Forgery (CSRF) Schutz
|
||||||
|
- Implementierung von Security Headers
|
||||||
|
- Verwendung von TypeScript anstelle von JavaScript
|
||||||
|
- Implementierung von OAuth oder anderen externen Authentifizierungsmethoden
|
||||||
|
|
||||||
|
# HÄUFIGE FEHLER:
|
||||||
|
- Verwendung der falschen Datenbank (die korrekte ist: database/systades.db)
|
||||||
|
- Falsche Pfadangaben bei statischen Dateien
|
||||||
|
- Vergessen der deutschen Spracheinstellungen in Templates
|
||||||
|
- Nicht beachten der vorhandenen Projektstruktur
|
||||||
|
- Falsche Einbindung der Neural Network Background Animation
|
||||||
|
- Verwendung von englischen Variablennamen in deutschen Funktionen
|
||||||
|
- Vergessen der Mindmap-Datenstruktur gemäß der Roadmap
|
||||||
|
|
||||||
|
# Häufige Fehler und Lösungen
|
||||||
|
|
||||||
|
## Datenbankfehler
|
||||||
|
|
||||||
|
### Fehler: "no such column: user.password"
|
||||||
|
|
||||||
|
**Fehlerbeschreibung:**
|
||||||
|
```
|
||||||
|
sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) no such column: user.password
|
||||||
|
[SQL: SELECT user.id AS user_id, user.username AS user_username, user.email AS user_email, user.password AS user_password, user.created_at AS user_created_at, user.is_active AS user_is_active, user.role AS user_role
|
||||||
|
FROM user
|
||||||
|
WHERE user.id = ?]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ursache:**
|
||||||
|
Die Spalte `password` fehlt in der Tabelle `user` der SQLite-Datenbank. Dies kann durch eine unvollständige Datenbankinitialisierung oder ein fehlerhaftes Schema-Update verursacht worden sein.
|
||||||
|
|
||||||
|
**Lösung:**
|
||||||
|
|
||||||
|
1. **Datenbank reparieren mit dem Fix-Skript**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python fix_user_table.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Dieses Skript:
|
||||||
|
- Prüft, ob die Tabelle `user` existiert und erstellt sie, falls nicht
|
||||||
|
- Prüft, ob die Spalte `password` existiert und fügt sie hinzu, falls nicht
|
||||||
|
|
||||||
|
2. **Standardbenutzer erstellen**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python create_default_users.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Dieses Skript:
|
||||||
|
- Erstellt Standardbenutzer (admin, user), falls keine vorhanden sind
|
||||||
|
- Setzt Passwörter mit korrektem Hashing
|
||||||
|
|
||||||
|
3. **Datenbank testen**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python test_app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Dieses Skript überprüft:
|
||||||
|
- Ob die Datenbank existiert
|
||||||
|
- Ob die Tabelle `user` korrekt konfiguriert ist
|
||||||
|
- Ob Benutzer vorhanden sind
|
||||||
|
|
||||||
|
## Häufige Ursachen für Datenbankfehler
|
||||||
|
|
||||||
|
1. **Inkonsistente Datenbankschemas**
|
||||||
|
- Unterschiede zwischen dem SQLAlchemy-Modell und der tatsächlichen Datenbankstruktur
|
||||||
|
- Fehlende Spalten, die in den Modellen definiert sind
|
||||||
|
|
||||||
|
2. **Falsche Datenbankinitialisierung**
|
||||||
|
- Die Datenbank wurde nicht korrekt initialisiert
|
||||||
|
- Fehler bei der Migration oder dem Schema-Update
|
||||||
|
|
||||||
|
3. **Datenbankdatei-Korrumpierung**
|
||||||
|
- Die SQLite-Datenbankdatei wurde beschädigt
|
||||||
|
- Lösung: Sicherung wiederherstellen oder Datenbank neu erstellen
|
||||||
|
|
||||||
|
## Vorbeugende Maßnahmen
|
||||||
|
|
||||||
|
1. **Regelmäßige Backups**
|
||||||
|
- Tägliche Sicherung der Datenbankdatei
|
||||||
|
|
||||||
|
2. **Schema-Validierung**
|
||||||
|
- Regelmäßige Überprüfung der Datenbankstruktur
|
||||||
|
- Automatisierte Tests für Datenbankschema
|
||||||
|
|
||||||
|
3. **Datenbankmigration**
|
||||||
|
- Verwenden Sie Flask-Migrate für strukturierte Datenbank-Updates
|
||||||
|
- Dokumentieren Sie alle Schemaänderungen
|
||||||
|
|
||||||
|
## Content Security Policy (CSP)
|
||||||
|
|
||||||
|
### Problem: Externe Ressourcen werden nicht geladen
|
||||||
|
**Fehler:** Externe Ressourcen wie CDNs werden nicht korrekt geladen.
|
||||||
|
|
||||||
|
**Lösung:**
|
||||||
|
1. Stellen Sie sicher, dass die URLs in den Templates korrekt sind:
|
||||||
|
```html
|
||||||
|
<link href="https://cdn.tailwindcss.com" rel="stylesheet">
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Überprüfen Sie die Netzwerkverbindung und ob die CDN-Domains erreichbar sind.
|
||||||
|
|
||||||
|
3. Verwenden Sie lokale Ressourcen als Alternative:
|
||||||
|
```html
|
||||||
|
<link href="{{ url_for('static', filename='css/tailwind.min.css') }}" rel="stylesheet">
|
||||||
|
```
|
||||||
|
|
||||||
|
### Problem: CSP-Fehler (script-src-elem 'none', font-src 'none', etc.)
|
||||||
|
**Fehler:** Skripte werden aufgrund von CSP-Einschränkungen blockiert.
|
||||||
|
|
||||||
|
**Lösung:**
|
||||||
|
1. WICHTIG: CSP-Einschränkungen NICHT implementieren! Diese sind im Projekt unerwünscht.
|
||||||
|
|
||||||
|
2. Falls CSP-Header bereits vorhanden sind, entfernen Sie diese aus der Anwendung:
|
||||||
|
```python
|
||||||
|
# Entfernen Sie diesen Code, falls vorhanden:
|
||||||
|
# @app.after_request
|
||||||
|
# def add_security_headers(response):
|
||||||
|
# response.headers['Content-Security-Policy'] = '...'
|
||||||
|
# return response
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Stellen Sie sicher, dass externe Ressourcen direkt geladen werden:
|
||||||
|
```html
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||||
|
<script src="{{ url_for('static', filename='js/alpine.min.js') }}" defer></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Für Inline-Skripte keine Nonce verwenden:
|
||||||
|
```html
|
||||||
|
<script>
|
||||||
|
// Inline-JavaScript-Code ohne Einschränkungen
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Problem: Tailwind CSS CDN wird blockiert
|
||||||
|
**Fehler:** Tailwind CSS kann nicht von CDN geladen werden.
|
||||||
|
|
||||||
|
**Lösung:**
|
||||||
|
1. Verwenden Sie die lokale Version von Tailwind CSS:
|
||||||
|
```html
|
||||||
|
<link href="{{ url_for('static', filename='css/tailwind.min.css') }}" rel="stylesheet">
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Alternativ können Sie die CDN-Version direkt im Template einbinden:
|
||||||
|
```html
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Stellen Sie sicher, dass die Datei `static/css/tailwind.min.css` existiert und aktuell ist.
|
||||||
|
|
||||||
|
## Authentifizierung
|
||||||
|
|
||||||
|
### Problem: Login funktioniert nicht
|
||||||
|
**Fehler:** Benutzer kann sich nicht einloggen.
|
||||||
|
|
||||||
|
**Lösung:**
|
||||||
|
1. Standard-Admin-Benutzer erstellen: `python TOOLS.py user:admin`
|
||||||
|
2. Passwort zurücksetzen: `python TOOLS.py user:reset-pw -u USERNAME -p NEWPASSWORD`
|
||||||
|
|
||||||
|
## Neural Network Background
|
||||||
|
|
||||||
|
### Problem: Hintergrund-Animation wird nicht angezeigt
|
||||||
|
**Fehler:** Die Neural Network Animation im Hintergrund erscheint nicht.
|
||||||
|
|
||||||
|
**Lösung:**
|
||||||
|
1. Überprüfen Sie, ob die Datei `static/neural-network-background.js` korrekt eingebunden ist:
|
||||||
|
```html
|
||||||
|
<script src="{{ url_for('static', filename='neural-network-background.js') }}"></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Initialisieren Sie die Animation im Template:
|
||||||
|
```html
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const background = new NeuralNetworkBackground();
|
||||||
|
background.initialize();
|
||||||
|
background.animate();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Stellen Sie sicher, dass keine CSS-Regeln die Animation überdecken:
|
||||||
|
```css
|
||||||
|
#neural-network-background {
|
||||||
|
z-index: -10;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mindmap-Funktionalität
|
||||||
|
|
||||||
|
### Problem: Mindmap-Daten werden nicht geladen
|
||||||
|
**Fehler:** Die dynamische Mindmap zeigt keine Daten an.
|
||||||
|
|
||||||
|
**Lösung:**
|
||||||
|
1. Überprüfen Sie die API-Endpunkte für die Mindmap-Daten:
|
||||||
|
```python
|
||||||
|
@app.route('/api/mindmap/nodes', methods=['GET'])
|
||||||
|
def get_mindmap_nodes():
|
||||||
|
# Implementierung...
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Stellen Sie sicher, dass die AJAX-Anfragen korrekt implementiert sind:
|
||||||
|
```javascript
|
||||||
|
fetch('/api/mindmap/nodes')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
// Verarbeitung der Mindmap-Daten
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Überprüfen Sie die Datenbankeinträge für Mindmap-Knoten und -Verbindungen.
|
||||||
|
|
||||||
|
## ChatGPT-Assistent
|
||||||
|
|
||||||
|
### Problem: Assistent reagiert nicht auf Eingaben
|
||||||
|
**Fehler:** Der ChatGPT-Assistent verarbeitet keine Benutzereingaben.
|
||||||
|
|
||||||
|
**Lösung:**
|
||||||
|
1. Überprüfen Sie die Einbindung der JavaScript-Datei:
|
||||||
|
```html
|
||||||
|
<script src="{{ url_for('static', filename='js/modules/chatgpt-assistant.js') }}"></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Stellen Sie sicher, dass der Assistent korrekt initialisiert wird:
|
||||||
|
```javascript
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const assistant = new ChatGPTAssistant();
|
||||||
|
assistant.initialize();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Überprüfen Sie die API-Endpunkte für die Kommunikation mit dem Assistenten.
|
||||||
33
Dockerfile
Normal file
33
Dockerfile
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# Dockerfile
|
||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
# Arbeitsverzeichnis in Container
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Systemabhängigkeiten installieren und Verzeichnisse anlegen
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y --no-install-recommends gcc && \
|
||||||
|
rm -rf /var/lib/apt/lists/* && \
|
||||||
|
mkdir -p /app/database
|
||||||
|
|
||||||
|
# pip auf den neuesten Stand bringen und Requirements installieren
|
||||||
|
COPY requirements.txt ./
|
||||||
|
RUN pip install --upgrade pip && \
|
||||||
|
pip install --no-cache-dir -U -r requirements.txt
|
||||||
|
|
||||||
|
# Anwendungscode kopieren
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Berechtigungen für database-Ordner
|
||||||
|
RUN chmod -R 777 /app/database
|
||||||
|
|
||||||
|
# Exponiere Port 5000 für Flask
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
|
# Setze Umgebungsvariablen
|
||||||
|
ENV FLASK_APP=app.py
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
|
|
||||||
|
# Startkommando mit spezifischen Flags für Produktion
|
||||||
|
CMD ["python", "app.py"]
|
||||||
80
README.md
80
README.md
@@ -6,9 +6,10 @@ Das MindMapProjekt ist eine interaktive Plattform zum Visualisieren, Erforschen
|
|||||||
## Technischer Stack
|
## Technischer Stack
|
||||||
- **Backend**: Python/Flask
|
- **Backend**: Python/Flask
|
||||||
- **Frontend**:
|
- **Frontend**:
|
||||||
- Tailwind CSS für moderne UI
|
- Tailwind CSS (via CDN) für moderne UI
|
||||||
- SVG-Bibliotheken für Visualisierungen (D3.js)
|
- SVG-Bibliotheken für Visualisierungen (D3.js)
|
||||||
- JavaScript/Alpine.js für interaktive Komponenten
|
- JavaScript/Alpine.js für interaktive Komponenten
|
||||||
|
- WebGL für animierte Hintergrundeffekte
|
||||||
- **Datenbank**: SQLite mit SQLAlchemy
|
- **Datenbank**: SQLite mit SQLAlchemy
|
||||||
- **KI-Integration**: OpenAI API für intelligente Assistenz
|
- **KI-Integration**: OpenAI API für intelligente Assistenz
|
||||||
|
|
||||||
@@ -61,16 +62,20 @@ Für detaillierte Hilfe: `python TOOLS.py -h`
|
|||||||
- [x] Favicon erstellen
|
- [x] Favicon erstellen
|
||||||
- [x] Setup-Skript für einfache Installation
|
- [x] Setup-Skript für einfache Installation
|
||||||
|
|
||||||
### Phase 2: Design-Überarbeitung 🔄
|
### Phase 2: Design-Überarbeitung ✅
|
||||||
- [x] Implementierung des Dark Mode
|
- [x] Implementierung des Dark Mode
|
||||||
- [x] Erstellung eines modernen, minimalistischen UI mit Tech-Ästhetik
|
- [x] Erstellung eines modernen, minimalistischen UI mit Tech-Ästhetik
|
||||||
- [x] Responsive Design für alle Geräte
|
- [x] Responsive Design für alle Geräte
|
||||||
- [ ] Gestaltung der Landing Page mit großer Typografie
|
- [x] Gestaltung der Landing Page mit großer Typografie
|
||||||
|
- [x] Animierter Neurales Netzwerk-Hintergrund mit WebGL
|
||||||
|
|
||||||
### Phase 3: Mindmap-Funktionalitäten 🔄
|
### Phase 3: Mindmap-Funktionalitäten 🔄
|
||||||
- [x] Verbesserte Visualisierung mit SVG und D3.js
|
- [x] Verbesserte Visualisierung mit SVG und D3.js
|
||||||
- [x] Implementierung der Mouseover-Funktion
|
- [x] Implementierung der Mouseover-Funktion
|
||||||
- [x] Entwicklung der Suchfunktion für Knoten
|
- [x] Entwicklung der Suchfunktion für Knoten
|
||||||
|
- [x] Clustertopologie für neuronale Netzwerkdarstellung
|
||||||
|
- [x] Fehlerbehandlung für robuste Datenverarbeitung und Knotenverweise
|
||||||
|
- [x] Verbesserte Verbindungserkennung zwischen Knoten
|
||||||
- [ ] Tagging-System für Inhalte
|
- [ ] Tagging-System für Inhalte
|
||||||
- [ ] Quellenmanagement und -verlinkung
|
- [ ] Quellenmanagement und -verlinkung
|
||||||
- [ ] Upload-Funktionalität an Knotenpunkten
|
- [ ] Upload-Funktionalität an Knotenpunkten
|
||||||
@@ -115,8 +120,8 @@ Für detaillierte Hilfe: `python TOOLS.py -h`
|
|||||||
|
|
||||||
## Aktueller Status
|
## Aktueller Status
|
||||||
- **Phase 1**: ✅ Abgeschlossen
|
- **Phase 1**: ✅ Abgeschlossen
|
||||||
- **Phase 2**: 🔄 In Bearbeitung (75% abgeschlossen)
|
- **Phase 2**: ✅ Abgeschlossen
|
||||||
- **Phase 3**: 🔄 In Bearbeitung (50% abgeschlossen)
|
- **Phase 3**: 🔄 In Bearbeitung (75% abgeschlossen)
|
||||||
|
|
||||||
## Aktuelle Fortschritte
|
## Aktuelle Fortschritte
|
||||||
- Grundlegende UI modernisiert mit Tailwind CSS und Dark Mode
|
- Grundlegende UI modernisiert mit Tailwind CSS und Dark Mode
|
||||||
@@ -124,11 +129,68 @@ Für detaillierte Hilfe: `python TOOLS.py -h`
|
|||||||
- Setup-Prozess vereinfacht mit einem Shell-Skript
|
- Setup-Prozess vereinfacht mit einem Shell-Skript
|
||||||
- Mindmap-Visualisierung komplett überarbeitet mit D3.js für eine interaktivere Erfahrung
|
- Mindmap-Visualisierung komplett überarbeitet mit D3.js für eine interaktivere Erfahrung
|
||||||
- Responsive Design für optimale Darstellung auf allen Geräten
|
- Responsive Design für optimale Darstellung auf allen Geräten
|
||||||
|
- Animierter neuronaler Netzwerk-Hintergrund mit WebGL implementiert
|
||||||
|
- Verbesserte neuronale Cluster-Darstellung in der MindMap-Ansicht
|
||||||
|
- Behebung von kritischen Fehlern in der Knotenvisualisierung und Verbindungserkennung
|
||||||
|
- Robustere Datenverarbeitung für Mindmap-Knoten implementiert
|
||||||
|
- Fehlerbehandlung für verschiedene API-Datenformate verbessert
|
||||||
|
|
||||||
|
## Neuronaler Netzwerk-Hintergrund
|
||||||
|
|
||||||
|
Ein wesentliches neues Feature ist der animierte Hintergrund, der ein neuronales Netzwerk simuliert:
|
||||||
|
|
||||||
|
- **WebGL-basierte Rendering-Engine** für hohe Performance
|
||||||
|
- **Dynamische Knoten und Verbindungen** mit realistischem Bewegungsverhalten
|
||||||
|
- **Neuronenfeuer-Simulation** mit Signalweiterleitung zwischen Knoten
|
||||||
|
- **Clustertopologie** für realistisches Erscheinungsbild
|
||||||
|
- **Anpassbare Farbgebung und Animationsparameter**
|
||||||
|
- **Flüssige Animationen** mit über 100 Knotenpunkten
|
||||||
|
|
||||||
|
Die Animation ist vollständig responsiv und passt sich automatisch an verschiedene Bildschirmgrößen an, ohne die Browser-Performance zu beeinträchtigen.
|
||||||
|
|
||||||
|
## Mindmap-Verbesserungen
|
||||||
|
|
||||||
|
Die Mindmap-Darstellung wurde grundlegend überarbeitet:
|
||||||
|
|
||||||
|
- **D3.js Force-Directed Graph** für intuitive Knotenpositionierung
|
||||||
|
- **Verbesserte Fehlerbehandlung** für robustere Datenverarbeitung
|
||||||
|
- **Neuronale Cluster-Gruppierung** von thematisch zusammengehörigen Inhalten
|
||||||
|
- **Glasmorphismus-Effekte** für moderne visuelle Darstellung
|
||||||
|
- **Verbesserte Hover- und Selektionseffekte**
|
||||||
|
- **Flüssige Animationen** bei Knotenauswahl und -fokussierung
|
||||||
|
|
||||||
## Nächste Schritte
|
## Nächste Schritte
|
||||||
- Fertigstellung der Landing Page
|
- Fertigstellung des Tagging-Systems für Gedanken
|
||||||
- Erstellung der "Wer sind wir?"-Seite
|
|
||||||
- Implementierung des Tagging-Systems für Gedanken
|
|
||||||
- Verbesserung der Gedankenansicht im Mindmap-Bereich
|
- Verbesserung der Gedankenansicht im Mindmap-Bereich
|
||||||
|
- Implementierung von Quellenmanagement
|
||||||
|
- Überarbeitung der Startseite mit neuen Features
|
||||||
|
|
||||||
*Zuletzt aktualisiert: 01.06.2024*
|
## Content Security Policy (CSP)
|
||||||
|
|
||||||
|
Die Anwendung implementiert eine Content Security Policy, um die Sicherheit zu erhöhen und unerwünschte externe Ressourcen zu blockieren. CSP wird in `app.py` konfiguriert und schränkt ein, welche Ressourcen geladen werden dürfen.
|
||||||
|
|
||||||
|
### Aktualisierung (06.06.2024)
|
||||||
|
Die Anwendung verwendet nun die Tailwind CSS CDN für vereinfachte Entwicklung. Die CSP wurde entsprechend angepasst, um die Domain `cdn.tailwindcss.com` zu erlauben.
|
||||||
|
|
||||||
|
### Lokale und CDN-Ressourcen
|
||||||
|
|
||||||
|
Die Anwendung nutzt eine Mischung aus lokalen Ressourcen und CDNs:
|
||||||
|
- **CDN-Ressourcen**:
|
||||||
|
- Tailwind CSS (cdn.tailwindcss.com)
|
||||||
|
- **Lokale Ressourcen**:
|
||||||
|
- Alpine.js
|
||||||
|
- Font Awesome
|
||||||
|
- Google Fonts (Inter und JetBrains Mono)
|
||||||
|
- WebGL-Animation (neural-network-background.js)
|
||||||
|
|
||||||
|
### CSP-Nonces
|
||||||
|
|
||||||
|
Die Anwendung verwendet Nonces für Inline-Skripte. In den Templates wird `{{ csp_nonce }}` verwendet, um den Nonce-Wert einzufügen:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script nonce="{{ csp_nonce }}">
|
||||||
|
// JavaScript Code
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
*Zuletzt aktualisiert: 15.06.2024*
|
||||||
98
ROADMAP.md
98
ROADMAP.md
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Diese Roadmap beschreibt die geplante Entwicklung der dynamischen, benutzerorientierten Mindmap-Funktionalität für das Systades-Projekt.
|
Diese Roadmap beschreibt die geplante Entwicklung der dynamischen, benutzerorientierten Mindmap-Funktionalität für das Systades-Projekt.
|
||||||
|
|
||||||
## Phase 1: Grundlegendes Datenmodell und Backend (Abgeschlossen)
|
## Phase 1: Grundlegendes Datenmodell und Backend (Abgeschlossen ✅)
|
||||||
|
|
||||||
- [x] Entwurf des Datenbankschemas für benutzerorientierte Mindmaps
|
- [x] Entwurf des Datenbankschemas für benutzerorientierte Mindmaps
|
||||||
- [x] Implementierung der Modelle in models.py
|
- [x] Implementierung der Modelle in models.py
|
||||||
@@ -10,31 +10,54 @@ Diese Roadmap beschreibt die geplante Entwicklung der dynamischen, benutzerorien
|
|||||||
- [x] Integration mit der bestehenden Benutzerauthentifizierung
|
- [x] Integration mit der bestehenden Benutzerauthentifizierung
|
||||||
- [x] Seed-Daten für die Entwicklung und Tests
|
- [x] Seed-Daten für die Entwicklung und Tests
|
||||||
|
|
||||||
## Phase 2: Dynamische Mindmap-Visualisierung (Aktuell)
|
## Phase 2: Dynamische Mindmap-Visualisierung (Abgeschlossen ✅)
|
||||||
|
|
||||||
- [ ] Anpassung des Frontend-Codes zur Verwendung der DB-Daten anstelle des SVG
|
- [x] Anpassung des Frontend-Codes zur Verwendung der DB-Daten anstelle des SVG
|
||||||
- [ ] Implementierung von AJAX-Anfragen zum Laden der Mindmap-Daten
|
- [x] Implementierung von AJAX-Anfragen zum Laden der Mindmap-Daten
|
||||||
- [ ] Dynamisches Rendering der Knoten, Verbindungen und Labels
|
- [x] Dynamisches Rendering der Knoten, Verbindungen und Labels
|
||||||
- [ ] Drag-and-Drop-Funktionalität für die Bewegung von Knoten
|
- [x] Drag-and-Drop-Funktionalität für die Bewegung von Knoten
|
||||||
- [ ] Zoom- und Pan-Funktionalität mit Persistenz der Ansicht
|
- [x] Zoom- und Pan-Funktionalität mit Persistenz der Ansicht
|
||||||
|
- [x] Verbesserte Fehlerbehandlung in der Knotenvisualisierung
|
||||||
|
- [x] Robustere Verbindungserkennung zwischen Knoten
|
||||||
|
- [x] Implementierung von Glasmorphismus-Effekten für moderneres UI
|
||||||
|
|
||||||
## Phase 3: Benutzerdefinierte Mindmaps
|
## Phase 3: Visuelles Design und UX (Abgeschlossen ✅)
|
||||||
|
|
||||||
- [ ] UI für das Erstellen, Bearbeiten und Löschen eigener Mindmaps
|
- [x] Implementierung des Dark Mode
|
||||||
|
- [x] Entwicklung eines modernen, minimalistischen UI
|
||||||
|
- [x] Animierter neuronaler Netzwerk-Hintergrund mit WebGL
|
||||||
|
- [x] Responsive Design für alle Geräte
|
||||||
|
- [x] Verbesserte Hover- und Selektionseffekte
|
||||||
|
- [x] Clustertopologie für neuronale Netzwerkdarstellung
|
||||||
|
- [x] Animierte Neuronenfeuer-Simulation mit Signalweiterleitung
|
||||||
|
|
||||||
|
## Phase 4: Benutzerdefinierte Mindmaps (Aktuell 🔄)
|
||||||
|
|
||||||
|
- [x] UI für das Betrachten bestehender Mindmaps
|
||||||
|
- [ ] UI für das Erstellen und Bearbeiten eigener Mindmaps
|
||||||
- [ ] Funktion zum Hinzufügen/Entfernen von Knoten aus der öffentlichen Mindmap
|
- [ ] Funktion zum Hinzufügen/Entfernen von Knoten aus der öffentlichen Mindmap
|
||||||
- [ ] Speichern der Knotenpositionen und Ansichtseinstellungen
|
- [ ] Speichern der Knotenpositionen und Ansichtseinstellungen
|
||||||
- [ ] Benutzerspezifische Visualisierungseinstellungen
|
- [ ] Benutzerspezifische Visualisierungseinstellungen
|
||||||
- [ ] Dashboard mit Übersicht aller Mindmaps des Benutzers
|
- [ ] Dashboard mit Übersicht aller Mindmaps des Benutzers
|
||||||
|
|
||||||
## Phase 4: Notizen und Annotationen
|
## Phase 5: Notizen und Annotationen
|
||||||
|
|
||||||
|
- [x] Anzeige von Gedanken zu Mindmap-Knoten
|
||||||
- [ ] UI für das Hinzufügen privater Notizen zu Knoten
|
- [ ] UI für das Hinzufügen privater Notizen zu Knoten
|
||||||
- [ ] Visuelle Anzeige von Notizen in der Mindmap
|
- [ ] Visuelle Anzeige von Notizen in der Mindmap
|
||||||
- [ ] Texteditor mit Markdown-Unterstützung für Notizen
|
- [ ] Texteditor mit Markdown-Unterstützung für Notizen
|
||||||
- [ ] Kategorisierung und Farbkodierung von Notizen
|
- [ ] Kategorisierung und Farbkodierung von Notizen
|
||||||
- [ ] Suchfunktion für Notizen
|
- [ ] Suchfunktion für Notizen
|
||||||
|
|
||||||
## Phase 5: Integrationen und Erweiterungen
|
## Phase 6: Tagging und Quellenmanagement
|
||||||
|
|
||||||
|
- [ ] Tagging-System für Inhalte implementieren
|
||||||
|
- [ ] Verknüpfen von Quellen mit Mindmap-Knoten
|
||||||
|
- [ ] Upload-Funktionalität für Dateien und Medien
|
||||||
|
- [ ] Verwaltung von Zitaten und Referenzen
|
||||||
|
- [ ] Visuelles Feedback für Tags und Quellen in der Mindmap
|
||||||
|
|
||||||
|
## Phase 7: Integrationen und Erweiterungen
|
||||||
|
|
||||||
- [ ] Import/Export-Funktionalität für Mindmaps (JSON, PNG)
|
- [ ] Import/Export-Funktionalität für Mindmaps (JSON, PNG)
|
||||||
- [ ] Teilen von Mindmaps (öffentlich/privat/mit bestimmten Benutzern)
|
- [ ] Teilen von Mindmaps (öffentlich/privat/mit bestimmten Benutzern)
|
||||||
@@ -42,7 +65,7 @@ Diese Roadmap beschreibt die geplante Entwicklung der dynamischen, benutzerorien
|
|||||||
- [ ] Verknüpfung mit externen Ressourcen (Links, Dateien)
|
- [ ] Verknüpfung mit externen Ressourcen (Links, Dateien)
|
||||||
- [ ] Versionierung von Mindmaps
|
- [ ] Versionierung von Mindmaps
|
||||||
|
|
||||||
## Phase 6: KI-Integration und Analyse
|
## Phase 8: KI-Integration und Analyse
|
||||||
|
|
||||||
- [ ] KI-gestützte Vorschläge für Verbindungen zwischen Knoten
|
- [ ] KI-gestützte Vorschläge für Verbindungen zwischen Knoten
|
||||||
- [ ] Automatische Kategorisierung von Inhalten
|
- [ ] Automatische Kategorisierung von Inhalten
|
||||||
@@ -50,7 +73,7 @@ Diese Roadmap beschreibt die geplante Entwicklung der dynamischen, benutzerorien
|
|||||||
- [ ] Mindmap-Statistiken und Analysen
|
- [ ] Mindmap-Statistiken und Analysen
|
||||||
- [ ] KI-basierte Zusammenfassung von Teilbereichen der Mindmap
|
- [ ] KI-basierte Zusammenfassung von Teilbereichen der Mindmap
|
||||||
|
|
||||||
## Phase 7: Optimierung und Skalierung
|
## Phase 9: Optimierung und Skalierung
|
||||||
|
|
||||||
- [ ] Performance-Optimierung für große Mindmaps
|
- [ ] Performance-Optimierung für große Mindmaps
|
||||||
- [ ] Verbesserung der Benutzerfreundlichkeit basierend auf Feedback
|
- [ ] Verbesserung der Benutzerfreundlichkeit basierend auf Feedback
|
||||||
@@ -66,6 +89,20 @@ Diese Roadmap beschreibt die geplante Entwicklung der dynamischen, benutzerorien
|
|||||||
- [ ] Caching-Strategien für bessere Performance
|
- [ ] Caching-Strategien für bessere Performance
|
||||||
- [ ] Verbesserte Fehlerbehandlung und Logging
|
- [ ] Verbesserte Fehlerbehandlung und Logging
|
||||||
|
|
||||||
|
## KI-Integration
|
||||||
|
|
||||||
|
### Aktuelle Implementation
|
||||||
|
- Integration von OpenAI mit dem gpt-4o-mini-Modell für den KI-Assistenten
|
||||||
|
- Datenbankzugriff für den KI-Assistenten, um direkt Informationen aus der Datenbank abzufragen
|
||||||
|
- Verbesserte Benutzeroberfläche für den KI-Assistenten mit kontextbezogenen Vorschlägen
|
||||||
|
|
||||||
|
### Zukünftige Verbesserungen
|
||||||
|
- Implementierung von Vektorsuche für präzisere Datenbank-Abfragen durch die KI
|
||||||
|
- Erweiterung der KI-Funktionalität für tiefere Analyse von Zusammenhängen zwischen Gedanken
|
||||||
|
- KI-gestützte Vorschläge für neue Verbindungen zwischen Gedanken basierend auf Inhaltsanalyse
|
||||||
|
- Finetuning des KI-Modells auf die spezifischen Anforderungen der Anwendung
|
||||||
|
- Erweiterung auf multimodale Fähigkeiten (Bild- und Textanalyse)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Implementierungsdetails
|
## Implementierungsdetails
|
||||||
@@ -85,6 +122,7 @@ Das Datenbankschema umfasst folgende Hauptentitäten:
|
|||||||
### Frontend-Technologien
|
### Frontend-Technologien
|
||||||
|
|
||||||
- D3.js für die Visualisierung der Mindmap
|
- D3.js für die Visualisierung der Mindmap
|
||||||
|
- WebGL für den neuronalen Netzwerk-Hintergrund
|
||||||
- AJAX für dynamisches Laden von Daten
|
- AJAX für dynamisches Laden von Daten
|
||||||
- Interaktive Bedienelemente mit JavaScript
|
- Interaktive Bedienelemente mit JavaScript
|
||||||
- Responsive Design mit Tailwind CSS
|
- Responsive Design mit Tailwind CSS
|
||||||
@@ -99,4 +137,36 @@ Die implementierten API-Endpunkte umfassen:
|
|||||||
- `/api/mindmap/<id>/remove_node/<node_id>` - Entfernen eines Knotens
|
- `/api/mindmap/<id>/remove_node/<node_id>` - Entfernen eines Knotens
|
||||||
- `/api/mindmap/<id>/update_node_position` - Aktualisierung von Knotenpositionen
|
- `/api/mindmap/<id>/update_node_position` - Aktualisierung von Knotenpositionen
|
||||||
- `/api/mindmap/<id>/notes` - Verwaltung von Notizen
|
- `/api/mindmap/<id>/notes` - Verwaltung von Notizen
|
||||||
- `/api/nodes/<id>/thoughts` - Abrufen und Hinzufügen von Gedanken zu Knoten
|
- `/api/nodes/<id>/thoughts` - Abrufen und Hinzufügen von Gedanken zu Knoten
|
||||||
|
- `/api/get_dark_mode` - Abrufen der Dark Mode Einstellung
|
||||||
|
|
||||||
|
## Neuronaler Netzwerk-Hintergrund
|
||||||
|
|
||||||
|
Der neue WebGL-basierte Hintergrund bietet:
|
||||||
|
|
||||||
|
- WebGL-basierte Rendering-Engine für optimale Performance
|
||||||
|
- Dynamische Knoten und Verbindungen mit realistischem Verhalten
|
||||||
|
- Clustering von neuronalen Knoten für natürlicheres Erscheinungsbild
|
||||||
|
- Simulation von neuronaler Aktivität und Signalweiterleitung
|
||||||
|
- Anpassbare visuelle Parameter (Helligkeit, Dichte, Geschwindigkeit)
|
||||||
|
- Vollständig responsives Design für alle Bildschirmgrößen
|
||||||
|
|
||||||
|
## Aktuelle Verbesserungen
|
||||||
|
- Tailwind CSS wurde auf CDN-Version aktualisiert (06.06.2024)
|
||||||
|
- Content Security Policy (CSP) für Tailwind CSS CDN und WebGL konfiguriert
|
||||||
|
- Behebung kritischer Fehler in der Mindmap-Knotenvisualisierung (15.06.2024)
|
||||||
|
- Verbesserte Verbindungserkennung zwischen Knoten implementiert
|
||||||
|
- Robuste Fehlerbehandlung für verschiedene API-Datenformate
|
||||||
|
|
||||||
|
## Zukünftige Aufgaben (Q3 2024)
|
||||||
|
- Implementierung des Tagging-Systems für Gedanken
|
||||||
|
- Quellenmanagement für Mindmap-Knoten
|
||||||
|
- Erweiterte Benutzerprofilfunktionen
|
||||||
|
- Verbesserung der mobilen Benutzererfahrung
|
||||||
|
- Integration von Exportfunktionen für Mindmaps
|
||||||
|
|
||||||
|
*Zuletzt aktualisiert: 15.06.2024*
|
||||||
|
|
||||||
|
## [Entfernt] CORS-Unterstützung (flask-cors)
|
||||||
|
- Die flask-cors-Bibliothek und alle zugehörigen Initialisierungen wurden entfernt.
|
||||||
|
- CORS wird nicht mehr unterstützt oder benötigt.
|
||||||
Binary file not shown.
BIN
__pycache__/app.cpython-313.pyc
Normal file
BIN
__pycache__/app.cpython-313.pyc
Normal file
Binary file not shown.
BIN
__pycache__/app.cpython-36.pyc
Normal file
BIN
__pycache__/app.cpython-36.pyc
Normal file
Binary file not shown.
BIN
__pycache__/init_db.cpython-311.pyc
Normal file
BIN
__pycache__/init_db.cpython-311.pyc
Normal file
Binary file not shown.
BIN
__pycache__/init_db.cpython-313.pyc
Normal file
BIN
__pycache__/init_db.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
__pycache__/models.cpython-313.pyc
Normal file
BIN
__pycache__/models.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backup/archiv_0.1.zip
Normal file
BIN
backup/archiv_0.1.zip
Normal file
Binary file not shown.
@@ -1,4 +0,0 @@
|
|||||||
# Netscape HTTP Cookie File
|
|
||||||
# https://curl.se/docs/http-cookies.html
|
|
||||||
# This file was generated by libcurl! Edit at your own risk.
|
|
||||||
|
|
||||||
BIN
database/__pycache__/models.cpython-313.pyc
Normal file
BIN
database/__pycache__/models.cpython-313.pyc
Normal file
Binary file not shown.
BIN
database/systades.V2.db.backup
Normal file
BIN
database/systades.V2.db.backup
Normal file
Binary file not shown.
Binary file not shown.
17
docker-compose.yml
Normal file
17
docker-compose.yml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
version: '3.9'
|
||||||
|
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
build: .
|
||||||
|
image: systades_app:latest
|
||||||
|
container_name: systades_app
|
||||||
|
restart: always
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
ports:
|
||||||
|
- "5000:5000"
|
||||||
|
volumes:
|
||||||
|
- ./database:/app/database
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
db_data:
|
||||||
@@ -2,10 +2,12 @@
|
|||||||
# Kopiere diese Datei zu .env und passe die Werte an
|
# Kopiere diese Datei zu .env und passe die Werte an
|
||||||
|
|
||||||
# Flask
|
# Flask
|
||||||
SECRET_KEY=dein-geheimer-schluessel-hier
|
FLASK_APP=app.py
|
||||||
|
FLASK_ENV=development
|
||||||
|
SECRET_KEY=your-secret-key-replace-in-production
|
||||||
|
|
||||||
# OpenAI API
|
# OpenAI API
|
||||||
OPENAI_API_KEY=sk-dein-openai-api-schluessel-hier
|
|
||||||
|
|
||||||
# Datenbank
|
# Datenbank
|
||||||
# Bei Bedarf kann hier eine andere Datenbank-URL angegeben werden
|
# Bei Bedarf kann hier eine andere Datenbank-URL angegeben werden
|
||||||
|
|||||||
470
init_db.py
Executable file → Normal file
470
init_db.py
Executable file → Normal file
@@ -5,252 +5,240 @@ from app import app, initialize_database, db_path
|
|||||||
from models import db, User, Thought, Comment, MindMapNode, ThoughtRelation, ThoughtRating, RelationType
|
from models import db, User, Thought, Comment, MindMapNode, ThoughtRelation, ThoughtRating, RelationType
|
||||||
from models import Category, UserMindmap, UserMindmapNode, MindmapNote
|
from models import Category, UserMindmap, UserMindmapNode, MindmapNote
|
||||||
import os
|
import os
|
||||||
|
import sqlite3
|
||||||
|
from flask import Flask
|
||||||
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
def init_database():
|
# Erstelle eine temporäre Flask-App, um die Datenbank zu initialisieren
|
||||||
"""Initialisiert die Datenbank mit Beispieldaten."""
|
app = Flask(__name__)
|
||||||
with app.app_context():
|
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///systades.db'
|
||||||
# Datenbank löschen und neu erstellen
|
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||||
if os.path.exists(db_path):
|
db.init_app(app)
|
||||||
os.remove(db_path)
|
|
||||||
|
|
||||||
# Stellen Sie sicher, dass das Verzeichnis existiert
|
|
||||||
os.makedirs(os.path.dirname(db_path), exist_ok=True)
|
|
||||||
|
|
||||||
db.create_all()
|
|
||||||
|
|
||||||
# Admin-Benutzer erstellen
|
|
||||||
admin = User(username='admin', email='admin@example.com', is_admin=True)
|
|
||||||
admin.set_password('admin')
|
|
||||||
db.session.add(admin)
|
|
||||||
|
|
||||||
# Beispiel-Benutzer erstellen
|
|
||||||
user = User(username='user', email='user@example.com')
|
|
||||||
user.set_password('user')
|
|
||||||
db.session.add(user)
|
|
||||||
|
|
||||||
# Commit, um IDs zu generieren
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
# Wissenschaftliche Kategorien erstellen
|
|
||||||
science = Category(name='Wissenschaft', description='Wissenschaftliche Erkenntnisse',
|
|
||||||
color_code='#4CAF50', icon='flask')
|
|
||||||
db.session.add(science)
|
|
||||||
|
|
||||||
philosophy = Category(name='Philosophie', description='Philosophische Theorien und Gedanken',
|
|
||||||
color_code='#9C27B0', icon='lightbulb')
|
|
||||||
db.session.add(philosophy)
|
|
||||||
|
|
||||||
technology = Category(name='Technologie', description='Technologische Entwicklungen',
|
|
||||||
color_code='#FF9800', icon='microchip')
|
|
||||||
db.session.add(technology)
|
|
||||||
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
# Wissenschaftliche Unterkategorien
|
|
||||||
physics = Category(name='Physik', description='Studium der Materie und Energie',
|
|
||||||
color_code='#81C784', icon='atom', parent_id=science.id)
|
|
||||||
biology = Category(name='Biologie', description='Studium lebender Organismen',
|
|
||||||
color_code='#66BB6A', icon='leaf', parent_id=science.id)
|
|
||||||
chemistry = Category(name='Chemie', description='Studium der Stoffe und ihrer Reaktionen',
|
|
||||||
color_code='#A5D6A7', icon='vial', parent_id=science.id)
|
|
||||||
|
|
||||||
db.session.add_all([physics, biology, chemistry])
|
|
||||||
|
|
||||||
# Technologie-Unterkategorien
|
|
||||||
informatics = Category(name='Informatik', description='Studium der Informationsverarbeitung',
|
|
||||||
color_code='#FFB74D', icon='laptop-code', parent_id=technology.id)
|
|
||||||
ai = Category(name='Künstliche Intelligenz', description='Entwicklung intelligenter Systeme',
|
|
||||||
color_code='#FFA726', icon='robot', parent_id=technology.id)
|
|
||||||
|
|
||||||
db.session.add_all([informatics, ai])
|
|
||||||
|
|
||||||
# Philosophie-Unterkategorien
|
|
||||||
ethics = Category(name='Ethik', description='Moralphilosophie und Wertesysteme',
|
|
||||||
color_code='#BA68C8', icon='balance-scale', parent_id=philosophy.id)
|
|
||||||
logic = Category(name='Logik', description='Studie der gültigen Schlussfolgerungen',
|
|
||||||
color_code='#AB47BC', icon='project-diagram', parent_id=philosophy.id)
|
|
||||||
|
|
||||||
db.session.add_all([ethics, logic])
|
|
||||||
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
# Knoten für die öffentliche Mindmap erstellen
|
|
||||||
nodes = {
|
|
||||||
'quantenmechanik': MindMapNode(
|
|
||||||
name='Quantenmechanik',
|
|
||||||
description='Physikalische Theorie zur Beschreibung der Materie auf atomarer Ebene',
|
|
||||||
color_code='#81C784',
|
|
||||||
category_id=physics.id,
|
|
||||||
created_by_id=admin.id
|
|
||||||
),
|
|
||||||
'relativitaetstheorie': MindMapNode(
|
|
||||||
name='Relativitätstheorie',
|
|
||||||
description='Einsteins Theorien zur Raumzeit und Gravitation',
|
|
||||||
color_code='#81C784',
|
|
||||||
category_id=physics.id,
|
|
||||||
created_by_id=admin.id
|
|
||||||
),
|
|
||||||
'genetik': MindMapNode(
|
|
||||||
name='Genetik',
|
|
||||||
description='Wissenschaft der Gene und Vererbung',
|
|
||||||
color_code='#66BB6A',
|
|
||||||
category_id=biology.id,
|
|
||||||
created_by_id=admin.id
|
|
||||||
),
|
|
||||||
'machine_learning': MindMapNode(
|
|
||||||
name='Machine Learning',
|
|
||||||
description='Algorithmen, die aus Daten lernen können',
|
|
||||||
color_code='#FFA726',
|
|
||||||
category_id=ai.id,
|
|
||||||
created_by_id=admin.id
|
|
||||||
),
|
|
||||||
'ki_ethik': MindMapNode(
|
|
||||||
name='KI-Ethik',
|
|
||||||
description='Moralische Implikationen künstlicher Intelligenz',
|
|
||||||
color_code='#BA68C8',
|
|
||||||
category_id=ethics.id,
|
|
||||||
created_by_id=user.id
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
for node in nodes.values():
|
|
||||||
db.session.add(node)
|
|
||||||
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
# Verknüpfungen zwischen Knoten herstellen (Hierarchie)
|
|
||||||
nodes['machine_learning'].parents.append(nodes['ki_ethik'])
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
# Gedanken erstellen
|
|
||||||
thoughts = [
|
|
||||||
{
|
|
||||||
'title': 'Künstliche Intelligenz und Bewusstsein',
|
|
||||||
'content': 'Die Frage nach maschinellem Bewusstsein ist fundamental für die KI-Ethik. Aktuelle KI-Systeme haben kein Bewusstsein, aber fortschrittliche KI könnte in Zukunft Eigenschaften entwickeln, die diesem nahekommen.',
|
|
||||||
'abstract': 'Eine Untersuchung der philosophischen Implikationen von KI-Bewusstsein.',
|
|
||||||
'keywords': 'KI, Bewusstsein, Ethik, Philosophie',
|
|
||||||
'branch': 'Philosophie',
|
|
||||||
'color_code': '#BA68C8',
|
|
||||||
'source_type': 'Markdown',
|
|
||||||
'user_id': user.id,
|
|
||||||
'node': nodes['ki_ethik']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'title': 'Quantenmechanik und Realität',
|
|
||||||
'content': 'Die Kopenhagener Deutung und ihre Auswirkungen auf unser Verständnis der Realität. Quantenmechanik stellt grundlegende Annahmen über Determinismus und Lokalität in Frage.',
|
|
||||||
'abstract': 'Eine Analyse verschiedener Interpretationen der Quantenmechanik.',
|
|
||||||
'keywords': 'Quantenmechanik, Physik, Realität',
|
|
||||||
'branch': 'Physik',
|
|
||||||
'color_code': '#81C784',
|
|
||||||
'source_type': 'PDF',
|
|
||||||
'user_id': admin.id,
|
|
||||||
'node': nodes['quantenmechanik']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'title': 'Deep Learning Fortschritte',
|
|
||||||
'content': 'Die neuesten Fortschritte im Deep Learning haben zu beeindruckenden Ergebnissen in Bereichen wie Computer Vision, Natural Language Processing und Reinforcement Learning geführt.',
|
|
||||||
'abstract': 'Überblick über aktuelle Deep Learning-Techniken und ihre Anwendungen.',
|
|
||||||
'keywords': 'Deep Learning, Neural Networks, AI',
|
|
||||||
'branch': 'Technologie',
|
|
||||||
'color_code': '#FFA726',
|
|
||||||
'source_type': 'Webpage',
|
|
||||||
'user_id': admin.id,
|
|
||||||
'node': nodes['machine_learning']
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
thought_objects = []
|
|
||||||
for t_data in thoughts:
|
|
||||||
node = t_data.pop('node')
|
|
||||||
thought = Thought(**t_data)
|
|
||||||
node.thoughts.append(thought)
|
|
||||||
thought_objects.append(thought)
|
|
||||||
db.session.add(thought)
|
|
||||||
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
# Beziehungen zwischen Gedanken
|
|
||||||
relation = ThoughtRelation(
|
|
||||||
source_id=thought_objects[0].id,
|
|
||||||
target_id=thought_objects[2].id,
|
|
||||||
relation_type=RelationType.INSPIRES,
|
|
||||||
created_by_id=user.id
|
|
||||||
)
|
|
||||||
db.session.add(relation)
|
|
||||||
|
|
||||||
# Bewertungen erstellen
|
|
||||||
rating1 = ThoughtRating(
|
|
||||||
thought_id=thought_objects[0].id,
|
|
||||||
user_id=admin.id,
|
|
||||||
relevance_score=5
|
|
||||||
)
|
|
||||||
rating2 = ThoughtRating(
|
|
||||||
thought_id=thought_objects[2].id,
|
|
||||||
user_id=user.id,
|
|
||||||
relevance_score=4
|
|
||||||
)
|
|
||||||
db.session.add_all([rating1, rating2])
|
|
||||||
|
|
||||||
# Kommentare erstellen
|
|
||||||
for thought in thought_objects:
|
|
||||||
comment = Comment(
|
|
||||||
content=f'Interessante Perspektive zu {thought.title}!',
|
|
||||||
thought_id=thought.id,
|
|
||||||
user_id=admin.id if thought.user_id != admin.id else user.id
|
|
||||||
)
|
|
||||||
db.session.add(comment)
|
|
||||||
|
|
||||||
# Benutzer-Mindmaps erstellen
|
|
||||||
user_mindmap = UserMindmap(
|
|
||||||
name='Meine KI-Forschung',
|
|
||||||
description='Meine persönliche Sammlung zu KI und Ethik',
|
|
||||||
user_id=user.id
|
|
||||||
)
|
|
||||||
db.session.add(user_mindmap)
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
# Knoten zur Benutzer-Mindmap hinzufügen
|
|
||||||
user_mindmap_nodes = [
|
|
||||||
UserMindmapNode(
|
|
||||||
user_mindmap_id=user_mindmap.id,
|
|
||||||
node_id=nodes['machine_learning'].id,
|
|
||||||
x_position=200,
|
|
||||||
y_position=300
|
|
||||||
),
|
|
||||||
UserMindmapNode(
|
|
||||||
user_mindmap_id=user_mindmap.id,
|
|
||||||
node_id=nodes['ki_ethik'].id,
|
|
||||||
x_position=500,
|
|
||||||
y_position=200
|
|
||||||
)
|
|
||||||
]
|
|
||||||
db.session.add_all(user_mindmap_nodes)
|
|
||||||
|
|
||||||
# Private Notizen
|
|
||||||
note = MindmapNote(
|
|
||||||
user_id=user.id,
|
|
||||||
mindmap_id=user_mindmap.id,
|
|
||||||
node_id=nodes['ki_ethik'].id,
|
|
||||||
content="Recherchiere mehr über aktuelle ethische Richtlinien für KI-Entwicklung!",
|
|
||||||
color_code="#FFF59D"
|
|
||||||
)
|
|
||||||
db.session.add(note)
|
|
||||||
|
|
||||||
# Gedanken zu Bookmarks hinzufügen
|
|
||||||
user.bookmarked_thoughts.append(thought_objects[0])
|
|
||||||
admin.bookmarked_thoughts.append(thought_objects[1])
|
|
||||||
|
|
||||||
# Finaler Commit
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
print("Datenbank wurde erfolgreich initialisiert!")
|
|
||||||
|
|
||||||
def init_db():
|
def init_db():
|
||||||
"""Alias für Kompatibilität mit älteren Scripts."""
|
with app.app_context():
|
||||||
init_database()
|
print("Initialisiere Datenbank...")
|
||||||
|
|
||||||
|
# Tabellen erstellen
|
||||||
|
db.create_all()
|
||||||
|
print("Tabellen wurden erstellt.")
|
||||||
|
|
||||||
|
# Standardbenutzer erstellen, falls keine vorhanden sind
|
||||||
|
if User.query.count() == 0:
|
||||||
|
print("Erstelle Standardbenutzer...")
|
||||||
|
create_default_users()
|
||||||
|
|
||||||
|
# Standardkategorien erstellen, falls keine vorhanden sind
|
||||||
|
if Category.query.count() == 0:
|
||||||
|
print("Erstelle Standardkategorien...")
|
||||||
|
create_default_categories()
|
||||||
|
|
||||||
|
# Beispiel-Mindmap erstellen, falls keine Knoten vorhanden sind
|
||||||
|
if MindMapNode.query.count() == 0:
|
||||||
|
print("Erstelle Beispiel-Mindmap...")
|
||||||
|
create_sample_mindmap()
|
||||||
|
|
||||||
|
print("Datenbankinitialisierung abgeschlossen.")
|
||||||
|
|
||||||
|
def create_default_users():
|
||||||
|
"""Erstellt Standardbenutzer für die Anwendung"""
|
||||||
|
users = [
|
||||||
|
{
|
||||||
|
'username': 'admin',
|
||||||
|
'email': 'admin@example.com',
|
||||||
|
'password': 'admin',
|
||||||
|
'role': 'admin'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'username': 'user',
|
||||||
|
'email': 'user@example.com',
|
||||||
|
'password': 'user',
|
||||||
|
'role': 'user'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
for user_data in users:
|
||||||
|
password = user_data.pop('password')
|
||||||
|
user = User(**user_data)
|
||||||
|
user.set_password(password)
|
||||||
|
db.session.add(user)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
print(f"{len(users)} Benutzer wurden erstellt.")
|
||||||
|
|
||||||
|
def create_default_categories():
|
||||||
|
"""Erstellt die Standardkategorien für die Mindmap"""
|
||||||
|
categories = [
|
||||||
|
{
|
||||||
|
'name': 'Konzept',
|
||||||
|
'description': 'Abstrakte Ideen und theoretische Konzepte',
|
||||||
|
'color_code': '#6366f1',
|
||||||
|
'icon': 'lightbulb'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Technologie',
|
||||||
|
'description': 'Hardware, Software, Tools und Plattformen',
|
||||||
|
'color_code': '#10b981',
|
||||||
|
'icon': 'cpu'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Prozess',
|
||||||
|
'description': 'Workflows, Methodologien und Vorgehensweisen',
|
||||||
|
'color_code': '#f59e0b',
|
||||||
|
'icon': 'git-branch'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Person',
|
||||||
|
'description': 'Personen, Teams und Organisationen',
|
||||||
|
'color_code': '#ec4899',
|
||||||
|
'icon': 'user'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Dokument',
|
||||||
|
'description': 'Dokumentationen, Referenzen und Ressourcen',
|
||||||
|
'color_code': '#3b82f6',
|
||||||
|
'icon': 'file-text'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
for cat_data in categories:
|
||||||
|
category = Category(**cat_data)
|
||||||
|
db.session.add(category)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
print(f"{len(categories)} Kategorien wurden erstellt.")
|
||||||
|
|
||||||
|
def create_sample_mindmap():
|
||||||
|
"""Erstellt eine Beispiel-Mindmap mit Knoten und Beziehungen"""
|
||||||
|
|
||||||
|
# Kategorien für die Zuordnung
|
||||||
|
categories = Category.query.all()
|
||||||
|
category_map = {cat.name: cat for cat in categories}
|
||||||
|
|
||||||
|
# Beispielknoten erstellen
|
||||||
|
nodes = [
|
||||||
|
{
|
||||||
|
'name': 'Wissensmanagement',
|
||||||
|
'description': 'Systematische Erfassung, Speicherung und Nutzung von Wissen in Organisationen.',
|
||||||
|
'color_code': '#6366f1',
|
||||||
|
'icon': 'database',
|
||||||
|
'category': category_map.get('Konzept'),
|
||||||
|
'x': 0,
|
||||||
|
'y': 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Mind-Mapping',
|
||||||
|
'description': 'Technik zur visuellen Darstellung von Informationen und Zusammenhängen.',
|
||||||
|
'color_code': '#10b981',
|
||||||
|
'icon': 'git-branch',
|
||||||
|
'category': category_map.get('Prozess'),
|
||||||
|
'x': 200,
|
||||||
|
'y': -150
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Cytoscape.js',
|
||||||
|
'description': 'JavaScript-Bibliothek für die Visualisierung und Manipulation von Graphen.',
|
||||||
|
'color_code': '#3b82f6',
|
||||||
|
'icon': 'code',
|
||||||
|
'category': category_map.get('Technologie'),
|
||||||
|
'x': 350,
|
||||||
|
'y': -50
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Socket.IO',
|
||||||
|
'description': 'Bibliothek für Echtzeit-Kommunikation zwischen Client und Server.',
|
||||||
|
'color_code': '#3b82f6',
|
||||||
|
'icon': 'zap',
|
||||||
|
'category': category_map.get('Technologie'),
|
||||||
|
'x': 350,
|
||||||
|
'y': 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Kollaboration',
|
||||||
|
'description': 'Zusammenarbeit mehrerer Benutzer an gemeinsamen Inhalten.',
|
||||||
|
'color_code': '#f59e0b',
|
||||||
|
'icon': 'users',
|
||||||
|
'category': category_map.get('Prozess'),
|
||||||
|
'x': 200,
|
||||||
|
'y': 150
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'SQLite',
|
||||||
|
'description': 'Leichtgewichtige relationale Datenbank, die ohne Server-Prozess auskommt.',
|
||||||
|
'color_code': '#3b82f6',
|
||||||
|
'icon': 'database',
|
||||||
|
'category': category_map.get('Technologie'),
|
||||||
|
'x': 0,
|
||||||
|
'y': 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Flask',
|
||||||
|
'description': 'Leichtgewichtiges Python-Webframework für die Entwicklung von Webanwendungen.',
|
||||||
|
'color_code': '#3b82f6',
|
||||||
|
'icon': 'server',
|
||||||
|
'category': category_map.get('Technologie'),
|
||||||
|
'x': -200,
|
||||||
|
'y': 150
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'REST API',
|
||||||
|
'description': 'Architekturstil für verteilte Systeme, insbesondere Webanwendungen.',
|
||||||
|
'color_code': '#10b981',
|
||||||
|
'icon': 'link',
|
||||||
|
'category': category_map.get('Konzept'),
|
||||||
|
'x': -200,
|
||||||
|
'y': -150
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Dokumentation',
|
||||||
|
'description': 'Strukturierte Erfassung und Beschreibung von Informationen und Prozessen.',
|
||||||
|
'color_code': '#ec4899',
|
||||||
|
'icon': 'file-text',
|
||||||
|
'category': category_map.get('Dokument'),
|
||||||
|
'x': -350,
|
||||||
|
'y': 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
# Knoten in die Datenbank einfügen
|
||||||
|
node_objects = {}
|
||||||
|
for node_data in nodes:
|
||||||
|
category = node_data.pop('category', None)
|
||||||
|
x = node_data.pop('x', 0)
|
||||||
|
y = node_data.pop('y', 0)
|
||||||
|
node = MindMapNode(**node_data)
|
||||||
|
if category:
|
||||||
|
node.category_id = category.id
|
||||||
|
db.session.add(node)
|
||||||
|
db.session.flush() # Generiert IDs für neue Objekte
|
||||||
|
node_objects[node.name] = node
|
||||||
|
|
||||||
|
# Beziehungen erstellen
|
||||||
|
relationships = [
|
||||||
|
('Wissensmanagement', 'Mind-Mapping'),
|
||||||
|
('Wissensmanagement', 'Kollaboration'),
|
||||||
|
('Wissensmanagement', 'Dokumentation'),
|
||||||
|
('Mind-Mapping', 'Cytoscape.js'),
|
||||||
|
('Kollaboration', 'Socket.IO'),
|
||||||
|
('Wissensmanagement', 'SQLite'),
|
||||||
|
('SQLite', 'Flask'),
|
||||||
|
('Flask', 'REST API'),
|
||||||
|
('REST API', 'Socket.IO'),
|
||||||
|
('REST API', 'Dokumentation')
|
||||||
|
]
|
||||||
|
|
||||||
|
for parent_name, child_name in relationships:
|
||||||
|
parent = node_objects.get(parent_name)
|
||||||
|
child = node_objects.get(child_name)
|
||||||
|
if parent and child:
|
||||||
|
parent.children.append(child)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
print(f"{len(nodes)} Knoten und {len(relationships)} Beziehungen wurden erstellt.")
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
init_database()
|
init_db()
|
||||||
print("Datenbank wurde erfolgreich initialisiert!")
|
print("Datenbank wurde erfolgreich initialisiert!")
|
||||||
print("Sie können die Anwendung jetzt mit 'python app.py' starten")
|
print("Sie können die Anwendung jetzt mit 'python app.py' starten")
|
||||||
print("Anmelden mit:")
|
print("Anmelden mit:")
|
||||||
|
|||||||
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 ###
|
||||||
149
models.py
Executable file → Normal file
149
models.py
Executable file → Normal file
@@ -6,6 +6,8 @@ from flask_login import UserMixin
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from werkzeug.security import generate_password_hash, check_password_hash
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
import uuid as uuid_pkg
|
||||||
|
import os
|
||||||
|
|
||||||
db = SQLAlchemy()
|
db = SQLAlchemy()
|
||||||
|
|
||||||
@@ -43,30 +45,28 @@ user_thought_bookmark = db.Table('user_thought_bookmark',
|
|||||||
db.Column('created_at', db.DateTime, default=datetime.utcnow)
|
db.Column('created_at', db.DateTime, default=datetime.utcnow)
|
||||||
)
|
)
|
||||||
|
|
||||||
class User(UserMixin, db.Model):
|
class User(db.Model, UserMixin):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
username = db.Column(db.String(80), unique=True, nullable=False)
|
username = db.Column(db.String(80), unique=True, nullable=False)
|
||||||
email = db.Column(db.String(120), unique=True, nullable=False)
|
email = db.Column(db.String(120), unique=True, nullable=False)
|
||||||
password_hash = db.Column(db.String(128))
|
password = db.Column(db.String(512), nullable=False)
|
||||||
is_admin = db.Column(db.Boolean, default=False)
|
|
||||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
last_login = db.Column(db.DateTime)
|
is_active = db.Column(db.Boolean, default=True)
|
||||||
avatar = db.Column(db.String(200))
|
role = db.Column(db.String(20), default="user") # 'user', 'admin', 'moderator'
|
||||||
bio = db.Column(db.Text)
|
|
||||||
|
|
||||||
# Beziehungen
|
# Relationships
|
||||||
thoughts = db.relationship('Thought', backref='author', lazy=True)
|
threads = db.relationship('Thread', backref='creator', lazy=True)
|
||||||
comments = db.relationship('Comment', backref='author', lazy=True)
|
messages = db.relationship('Message', backref='author', lazy=True)
|
||||||
user_mindmaps = db.relationship('UserMindmap', backref='user', lazy=True)
|
projects = db.relationship('Project', backref='owner', lazy=True)
|
||||||
mindmap_notes = db.relationship('MindmapNote', backref='user', lazy=True)
|
|
||||||
bookmarked_thoughts = db.relationship('Thought', secondary=user_thought_bookmark,
|
|
||||||
backref=db.backref('bookmarked_by', lazy='dynamic'))
|
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<User {self.username}>'
|
||||||
|
|
||||||
def set_password(self, password):
|
def set_password(self, password):
|
||||||
self.password_hash = generate_password_hash(password)
|
self.password = generate_password_hash(password)
|
||||||
|
|
||||||
def check_password(self, password):
|
def check_password(self, password):
|
||||||
return check_password_hash(self.password_hash, password)
|
return check_password_hash(self.password, password)
|
||||||
|
|
||||||
class Category(db.Model):
|
class Category(db.Model):
|
||||||
"""Wissenschaftliche Kategorien für die Gliederung der öffentlichen Mindmap"""
|
"""Wissenschaftliche Kategorien für die Gliederung der öffentlichen Mindmap"""
|
||||||
@@ -81,6 +81,9 @@ class Category(db.Model):
|
|||||||
children = db.relationship('Category', backref=db.backref('parent', remote_side=[id]))
|
children = db.relationship('Category', backref=db.backref('parent', remote_side=[id]))
|
||||||
nodes = db.relationship('MindMapNode', backref='category', lazy=True)
|
nodes = db.relationship('MindMapNode', backref='category', lazy=True)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<Category {self.name}>'
|
||||||
|
|
||||||
class MindMapNode(db.Model):
|
class MindMapNode(db.Model):
|
||||||
"""Öffentliche Mindmap-Knoten, die für alle Benutzer sichtbar sind"""
|
"""Öffentliche Mindmap-Knoten, die für alle Benutzer sichtbar sind"""
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
@@ -92,7 +95,9 @@ class MindMapNode(db.Model):
|
|||||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
created_by_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True)
|
created_by_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True)
|
||||||
category_id = db.Column(db.Integer, db.ForeignKey('category.id'), nullable=True)
|
category_id = db.Column(db.Integer, db.ForeignKey('category.id'), nullable=True)
|
||||||
|
|
||||||
|
__table_args__ = {'extend_existing': True}
|
||||||
|
|
||||||
# Beziehungen für Baumstruktur (mehrere Eltern möglich)
|
# Beziehungen für Baumstruktur (mehrere Eltern möglich)
|
||||||
parents = db.relationship(
|
parents = db.relationship(
|
||||||
'MindMapNode',
|
'MindMapNode',
|
||||||
@@ -111,6 +116,20 @@ class MindMapNode(db.Model):
|
|||||||
# Beziehung zum Ersteller
|
# Beziehung zum Ersteller
|
||||||
created_by = db.relationship('User', backref='created_nodes')
|
created_by = db.relationship('User', backref='created_nodes')
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<MindMapNode {self.name}>'
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
'id': self.id,
|
||||||
|
'name': self.name,
|
||||||
|
'description': self.description,
|
||||||
|
'color_code': self.color_code,
|
||||||
|
'icon': self.icon,
|
||||||
|
'category_id': self.category_id,
|
||||||
|
'created_at': self.created_at.isoformat() if self.created_at else None
|
||||||
|
}
|
||||||
|
|
||||||
class UserMindmap(db.Model):
|
class UserMindmap(db.Model):
|
||||||
"""Benutzerspezifische Mindmap, die vom Benutzer personalisierbar ist"""
|
"""Benutzerspezifische Mindmap, die vom Benutzer personalisierbar ist"""
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
@@ -227,4 +246,100 @@ class Comment(db.Model):
|
|||||||
thought_id = db.Column(db.Integer, db.ForeignKey('thought.id'), nullable=False)
|
thought_id = db.Column(db.Integer, db.ForeignKey('thought.id'), nullable=False)
|
||||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
last_modified = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
last_modified = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||||
|
|
||||||
|
# Thread model
|
||||||
|
class Thread(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
title = db.Column(db.String(200), nullable=False)
|
||||||
|
description = db.Column(db.Text)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
messages = db.relationship('Message', backref='thread', lazy=True, cascade="all, delete-orphan")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<Thread {self.title}>'
|
||||||
|
|
||||||
|
# Message model
|
||||||
|
class Message(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
content = db.Column(db.Text, nullable=False)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||||
|
thread_id = db.Column(db.Integer, db.ForeignKey('thread.id'), nullable=False)
|
||||||
|
role = db.Column(db.String(20), default="user") # 'user', 'assistant', 'system'
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<Message {self.id} by {self.user_id}>'
|
||||||
|
|
||||||
|
# Project model
|
||||||
|
class Project(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
title = db.Column(db.String(150), nullable=False)
|
||||||
|
description = db.Column(db.Text)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||||
|
category_id = db.Column(db.Integer, db.ForeignKey('category.id'))
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
documents = db.relationship('Document', backref='project', lazy=True, cascade="all, delete-orphan")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<Project {self.title}>'
|
||||||
|
|
||||||
|
# Document model
|
||||||
|
class Document(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
title = db.Column(db.String(150), nullable=False)
|
||||||
|
content = db.Column(db.Text)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||||
|
project_id = db.Column(db.Integer, db.ForeignKey('project.id'), nullable=False)
|
||||||
|
filename = db.Column(db.String(150), nullable=True)
|
||||||
|
file_path = db.Column(db.String(300), nullable=True)
|
||||||
|
file_type = db.Column(db.String(50), nullable=True)
|
||||||
|
file_size = db.Column(db.Integer, nullable=True)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<Document {self.title}>'
|
||||||
|
|
||||||
|
# Forum-Kategorie-Modell - entspricht den Hauptknotenpunkten der Mindmap
|
||||||
|
class ForumCategory(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
node_id = db.Column(db.Integer, db.ForeignKey('mind_map_node.id'), nullable=False)
|
||||||
|
title = db.Column(db.String(200), nullable=False)
|
||||||
|
description = db.Column(db.Text)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
is_active = db.Column(db.Boolean, default=True)
|
||||||
|
|
||||||
|
# Beziehungen
|
||||||
|
node = db.relationship('MindMapNode', backref='forum_category')
|
||||||
|
posts = db.relationship('ForumPost', backref='category', lazy=True, cascade="all, delete-orphan")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<ForumCategory {self.title}>'
|
||||||
|
|
||||||
|
# Forum-Beitrag-Modell
|
||||||
|
class ForumPost(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
title = db.Column(db.String(200), nullable=False)
|
||||||
|
content = db.Column(db.Text, nullable=False)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||||
|
category_id = db.Column(db.Integer, db.ForeignKey('forum_category.id'), nullable=False)
|
||||||
|
parent_id = db.Column(db.Integer, db.ForeignKey('forum_post.id'), nullable=True)
|
||||||
|
is_pinned = db.Column(db.Boolean, default=False)
|
||||||
|
is_locked = db.Column(db.Boolean, default=False)
|
||||||
|
view_count = db.Column(db.Integer, default=0)
|
||||||
|
|
||||||
|
# Beziehungen
|
||||||
|
author = db.relationship('User', backref='forum_posts')
|
||||||
|
replies = db.relationship('ForumPost', backref=db.backref('parent', remote_side=[id]), lazy=True)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<ForumPost {self.title}>'
|
||||||
@@ -5,10 +5,13 @@ email-validator
|
|||||||
python-dotenv
|
python-dotenv
|
||||||
werkzeug==2.2.3
|
werkzeug==2.2.3
|
||||||
flask-sqlalchemy==3.0.5
|
flask-sqlalchemy==3.0.5
|
||||||
openai==1.3.0
|
openai
|
||||||
requests==2.31.0
|
requests==2.31.0
|
||||||
flask-cors==4.0.0
|
|
||||||
gunicorn==21.2.0
|
gunicorn==21.2.0
|
||||||
#pillow==10.0.1
|
#pillow==10.0.1
|
||||||
pytest==7.4.0
|
pytest==7.4.0
|
||||||
pytest-flask==1.2.0
|
pytest-flask==1.2.0
|
||||||
|
Flask-Migrate
|
||||||
|
flask-socketio==5.3.6
|
||||||
|
python-engineio==4.8.2
|
||||||
|
python-socketio==5.11.1
|
||||||
53
start.sh
Normal file
53
start.sh
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
#!/usr/bin/env powershell
|
||||||
|
# Windows PowerShell-Version des Start-Skripts
|
||||||
|
# Datum: 01.05.2025
|
||||||
|
|
||||||
|
# Docker-Status prüfen
|
||||||
|
Write-Host "Prüfe Docker-Status..." -ForegroundColor Cyan
|
||||||
|
try {
|
||||||
|
$status = docker ps -q
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Host "Docker ist nicht gestartet. Bitte starten Sie Docker Desktop." -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Write-Host "Docker ist nicht verfügbar. Bitte installieren Sie Docker Desktop und starten Sie es." -ForegroundColor Red
|
||||||
|
Write-Host $_.Exception.Message
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Alte Container stoppen und entfernen
|
||||||
|
$containerExists = docker ps -a --filter "name=systades_app" -q
|
||||||
|
if ($containerExists) {
|
||||||
|
Write-Host "Stoppe und entferne alten Container..." -ForegroundColor Yellow
|
||||||
|
docker rm -f systades_app
|
||||||
|
}
|
||||||
|
|
||||||
|
# Alte Images löschen
|
||||||
|
Write-Host "Entferne altes Image..." -ForegroundColor Yellow
|
||||||
|
docker rmi -f systades_app:latest
|
||||||
|
|
||||||
|
# Stelle sicher, dass das Datenbankverzeichnis existiert
|
||||||
|
if (-not (Test-Path "database")) {
|
||||||
|
New-Item -Path "database" -ItemType Directory -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
# Docker-Compose Setup neu bauen
|
||||||
|
Write-Host "Baue Container neu..." -ForegroundColor Green
|
||||||
|
docker-compose build --no-cache
|
||||||
|
|
||||||
|
# Docker-Compose neu starten
|
||||||
|
Write-Host "Starte Container..." -ForegroundColor Green
|
||||||
|
docker-compose up -d --force-recreate
|
||||||
|
|
||||||
|
# Warte kurz und prüfe, ob der Container läuft
|
||||||
|
Write-Host "Prüfe Container-Status..." -ForegroundColor Cyan
|
||||||
|
Start-Sleep -Seconds 3
|
||||||
|
docker ps | Select-String "systades_app"
|
||||||
|
|
||||||
|
# Ausgabe
|
||||||
|
Write-Host "`nSystemstatus:" -ForegroundColor Cyan
|
||||||
|
Write-Host "----------------------------------------"
|
||||||
|
Write-Host "Systades-Anwendung ist jetzt unter http://localhost:5000 erreichbar." -ForegroundColor Green
|
||||||
|
Write-Host "Container-Logs können mit 'docker logs -f systades_app' angezeigt werden." -ForegroundColor Green
|
||||||
|
Write-Host "----------------------------------------"
|
||||||
1
static/541AABD7-4E37-44ED-B491-1459C8C19699.PNG
Normal file
1
static/541AABD7-4E37-44ED-B491-1459C8C19699.PNG
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
// Background animation with Three.js
|
|
||||||
let scene, camera, renderer, stars = [];
|
|
||||||
|
|
||||||
function initBackground() {
|
|
||||||
// Setup scene
|
|
||||||
scene = new THREE.Scene();
|
|
||||||
|
|
||||||
// Setup camera
|
|
||||||
camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 1000);
|
|
||||||
camera.position.z = 100;
|
|
||||||
|
|
||||||
// Setup renderer
|
|
||||||
renderer = new THREE.WebGLRenderer({ alpha: true });
|
|
||||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
||||||
renderer.setClearColor(0x000000, 0); // Transparent background
|
|
||||||
|
|
||||||
// Append renderer to DOM
|
|
||||||
const backgroundContainer = document.getElementById('background-container');
|
|
||||||
if (backgroundContainer) {
|
|
||||||
backgroundContainer.appendChild(renderer.domElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add stars
|
|
||||||
for (let i = 0; i < 1000; i++) {
|
|
||||||
const geometry = new THREE.SphereGeometry(0.2, 8, 8);
|
|
||||||
const material = new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true, opacity: Math.random() * 0.5 + 0.1 });
|
|
||||||
const star = new THREE.Mesh(geometry, material);
|
|
||||||
|
|
||||||
// Random position
|
|
||||||
star.position.x = Math.random() * 600 - 300;
|
|
||||||
star.position.y = Math.random() * 600 - 300;
|
|
||||||
star.position.z = Math.random() * 600 - 300;
|
|
||||||
|
|
||||||
// Store reference to move in animation
|
|
||||||
star.velocity = Math.random() * 0.02 + 0.005;
|
|
||||||
stars.push(star);
|
|
||||||
|
|
||||||
scene.add(star);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add large glowing particles
|
|
||||||
for (let i = 0; i < 15; i++) {
|
|
||||||
const size = Math.random() * 5 + 2;
|
|
||||||
const geometry = new THREE.SphereGeometry(size, 16, 16);
|
|
||||||
|
|
||||||
// Create a glowing material
|
|
||||||
const color = new THREE.Color();
|
|
||||||
color.setHSL(Math.random(), 0.7, 0.5); // Random hue
|
|
||||||
|
|
||||||
const material = new THREE.MeshBasicMaterial({
|
|
||||||
color: color,
|
|
||||||
transparent: true,
|
|
||||||
opacity: 0.2
|
|
||||||
});
|
|
||||||
|
|
||||||
const particle = new THREE.Mesh(geometry, material);
|
|
||||||
|
|
||||||
// Random position but further away
|
|
||||||
particle.position.x = Math.random() * 1000 - 500;
|
|
||||||
particle.position.y = Math.random() * 1000 - 500;
|
|
||||||
particle.position.z = Math.random() * 200 - 400;
|
|
||||||
|
|
||||||
// Store reference to move in animation
|
|
||||||
particle.velocity = Math.random() * 0.01 + 0.002;
|
|
||||||
stars.push(particle);
|
|
||||||
|
|
||||||
scene.add(particle);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle window resize
|
|
||||||
window.addEventListener('resize', onWindowResize);
|
|
||||||
|
|
||||||
// Start animation
|
|
||||||
animate();
|
|
||||||
}
|
|
||||||
|
|
||||||
function animate() {
|
|
||||||
requestAnimationFrame(animate);
|
|
||||||
|
|
||||||
// Move stars
|
|
||||||
stars.forEach(star => {
|
|
||||||
star.position.z += star.velocity;
|
|
||||||
|
|
||||||
// Reset position if star moves too close
|
|
||||||
if (star.position.z > 100) {
|
|
||||||
star.position.z = -300;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Rotate the entire scene slightly for a dreamy effect
|
|
||||||
scene.rotation.y += 0.0003;
|
|
||||||
scene.rotation.x += 0.0001;
|
|
||||||
|
|
||||||
renderer.render(scene, camera);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onWindowResize() {
|
|
||||||
camera.aspect = window.innerWidth / window.innerHeight;
|
|
||||||
camera.updateProjectionMatrix();
|
|
||||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize background when the DOM is loaded
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', initBackground);
|
|
||||||
} else {
|
|
||||||
initBackground();
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
C:\Users\firem\Downloads\background.mp4
|
|
||||||
27
static/css/all.min.css
vendored
Normal file
27
static/css/all.min.css
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* Font Awesome 6.4.0
|
||||||
|
*
|
||||||
|
* This is a placeholder file. For production, you should:
|
||||||
|
* 1. Download Font Awesome from https://fontawesome.com/download
|
||||||
|
* 2. Extract the downloaded package
|
||||||
|
* 3. Copy the 'css/all.min.css' file to this location
|
||||||
|
* 4. Copy the 'webfonts' folder to '/static/webfonts/'
|
||||||
|
*
|
||||||
|
* Alternatively, you can install via npm and copy the files:
|
||||||
|
* npm install @fortawesome/fontawesome-free
|
||||||
|
* cp -r node_modules/@fortawesome/fontawesome-free/css/all.min.css static/css/
|
||||||
|
* cp -r node_modules/@fortawesome/fontawesome-free/webfonts/ static/
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Placeholder styles for common Font Awesome icons */
|
||||||
|
.fa, .fas, .far, .fab {
|
||||||
|
display: inline-block;
|
||||||
|
width: 1em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Warning message */
|
||||||
|
body::before {
|
||||||
|
content: "Font Awesome CSS placeholder. Please replace with the actual file.";
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
@@ -1,21 +1,25 @@
|
|||||||
/* ChatGPT Assistent Styles */
|
/* ChatGPT Assistent Styles - Verbesserte Version */
|
||||||
#chatgpt-assistant {
|
#chatgpt-assistant {
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: 'Inter', sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
#assistant-chat {
|
#assistant-chat {
|
||||||
transition: max-height 0.3s ease, opacity 0.3s ease;
|
transition: max-height 0.4s cubic-bezier(0.25, 0.1, 0.25, 1),
|
||||||
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.2);
|
opacity 0.3s cubic-bezier(0.25, 0.1, 0.25, 1);
|
||||||
|
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.25);
|
||||||
border-radius: 0.75rem;
|
border-radius: 0.75rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
max-width: calc(100vw - 2rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
#assistant-toggle {
|
#assistant-toggle {
|
||||||
transition: transform 0.3s ease;
|
transition: transform 0.3s ease, background-color 0.2s ease;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
z-index: 60;
|
||||||
}
|
}
|
||||||
|
|
||||||
#assistant-toggle:hover {
|
#assistant-toggle:hover {
|
||||||
transform: scale(1.1);
|
transform: scale(1.1) rotate(10deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
#assistant-history {
|
#assistant-history {
|
||||||
@@ -40,27 +44,74 @@
|
|||||||
background-color: rgba(156, 163, 175, 0.3);
|
background-color: rgba(156, 163, 175, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Verbesserte Message-Bubbles mit Schatten und Animation */
|
||||||
|
#assistant-history .flex > div {
|
||||||
|
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
|
||||||
|
animation: messageAppear 0.3s ease-out forwards;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes messageAppear {
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Verzögerte Animation für Messages */
|
||||||
|
#assistant-history .flex:nth-child(1) > div { animation-delay: 0.05s; }
|
||||||
|
#assistant-history .flex:nth-child(2) > div { animation-delay: 0.1s; }
|
||||||
|
#assistant-history .flex:nth-child(3) > div { animation-delay: 0.15s; }
|
||||||
|
#assistant-history .flex:nth-child(4) > div { animation-delay: 0.2s; }
|
||||||
|
#assistant-history .flex:nth-child(5) > div { animation-delay: 0.25s; }
|
||||||
|
|
||||||
|
/* Vorschläge styling */
|
||||||
|
#assistant-suggestions {
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestion-pill {
|
||||||
|
animation: pillAppear 0.4s ease forwards;
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.9);
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pillAppear {
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Styling für verschiedene Verzögerungen bei Vorschlägen */
|
||||||
|
#assistant-suggestions button:nth-child(1) { animation-delay: 0.1s; }
|
||||||
|
#assistant-suggestions button:nth-child(2) { animation-delay: 0.2s; }
|
||||||
|
#assistant-suggestions button:nth-child(3) { animation-delay: 0.3s; }
|
||||||
|
|
||||||
/* Mach Platz für Notifications, damit sie nicht mit dem Assistenten überlappen */
|
/* Mach Platz für Notifications, damit sie nicht mit dem Assistenten überlappen */
|
||||||
.notification-area {
|
.notification-area {
|
||||||
bottom: 5rem;
|
bottom: 5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Verbesserter Glassmorphism-Effekt */
|
/* Verbesserte Glassmorphism-Effekt */
|
||||||
.glass-morphism {
|
.glass-morphism {
|
||||||
background: rgba(255, 255, 255, 0.2);
|
background: rgba(255, 255, 255, 0.25);
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(12px);
|
||||||
-webkit-backdrop-filter: blur(10px);
|
-webkit-backdrop-filter: blur(12px);
|
||||||
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.15);
|
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.15);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .glass-morphism {
|
.dark .glass-morphism {
|
||||||
background: rgba(15, 23, 42, 0.3);
|
background: rgba(15, 23, 42, 0.35);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.4);
|
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dunkleres Dark Theme */
|
/* Verbesserte Farbpalette für Dark Theme */
|
||||||
.dark {
|
.dark {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgba(10, 15, 25, var(--tw-bg-opacity)) !important;
|
background-color: rgba(10, 15, 25, var(--tw-bg-opacity)) !important;
|
||||||
@@ -82,6 +133,54 @@
|
|||||||
background-color: rgba(23, 33, 64, var(--tw-bg-opacity)) !important;
|
background-color: rgba(23, 33, 64, var(--tw-bg-opacity)) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Typing Indicator Animation Styles */
|
||||||
|
.typing-indicator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-indicator span {
|
||||||
|
height: 8px;
|
||||||
|
width: 8px;
|
||||||
|
background-color: #888;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0 2px;
|
||||||
|
opacity: 0.4;
|
||||||
|
animation: bounce 1.4s infinite ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-indicator span:nth-child(1) { animation-delay: 0s; }
|
||||||
|
.typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
|
||||||
|
.typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
|
||||||
|
|
||||||
|
@keyframes bounce {
|
||||||
|
0%, 80%, 100% { transform: translateY(0); }
|
||||||
|
40% { transform: translateY(-8px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chat Input Fokus-Effekt */
|
||||||
|
#assistant-chat input:focus {
|
||||||
|
border-color: var(--primary-500, #3B82F6);
|
||||||
|
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark #assistant-chat input:focus {
|
||||||
|
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Verbesserte Responsive Layouts */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
#assistant-chat {
|
||||||
|
width: calc(100vw - 2rem) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chatgpt-assistant {
|
||||||
|
right: 1rem;
|
||||||
|
bottom: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Footer immer unten */
|
/* Footer immer unten */
|
||||||
html, body {
|
html, body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
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;
|
||||||
|
}
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
/* Cybertechnisches Netzwerk Hintergrund-Overlay */
|
|
||||||
.cyber-network-bg {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: -1;
|
|
||||||
pointer-events: none;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cyber-network-bg::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: linear-gradient(125deg,
|
|
||||||
rgba(14, 14, 22, 0.95) 0%,
|
|
||||||
rgba(30, 30, 46, 0.98) 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.network-grid {
|
|
||||||
position: absolute;
|
|
||||||
width: 200%;
|
|
||||||
height: 200%;
|
|
||||||
top: -50%;
|
|
||||||
left: -50%;
|
|
||||||
background-size: 40px 40px;
|
|
||||||
background-image:
|
|
||||||
linear-gradient(to right, rgba(108, 93, 211, 0.05) 1px, transparent 1px),
|
|
||||||
linear-gradient(to bottom, rgba(108, 93, 211, 0.05) 1px, transparent 1px);
|
|
||||||
transform: perspective(500px) rotateX(60deg);
|
|
||||||
animation: grid-move 20s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.node {
|
|
||||||
position: absolute;
|
|
||||||
width: 4px;
|
|
||||||
height: 4px;
|
|
||||||
background: rgba(76, 223, 255, 0.8);
|
|
||||||
border-radius: 50%;
|
|
||||||
box-shadow: 0 0 10px rgba(76, 223, 255, 0.6);
|
|
||||||
filter: blur(1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.connection {
|
|
||||||
position: absolute;
|
|
||||||
height: 1px;
|
|
||||||
background: linear-gradient(90deg,
|
|
||||||
rgba(76, 223, 255, 0.2) 0%,
|
|
||||||
rgba(108, 93, 211, 0.3) 50%,
|
|
||||||
rgba(76, 223, 255, 0.2) 100%);
|
|
||||||
transform-origin: left center;
|
|
||||||
animation: pulse 4s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.data-packet {
|
|
||||||
position: absolute;
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: rgba(118, 69, 217, 0.8);
|
|
||||||
filter: blur(1px);
|
|
||||||
animation: travel var(--travel-time, 6s) linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.glow-overlay {
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: radial-gradient(
|
|
||||||
circle at 50% 40%,
|
|
||||||
rgba(76, 223, 255, 0.03) 0%,
|
|
||||||
rgba(108, 93, 211, 0.03) 45%,
|
|
||||||
transparent 70%
|
|
||||||
);
|
|
||||||
opacity: 0.8;
|
|
||||||
animation: pulse-glow 8s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Animations */
|
|
||||||
@keyframes grid-move {
|
|
||||||
0% {
|
|
||||||
transform: perspective(500px) rotateX(60deg) translateY(0);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: perspective(500px) rotateX(60deg) translateY(40px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse {
|
|
||||||
0%, 100% {
|
|
||||||
opacity: 0.2;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
opacity: 0.4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes travel {
|
|
||||||
0% {
|
|
||||||
transform: translateX(0) translateY(0);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
10% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
90% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: translateX(var(--travel-x, 100px)) translateY(var(--travel-y, 100px));
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse-glow {
|
|
||||||
0%, 100% {
|
|
||||||
opacity: 0.4;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1434,16 +1434,211 @@ html, body {
|
|||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
background: linear-gradient(135deg, var(--background-start), var(--background-end));
|
background: linear-gradient(135deg, var(--background-start), var(--background-end));
|
||||||
background-attachment: fixed;
|
background-attachment: fixed;
|
||||||
scroll-behavior: smooth;
|
|
||||||
height: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Sticky navbar */
|
/* Sticky navbar */
|
||||||
.navbar.sticky-top {
|
.navbar.sticky-top {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 50;
|
z-index: 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Importiere das Cyber-Network CSS */
|
/* Light Mode Optimierungen für wichtige UI-Komponenten */
|
||||||
@import url('/static/css/src/cybernetwork-bg.css');
|
|
||||||
|
/* Buttons im Light Mode */
|
||||||
|
.btn-primary:not(.dark-mode .btn-primary) {
|
||||||
|
background-color: var(--light-primary, #3b82f6);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:not(.dark-mode .btn-primary):hover {
|
||||||
|
background-color: var(--light-primary-hover, #4f46e5);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:not(.dark-mode .btn-secondary) {
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
color: #374151;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:not(.dark-mode .btn-secondary):hover {
|
||||||
|
background-color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navbar im Light Mode */
|
||||||
|
.navbar:not(.dark-mode .navbar),
|
||||||
|
.nav:not(.dark-mode .nav) {
|
||||||
|
background-color: rgba(255, 255, 255, 0.95);
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar:not(.dark-mode .navbar) .nav-link,
|
||||||
|
.nav:not(.dark-mode .nav) .nav-link {
|
||||||
|
color: #1e3a8a;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar:not(.dark-mode .navbar) .nav-link:hover,
|
||||||
|
.nav:not(.dark-mode .nav) .nav-link:hover {
|
||||||
|
color: #4f46e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar:not(.dark-mode .navbar) .navbar-brand,
|
||||||
|
.nav:not(.dark-mode .nav) .navbar-brand {
|
||||||
|
color: #0f172a;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dropdown Menüs im Light Mode */
|
||||||
|
.dropdown-menu:not(.dark-mode .dropdown-menu) {
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05), 0 10px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item:not(.dark-mode .dropdown-item) {
|
||||||
|
color: #1e293b;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item:not(.dark-mode .dropdown-item):hover {
|
||||||
|
background-color: #f1f5f9;
|
||||||
|
color: #4f46e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Karten im Light Mode */
|
||||||
|
.card:not(.dark-mode .card) {
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.03), 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header:not(.dark-mode .card-header) {
|
||||||
|
background-color: #f8fafc;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-footer:not(.dark-mode .card-footer) {
|
||||||
|
background-color: #f8fafc;
|
||||||
|
border-top: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Formulare im Light Mode */
|
||||||
|
.form-control:not(.dark-mode .form-control) {
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:not(.dark-mode .form-control):focus {
|
||||||
|
border-color: #3b82f6;
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tabs im Light Mode */
|
||||||
|
.nav-tabs:not(.dark-mode .nav-tabs) {
|
||||||
|
border-bottom-color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tabs:not(.dark-mode .nav-tabs) .nav-link {
|
||||||
|
color: #64748b;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tabs:not(.dark-mode .nav-tabs) .nav-link:hover {
|
||||||
|
border-color: #e5e7eb #e5e7eb #e5e7eb;
|
||||||
|
color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tabs:not(.dark-mode .nav-tabs) .nav-link.active {
|
||||||
|
color: #0f172a;
|
||||||
|
background-color: white;
|
||||||
|
border-color: #e5e7eb #e5e7eb white;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Alerts im Light Mode */
|
||||||
|
.alert:not(.dark-mode .alert) {
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-primary:not(.dark-mode .alert-primary) {
|
||||||
|
background-color: #eff6ff;
|
||||||
|
border-color: #bfdbfe;
|
||||||
|
color: #1e40af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-success:not(.dark-mode .alert-success) {
|
||||||
|
background-color: #f0fdf4;
|
||||||
|
border-color: #bbf7d0;
|
||||||
|
color: #166534;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-warning:not(.dark-mode .alert-warning) {
|
||||||
|
background-color: #fffbeb;
|
||||||
|
border-color: #fef3c7;
|
||||||
|
color: #92400e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-danger:not(.dark-mode .alert-danger) {
|
||||||
|
background-color: #fef2f2;
|
||||||
|
border-color: #fecaca;
|
||||||
|
color: #b91c1c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Badges im Light Mode */
|
||||||
|
.badge:not(.dark-mode .badge) {
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 0.25em 0.6em;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-primary:not(.dark-mode .badge-primary) {
|
||||||
|
background-color: #3b82f6;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-secondary:not(.dark-mode .badge-secondary) {
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tabellen im Light Mode */
|
||||||
|
table:not(.dark-mode table) {
|
||||||
|
background-color: white;
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
table:not(.dark-mode table) th {
|
||||||
|
background-color: #f8fafc;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
color: #0f172a;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0.75rem;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
table:not(.dark-mode table) td {
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
padding: 0.75rem;
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
table:not(.dark-mode table) tr:hover {
|
||||||
|
background-color: #f8fafc;
|
||||||
|
}
|
||||||
6
static/css/tailwind.min.css
vendored
Normal file
6
static/css/tailwind.min.css
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* Failed to bundle using Rollup v2.79.2: the file imports a not supported node.js built-in module "fs".
|
||||||
|
* If you believe this to be an issue with jsDelivr, and not with the package itself, please open an issue at https://github.com/jsdelivr/jsdelivr
|
||||||
|
*/
|
||||||
|
|
||||||
|
throw new Error('Failed to bundle using Rollup v2.79.2: the file imports a not supported node.js built-in module "fs". If you believe this to be an issue with jsDelivr, and not with the package itself, please open an issue at https://github.com/jsdelivr/jsdelivr');
|
||||||
70
static/d3-extensions.js
vendored
70
static/d3-extensions.js
vendored
@@ -450,6 +450,76 @@ class D3Extensions {
|
|||||||
// Pulsanimation starten
|
// Pulsanimation starten
|
||||||
pulse();
|
pulse();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verarbeitet Daten aus der Datenbank für die Mindmap-Visualisierung
|
||||||
|
* @param {Array} databaseNodes - Knotendaten aus der Datenbank
|
||||||
|
* @param {Array} links - Verbindungsdaten oder null für automatische Extraktion
|
||||||
|
* @returns {Object} Aufbereitete Daten für D3.js
|
||||||
|
*/
|
||||||
|
static processDbNodesForVisualization(databaseNodes, links = null) {
|
||||||
|
// Überprüfe, ob Daten vorhanden sind
|
||||||
|
if (!databaseNodes || databaseNodes.length === 0) {
|
||||||
|
console.warn('Keine Knotendaten zum Verarbeiten vorhanden');
|
||||||
|
return { nodes: [], links: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Knoten mit D3-Kompatiblem Format erstellen
|
||||||
|
const nodes = databaseNodes.map(node => {
|
||||||
|
// Farbgenerierung, falls keine vorhanden
|
||||||
|
const nodeColor = node.color_code ||
|
||||||
|
node.color ||
|
||||||
|
D3Extensions.stringToColor(node.name || 'default');
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: node.id,
|
||||||
|
name: node.name,
|
||||||
|
description: node.description || '',
|
||||||
|
thought_count: node.thought_count || 0,
|
||||||
|
color: nodeColor,
|
||||||
|
// Zusätzliche Attribute
|
||||||
|
category_id: node.category_id,
|
||||||
|
is_public: node.is_public !== undefined ? node.is_public : true,
|
||||||
|
// Position, falls vorhanden
|
||||||
|
x: node.x_position,
|
||||||
|
y: node.y_position,
|
||||||
|
// Größe, falls vorhanden
|
||||||
|
scale: node.scale || 1.0
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verbindungen verarbeiten
|
||||||
|
let processedLinks = [];
|
||||||
|
|
||||||
|
if (links && Array.isArray(links)) {
|
||||||
|
// Verwende übergebene Verbindungen
|
||||||
|
processedLinks = links.map(link => {
|
||||||
|
return {
|
||||||
|
source: link.source,
|
||||||
|
target: link.target,
|
||||||
|
// Zusätzliche Attribute
|
||||||
|
type: link.type || 'default',
|
||||||
|
strength: link.strength || 1
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Extrahiere Verbindungen aus den Knoten
|
||||||
|
databaseNodes.forEach(node => {
|
||||||
|
if (node.connections && Array.isArray(node.connections)) {
|
||||||
|
node.connections.forEach(conn => {
|
||||||
|
processedLinks.push({
|
||||||
|
source: node.id,
|
||||||
|
target: conn.target,
|
||||||
|
type: conn.type || 'default',
|
||||||
|
strength: conn.strength || 1
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { nodes, links: processedLinks };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Globale Verfügbarkeit sicherstellen
|
// Globale Verfügbarkeit sicherstellen
|
||||||
|
|||||||
BIN
static/example.png
Normal file
BIN
static/example.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
35
static/fonts/inter.css
Normal file
35
static/fonts/inter.css
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
/* Inter font - Local version */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
src: url('../fonts/inter-light.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: url('../fonts/inter-regular.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
src: url('../fonts/inter-medium.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
src: url('../fonts/inter-semibold.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
src: url('../fonts/inter-bold.woff2') format('woff2');
|
||||||
|
}
|
||||||
21
static/fonts/jetbrains-mono.css
Normal file
21
static/fonts/jetbrains-mono.css
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
/* JetBrains Mono font - Local version */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: url('../fonts/jetbrainsmono-regular.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
src: url('../fonts/jetbrainsmono-medium.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
src: url('../fonts/jetbrainsmono-bold.woff2') format('woff2');
|
||||||
|
}
|
||||||
5
static/js/alpine.min.js
vendored
Normal file
5
static/js/alpine.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -2,23 +2,11 @@
|
|||||||
* MindMap - Hauptdatei für globale JavaScript-Funktionen
|
* MindMap - Hauptdatei für globale JavaScript-Funktionen
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Import des ChatGPT-Assistenten
|
|
||||||
import ChatGPTAssistant from './modules/chatgpt-assistant.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hauptmodul für die MindMap-Anwendung
|
* Hauptmodul für die MindMap-Anwendung
|
||||||
* Verwaltet die globale Anwendungslogik
|
* Verwaltet die globale Anwendungslogik
|
||||||
*/
|
*/
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
// Initialisiere die Anwendung
|
|
||||||
MindMap.init();
|
|
||||||
|
|
||||||
// Wende Dunkel-/Hellmodus an
|
|
||||||
const isDarkMode = localStorage.getItem('darkMode') === 'dark';
|
|
||||||
document.documentElement.classList.toggle('dark', isDarkMode);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hauptobjekt der MindMap-Anwendung
|
* Hauptobjekt der MindMap-Anwendung
|
||||||
*/
|
*/
|
||||||
@@ -27,7 +15,7 @@ const MindMap = {
|
|||||||
initialized: false,
|
initialized: false,
|
||||||
darkMode: document.documentElement.classList.contains('dark'),
|
darkMode: document.documentElement.classList.contains('dark'),
|
||||||
pageInitializers: {},
|
pageInitializers: {},
|
||||||
currentPage: document.body.dataset.page,
|
currentPage: null,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialisiert die MindMap-Anwendung
|
* Initialisiert die MindMap-Anwendung
|
||||||
@@ -35,13 +23,18 @@ const MindMap = {
|
|||||||
init() {
|
init() {
|
||||||
if (this.initialized) return;
|
if (this.initialized) return;
|
||||||
|
|
||||||
|
// Setze currentPage erst jetzt, wenn DOM garantiert geladen ist
|
||||||
|
this.currentPage = document.body && document.body.dataset ? document.body.dataset.page : null;
|
||||||
|
|
||||||
console.log('MindMap-Anwendung wird initialisiert...');
|
console.log('MindMap-Anwendung wird initialisiert...');
|
||||||
|
|
||||||
// Initialisiere den ChatGPT-Assistenten
|
// Initialisiere den ChatGPT-Assistenten
|
||||||
const assistant = new ChatGPTAssistant();
|
if (typeof ChatGPTAssistant !== 'undefined') {
|
||||||
assistant.init();
|
const assistant = new ChatGPTAssistant();
|
||||||
// Speichere als Teil von MindMap
|
assistant.init();
|
||||||
this.assistant = assistant;
|
// Speichere als Teil von MindMap
|
||||||
|
this.assistant = assistant;
|
||||||
|
}
|
||||||
|
|
||||||
// Seiten-spezifische Initialisierer aufrufen
|
// Seiten-spezifische Initialisierer aufrufen
|
||||||
if (this.currentPage && this.pageInitializers[this.currentPage]) {
|
if (this.currentPage && this.pageInitializers[this.currentPage]) {
|
||||||
@@ -74,6 +67,12 @@ const MindMap = {
|
|||||||
try {
|
try {
|
||||||
console.log('Initialisiere Mindmap...');
|
console.log('Initialisiere Mindmap...');
|
||||||
|
|
||||||
|
// Prüfe, ob MindMapVisualization geladen ist
|
||||||
|
if (typeof MindMapVisualization === 'undefined') {
|
||||||
|
console.error('MindMapVisualization-Klasse ist nicht definiert!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Initialisiere die Mindmap
|
// Initialisiere die Mindmap
|
||||||
const mindmap = new MindMapVisualization('#mindmap-container', {
|
const mindmap = new MindMapVisualization('#mindmap-container', {
|
||||||
height: mindmapContainer.clientHeight || 600,
|
height: mindmapContainer.clientHeight || 600,
|
||||||
@@ -224,6 +223,13 @@ const MindMap = {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
window.MindMap = MindMap;
|
||||||
|
|
||||||
// Globale Export für andere Module
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
window.MindMap = MindMap;
|
// Initialisiere die Anwendung
|
||||||
|
MindMap.init();
|
||||||
|
|
||||||
|
// Wende Dunkel-/Hellmodus an
|
||||||
|
const isDarkMode = localStorage.getItem('darkMode') === 'dark';
|
||||||
|
document.documentElement.classList.toggle('dark', isDarkMode);
|
||||||
|
});
|
||||||
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');
|
||||||
|
})();
|
||||||
@@ -11,6 +11,62 @@ class ChatGPTAssistant {
|
|||||||
this.container = null;
|
this.container = null;
|
||||||
this.chatHistory = null;
|
this.chatHistory = null;
|
||||||
this.inputField = null;
|
this.inputField = null;
|
||||||
|
this.suggestionArea = null;
|
||||||
|
this.maxRetries = 2;
|
||||||
|
this.retryCount = 0;
|
||||||
|
this.markdownParser = null;
|
||||||
|
this.initializeMarkdownParser();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialisiert den Markdown-Parser
|
||||||
|
*/
|
||||||
|
async initializeMarkdownParser() {
|
||||||
|
// Dynamisch marked.js laden, wenn noch nicht vorhanden
|
||||||
|
if (!window.marked) {
|
||||||
|
try {
|
||||||
|
// Prüfen, ob marked.js bereits im Dokument geladen ist
|
||||||
|
if (!document.querySelector('script[src*="marked"]')) {
|
||||||
|
// Falls nicht, Script-Tag erstellen und einfügen
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = 'https://cdn.jsdelivr.net/npm/marked/marked.min.js';
|
||||||
|
script.async = true;
|
||||||
|
|
||||||
|
// Promise erstellen, das resolved wird, wenn das Script geladen wurde
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
script.onload = resolve;
|
||||||
|
script.onerror = reject;
|
||||||
|
document.head.appendChild(script);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Marked.js erfolgreich geladen');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marked konfigurieren
|
||||||
|
this.markdownParser = window.marked;
|
||||||
|
this.markdownParser.setOptions({
|
||||||
|
gfm: true,
|
||||||
|
breaks: true,
|
||||||
|
sanitize: true,
|
||||||
|
smartLists: true,
|
||||||
|
smartypants: true
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Laden von marked.js:', error);
|
||||||
|
// Fallback-Parser, der nur einfache Absätze erkennt
|
||||||
|
this.markdownParser = {
|
||||||
|
parse: (text) => {
|
||||||
|
return text.split('\n').map(line => {
|
||||||
|
if (line.trim() === '') return '<br>';
|
||||||
|
return `<p>${line}</p>`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Marked ist bereits geladen
|
||||||
|
this.markdownParser = window.marked;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -24,7 +80,16 @@ class ChatGPTAssistant {
|
|||||||
this.setupEventListeners();
|
this.setupEventListeners();
|
||||||
|
|
||||||
// Ersten Willkommensnachricht anzeigen
|
// Ersten Willkommensnachricht anzeigen
|
||||||
this.addMessage("assistant", "Frage den KI-Assistenten");
|
this.addMessage("assistant", "Hallo! Ich bin dein KI-Assistent (4o-mini) und habe Zugriff auf die Wissensdatenbank. Wie kann ich dir helfen?\n\nDu kannst mir Fragen über:\n- **Gedanken** in der Datenbank\n- **Kategorien** und Wissenschaftsbereiche\n- **Mindmaps** und Wissensverknüpfungen\n\nstellen.");
|
||||||
|
|
||||||
|
// Vorschläge anzeigen
|
||||||
|
this.showSuggestions([
|
||||||
|
"Zeige mir Gedanken zur künstlichen Intelligenz",
|
||||||
|
"Welche Kategorien gibt es in der Datenbank?",
|
||||||
|
"Suche nach Mindmaps zum Thema Informatik"
|
||||||
|
]);
|
||||||
|
|
||||||
|
console.log('KI-Assistent initialisiert!');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -45,7 +110,7 @@ class ChatGPTAssistant {
|
|||||||
// Chat-Container
|
// Chat-Container
|
||||||
const chatContainer = document.createElement('div');
|
const chatContainer = document.createElement('div');
|
||||||
chatContainer.id = 'assistant-chat';
|
chatContainer.id = 'assistant-chat';
|
||||||
chatContainer.className = 'bg-white dark:bg-dark-800 rounded-lg shadow-xl overflow-hidden transition-all duration-300 w-80 sm:w-96 max-h-0 opacity-0';
|
chatContainer.className = 'bg-white dark:bg-dark-800 rounded-lg shadow-xl overflow-hidden transition-all duration-300 w-80 md:w-96 max-h-0 opacity-0';
|
||||||
|
|
||||||
// Chat-Header
|
// Chat-Header
|
||||||
const header = document.createElement('div');
|
const header = document.createElement('div');
|
||||||
@@ -53,7 +118,7 @@ class ChatGPTAssistant {
|
|||||||
header.innerHTML = `
|
header.innerHTML = `
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<i class="fas fa-robot mr-2"></i>
|
<i class="fas fa-robot mr-2"></i>
|
||||||
<span>KI-Assistent</span>
|
<span>KI-Assistent (4o-mini)</span>
|
||||||
</div>
|
</div>
|
||||||
<button id="assistant-close" class="text-white hover:text-gray-200">
|
<button id="assistant-close" class="text-white hover:text-gray-200">
|
||||||
<i class="fas fa-times"></i>
|
<i class="fas fa-times"></i>
|
||||||
@@ -63,7 +128,12 @@ class ChatGPTAssistant {
|
|||||||
// Chat-Verlauf
|
// Chat-Verlauf
|
||||||
this.chatHistory = document.createElement('div');
|
this.chatHistory = document.createElement('div');
|
||||||
this.chatHistory.id = 'assistant-history';
|
this.chatHistory.id = 'assistant-history';
|
||||||
this.chatHistory.className = 'p-3 overflow-y-auto max-h-80 space-y-3';
|
this.chatHistory.className = 'p-3 overflow-y-auto max-h-96 space-y-3';
|
||||||
|
|
||||||
|
// Vorschlagsbereich
|
||||||
|
this.suggestionArea = document.createElement('div');
|
||||||
|
this.suggestionArea.id = 'assistant-suggestions';
|
||||||
|
this.suggestionArea.className = 'px-3 pb-2 flex flex-wrap gap-2 overflow-x-auto hidden';
|
||||||
|
|
||||||
// Chat-Eingabe
|
// Chat-Eingabe
|
||||||
const inputContainer = document.createElement('div');
|
const inputContainer = document.createElement('div');
|
||||||
@@ -71,7 +141,7 @@ class ChatGPTAssistant {
|
|||||||
|
|
||||||
this.inputField = document.createElement('input');
|
this.inputField = document.createElement('input');
|
||||||
this.inputField.type = 'text';
|
this.inputField.type = 'text';
|
||||||
this.inputField.placeholder = 'Frage den KI-Assistenten';
|
this.inputField.placeholder = 'Stelle eine Frage zur Wissensdatenbank...';
|
||||||
this.inputField.className = 'flex-1 border border-gray-300 dark:border-dark-600 dark:bg-dark-700 dark:text-white rounded-l-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary-500';
|
this.inputField.className = 'flex-1 border border-gray-300 dark:border-dark-600 dark:bg-dark-700 dark:text-white rounded-l-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary-500';
|
||||||
|
|
||||||
const sendButton = document.createElement('button');
|
const sendButton = document.createElement('button');
|
||||||
@@ -85,6 +155,7 @@ class ChatGPTAssistant {
|
|||||||
|
|
||||||
chatContainer.appendChild(header);
|
chatContainer.appendChild(header);
|
||||||
chatContainer.appendChild(this.chatHistory);
|
chatContainer.appendChild(this.chatHistory);
|
||||||
|
chatContainer.appendChild(this.suggestionArea);
|
||||||
chatContainer.appendChild(inputContainer);
|
chatContainer.appendChild(inputContainer);
|
||||||
|
|
||||||
this.container.appendChild(toggleButton);
|
this.container.appendChild(toggleButton);
|
||||||
@@ -100,22 +171,40 @@ class ChatGPTAssistant {
|
|||||||
setupEventListeners() {
|
setupEventListeners() {
|
||||||
// Toggle-Button
|
// Toggle-Button
|
||||||
const toggleButton = document.getElementById('assistant-toggle');
|
const toggleButton = document.getElementById('assistant-toggle');
|
||||||
toggleButton.addEventListener('click', () => this.toggleAssistant());
|
if (toggleButton) {
|
||||||
|
toggleButton.addEventListener('click', () => this.toggleAssistant());
|
||||||
|
}
|
||||||
|
|
||||||
// Schließen-Button
|
// Schließen-Button
|
||||||
const closeButton = document.getElementById('assistant-close');
|
const closeButton = document.getElementById('assistant-close');
|
||||||
closeButton.addEventListener('click', () => this.toggleAssistant(false));
|
if (closeButton) {
|
||||||
|
closeButton.addEventListener('click', () => this.toggleAssistant(false));
|
||||||
|
}
|
||||||
|
|
||||||
// Senden-Button
|
// Senden-Button
|
||||||
const sendButton = document.getElementById('assistant-send');
|
const sendButton = document.getElementById('assistant-send');
|
||||||
sendButton.addEventListener('click', () => this.sendMessage());
|
if (sendButton) {
|
||||||
|
sendButton.addEventListener('click', () => this.sendMessage());
|
||||||
|
}
|
||||||
|
|
||||||
// Enter-Taste im Eingabefeld
|
// Enter-Taste im Eingabefeld
|
||||||
this.inputField.addEventListener('keyup', (e) => {
|
if (this.inputField) {
|
||||||
if (e.key === 'Enter') {
|
this.inputField.addEventListener('keyup', (e) => {
|
||||||
this.sendMessage();
|
if (e.key === 'Enter') {
|
||||||
}
|
this.sendMessage();
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vorschläge klickbar machen
|
||||||
|
if (this.suggestionArea) {
|
||||||
|
this.suggestionArea.addEventListener('click', (e) => {
|
||||||
|
if (e.target.classList.contains('suggestion-pill')) {
|
||||||
|
this.inputField.value = e.target.textContent;
|
||||||
|
this.sendMessage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -124,14 +213,21 @@ class ChatGPTAssistant {
|
|||||||
*/
|
*/
|
||||||
toggleAssistant(state = null) {
|
toggleAssistant(state = null) {
|
||||||
const chatContainer = document.getElementById('assistant-chat');
|
const chatContainer = document.getElementById('assistant-chat');
|
||||||
|
if (!chatContainer) return;
|
||||||
|
|
||||||
this.isOpen = state !== null ? state : !this.isOpen;
|
this.isOpen = state !== null ? state : !this.isOpen;
|
||||||
|
|
||||||
if (this.isOpen) {
|
if (this.isOpen) {
|
||||||
chatContainer.classList.remove('max-h-0', 'opacity-0');
|
chatContainer.classList.remove('max-h-0', 'opacity-0');
|
||||||
chatContainer.classList.add('max-h-96', 'opacity-100');
|
chatContainer.classList.add('max-h-[32rem]', 'opacity-100');
|
||||||
this.inputField.focus();
|
if (this.inputField) this.inputField.focus();
|
||||||
|
|
||||||
|
// Zeige Vorschläge wenn verfügbar
|
||||||
|
if (this.suggestionArea && this.suggestionArea.children.length > 0) {
|
||||||
|
this.suggestionArea.classList.remove('hidden');
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
chatContainer.classList.remove('max-h-96', 'opacity-100');
|
chatContainer.classList.remove('max-h-[32rem]', 'opacity-100');
|
||||||
chatContainer.classList.add('max-h-0', 'opacity-0');
|
chatContainer.classList.add('max-h-0', 'opacity-0');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -153,22 +249,144 @@ class ChatGPTAssistant {
|
|||||||
bubble.className = sender === 'user'
|
bubble.className = sender === 'user'
|
||||||
? 'bg-primary-100 dark:bg-primary-900 text-gray-800 dark:text-white rounded-lg py-2 px-3 max-w-[85%]'
|
? 'bg-primary-100 dark:bg-primary-900 text-gray-800 dark:text-white rounded-lg py-2 px-3 max-w-[85%]'
|
||||||
: 'bg-gray-100 dark:bg-dark-700 text-gray-800 dark:text-white rounded-lg py-2 px-3 max-w-[85%]';
|
: 'bg-gray-100 dark:bg-dark-700 text-gray-800 dark:text-white rounded-lg py-2 px-3 max-w-[85%]';
|
||||||
bubble.textContent = text;
|
|
||||||
|
// Formatierung des Texts (mit Markdown für Assistent-Nachrichten)
|
||||||
|
let formattedText = '';
|
||||||
|
|
||||||
|
if (sender === 'assistant' && this.markdownParser) {
|
||||||
|
// Für Assistentnachrichten Markdown verwenden
|
||||||
|
try {
|
||||||
|
formattedText = this.markdownParser.parse(text);
|
||||||
|
|
||||||
|
// CSS für Markdown-Formatierung hinzufügen
|
||||||
|
const markdownStyles = `
|
||||||
|
.markdown-bubble h1, .markdown-bubble h2, .markdown-bubble h3,
|
||||||
|
.markdown-bubble h4, .markdown-bubble h5, .markdown-bubble h6 {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
.markdown-bubble h1 { font-size: 1.4rem; }
|
||||||
|
.markdown-bubble h2 { font-size: 1.3rem; }
|
||||||
|
.markdown-bubble h3 { font-size: 1.2rem; }
|
||||||
|
.markdown-bubble h4 { font-size: 1.1rem; }
|
||||||
|
.markdown-bubble ul, .markdown-bubble ol {
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
|
.markdown-bubble ul { list-style-type: disc; }
|
||||||
|
.markdown-bubble ol { list-style-type: decimal; }
|
||||||
|
.markdown-bubble p { margin: 0.5rem 0; }
|
||||||
|
.markdown-bubble code {
|
||||||
|
font-family: monospace;
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
padding: 1px 4px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
.markdown-bubble pre {
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
|
.markdown-bubble pre code {
|
||||||
|
background-color: transparent;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.markdown-bubble blockquote {
|
||||||
|
border-left: 3px solid rgba(0, 0, 0, 0.2);
|
||||||
|
padding-left: 0.8rem;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
.dark .markdown-bubble code {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
.dark .markdown-bubble pre {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
.dark .markdown-bubble blockquote {
|
||||||
|
border-left-color: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Füge die Styles hinzu, wenn sie noch nicht vorhanden sind
|
||||||
|
if (!document.querySelector('#markdown-chat-styles')) {
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.id = 'markdown-chat-styles';
|
||||||
|
style.textContent = markdownStyles;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Klasse für Markdown-Formatierung hinzufügen
|
||||||
|
bubble.classList.add('markdown-bubble');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler bei der Markdown-Formatierung:', error);
|
||||||
|
// Fallback zur einfachen Formatierung
|
||||||
|
formattedText = text.split('\n').map(line => {
|
||||||
|
if (line.trim() === '') return '<br>';
|
||||||
|
return `<p>${line}</p>`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Für Benutzernachrichten einfache Formatierung
|
||||||
|
formattedText = text.split('\n').map(line => {
|
||||||
|
if (line.trim() === '') return '<br>';
|
||||||
|
return `<p>${line}</p>`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
bubble.innerHTML = formattedText;
|
||||||
|
|
||||||
messageEl.appendChild(bubble);
|
messageEl.appendChild(bubble);
|
||||||
this.chatHistory.appendChild(messageEl);
|
|
||||||
|
|
||||||
// Scroll zum Ende des Verlaufs
|
if (this.chatHistory) {
|
||||||
this.chatHistory.scrollTop = this.chatHistory.scrollHeight;
|
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
|
* Sendet die Benutzernachricht an den Server und zeigt die Antwort an
|
||||||
*/
|
*/
|
||||||
async sendMessage() {
|
async sendMessage() {
|
||||||
|
if (!this.inputField) return;
|
||||||
|
|
||||||
const userInput = this.inputField.value.trim();
|
const userInput = this.inputField.value.trim();
|
||||||
if (!userInput || this.isLoading) return;
|
if (!userInput || this.isLoading) return;
|
||||||
|
|
||||||
|
// Vorschläge ausblenden
|
||||||
|
if (this.suggestionArea) {
|
||||||
|
this.suggestionArea.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
// Benutzernachricht anzeigen
|
// Benutzernachricht anzeigen
|
||||||
this.addMessage('user', userInput);
|
this.addMessage('user', userInput);
|
||||||
|
|
||||||
@@ -180,6 +398,7 @@ class ChatGPTAssistant {
|
|||||||
this.showLoadingIndicator();
|
this.showLoadingIndicator();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
console.log('Sende Anfrage an KI-Assistent API...');
|
||||||
// Anfrage an den Server senden
|
// Anfrage an den Server senden
|
||||||
const response = await fetch('/api/assistant', {
|
const response = await fetch('/api/assistant', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -189,36 +408,118 @@ class ChatGPTAssistant {
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
messages: this.messages
|
messages: this.messages
|
||||||
}),
|
}),
|
||||||
|
cache: 'no-cache', // Kein Cache verwenden
|
||||||
|
credentials: 'same-origin' // Cookies senden
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Ladeindikator entfernen
|
||||||
|
this.removeLoadingIndicator();
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Netzwerkfehler oder Serverproblem');
|
throw new Error(`Serverfehler: ${response.status} ${response.statusText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
console.log('Antwort erhalten:', data);
|
||||||
// Ladeindikator entfernen
|
|
||||||
this.removeLoadingIndicator();
|
|
||||||
|
|
||||||
// Antwort anzeigen
|
// Antwort anzeigen
|
||||||
this.addMessage('assistant', data.response);
|
if (data.response) {
|
||||||
|
this.addMessage('assistant', data.response);
|
||||||
|
|
||||||
|
// Neue Vorschläge basierend auf dem aktuellen Kontext anzeigen
|
||||||
|
this.generateContextualSuggestions();
|
||||||
|
|
||||||
|
// Erfolgreiche Anfrage zurücksetzen
|
||||||
|
this.retryCount = 0;
|
||||||
|
} else if (data.error) {
|
||||||
|
this.addMessage('assistant', `Fehler: ${data.error}`);
|
||||||
|
} else {
|
||||||
|
throw new Error('Unerwartetes Antwortformat vom Server');
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fehler bei der Kommunikation mit dem Assistenten:', error);
|
console.error('Fehler bei der Kommunikation mit dem Assistenten:', error);
|
||||||
|
|
||||||
// Ladeindikator entfernen
|
// Ladeindikator entfernen, falls noch vorhanden
|
||||||
this.removeLoadingIndicator();
|
this.removeLoadingIndicator();
|
||||||
|
|
||||||
// Fehlermeldung anzeigen
|
// Fehlermeldung anzeigen oder Wiederholungsversuch starten
|
||||||
this.addMessage('assistant', 'Es tut mir leid, aber es gab ein Problem bei der Verarbeitung deiner Anfrage. Bitte versuche es später noch einmal.');
|
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 {
|
} finally {
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generiert kontextbasierte Vorschläge basierend auf dem aktuellen Chat-Verlauf
|
||||||
|
*/
|
||||||
|
generateContextualSuggestions() {
|
||||||
|
// Basierend auf letzter Antwort des Assistenten, verschiedene Vorschläge generieren
|
||||||
|
const lastAssistantMessage = this.messages.findLast(msg => msg.role === 'assistant')?.content || '';
|
||||||
|
|
||||||
|
let suggestions = [];
|
||||||
|
|
||||||
|
// Intelligente Vorschläge basierend auf Kontext
|
||||||
|
if (lastAssistantMessage.includes('Künstliche Intelligenz') ||
|
||||||
|
lastAssistantMessage.includes('KI ') ||
|
||||||
|
lastAssistantMessage.includes('AI ')) {
|
||||||
|
suggestions = [
|
||||||
|
"Wie wird KI in der Wissenschaft eingesetzt?",
|
||||||
|
"Zeige mir Gedanken zum maschinellen Lernen",
|
||||||
|
"Was ist der Unterschied zwischen KI und ML?"
|
||||||
|
];
|
||||||
|
} else if (lastAssistantMessage.includes('Kategorie') ||
|
||||||
|
lastAssistantMessage.includes('Kategorien')) {
|
||||||
|
suggestions = [
|
||||||
|
"Zeige mir die Unterkategorien",
|
||||||
|
"Welche Gedanken gehören zu dieser Kategorie?",
|
||||||
|
"Liste alle Wissenschaftskategorien auf"
|
||||||
|
];
|
||||||
|
} else if (lastAssistantMessage.includes('Mindmap') ||
|
||||||
|
lastAssistantMessage.includes('Visualisierung')) {
|
||||||
|
suggestions = [
|
||||||
|
"Wie kann ich eine eigene Mindmap erstellen?",
|
||||||
|
"Zeige mir Beispiele für Mindmaps",
|
||||||
|
"Wie funktionieren die Verbindungen in Mindmaps?"
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
// Standardvorschläge
|
||||||
|
suggestions = [
|
||||||
|
"Erzähle mir mehr dazu",
|
||||||
|
"Gibt es Beispiele dafür?",
|
||||||
|
"Wie kann ich diese Information nutzen?"
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.showSuggestions(suggestions);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Zeigt einen Ladeindikator im Chat an
|
* Zeigt einen Ladeindikator im Chat an
|
||||||
*/
|
*/
|
||||||
showLoadingIndicator() {
|
showLoadingIndicator() {
|
||||||
|
if (!this.chatHistory) return;
|
||||||
|
|
||||||
|
// Entferne vorhandenen Ladeindikator (falls vorhanden)
|
||||||
|
this.removeLoadingIndicator();
|
||||||
|
|
||||||
const loadingEl = document.createElement('div');
|
const loadingEl = document.createElement('div');
|
||||||
loadingEl.id = 'assistant-loading';
|
loadingEl.id = 'assistant-loading';
|
||||||
loadingEl.className = 'flex justify-start';
|
loadingEl.className = 'flex justify-start';
|
||||||
@@ -232,49 +533,40 @@ class ChatGPTAssistant {
|
|||||||
|
|
||||||
// Scroll zum Ende des Verlaufs
|
// Scroll zum Ende des Verlaufs
|
||||||
this.chatHistory.scrollTop = this.chatHistory.scrollHeight;
|
this.chatHistory.scrollTop = this.chatHistory.scrollHeight;
|
||||||
|
|
||||||
// Stil für den Typing-Indikator
|
|
||||||
const style = document.createElement('style');
|
|
||||||
style.textContent = `
|
|
||||||
.typing-indicator {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.typing-indicator span {
|
|
||||||
height: 8px;
|
|
||||||
width: 8px;
|
|
||||||
background-color: #888;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: inline-block;
|
|
||||||
margin: 0 2px;
|
|
||||||
opacity: 0.4;
|
|
||||||
animation: typing 1.5s infinite ease-in-out;
|
|
||||||
}
|
|
||||||
.typing-indicator span:nth-child(2) {
|
|
||||||
animation-delay: 0.2s;
|
|
||||||
}
|
|
||||||
.typing-indicator span:nth-child(3) {
|
|
||||||
animation-delay: 0.4s;
|
|
||||||
}
|
|
||||||
@keyframes typing {
|
|
||||||
0% { transform: translateY(0); }
|
|
||||||
50% { transform: translateY(-5px); }
|
|
||||||
100% { transform: translateY(0); }
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
document.head.appendChild(style);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Entfernt den Ladeindikator aus dem Chat
|
* Entfernt den Ladeindikator aus dem Chat
|
||||||
*/
|
*/
|
||||||
removeLoadingIndicator() {
|
removeLoadingIndicator() {
|
||||||
const loadingEl = document.getElementById('assistant-loading');
|
const loadingIndicator = document.getElementById('assistant-loading');
|
||||||
if (loadingEl) {
|
if (loadingIndicator) {
|
||||||
loadingEl.remove();
|
loadingIndicator.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Öffnet den Assistenten und sendet eine vorgegebene Frage
|
||||||
|
* @param {string} question - Die zu stellende Frage
|
||||||
|
*/
|
||||||
|
async sendQuestion(question) {
|
||||||
|
if (!question || this.isLoading) return;
|
||||||
|
|
||||||
|
// Assistenten öffnen
|
||||||
|
this.toggleAssistant(true);
|
||||||
|
|
||||||
|
// Kurze Verzögerung, um sicherzustellen, dass der UI vollständig geöffnet ist
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 300));
|
||||||
|
|
||||||
|
// Frage in Eingabefeld setzen
|
||||||
|
if (this.inputField) {
|
||||||
|
this.inputField.value = question;
|
||||||
|
|
||||||
|
// Sende die Frage
|
||||||
|
this.sendMessage();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exportiere die Klasse für die Verwendung in anderen Modulen
|
// Mache die Klasse global verfügbar
|
||||||
export default ChatGPTAssistant;
|
window.ChatGPTAssistant = ChatGPTAssistant;
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
/**
|
|
||||||
* Initialisierungsmodul für den CyberNetwork-Hintergrund
|
|
||||||
* Importiert und startet die Animation
|
|
||||||
*/
|
|
||||||
|
|
||||||
import CyberNetwork from './cyber-network.js';
|
|
||||||
|
|
||||||
// Beim Laden des Dokuments starten
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
console.log('CyberNetwork: Initialisierung gestartet');
|
|
||||||
|
|
||||||
// Prüfen ob das CSS bereits geladen ist, wenn nicht, dann laden
|
|
||||||
if (!document.querySelector('link[href*="cybernetwork-bg.css"]')) {
|
|
||||||
console.log('CyberNetwork: CSS wird geladen');
|
|
||||||
const cyberNetworkCss = document.createElement('link');
|
|
||||||
cyberNetworkCss.rel = 'stylesheet';
|
|
||||||
cyberNetworkCss.href = '/static/css/src/cybernetwork-bg.css';
|
|
||||||
document.head.appendChild(cyberNetworkCss);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Container-Element für das Netzwerk finden
|
|
||||||
const container = document.getElementById('cyber-background-container');
|
|
||||||
|
|
||||||
if (!container) {
|
|
||||||
console.error('CyberNetwork: Container #cyber-background-container nicht gefunden!');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('CyberNetwork: Container gefunden', container);
|
|
||||||
|
|
||||||
// Konfiguration für den Netzwerk-Hintergrund
|
|
||||||
const networkConfig = {
|
|
||||||
container: container,
|
|
||||||
nodeCount: window.innerWidth < 768 ? 15 : 30, // Weniger Nodes auf mobilen Geräten
|
|
||||||
connectionCount: window.innerWidth < 768 ? 25 : 50,
|
|
||||||
packetCount: window.innerWidth < 768 ? 8 : 15,
|
|
||||||
animationSpeed: 1.0
|
|
||||||
};
|
|
||||||
|
|
||||||
// Netzwerk erstellen und initialisieren
|
|
||||||
const cyberNetwork = new CyberNetwork(networkConfig);
|
|
||||||
cyberNetwork.init();
|
|
||||||
console.log('CyberNetwork: Netzwerk initialisiert');
|
|
||||||
|
|
||||||
// Globale Referenz für Debug-Zwecke
|
|
||||||
window.cyberNetwork = cyberNetwork;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Funktion zum manuellen Initialisieren, falls notwendig
|
|
||||||
export function initCyberNetwork(config = {}) {
|
|
||||||
console.log('CyberNetwork: Manuelle Initialisierung gestartet');
|
|
||||||
|
|
||||||
// CSS laden, falls nicht vorhanden
|
|
||||||
if (!document.querySelector('link[href*="cybernetwork-bg.css"]')) {
|
|
||||||
console.log('CyberNetwork: CSS wird geladen (manuell)');
|
|
||||||
const cyberNetworkCss = document.createElement('link');
|
|
||||||
cyberNetworkCss.rel = 'stylesheet';
|
|
||||||
cyberNetworkCss.href = '/static/css/src/cybernetwork-bg.css';
|
|
||||||
document.head.appendChild(cyberNetworkCss);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Container-Element für das Netzwerk finden
|
|
||||||
const container = document.getElementById('cyber-background-container');
|
|
||||||
|
|
||||||
if (!container) {
|
|
||||||
console.error('CyberNetwork: Container #cyber-background-container nicht gefunden!');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bestehende Instanz zurücksetzen, falls vorhanden
|
|
||||||
if (window.cyberNetwork) {
|
|
||||||
console.log('CyberNetwork: Bestehende Instanz wird zurückgesetzt');
|
|
||||||
window.cyberNetwork.reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Netzwerk mit benutzerdefinierten Optionen erstellen
|
|
||||||
const networkConfig = {
|
|
||||||
container: container,
|
|
||||||
nodeCount: window.innerWidth < 768 ? 15 : 30,
|
|
||||||
connectionCount: window.innerWidth < 768 ? 25 : 50,
|
|
||||||
packetCount: window.innerWidth < 768 ? 8 : 15,
|
|
||||||
animationSpeed: 1.0,
|
|
||||||
...config
|
|
||||||
};
|
|
||||||
|
|
||||||
// Neue Instanz erstellen und initialisieren
|
|
||||||
const cyberNetwork = new CyberNetwork(networkConfig);
|
|
||||||
cyberNetwork.init();
|
|
||||||
console.log('CyberNetwork: Netzwerk manuell initialisiert');
|
|
||||||
|
|
||||||
// Globale Referenz aktualisieren
|
|
||||||
window.cyberNetwork = cyberNetwork;
|
|
||||||
|
|
||||||
return cyberNetwork;
|
|
||||||
}
|
|
||||||
@@ -1,240 +0,0 @@
|
|||||||
/**
|
|
||||||
* Cyber Network Background Animation
|
|
||||||
* Generiert dynamisch ein animiertes Netzwerk für den Hintergrund
|
|
||||||
*/
|
|
||||||
|
|
||||||
class CyberNetwork {
|
|
||||||
constructor(options = {}) {
|
|
||||||
this.options = {
|
|
||||||
container: options.container || document.body,
|
|
||||||
nodeCount: options.nodeCount || 30,
|
|
||||||
connectionCount: options.connectionCount || 50,
|
|
||||||
packetCount: options.packetCount || 15,
|
|
||||||
animationSpeed: options.animationSpeed || 1.0,
|
|
||||||
...options
|
|
||||||
};
|
|
||||||
|
|
||||||
this.nodes = [];
|
|
||||||
this.connections = [];
|
|
||||||
this.packets = [];
|
|
||||||
this.initialized = false;
|
|
||||||
|
|
||||||
this.containerElement = null;
|
|
||||||
this.networkGridElement = null;
|
|
||||||
this.glowOverlayElement = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
if (this.initialized) return;
|
|
||||||
|
|
||||||
// Container erstellen
|
|
||||||
this.containerElement = document.createElement('div');
|
|
||||||
this.containerElement.className = 'cyber-network-bg';
|
|
||||||
|
|
||||||
// Grid erstellen
|
|
||||||
this.networkGridElement = document.createElement('div');
|
|
||||||
this.networkGridElement.className = 'network-grid';
|
|
||||||
this.containerElement.appendChild(this.networkGridElement);
|
|
||||||
|
|
||||||
// Glow Overlay erstellen
|
|
||||||
this.glowOverlayElement = document.createElement('div');
|
|
||||||
this.glowOverlayElement.className = 'glow-overlay';
|
|
||||||
this.containerElement.appendChild(this.glowOverlayElement);
|
|
||||||
|
|
||||||
// Nodes generieren
|
|
||||||
this.generateNodes();
|
|
||||||
|
|
||||||
// Connections generieren
|
|
||||||
this.generateConnections();
|
|
||||||
|
|
||||||
// Data packets generieren
|
|
||||||
this.generateDataPackets();
|
|
||||||
|
|
||||||
// Container zum DOM hinzufügen
|
|
||||||
if (typeof this.options.container === 'string') {
|
|
||||||
const container = document.querySelector(this.options.container);
|
|
||||||
if (container) {
|
|
||||||
container.appendChild(this.containerElement);
|
|
||||||
} else {
|
|
||||||
document.body.appendChild(this.containerElement);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.options.container.appendChild(this.containerElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.initialized = true;
|
|
||||||
|
|
||||||
// Animation starten
|
|
||||||
window.addEventListener('resize', this.handleResize.bind(this));
|
|
||||||
this.startAnimationCycle();
|
|
||||||
}
|
|
||||||
|
|
||||||
generateNodes() {
|
|
||||||
const containerWidth = window.innerWidth;
|
|
||||||
const containerHeight = window.innerHeight;
|
|
||||||
|
|
||||||
for (let i = 0; i < this.options.nodeCount; i++) {
|
|
||||||
const x = Math.random() * containerWidth;
|
|
||||||
const y = Math.random() * containerHeight;
|
|
||||||
|
|
||||||
const node = document.createElement('div');
|
|
||||||
node.className = 'node';
|
|
||||||
node.style.left = `${x}px`;
|
|
||||||
node.style.top = `${y}px`;
|
|
||||||
|
|
||||||
// Größen-Variation für visuelle Tiefe
|
|
||||||
const size = 2 + Math.random() * 4;
|
|
||||||
node.style.width = `${size}px`;
|
|
||||||
node.style.height = `${size}px`;
|
|
||||||
|
|
||||||
// Speichern der Position für spätere Referenz
|
|
||||||
node._data = { x, y, size };
|
|
||||||
|
|
||||||
this.containerElement.appendChild(node);
|
|
||||||
this.nodes.push(node);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
generateConnections() {
|
|
||||||
for (let i = 0; i < this.options.connectionCount; i++) {
|
|
||||||
// Zufällige Nodes auswählen
|
|
||||||
const startNodeIndex = Math.floor(Math.random() * this.nodes.length);
|
|
||||||
let endNodeIndex;
|
|
||||||
do {
|
|
||||||
endNodeIndex = Math.floor(Math.random() * this.nodes.length);
|
|
||||||
} while (endNodeIndex === startNodeIndex);
|
|
||||||
|
|
||||||
const startNode = this.nodes[startNodeIndex];
|
|
||||||
const endNode = this.nodes[endNodeIndex];
|
|
||||||
const startData = startNode._data;
|
|
||||||
const endData = endNode._data;
|
|
||||||
|
|
||||||
// Verbindung erstellen
|
|
||||||
const connection = document.createElement('div');
|
|
||||||
connection.className = 'connection';
|
|
||||||
|
|
||||||
// Position und Rotation berechnen
|
|
||||||
const dx = endData.x - startData.x;
|
|
||||||
const dy = endData.y - startData.y;
|
|
||||||
const length = Math.sqrt(dx * dx + dy * dy);
|
|
||||||
const angle = Math.atan2(dy, dx) * (180 / Math.PI);
|
|
||||||
|
|
||||||
connection.style.width = `${length}px`;
|
|
||||||
connection.style.left = `${startData.x}px`;
|
|
||||||
connection.style.top = `${startData.y}px`;
|
|
||||||
connection.style.transform = `rotate(${angle}deg)`;
|
|
||||||
|
|
||||||
// Variation in der Animations-Geschwindigkeit
|
|
||||||
connection.style.animationDuration = `${3 + Math.random() * 4}s`;
|
|
||||||
|
|
||||||
// Speichern der verbundenen Nodes
|
|
||||||
connection._data = {
|
|
||||||
startNode: startNodeIndex,
|
|
||||||
endNode: endNodeIndex,
|
|
||||||
length
|
|
||||||
};
|
|
||||||
|
|
||||||
this.containerElement.appendChild(connection);
|
|
||||||
this.connections.push(connection);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
generateDataPackets() {
|
|
||||||
for (let i = 0; i < this.options.packetCount; i++) {
|
|
||||||
this.createNewDataPacket();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
createNewDataPacket() {
|
|
||||||
if (this.connections.length === 0) return;
|
|
||||||
|
|
||||||
// Zufällige Verbindung auswählen
|
|
||||||
const connectionIndex = Math.floor(Math.random() * this.connections.length);
|
|
||||||
const connection = this.connections[connectionIndex];
|
|
||||||
const connectionData = connection._data;
|
|
||||||
|
|
||||||
const startNode = this.nodes[connectionData.startNode];
|
|
||||||
const startData = startNode._data;
|
|
||||||
|
|
||||||
// Data Packet erstellen
|
|
||||||
const packet = document.createElement('div');
|
|
||||||
packet.className = 'data-packet';
|
|
||||||
|
|
||||||
// Position auf dem Startknoten
|
|
||||||
packet.style.left = `${startData.x}px`;
|
|
||||||
packet.style.top = `${startData.y}px`;
|
|
||||||
|
|
||||||
// Zufällige Geschwindigkeit
|
|
||||||
const travelTime = (4 + Math.random() * 4) / this.options.animationSpeed;
|
|
||||||
packet.style.setProperty('--travel-time', `${travelTime}s`);
|
|
||||||
|
|
||||||
// Ziel-Koordinaten berechnen
|
|
||||||
const endNode = this.nodes[connectionData.endNode];
|
|
||||||
const endData = endNode._data;
|
|
||||||
const travelX = endData.x - startData.x;
|
|
||||||
const travelY = endData.y - startData.y;
|
|
||||||
|
|
||||||
packet.style.setProperty('--travel-x', `${travelX}px`);
|
|
||||||
packet.style.setProperty('--travel-y', `${travelY}px`);
|
|
||||||
|
|
||||||
// Farb-Variation
|
|
||||||
if (Math.random() > 0.5) {
|
|
||||||
packet.style.background = 'rgba(76, 223, 255, 0.8)'; // Akzentfarbe
|
|
||||||
}
|
|
||||||
|
|
||||||
this.containerElement.appendChild(packet);
|
|
||||||
this.packets.push(packet);
|
|
||||||
|
|
||||||
// Nach Ende der Animation neues Paket erstellen
|
|
||||||
setTimeout(() => {
|
|
||||||
if (this.containerElement.contains(packet)) {
|
|
||||||
this.containerElement.removeChild(packet);
|
|
||||||
}
|
|
||||||
|
|
||||||
const index = this.packets.indexOf(packet);
|
|
||||||
if (index > -1) {
|
|
||||||
this.packets.splice(index, 1);
|
|
||||||
this.createNewDataPacket();
|
|
||||||
}
|
|
||||||
}, travelTime * 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleResize() {
|
|
||||||
if (!this.initialized) return;
|
|
||||||
|
|
||||||
// Bei Größenänderung alles neu generieren
|
|
||||||
this.reset();
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
reset() {
|
|
||||||
if (!this.initialized) return;
|
|
||||||
|
|
||||||
// Alle Elemente entfernen
|
|
||||||
this.nodes.forEach(node => node.remove());
|
|
||||||
this.connections.forEach(connection => connection.remove());
|
|
||||||
this.packets.forEach(packet => packet.remove());
|
|
||||||
|
|
||||||
this.nodes = [];
|
|
||||||
this.connections = [];
|
|
||||||
this.packets = [];
|
|
||||||
|
|
||||||
if (this.containerElement) {
|
|
||||||
this.containerElement.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.initialized = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
startAnimationCycle() {
|
|
||||||
// Regelmäßig neue Pakete erstellen für mehr Dynamik
|
|
||||||
setInterval(() => {
|
|
||||||
if (this.packets.length < this.options.packetCount * 1.5) {
|
|
||||||
this.createNewDataPacket();
|
|
||||||
}
|
|
||||||
}, 1000 / this.options.animationSpeed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exportieren als Modul
|
|
||||||
export default CyberNetwork;
|
|
||||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -477,6 +477,9 @@ class MindMapVisualization {
|
|||||||
this.nodes = processed.nodes;
|
this.nodes = processed.nodes;
|
||||||
this.links = processed.links;
|
this.links = processed.links;
|
||||||
|
|
||||||
|
// Verbindungszählungen aktualisieren
|
||||||
|
this.updateConnectionCounts();
|
||||||
|
|
||||||
// Visualisierung aktualisieren
|
// Visualisierung aktualisieren
|
||||||
this.updateVisualization();
|
this.updateVisualization();
|
||||||
|
|
||||||
@@ -492,6 +495,9 @@ class MindMapVisualization {
|
|||||||
this.nodes = this.defaultNodes;
|
this.nodes = this.defaultNodes;
|
||||||
this.links = this.defaultLinks;
|
this.links = this.defaultLinks;
|
||||||
|
|
||||||
|
// Verbindungszählungen auch für Fallback-Daten aktualisieren
|
||||||
|
this.updateConnectionCounts();
|
||||||
|
|
||||||
// Fehler anzeigen
|
// Fehler anzeigen
|
||||||
this.showError('Mindmap-Daten konnten nicht geladen werden. Verwende Standarddaten.');
|
this.showError('Mindmap-Daten konnten nicht geladen werden. Verwende Standarddaten.');
|
||||||
this.showFlash('Fehler beim Laden der Mindmap-Daten. Standarddaten werden angezeigt.', 'error');
|
this.showFlash('Fehler beim Laden der Mindmap-Daten. Standarddaten werden angezeigt.', 'error');
|
||||||
@@ -936,7 +942,7 @@ class MindMapVisualization {
|
|||||||
// Highlights für verbundene Nodes und Links hinzufügen
|
// Highlights für verbundene Nodes und Links hinzufügen
|
||||||
if (this.g) {
|
if (this.g) {
|
||||||
// Verbundene Nodes identifizieren
|
// Verbundene Nodes identifizieren
|
||||||
const connectedNodes = this.getConnectedNodes(d);
|
const connectedNodes = this.getConnectedNodesById(d.id);
|
||||||
const connectedNodeIds = connectedNodes.map(node => node.id);
|
const connectedNodeIds = connectedNodes.map(node => node.id);
|
||||||
|
|
||||||
// Alle Nodes etwas transparenter machen
|
// Alle Nodes etwas transparenter machen
|
||||||
@@ -1035,7 +1041,7 @@ class MindMapVisualization {
|
|||||||
}
|
}
|
||||||
// Falls ein Node ausgewählt ist, den Highlight-Status für diesen beibehalten
|
// Falls ein Node ausgewählt ist, den Highlight-Status für diesen beibehalten
|
||||||
else if (this.selectedNode && this.g) {
|
else if (this.selectedNode && this.g) {
|
||||||
const connectedNodes = this.getConnectedNodes(this.selectedNode);
|
const connectedNodes = this.getConnectedNodesById(this.selectedNode.id);
|
||||||
const connectedNodeIds = connectedNodes.map(node => node.id);
|
const connectedNodeIds = connectedNodes.map(node => node.id);
|
||||||
|
|
||||||
// Alle Nodes auf den richtigen Highlight-Status setzen
|
// Alle Nodes auf den richtigen Highlight-Status setzen
|
||||||
@@ -1101,23 +1107,87 @@ class MindMapVisualization {
|
|||||||
|
|
||||||
// Findet alle verbundenen Knoten zu einem gegebenen Knoten
|
// Findet alle verbundenen Knoten zu einem gegebenen Knoten
|
||||||
getConnectedNodes(node) {
|
getConnectedNodes(node) {
|
||||||
if (!this.links || !this.nodes) return [];
|
if (!this.links || !this.nodes || !node) return [];
|
||||||
|
|
||||||
|
// Sicherstellen, dass der Knoten eine ID hat
|
||||||
|
const nodeId = node.id || node;
|
||||||
|
|
||||||
return this.nodes.filter(n =>
|
return this.nodes.filter(n =>
|
||||||
this.links.some(link =>
|
this.links.some(link => {
|
||||||
(link.source.id === node.id && link.target.id === n.id) ||
|
const sourceId = link.source?.id || link.source;
|
||||||
(link.target.id === node.id && link.source.id === n.id)
|
const targetId = link.target?.id || link.target;
|
||||||
)
|
return (sourceId === nodeId && targetId === n.id) ||
|
||||||
|
(targetId === nodeId && sourceId === n.id);
|
||||||
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prüft, ob zwei Knoten verbunden sind
|
// Prüft, ob zwei Knoten verbunden sind
|
||||||
isConnected(a, b) {
|
isConnected(a, b) {
|
||||||
return this.links.some(link =>
|
if (!this.links || !a || !b) return false;
|
||||||
(link.source.id === a.id && link.target.id === b.id) ||
|
|
||||||
(link.target.id === a.id && link.source.id === b.id)
|
// Sicherstellen, dass die Knoten IDs haben
|
||||||
|
const aId = a.id || a;
|
||||||
|
const bId = b.id || b;
|
||||||
|
|
||||||
|
return this.links.some(link => {
|
||||||
|
const sourceId = link.source?.id || link.source;
|
||||||
|
const targetId = link.target?.id || link.target;
|
||||||
|
return (sourceId === aId && targetId === bId) ||
|
||||||
|
(targetId === aId && sourceId === bId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Überprüft, ob ein Link zwischen zwei Knoten existiert
|
||||||
|
hasLink(source, target) {
|
||||||
|
if (!this.links || !source || !target) return false;
|
||||||
|
|
||||||
|
// Sicherstellen, dass die Knoten IDs haben
|
||||||
|
const sourceId = source.id || source;
|
||||||
|
const targetId = target.id || target;
|
||||||
|
|
||||||
|
return this.links.some(link => {
|
||||||
|
const linkSourceId = link.source?.id || link.source;
|
||||||
|
const linkTargetId = link.target?.id || link.target;
|
||||||
|
return (linkSourceId === sourceId && linkTargetId === targetId) ||
|
||||||
|
(linkTargetId === sourceId && linkSourceId === targetId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sicherere Methode zum Abrufen verbundener Knoten, die Prüfungen enthält
|
||||||
|
getConnectedNodesById(nodeId) {
|
||||||
|
if (!this.links || !this.nodes || !nodeId) return [];
|
||||||
|
|
||||||
|
return this.nodes.filter(n =>
|
||||||
|
this.links.some(link => {
|
||||||
|
const sourceId = link.source.id || link.source;
|
||||||
|
const targetId = link.target.id || link.target;
|
||||||
|
return (sourceId === nodeId && targetId === n.id) ||
|
||||||
|
(targetId === nodeId && sourceId === n.id);
|
||||||
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Aktualisiert die Verbindungszählungen für alle Knoten
|
||||||
|
updateConnectionCounts() {
|
||||||
|
if (!this.nodes || !this.links) return;
|
||||||
|
|
||||||
|
// Für jeden Knoten die Anzahl der Verbindungen berechnen
|
||||||
|
this.nodes.forEach(node => {
|
||||||
|
// Sichere Methode, um verbundene Knoten zu zählen
|
||||||
|
const connectedNodes = this.nodes.filter(n =>
|
||||||
|
n.id !== node.id && this.links.some(link => {
|
||||||
|
const sourceId = link.source.id || link.source;
|
||||||
|
const targetId = link.target.id || link.target;
|
||||||
|
return (sourceId === node.id && targetId === n.id) ||
|
||||||
|
(targetId === node.id && sourceId === n.id);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Speichere die Anzahl als Eigenschaft des Knotens
|
||||||
|
node.connectionCount = connectedNodes.length;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Klick-Handler für Knoten
|
// Klick-Handler für Knoten
|
||||||
nodeClicked(event, d) {
|
nodeClicked(event, d) {
|
||||||
|
|||||||
@@ -1,88 +0,0 @@
|
|||||||
// Network Animation Effect
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
// Check if we're on the mindmap page
|
|
||||||
const mindmapContainer = document.getElementById('mindmap-container');
|
|
||||||
if (!mindmapContainer) return;
|
|
||||||
|
|
||||||
// Add enhanced animations for links and nodes
|
|
||||||
setTimeout(function() {
|
|
||||||
// Get all SVG links (connections between nodes)
|
|
||||||
const links = document.querySelectorAll('.link');
|
|
||||||
const nodes = document.querySelectorAll('.node');
|
|
||||||
|
|
||||||
// Add animation to links
|
|
||||||
links.forEach(link => {
|
|
||||||
// Create random animation duration between 15 and 30 seconds
|
|
||||||
const duration = 15 + Math.random() * 15;
|
|
||||||
link.style.animation = `dash ${duration}s linear infinite`;
|
|
||||||
link.style.strokeDasharray = '5, 5';
|
|
||||||
|
|
||||||
// Add pulse effect on hover
|
|
||||||
link.addEventListener('mouseover', function() {
|
|
||||||
this.classList.add('highlighted');
|
|
||||||
this.style.animation = 'dash 5s linear infinite';
|
|
||||||
});
|
|
||||||
|
|
||||||
link.addEventListener('mouseout', function() {
|
|
||||||
this.classList.remove('highlighted');
|
|
||||||
this.style.animation = `dash ${duration}s linear infinite`;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add effects to nodes
|
|
||||||
nodes.forEach(node => {
|
|
||||||
node.addEventListener('mouseover', function() {
|
|
||||||
this.querySelector('circle').style.filter = 'drop-shadow(0 0 15px rgba(179, 143, 255, 0.8))';
|
|
||||||
|
|
||||||
// Highlight connected links
|
|
||||||
const nodeId = this.getAttribute('data-id') || this.id;
|
|
||||||
links.forEach(link => {
|
|
||||||
const source = link.getAttribute('data-source');
|
|
||||||
const target = link.getAttribute('data-target');
|
|
||||||
|
|
||||||
if (source === nodeId || target === nodeId) {
|
|
||||||
link.classList.add('highlighted');
|
|
||||||
link.style.animation = 'dash 5s linear infinite';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
node.addEventListener('mouseout', function() {
|
|
||||||
this.querySelector('circle').style.filter = 'drop-shadow(0 0 8px rgba(179, 143, 255, 0.5))';
|
|
||||||
|
|
||||||
// Remove highlight from connected links
|
|
||||||
const nodeId = this.getAttribute('data-id') || this.id;
|
|
||||||
links.forEach(link => {
|
|
||||||
const source = link.getAttribute('data-source');
|
|
||||||
const target = link.getAttribute('data-target');
|
|
||||||
|
|
||||||
if (source === nodeId || target === nodeId) {
|
|
||||||
link.classList.remove('highlighted');
|
|
||||||
const duration = 15 + Math.random() * 15;
|
|
||||||
link.style.animation = `dash ${duration}s linear infinite`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}, 1000); // Wait for the mindmap to be fully loaded
|
|
||||||
|
|
||||||
// Add network background effect
|
|
||||||
const networkBackground = document.createElement('div');
|
|
||||||
networkBackground.className = 'network-background';
|
|
||||||
networkBackground.style.cssText = `
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: rgba(179, 143, 255, 0.05);
|
|
||||||
background-size: cover;
|
|
||||||
background-position: center;
|
|
||||||
opacity: 0.15;
|
|
||||||
z-index: -1;
|
|
||||||
pointer-events: none;
|
|
||||||
animation: pulse 10s ease-in-out infinite alternate;
|
|
||||||
`;
|
|
||||||
|
|
||||||
mindmapContainer.appendChild(networkBackground);
|
|
||||||
});
|
|
||||||
@@ -1,232 +0,0 @@
|
|||||||
// Animated Network Background
|
|
||||||
let canvas, ctx, networkImage;
|
|
||||||
let isImageLoaded = false;
|
|
||||||
let animationSpeed = 0.0003; // Reduzierte Geschwindigkeit für sanftere Rotation
|
|
||||||
let scaleSpeed = 0.0001; // Reduzierte Geschwindigkeit für sanftere Skalierung
|
|
||||||
let opacitySpeed = 0.0002; // Reduzierte Geschwindigkeit für sanftere Opazitätsänderung
|
|
||||||
let rotation = 0;
|
|
||||||
let scale = 1;
|
|
||||||
let opacity = 0.7; // Höhere Basisopazität für bessere Sichtbarkeit
|
|
||||||
let scaleDirection = 1;
|
|
||||||
let opacityDirection = 1;
|
|
||||||
let animationFrameId = null;
|
|
||||||
let isDarkMode = document.documentElement.classList.contains('dark');
|
|
||||||
let loadAttempts = 0;
|
|
||||||
const MAX_LOAD_ATTEMPTS = 2;
|
|
||||||
|
|
||||||
// Initialize the canvas and load the image
|
|
||||||
function initNetworkBackground() {
|
|
||||||
// Create canvas element if it doesn't exist
|
|
||||||
if (!document.getElementById('network-background')) {
|
|
||||||
canvas = document.createElement('canvas');
|
|
||||||
canvas.id = 'network-background';
|
|
||||||
canvas.style.position = 'fixed';
|
|
||||||
canvas.style.top = '0';
|
|
||||||
canvas.style.left = '0';
|
|
||||||
canvas.style.width = '100%';
|
|
||||||
canvas.style.height = '100%';
|
|
||||||
canvas.style.zIndex = '-5'; // Höher als -10 für den full-page-bg
|
|
||||||
canvas.style.pointerEvents = 'none'; // Stellt sicher, dass der Canvas keine Mausinteraktionen blockiert
|
|
||||||
document.body.appendChild(canvas);
|
|
||||||
} else {
|
|
||||||
canvas = document.getElementById('network-background');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set canvas size to window size with pixel ratio consideration
|
|
||||||
resizeCanvas();
|
|
||||||
|
|
||||||
// Get context with alpha enabled
|
|
||||||
ctx = canvas.getContext('2d', { alpha: true });
|
|
||||||
|
|
||||||
// Load the network image - versuche zuerst die SVG-Version
|
|
||||||
networkImage = new Image();
|
|
||||||
networkImage.crossOrigin = "anonymous"; // Vermeidet CORS-Probleme
|
|
||||||
|
|
||||||
// Keine Bilder laden, direkt Fallback-Hintergrund verwenden
|
|
||||||
console.log("Verwende einfachen Hintergrund ohne Bilddateien");
|
|
||||||
isImageLoaded = true; // Animation ohne Hintergrundbild starten
|
|
||||||
startAnimation();
|
|
||||||
|
|
||||||
// Handle window resize
|
|
||||||
window.addEventListener('resize', debounce(resizeCanvas, 250));
|
|
||||||
|
|
||||||
// Überwache Dark Mode-Änderungen
|
|
||||||
document.addEventListener('darkModeToggled', function(event) {
|
|
||||||
isDarkMode = event.detail.isDark;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hilfsfunktion zur Reduzierung der Resize-Event-Aufrufe
|
|
||||||
function debounce(func, wait) {
|
|
||||||
let timeout;
|
|
||||||
return function() {
|
|
||||||
const context = this;
|
|
||||||
const args = arguments;
|
|
||||||
clearTimeout(timeout);
|
|
||||||
timeout = setTimeout(function() {
|
|
||||||
func.apply(context, args);
|
|
||||||
}, wait);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resize canvas to match window size with proper pixel ratio
|
|
||||||
function resizeCanvas() {
|
|
||||||
if (!canvas) return;
|
|
||||||
|
|
||||||
const pixelRatio = window.devicePixelRatio || 1;
|
|
||||||
const width = window.innerWidth;
|
|
||||||
const height = window.innerHeight;
|
|
||||||
|
|
||||||
// Set display size (css pixels)
|
|
||||||
canvas.style.width = width + 'px';
|
|
||||||
canvas.style.height = height + 'px';
|
|
||||||
|
|
||||||
// Set actual size in memory (scaled for pixel ratio)
|
|
||||||
canvas.width = width * pixelRatio;
|
|
||||||
canvas.height = height * pixelRatio;
|
|
||||||
|
|
||||||
// Scale context to match pixel ratio
|
|
||||||
if (ctx) {
|
|
||||||
ctx.scale(pixelRatio, pixelRatio);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wenn Animation läuft und Bild geladen, zeichne erneut
|
|
||||||
if (isImageLoaded && animationFrameId) {
|
|
||||||
drawNetworkImage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start animation
|
|
||||||
function startAnimation() {
|
|
||||||
if (animationFrameId) {
|
|
||||||
cancelAnimationFrame(animationFrameId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start animation loop
|
|
||||||
animate();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw network image
|
|
||||||
function drawNetworkImage() {
|
|
||||||
if (!ctx) return;
|
|
||||||
|
|
||||||
// Clear canvas with proper clear method
|
|
||||||
ctx.clearRect(0, 0, canvas.width / (window.devicePixelRatio || 1), canvas.height / (window.devicePixelRatio || 1));
|
|
||||||
|
|
||||||
// Save context state
|
|
||||||
ctx.save();
|
|
||||||
|
|
||||||
// Move to center of canvas
|
|
||||||
ctx.translate(canvas.width / (2 * (window.devicePixelRatio || 1)), canvas.height / (2 * (window.devicePixelRatio || 1)));
|
|
||||||
|
|
||||||
// Rotate
|
|
||||||
ctx.rotate(rotation);
|
|
||||||
|
|
||||||
// Scale
|
|
||||||
ctx.scale(scale, scale);
|
|
||||||
|
|
||||||
// Set global opacity, angepasst für Dark Mode
|
|
||||||
ctx.globalAlpha = isDarkMode ? opacity : opacity * 0.8;
|
|
||||||
|
|
||||||
if (isImageLoaded && networkImage.complete) {
|
|
||||||
// Bildgröße berechnen, um den Bildschirm abzudecken
|
|
||||||
const imgAspect = networkImage.width / networkImage.height;
|
|
||||||
const canvasAspect = canvas.width / canvas.height;
|
|
||||||
|
|
||||||
let drawWidth, drawHeight;
|
|
||||||
|
|
||||||
if (canvasAspect > imgAspect) {
|
|
||||||
drawWidth = canvas.width / (window.devicePixelRatio || 1);
|
|
||||||
drawHeight = drawWidth / imgAspect;
|
|
||||||
} else {
|
|
||||||
drawHeight = canvas.height / (window.devicePixelRatio || 1);
|
|
||||||
drawWidth = drawHeight * imgAspect;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw image centered
|
|
||||||
ctx.drawImage(
|
|
||||||
networkImage,
|
|
||||||
-drawWidth / 2,
|
|
||||||
-drawHeight / 2,
|
|
||||||
drawWidth,
|
|
||||||
drawHeight
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// Fallback: Zeichne einen einfachen Hintergrund mit Punkten
|
|
||||||
drawFallbackBackground();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore context state
|
|
||||||
ctx.restore();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback-Hintergrund mit Punkten und Linien
|
|
||||||
function drawFallbackBackground() {
|
|
||||||
const width = canvas.width / (window.devicePixelRatio || 1);
|
|
||||||
const height = canvas.height / (window.devicePixelRatio || 1);
|
|
||||||
|
|
||||||
// Zeichne einige zufällige Punkte
|
|
||||||
ctx.fillStyle = isDarkMode ? 'rgba(139, 92, 246, 0.2)' : 'rgba(139, 92, 246, 0.1)';
|
|
||||||
|
|
||||||
for (let i = 0; i < 50; i++) {
|
|
||||||
const x = Math.random() * width;
|
|
||||||
const y = Math.random() * height;
|
|
||||||
const radius = Math.random() * 3 + 1;
|
|
||||||
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(x - width/2, y - height/2, radius, 0, Math.PI * 2);
|
|
||||||
ctx.fill();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Animation loop
|
|
||||||
function animate() {
|
|
||||||
// Update animation parameters
|
|
||||||
rotation += animationSpeed;
|
|
||||||
|
|
||||||
// Update scale with oscillation
|
|
||||||
scale += scaleSpeed * scaleDirection;
|
|
||||||
if (scale > 1.05) { // Kleinerer Skalierungsbereich für weniger starke Größenänderung
|
|
||||||
scaleDirection = -1;
|
|
||||||
} else if (scale < 0.95) {
|
|
||||||
scaleDirection = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update opacity with oscillation
|
|
||||||
opacity += opacitySpeed * opacityDirection;
|
|
||||||
if (opacity > 0.75) { // Kleinerer Opazitätsbereich für subtilere Änderungen
|
|
||||||
opacityDirection = -1;
|
|
||||||
} else if (opacity < 0.65) {
|
|
||||||
opacityDirection = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw the image
|
|
||||||
drawNetworkImage();
|
|
||||||
|
|
||||||
// Request next frame
|
|
||||||
animationFrameId = requestAnimationFrame(animate);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cleanup Funktion für Speicherbereinigung
|
|
||||||
function cleanupNetworkBackground() {
|
|
||||||
if (animationFrameId) {
|
|
||||||
cancelAnimationFrame(animationFrameId);
|
|
||||||
animationFrameId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (canvas && canvas.parentNode) {
|
|
||||||
canvas.parentNode.removeChild(canvas);
|
|
||||||
}
|
|
||||||
|
|
||||||
window.removeEventListener('resize', resizeCanvas);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Führe Initialisierung aus, wenn DOM geladen ist
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', initNetworkBackground);
|
|
||||||
} else {
|
|
||||||
initNetworkBackground();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Führe Cleanup durch, wenn das Fenster geschlossen wird
|
|
||||||
window.addEventListener('beforeunload', cleanupNetworkBackground);
|
|
||||||
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
6
static/three.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -1,100 +0,0 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
|
||||||
module.exports = {
|
|
||||||
darkMode: 'class',
|
|
||||||
content: [
|
|
||||||
"./templates/**/*.{html,jinja,jinja2}",
|
|
||||||
"./static/**/*.js"
|
|
||||||
],
|
|
||||||
theme: {
|
|
||||||
extend: {
|
|
||||||
colors: {
|
|
||||||
primary: {
|
|
||||||
50: '#eef5ff',
|
|
||||||
100: '#d9e7ff',
|
|
||||||
200: '#bcd4ff',
|
|
||||||
300: '#8eb8ff',
|
|
||||||
400: '#5a93ff',
|
|
||||||
500: '#2970ff',
|
|
||||||
600: '#1654f6',
|
|
||||||
700: '#1142e2',
|
|
||||||
800: '#1336b7',
|
|
||||||
900: '#153390',
|
|
||||||
},
|
|
||||||
secondary: {
|
|
||||||
50: '#f5f2ff',
|
|
||||||
100: '#ece8ff',
|
|
||||||
200: '#ddd5ff',
|
|
||||||
300: '#c4b3ff',
|
|
||||||
400: '#a685ff',
|
|
||||||
500: '#8b55ff',
|
|
||||||
600: '#7833f8',
|
|
||||||
700: '#6924e2',
|
|
||||||
800: '#5720b8',
|
|
||||||
900: '#481c96',
|
|
||||||
},
|
|
||||||
dark: {
|
|
||||||
50: '#f8f8f9',
|
|
||||||
100: '#e7e7ea',
|
|
||||||
200: '#d1d1d8',
|
|
||||||
300: '#aeaeba',
|
|
||||||
400: '#8a8a99',
|
|
||||||
500: '#6f6f7e',
|
|
||||||
600: '#5b5b69',
|
|
||||||
700: '#49494f',
|
|
||||||
800: '#2c2c33',
|
|
||||||
900: '#18181c',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
fontFamily: {
|
|
||||||
'sans': ['Inter', 'ui-sans-serif', 'system-ui', '-apple-system', 'BlinkMacSystemFont', 'sans-serif'],
|
|
||||||
'mono': ['JetBrains Mono', 'ui-monospace', 'SFMono-Regular', 'monospace']
|
|
||||||
},
|
|
||||||
backgroundImage: {
|
|
||||||
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
|
|
||||||
'gradient-conic': 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
|
|
||||||
'gradient-tech': 'linear-gradient(to right, var(--tw-gradient-stops))',
|
|
||||||
},
|
|
||||||
animation: {
|
|
||||||
'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
|
||||||
'float': 'float 6s ease-in-out infinite',
|
|
||||||
},
|
|
||||||
keyframes: {
|
|
||||||
float: {
|
|
||||||
'0%, 100%': { transform: 'translateY(0)' },
|
|
||||||
'50%': { transform: 'translateY(-10px)' },
|
|
||||||
}
|
|
||||||
},
|
|
||||||
typography: {
|
|
||||||
DEFAULT: {
|
|
||||||
css: {
|
|
||||||
color: 'rgb(31, 41, 55)',
|
|
||||||
a: {
|
|
||||||
color: 'rgb(41, 112, 255)',
|
|
||||||
'&:hover': {
|
|
||||||
color: 'rgb(22, 84, 246)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dark: {
|
|
||||||
css: {
|
|
||||||
color: 'rgb(229, 231, 235)',
|
|
||||||
a: {
|
|
||||||
color: 'rgb(90, 147, 255)',
|
|
||||||
'&:hover': {
|
|
||||||
color: 'rgb(142, 184, 255)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
boxShadow: {
|
|
||||||
'soft': '0 4px 15px rgba(0, 0, 0, 0.05)',
|
|
||||||
'glow': '0 0 15px rgba(32, 92, 245, 0.3)'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
// Typography and forms plugins removed, we'll implement their basic functionality in CSS
|
|
||||||
],
|
|
||||||
}
|
|
||||||
@@ -14,9 +14,11 @@
|
|||||||
<meta name="keywords" content="systades, wissen, visualisierung, lernen, gedanken, theorie">
|
<meta name="keywords" content="systades, wissen, visualisierung, lernen, gedanken, theorie">
|
||||||
<meta name="author" content="Systades-Team">
|
<meta name="author" content="Systades-Team">
|
||||||
|
|
||||||
<!-- Tailwind CSS über CDN -->
|
<!-- Tailwind CSS - CDN für Entwicklung und Produktion (laut Vorgabe) -->
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<!-- Alternative lokale Version, falls die CDN-Version blockiert wird -->
|
||||||
<script>
|
<script>
|
||||||
|
tailwind = window.tailwind || {};
|
||||||
tailwind.config = {
|
tailwind.config = {
|
||||||
darkMode: 'class',
|
darkMode: 'class',
|
||||||
theme: {
|
theme: {
|
||||||
@@ -63,17 +65,15 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Fonts -->
|
<!-- Local Font Files -->
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link href="{{ url_for('static', filename='fonts/inter.css') }}" rel="stylesheet">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link href="{{ url_for('static', filename='fonts/jetbrains-mono.css') }}" rel="stylesheet">
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
|
|
||||||
|
|
||||||
<!-- Icons -->
|
<!-- Font Awesome vom CDN -->
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" rel="stylesheet">
|
||||||
|
|
||||||
<!-- Assistent CSS -->
|
<!-- Assistent CSS -->
|
||||||
<link href="{{ url_for('static', filename='css/assistant.css') }}" rel="stylesheet">
|
<link href="{{ url_for('static', filename='css/assistant.css') }}" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/src/cybernetwork-bg.css') }}">
|
|
||||||
|
|
||||||
<!-- Basis-Stylesheet -->
|
<!-- Basis-Stylesheet -->
|
||||||
<link href="{{ url_for('static', filename='style.css') }}" rel="stylesheet">
|
<link href="{{ url_for('static', filename='style.css') }}" rel="stylesheet">
|
||||||
@@ -81,95 +81,178 @@
|
|||||||
<!-- Base-Styles ausgelagert in eigene Datei -->
|
<!-- Base-Styles ausgelagert in eigene Datei -->
|
||||||
<link href="{{ url_for('static', filename='css/base-styles.css') }}" rel="stylesheet">
|
<link href="{{ url_for('static', filename='css/base-styles.css') }}" rel="stylesheet">
|
||||||
|
|
||||||
<!-- Alpine.js -->
|
<!-- Alpine.js - CDN Version -->
|
||||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.12.3/dist/cdn.min.js"></script>
|
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.12.3/dist/cdn.min.js"></script>
|
||||||
|
|
||||||
<!-- Network Background Script -->
|
<!-- Neural Network Background CSS -->
|
||||||
<script src="{{ url_for('static', filename='network-background.js') }}"></script>
|
<link href="{{ url_for('static', filename='css/neural-network-background.css') }}" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- D3.js für Visualisierungen -->
|
||||||
|
<script src="https://d3js.org/d3.v7.min.js"></script>
|
||||||
|
|
||||||
<!-- Hauptmodul laden (als ES6 Modul) -->
|
<!-- Marked.js für Markdown-Parsing -->
|
||||||
<script type="module">
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||||
import MindMap from "{{ url_for('static', filename='js/main.js') }}";
|
|
||||||
// Alpine.js-Integration
|
<!-- ChatGPT Assistant -->
|
||||||
document.addEventListener('alpine:init', () => {
|
<script src="{{ url_for('static', filename='js/modules/chatgpt-assistant.js') }}"></script>
|
||||||
Alpine.data('layout', () => ({
|
|
||||||
darkMode: false,
|
<!-- MindMap Visualization Module -->
|
||||||
mobileMenuOpen: false,
|
<script src="{{ url_for('static', filename='js/modules/mindmap.js') }}"></script>
|
||||||
userMenuOpen: false,
|
|
||||||
showSettingsModal: false,
|
<!-- MindMap Page Module -->
|
||||||
|
<script src="{{ url_for('static', filename='js/modules/mindmap-page.js') }}"></script>
|
||||||
init() {
|
|
||||||
this.fetchDarkModeFromSession();
|
<!-- Neural Network Background Script -->
|
||||||
},
|
<script src="{{ url_for('static', filename='neural-network-background.js') }}"></script>
|
||||||
|
|
||||||
fetchDarkModeFromSession() {
|
<!-- Hauptmodul laden (als traditionelles Skript) -->
|
||||||
// Lade den Dark Mode-Status vom Server
|
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
|
||||||
fetch('/get_dark_mode')
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
if (data.success) {
|
|
||||||
this.darkMode = data.darkMode === 'true';
|
|
||||||
document.querySelector('html').classList.toggle('dark', this.darkMode);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Fehler beim Laden der Dark Mode-Einstellung:', error);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
toggleDarkMode() {
|
|
||||||
this.darkMode = !this.darkMode;
|
|
||||||
document.querySelector('html').classList.toggle('dark', this.darkMode);
|
|
||||||
|
|
||||||
// Speichere den Dark Mode-Status auf dem Server
|
|
||||||
fetch('/set_dark_mode', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ darkMode: this.darkMode })
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
if (data.success) {
|
|
||||||
// Zusätzlich im localStorage speichern für sofortige Reaktion bei Seitenwechsel
|
|
||||||
localStorage.setItem('darkMode', this.darkMode ? 'dark' : 'light');
|
|
||||||
// Event auslösen für andere Komponenten
|
|
||||||
document.dispatchEvent(new CustomEvent('darkModeToggled', {
|
|
||||||
detail: { isDark: this.darkMode }
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
console.error('Fehler beim Speichern der Dark Mode-Einstellung:', data.error);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Fehler beim Speichern der Dark Mode-Einstellung:', error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
// MindMap global verfügbar machen (für Alpine.js und andere nicht-Module Skripte)
|
|
||||||
window.MindMap = MindMap;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Seitenspezifische Styles -->
|
<!-- Seitenspezifische Styles -->
|
||||||
{% block extra_css %}{% endblock %}
|
{% block extra_css %}{% endblock %}
|
||||||
|
|
||||||
<!-- Cybertechnisches Netzwerk-Hintergrund -->
|
<!-- Custom dark/light mode styles -->
|
||||||
<script type="module" src="{{ url_for('static', filename='js/modules/cyber-network-init.js') }}"></script>
|
<!-- ► ► Farb‑Token strikt getrennt ◄ ◄ -->
|
||||||
|
<style>
|
||||||
|
/* Light‑Mode */
|
||||||
|
:root {
|
||||||
|
--bg-primary:#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); }
|
||||||
|
|
||||||
|
/* Light-Mode spezifische Stile */
|
||||||
|
body:not(.dark) {
|
||||||
|
background-color: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link-light {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link-light:hover {
|
||||||
|
color: var(--text-primary);
|
||||||
|
background-color: rgba(126, 34, 206, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link-light-active {
|
||||||
|
color: var(--accent-primary);
|
||||||
|
background-color: rgba(126, 34, 206, 0.15);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Kartendesign im Light-Mode */
|
||||||
|
body:not(.dark) .card {
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .card:hover {
|
||||||
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body data-page="{{ request.endpoint }}" class="relative overflow-x-hidden">
|
<body data-page="{{ request.endpoint }}" class="relative overflow-x-hidden dark bg-gray-900 text-white" x-data="{
|
||||||
<!-- Cybertechnisches Netzwerk-Hintergrund Container (wird via JavaScript befüllt) -->
|
darkMode: true,
|
||||||
<div id="cyber-background-container" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: -1; pointer-events: none; overflow: hidden;"></div>
|
mobileMenuOpen: false,
|
||||||
|
userMenuOpen: false,
|
||||||
|
showSettingsModal: false,
|
||||||
|
|
||||||
<!-- Globaler Hintergrund -->
|
init() {
|
||||||
<div class="full-page-bg"></div>
|
this.initDarkMode();
|
||||||
<!-- Statischer Fallback-Hintergrund (wird nur angezeigt, wenn JavaScript deaktiviert ist) -->
|
},
|
||||||
<div class="fixed inset-0 z-[-9] bg-cover bg-center opacity-50"></div>
|
|
||||||
|
|
||||||
|
initDarkMode() {
|
||||||
|
// Lade zuerst den Wert aus dem localStorage (client-seitig)
|
||||||
|
const storedMode = localStorage.getItem('colorMode');
|
||||||
|
if (storedMode) {
|
||||||
|
this.darkMode = storedMode === 'dark';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dann hole die Server-Einstellung, die Vorrang hat
|
||||||
|
this.fetchDarkModeFromSession();
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchDarkModeFromSession() {
|
||||||
|
fetch('/api/get_dark_mode')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
this.darkMode = data.darkMode === 'true';
|
||||||
|
this.applyDarkMode();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Fehler beim Laden der Dark Mode-Einstellung:', error);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
applyDarkMode() {
|
||||||
|
document.querySelector('html').classList.toggle('dark', this.darkMode);
|
||||||
|
document.querySelector('body').classList.toggle('dark', this.darkMode);
|
||||||
|
localStorage.setItem('colorMode', this.darkMode ? 'dark' : 'light');
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleDarkMode() {
|
||||||
|
this.darkMode = !this.darkMode;
|
||||||
|
this.applyDarkMode();
|
||||||
|
|
||||||
|
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) {
|
||||||
|
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 -->
|
<!-- App-Container -->
|
||||||
<div id="app-container" class="flex flex-col min-h-screen" x-data="layout">
|
<div id="app-container" class="flex flex-col min-h-screen">
|
||||||
<!-- Hauptnavigation -->
|
<!-- Hauptnavigation -->
|
||||||
<nav class="sticky top-0 left-0 right-0 z-50 transition-all duration-300 py-4 px-5 border-b glass-morphism"
|
<nav class="sticky top-0 left-0 right-0 z-50 transition-all duration-300 py-4 px-5 border-b glass-morphism"
|
||||||
x-bind:class="darkMode ? 'glass-navbar-dark border-white/10' : 'glass-navbar-light border-gray-200/50'">
|
x-bind:class="darkMode ? 'glass-navbar-dark border-white/10' : 'glass-navbar-light border-gray-200/50'">
|
||||||
@@ -195,6 +278,13 @@
|
|||||||
: '{{ 'nav-link-light-active' if request.endpoint == 'mindmap' else 'nav-link-light' }}'">
|
: '{{ 'nav-link-light-active' if request.endpoint == 'mindmap' else 'nav-link-light' }}'">
|
||||||
<i class="fa-solid fa-diagram-project mr-2"></i>Mindmap
|
<i class="fa-solid fa-diagram-project mr-2"></i>Mindmap
|
||||||
</a>
|
</a>
|
||||||
|
<a href="{{ url_for('forum') }}"
|
||||||
|
class="nav-link flex items-center"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? '{{ 'nav-link-active' if request.endpoint == 'forum' else '' }}'
|
||||||
|
: '{{ 'nav-link-light-active' if request.endpoint == 'forum' else 'nav-link-light' }}'">
|
||||||
|
<i class="fa-solid fa-users mr-2"></i>Community
|
||||||
|
</a>
|
||||||
<a href="{{ url_for('search_thoughts_page') }}"
|
<a href="{{ url_for('search_thoughts_page') }}"
|
||||||
class="nav-link flex items-center"
|
class="nav-link flex items-center"
|
||||||
x-bind:class="darkMode
|
x-bind:class="darkMode
|
||||||
@@ -206,8 +296,8 @@
|
|||||||
<button onclick="window.MindMap && window.MindMap.assistant && window.MindMap.assistant.toggleAssistant(true)"
|
<button onclick="window.MindMap && window.MindMap.assistant && window.MindMap.assistant.toggleAssistant(true)"
|
||||||
class="nav-link flex items-center"
|
class="nav-link flex items-center"
|
||||||
x-bind:class="darkMode
|
x-bind:class="darkMode
|
||||||
? 'bg-gradient-to-r from-purple-600/80 to-blue-500/80 text-white font-medium px-4 py-2 rounded-xl hover:shadow-lg transition-all duration-300 hover:-translate-y-0.5'
|
? 'bg-gradient-to-r from-purple-900/90 to-indigo-800/90 text-white font-medium px-4 py-2 rounded-xl hover:shadow-lg hover:shadow-purple-800/30 transition-all duration-300'
|
||||||
: 'bg-gradient-to-r from-purple-500/20 to-blue-400/20 text-gray-800 font-medium px-4 py-2 rounded-xl hover:shadow-md transition-all duration-300 hover:-translate-y-0.5'">
|
: 'bg-gradient-to-r from-purple-600/30 to-indigo-500/30 text-gray-800 font-medium px-4 py-2 rounded-xl hover:shadow-md transition-all duration-300'">
|
||||||
<i class="fa-solid fa-robot mr-2"></i>KI-Chat
|
<i class="fa-solid fa-robot mr-2"></i>KI-Chat
|
||||||
</button>
|
</button>
|
||||||
{% if current_user.is_authenticated %}
|
{% if current_user.is_authenticated %}
|
||||||
@@ -228,9 +318,9 @@
|
|||||||
<div class="relative w-12 h-6">
|
<div class="relative w-12 h-6">
|
||||||
<input type="checkbox" id="darkModeToggle" class="sr-only" x-model="darkMode">
|
<input type="checkbox" id="darkModeToggle" class="sr-only" x-model="darkMode">
|
||||||
<div class="block w-12 h-6 rounded-full transition-colors duration-300"
|
<div class="block w-12 h-6 rounded-full transition-colors duration-300"
|
||||||
x-bind:class="darkMode ? 'bg-blue-400/50' : 'bg-gray-400/50'"></div>
|
x-bind:class="darkMode ? 'bg-purple-800/50' : 'bg-gray-400/50'"></div>
|
||||||
<div class="dot absolute left-1 top-1 w-4 h-4 rounded-full transition-transform duration-300 shadow-md"
|
<div class="dot absolute left-1 top-1 w-4 h-4 rounded-full transition-transform duration-300 shadow-md"
|
||||||
x-bind:class="darkMode ? 'bg-blue-500 transform translate-x-6' : 'bg-white'"></div>
|
x-bind:class="darkMode ? 'bg-purple-600 transform translate-x-6' : 'bg-white'"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-3 hidden sm:block"
|
<div class="ml-3 hidden sm:block"
|
||||||
x-bind:class="darkMode ? 'text-white/90' : 'text-gray-700'">
|
x-bind:class="darkMode ? 'text-white/90' : 'text-gray-700'">
|
||||||
@@ -308,13 +398,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{{ url_for('login') }}"
|
<div class="flex items-center space-x-2">
|
||||||
class="flex items-center px-4 py-2.5 rounded-xl font-medium transition-all duration-300"
|
<a href="{{ url_for('login') }}"
|
||||||
x-bind:class="darkMode
|
class="py-2 px-4 rounded-lg transition-all duration-300"
|
||||||
? 'bg-gray-800/80 text-white hover:bg-gray-700/80 shadow-md hover:shadow-lg hover:-translate-y-0.5'
|
x-bind:class="darkMode
|
||||||
: 'bg-gray-200/80 text-gray-800 hover:bg-gray-300/80 shadow-sm hover:shadow-md hover:-translate-y-0.5'">
|
? 'text-white/90 hover:bg-dark-700/80'
|
||||||
<i class="fa-solid fa-user mr-2"></i>Mein Konto
|
: 'text-gray-700 hover:bg-gray-100/80'">
|
||||||
</a>
|
<i class="fa-solid fa-sign-in-alt mr-2"></i>Login
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('register') }}"
|
||||||
|
class="py-2 px-4 rounded-lg transition-all duration-300 font-medium"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-purple-800/80 text-white hover:bg-purple-700/80'
|
||||||
|
: 'bg-purple-600/20 text-gray-700 hover:bg-purple-600/30'">
|
||||||
|
Registrieren
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Mobilmenü-Button -->
|
<!-- Mobilmenü-Button -->
|
||||||
@@ -331,6 +430,7 @@
|
|||||||
|
|
||||||
<!-- Mobile Menü -->
|
<!-- Mobile Menü -->
|
||||||
<div x-show="mobileMenuOpen"
|
<div x-show="mobileMenuOpen"
|
||||||
|
x-cloak
|
||||||
x-transition:enter="transition ease-out duration-200"
|
x-transition:enter="transition ease-out duration-200"
|
||||||
x-transition:enter-start="opacity-0 -translate-y-4"
|
x-transition:enter-start="opacity-0 -translate-y-4"
|
||||||
x-transition:enter-end="opacity-100 translate-y-0"
|
x-transition:enter-end="opacity-100 translate-y-0"
|
||||||
@@ -356,6 +456,13 @@
|
|||||||
: '{{ 'bg-purple-500/10 text-gray-900' if request.endpoint == 'mindmap' else 'text-gray-700 hover:bg-gray-100 hover:text-gray-900' }}'">
|
: '{{ 'bg-purple-500/10 text-gray-900' if request.endpoint == 'mindmap' else 'text-gray-700 hover:bg-gray-100 hover:text-gray-900' }}'">
|
||||||
<i class="fa-solid fa-diagram-project w-5 mr-3"></i>Mindmap
|
<i class="fa-solid fa-diagram-project w-5 mr-3"></i>Mindmap
|
||||||
</a>
|
</a>
|
||||||
|
<a href="{{ url_for('forum') }}"
|
||||||
|
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 == 'forum' else 'text-white/80 hover:bg-gray-800/80 hover:text-white' }}'
|
||||||
|
: '{{ 'bg-purple-500/10 text-gray-900' if request.endpoint == 'forum' else 'text-gray-700 hover:bg-gray-100 hover:text-gray-900' }}'">
|
||||||
|
<i class="fa-solid fa-users w-5 mr-3"></i>Community
|
||||||
|
</a>
|
||||||
<a href="{{ url_for('search_thoughts_page') }}"
|
<a href="{{ url_for('search_thoughts_page') }}"
|
||||||
class="block py-3.5 px-4 rounded-xl transition-all duration-200 flex items-center"
|
class="block py-3.5 px-4 rounded-xl transition-all duration-200 flex items-center"
|
||||||
x-bind:class="darkMode
|
x-bind:class="darkMode
|
||||||
@@ -434,6 +541,10 @@
|
|||||||
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
||||||
Mindmap
|
Mindmap
|
||||||
</a>
|
</a>
|
||||||
|
<a href="{{ url_for('forum') }}" class="text-sm transition-all duration-200"
|
||||||
|
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
||||||
|
Community
|
||||||
|
</a>
|
||||||
{% if current_user.is_authenticated %}
|
{% if current_user.is_authenticated %}
|
||||||
<a href="{{ url_for('profile') }}" class="text-sm transition-all duration-200"
|
<a href="{{ url_for('profile') }}" class="text-sm transition-all duration-200"
|
||||||
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
||||||
@@ -462,6 +573,10 @@
|
|||||||
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
||||||
Impressum
|
Impressum
|
||||||
</a>
|
</a>
|
||||||
|
<a href="{{ url_for('ueber_uns') }}" class="text-sm transition-all duration-200"
|
||||||
|
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
||||||
|
Über uns
|
||||||
|
</a>
|
||||||
<a href="{{ url_for('datenschutz') }}" class="text-sm transition-all duration-200"
|
<a href="{{ url_for('datenschutz') }}" class="text-sm transition-all duration-200"
|
||||||
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
||||||
Datenschutz
|
Datenschutz
|
||||||
@@ -511,11 +626,9 @@
|
|||||||
{% block scripts %}{% endblock %}
|
{% block scripts %}{% endblock %}
|
||||||
|
|
||||||
<!-- KI-Chat Initialisierung -->
|
<!-- KI-Chat Initialisierung -->
|
||||||
<script type="module">
|
<script>
|
||||||
// Importiere und initialisiere den ChatGPT-Assistenten direkt, um sicherzustellen,
|
// Initialisiere den ChatGPT-Assistenten direkt, um sicherzustellen,
|
||||||
// dass er auf jeder Seite verfügbar ist, selbst wenn MindMap nicht geladen ist
|
// dass er auf jeder Seite verfügbar ist, selbst wenn MindMap nicht geladen ist
|
||||||
import ChatGPTAssistant from "{{ url_for('static', filename='js/modules/chatgpt-assistant.js') }}";
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
// Prüfen, ob der Assistent bereits durch MindMap initialisiert wurde
|
// Prüfen, ob der Assistent bereits durch MindMap initialisiert wurde
|
||||||
if (!window.MindMap || !window.MindMap.assistant) {
|
if (!window.MindMap || !window.MindMap.assistant) {
|
||||||
@@ -531,5 +644,43 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- Dark/Light-Mode vereinheitlicht -->
|
||||||
|
<script>
|
||||||
|
// Globaler Zugriff für externe Skripte
|
||||||
|
window.MindMap = window.MindMap || {};
|
||||||
|
|
||||||
|
window.MindMap.toggleDarkMode = function() {
|
||||||
|
// Alpine.js-Instanz benutzen, wenn verfügbar
|
||||||
|
const appEl = document.querySelector('body');
|
||||||
|
if (appEl && appEl.__x) {
|
||||||
|
appEl.__x.$data.toggleDarkMode();
|
||||||
|
} else {
|
||||||
|
// Fallback: Nur classList und localStorage
|
||||||
|
const isDark = document.documentElement.classList.contains('dark');
|
||||||
|
document.documentElement.classList.toggle('dark', !isDark);
|
||||||
|
document.body.classList.toggle('dark', !isDark);
|
||||||
|
localStorage.setItem('colorMode', !isDark ? 'dark' : 'light');
|
||||||
|
|
||||||
|
// Server aktualisieren
|
||||||
|
fetch('/api/set_dark_mode', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ darkMode: !isDark })
|
||||||
|
}).catch(console.error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fallback für Browser-Präferenz, falls keine Einstellung geladen werden konnte
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
if (!document.body.classList.contains('dark') && !document.documentElement.classList.contains('dark')) {
|
||||||
|
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
if (prefersDark) {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
document.body.classList.add('dark');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
192
templates/community/category.html
Normal file
192
templates/community/category.html
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}{{ category.title }} - Forum{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<style>
|
||||||
|
.thread-item {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
.thread-item:hover {
|
||||||
|
transform: translateX(2px);
|
||||||
|
}
|
||||||
|
.thread-pinned {
|
||||||
|
border-left-width: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mx-auto px-4 py-8">
|
||||||
|
<!-- Breadcrumb Navigation -->
|
||||||
|
<div class="mb-6 flex items-center text-sm">
|
||||||
|
<a href="{{ url_for('community') }}" class="opacity-75 hover:opacity-100 transition-opacity">
|
||||||
|
<i class="fas fa-home mr-1"></i> Forum
|
||||||
|
</a>
|
||||||
|
<span class="mx-2 opacity-50">/</span>
|
||||||
|
<span class="font-medium">{{ category.title }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Kategorie-Header -->
|
||||||
|
<div class="mb-8 flex flex-wrap items-center justify-between gap-4">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<!-- Kategorie-Icon -->
|
||||||
|
<div class="w-12 h-12 rounded-xl mr-4 flex items-center justify-center text-white"
|
||||||
|
style="background-color: {{ node.color_code or '#6d28d9' }}">
|
||||||
|
<i class="fas {{ node.icon or 'fa-folder' }} text-2xl"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Kategorie-Info -->
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-bold">{{ category.title }}</h1>
|
||||||
|
<p class="opacity-75">{{ category.description }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Neues Thema erstellen -->
|
||||||
|
<a href="{{ url_for('new_post', category_id=category.id) }}"
|
||||||
|
class="px-5 py-2.5 rounded-lg transition-all duration-300 flex items-center"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-indigo-700 hover:bg-indigo-600 text-white'
|
||||||
|
: 'bg-indigo-500 hover:bg-indigo-600 text-white'">
|
||||||
|
<i class="fas fa-plus-circle mr-2"></i>
|
||||||
|
Neues Thema
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Threads anzeigen -->
|
||||||
|
<div class="mb-8 rounded-xl overflow-hidden"
|
||||||
|
x-bind:class="darkMode ? 'bg-gray-800/60 border border-white/10' : 'bg-white border border-gray-200'">
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="p-4 border-b" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
|
||||||
|
<div class="grid grid-cols-12 gap-4">
|
||||||
|
<div class="col-span-7 font-medium">Thema</div>
|
||||||
|
<div class="col-span-1 text-center font-medium hidden md:block">Antworten</div>
|
||||||
|
<div class="col-span-2 text-center font-medium hidden md:block">Autor</div>
|
||||||
|
<div class="col-span-2 text-center font-medium hidden md:block">Letzte Antwort</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Thread-Liste -->
|
||||||
|
{% if threads_data %}
|
||||||
|
{% for thread_data in threads_data %}
|
||||||
|
{% set thread = thread_data.thread %}
|
||||||
|
<div class="thread-item p-4 border-b last:border-b-0 {{ 'thread-pinned' if thread.is_pinned }}"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'border-white/10 hover:bg-gray-700/50 {{ 'border-l-yellow-500' if thread.is_pinned }}'
|
||||||
|
: 'border-gray-200 hover:bg-gray-50 {{ 'border-l-yellow-500' if thread.is_pinned }}'">
|
||||||
|
<a href="{{ url_for('forum_post', post_id=thread.id) }}" class="block">
|
||||||
|
<div class="grid grid-cols-12 gap-4">
|
||||||
|
<!-- Thema -->
|
||||||
|
<div class="col-span-12 md:col-span-7">
|
||||||
|
<div class="flex items-start">
|
||||||
|
<!-- Status-Icons -->
|
||||||
|
<div class="flex flex-col items-center mr-3 pt-1">
|
||||||
|
{% if thread.is_pinned %}
|
||||||
|
<i class="fas fa-thumbtack text-yellow-500" title="Angepinnt"></i>
|
||||||
|
{% endif %}
|
||||||
|
{% if thread.is_locked %}
|
||||||
|
<i class="fas fa-lock text-red-500 mt-1" title="Gesperrt"></i>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Themen-Info -->
|
||||||
|
<div>
|
||||||
|
<h3 class="font-medium leading-snug mb-1 {% if thread.is_locked %}opacity-70{% endif %}">
|
||||||
|
{{ thread.title }}
|
||||||
|
</h3>
|
||||||
|
<div class="flex items-center text-xs opacity-70 mt-1">
|
||||||
|
<span><i class="fas fa-eye mr-1"></i> {{ thread.view_count }}</span>
|
||||||
|
<span class="mx-2 block md:hidden">•</span>
|
||||||
|
<span class="block md:hidden"><i class="fas fa-reply mr-1"></i> {{ thread_data.reply_count }}</span>
|
||||||
|
<span class="mx-2">•</span>
|
||||||
|
<span><i class="fas fa-clock mr-1"></i> {{ thread.created_at.strftime('%d.%m.%Y, %H:%M') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Antworten -->
|
||||||
|
<div class="col-span-1 text-center hidden md:flex items-center justify-center">
|
||||||
|
<span class="px-2.5 py-1 rounded-full text-sm font-medium"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-indigo-900/40 text-indigo-300'
|
||||||
|
: 'bg-indigo-100 text-indigo-800'">
|
||||||
|
{{ thread_data.reply_count }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Autor -->
|
||||||
|
<div class="col-span-2 text-center hidden md:flex items-center justify-center">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="w-7 h-7 rounded-full flex items-center justify-center text-white text-xs font-medium overflow-hidden mr-2"
|
||||||
|
style="background: linear-gradient(135deg, #8b5cf6, #6366f1);">
|
||||||
|
{% if thread.author.avatar %}
|
||||||
|
<img src="{{ thread.author.avatar }}" alt="{{ thread.author.username }}" class="w-full h-full object-cover">
|
||||||
|
{% else %}
|
||||||
|
{{ thread.author.username[0].upper() }}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<span class="text-sm truncate max-w-[80px]">{{ thread.author.username }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Letzte Antwort -->
|
||||||
|
<div class="col-span-2 text-center hidden md:block text-sm">
|
||||||
|
{% if thread_data.latest_reply %}
|
||||||
|
<div>{{ thread_data.latest_reply.created_at.strftime('%d.%m.%Y') }}</div>
|
||||||
|
<div class="opacity-75 text-xs">{{ thread_data.latest_reply.created_at.strftime('%H:%M') }} Uhr</div>
|
||||||
|
{% else %}
|
||||||
|
<span class="opacity-60">Keine Antworten</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<div class="p-8 text-center">
|
||||||
|
<div class="text-3xl mb-3 opacity-30"><i class="fas fa-comments"></i></div>
|
||||||
|
<h3 class="text-xl font-semibold mb-2">Keine Themen vorhanden</h3>
|
||||||
|
<p class="opacity-75 mb-4">In dieser Kategorie wurden noch keine Themen erstellt.</p>
|
||||||
|
<a href="{{ url_for('new_post', category_id=category.id) }}"
|
||||||
|
class="inline-block px-5 py-2.5 rounded-lg transition-all duration-300"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-indigo-700 hover:bg-indigo-600 text-white'
|
||||||
|
: 'bg-indigo-500 hover:bg-indigo-600 text-white'">
|
||||||
|
<i class="fas fa-plus-circle mr-2"></i>
|
||||||
|
Erstes Thema erstellen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Link zur Mindmap -->
|
||||||
|
<div class="rounded-xl p-5 mb-4 flex items-center"
|
||||||
|
x-bind:class="darkMode ? 'bg-purple-900/20 border border-purple-800/30' : 'bg-purple-50 border border-purple-100'">
|
||||||
|
<div class="text-3xl mr-4 opacity-80">
|
||||||
|
<i class="fas fa-diagram-project" style="color: {{ node.color_code }}"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-medium mb-1">Mindmap-Knotenpunkt: {{ node.name }}</h3>
|
||||||
|
<p class="text-sm opacity-75">In der Mindmap findest du weitere Informationen zu diesem Themenbereich.</p>
|
||||||
|
</div>
|
||||||
|
<div class="ml-auto">
|
||||||
|
<a href="{{ url_for('mindmap') }}"
|
||||||
|
class="px-4 py-2 rounded-lg inline-block text-sm transition-all"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-purple-800/60 hover:bg-purple-700/60 text-white'
|
||||||
|
: 'bg-white hover:bg-purple-100 text-purple-800 border border-purple-200'">
|
||||||
|
Zur Mindmap
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
// Hier können bei Bedarf kategoriespezifische Scripts eingefügt werden
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
344
templates/community/edit_post.html
Normal file
344
templates/community/edit_post.html
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Beitrag bearbeiten{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<style>
|
||||||
|
.markdown-preview {
|
||||||
|
min-height: 200px;
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.markdown-preview p {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.markdown-preview h1, .markdown-preview h2, .markdown-preview h3,
|
||||||
|
.markdown-preview h4, .markdown-preview h5, .markdown-preview h6 {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.markdown-preview h1 { font-size: 1.8rem; }
|
||||||
|
.markdown-preview h2 { font-size: 1.5rem; }
|
||||||
|
.markdown-preview h3 { font-size: 1.3rem; }
|
||||||
|
.markdown-preview h4 { font-size: 1.1rem; }
|
||||||
|
.markdown-preview ul, .markdown-preview ol {
|
||||||
|
margin-left: 1.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.markdown-preview ul { list-style-type: disc; }
|
||||||
|
.markdown-preview ol { list-style-type: decimal; }
|
||||||
|
.markdown-preview pre {
|
||||||
|
background-color: rgba(0,0,0,0.05);
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
.markdown-preview code {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 0.9em;
|
||||||
|
padding: 0.1em 0.3em;
|
||||||
|
border-radius: 0.3em;
|
||||||
|
background-color: rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
.markdown-preview pre code {
|
||||||
|
padding: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
.markdown-preview blockquote {
|
||||||
|
border-left: 4px solid;
|
||||||
|
padding-left: 1rem;
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
.dark .markdown-preview code {
|
||||||
|
background-color: rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
.dark .markdown-preview blockquote {
|
||||||
|
border-color: rgba(255,255,255,0.2);
|
||||||
|
}
|
||||||
|
.node-mention {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
background-color: rgba(109, 40, 217, 0.1);
|
||||||
|
color: #6d28d9;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 1px 6px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
margin: 0 2px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.dark .node-mention {
|
||||||
|
background-color: rgba(167, 139, 250, 0.2);
|
||||||
|
color: #a78bfa;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mx-auto px-4 py-8">
|
||||||
|
<!-- Breadcrumb Navigation -->
|
||||||
|
<div class="mb-6 flex items-center text-sm">
|
||||||
|
<a href="{{ url_for('community') }}" class="opacity-75 hover:opacity-100 transition-opacity">
|
||||||
|
<i class="fas fa-home mr-1"></i> Forum
|
||||||
|
</a>
|
||||||
|
<span class="mx-2 opacity-50">/</span>
|
||||||
|
<a href="{{ url_for('forum_category', category_id=post.category_id) }}" class="opacity-75 hover:opacity-100 transition-opacity">
|
||||||
|
{{ post.category.title }}
|
||||||
|
</a>
|
||||||
|
<span class="mx-2 opacity-50">/</span>
|
||||||
|
{% if post.parent_id %}
|
||||||
|
<a href="{{ url_for('forum_post', post_id=post.parent_id) }}" class="opacity-75 hover:opacity-100 transition-opacity truncate max-w-[200px]">
|
||||||
|
{{ post.parent.title }}
|
||||||
|
</a>
|
||||||
|
<span class="mx-2 opacity-50">/</span>
|
||||||
|
{% endif %}
|
||||||
|
<span class="font-medium">Beitrag bearbeiten</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Formular-Header -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<h1 class="text-2xl font-bold mb-2">Beitrag bearbeiten</h1>
|
||||||
|
<p class="opacity-75">
|
||||||
|
{% if post.parent_id %}
|
||||||
|
Antwort auf <span class="font-medium">{{ post.parent.title }}</span>
|
||||||
|
{% else %}
|
||||||
|
in der Kategorie <span class="font-medium">{{ post.category.title }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Formular -->
|
||||||
|
<div class="mb-8 rounded-xl overflow-hidden"
|
||||||
|
x-bind:class="darkMode ? 'bg-gray-800/60 border border-white/10' : 'bg-white border border-gray-200'">
|
||||||
|
<div class="p-4 border-b font-medium" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
|
||||||
|
<i class="fas fa-edit mr-2"></i>
|
||||||
|
Beitrag bearbeiten
|
||||||
|
</div>
|
||||||
|
<div class="p-6">
|
||||||
|
<form action="{{ url_for('edit_post', post_id=post.id) }}" method="POST" x-data="{
|
||||||
|
title: '{{ post.title|safe }}',
|
||||||
|
content: '{{ post.content|replace('\n', '\\n')|replace('\'', '\\\'')|safe }}',
|
||||||
|
showPreview: false,
|
||||||
|
previewHtml: '',
|
||||||
|
|
||||||
|
updatePreview() {
|
||||||
|
// Verarbeite den Inhalt
|
||||||
|
if (this.content.trim() === '') {
|
||||||
|
this.previewHtml = '<div class=\'opacity-50 italic\'>Die Vorschau wird hier angezeigt...</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verarbeite Markdown
|
||||||
|
let html = marked.parse(this.content);
|
||||||
|
|
||||||
|
// Ersetze @Knotenname mit entsprechenden Links
|
||||||
|
html = html.replace(/@([a-zA-Z0-9äöüÄÖÜß_-]+)/g, '<span class=\'node-mention\'><i class=\'fas fa-diagram-project fa-xs mr-1\'></i>$1</span>');
|
||||||
|
|
||||||
|
this.previewHtml = html;
|
||||||
|
}
|
||||||
|
}">
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="title" class="block mb-2 font-medium">Titel</label>
|
||||||
|
<div class="rounded-lg overflow-hidden"
|
||||||
|
x-bind:class="darkMode ? 'border border-white/20' : 'border border-gray-300'">
|
||||||
|
<input type="text" id="title" name="title"
|
||||||
|
class="w-full px-4 py-3"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-gray-700/50 text-white placeholder-gray-400 focus:border-indigo-500'
|
||||||
|
: 'bg-white text-gray-700 placeholder-gray-400 focus:border-indigo-500'"
|
||||||
|
x-model="title"
|
||||||
|
required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<div class="flex justify-between items-center mb-2">
|
||||||
|
<label for="content" class="font-medium">Inhalt</label>
|
||||||
|
<div class="flex space-x-2">
|
||||||
|
<button type="button"
|
||||||
|
class="px-3 py-1 rounded text-sm flex items-center"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-gray-700 hover:bg-gray-600 text-white'
|
||||||
|
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'"
|
||||||
|
@click="showPreview = false"
|
||||||
|
x-bind:disabled="!showPreview"
|
||||||
|
x-bind:class="{'opacity-50': !showPreview}">
|
||||||
|
<i class="fas fa-edit mr-1"></i> Bearbeiten
|
||||||
|
</button>
|
||||||
|
<button type="button"
|
||||||
|
class="px-3 py-1 rounded text-sm flex items-center"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-gray-700 hover:bg-gray-600 text-white'
|
||||||
|
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'"
|
||||||
|
@click="updatePreview(); showPreview = true"
|
||||||
|
x-bind:disabled="showPreview"
|
||||||
|
x-bind:class="{'opacity-50': showPreview}">
|
||||||
|
<i class="fas fa-eye mr-1"></i> Vorschau
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Editor -->
|
||||||
|
<div class="rounded-lg overflow-hidden mb-2"
|
||||||
|
x-bind:class="darkMode ? 'border border-white/20' : 'border border-gray-300'"
|
||||||
|
x-show="!showPreview">
|
||||||
|
<textarea id="content" name="content" rows="12"
|
||||||
|
class="w-full p-3 resize-y"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-gray-700/50 text-white placeholder-gray-400 focus:border-indigo-500'
|
||||||
|
: 'bg-white text-gray-700 placeholder-gray-400 focus:border-indigo-500'"
|
||||||
|
x-model="content"
|
||||||
|
required></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Preview -->
|
||||||
|
<div class="rounded-lg overflow-hidden mb-2 p-4 markdown-preview"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'border border-white/20 bg-gray-700/30'
|
||||||
|
: 'border border-gray-300 bg-gray-50'"
|
||||||
|
x-show="showPreview"
|
||||||
|
x-html="previewHtml">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Markdown-Hilfsmittel -->
|
||||||
|
<div class="mb-4" x-show="!showPreview">
|
||||||
|
<div class="text-xs opacity-70">
|
||||||
|
<p>Du kannst Knotenpunkte der Mindmap durch <code>@Knotenname</code> verlinken.</p>
|
||||||
|
<p>Dieser Editor unterstützt Markdown-Formatierung:</p>
|
||||||
|
<div class="flex flex-wrap gap-2 mt-1">
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="**" data-before="" data-after="" title="Fett">
|
||||||
|
<i class="fas fa-bold"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="*" data-before="" data-after="" title="Kursiv">
|
||||||
|
<i class="fas fa-italic"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="`" data-before="" data-after="" title="Code">
|
||||||
|
<i class="fas fa-code"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="[Link-Text](URL)" data-before="" data-after="" title="Link">
|
||||||
|
<i class="fas fa-link"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="\n```\nCode-Block\n```" data-before="" data-after="" title="Code-Block">
|
||||||
|
<i class="fas fa-file-code"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format=">" data-before="" data-after="" title="Zitat">
|
||||||
|
<i class="fas fa-quote-right"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="- " data-before="" data-after="" title="Liste">
|
||||||
|
<i class="fas fa-list-ul"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="1. " data-before="" data-after="" title="Nummerierte Liste">
|
||||||
|
<i class="fas fa-list-ol"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="# " data-before="" data-after="" title="Überschrift">
|
||||||
|
<i class="fas fa-heading"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<a href="{{ url_for('forum_post', post_id=post.parent_id or post.id) }}"
|
||||||
|
class="px-5 py-2.5 rounded-lg transition-all duration-300 flex items-center"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-gray-700 hover:bg-gray-600 text-white'
|
||||||
|
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'">
|
||||||
|
Abbrechen
|
||||||
|
</a>
|
||||||
|
<button type="submit"
|
||||||
|
class="px-5 py-2.5 rounded-lg transition-all duration-300 flex items-center"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-indigo-700 hover:bg-indigo-600 text-white'
|
||||||
|
: 'bg-indigo-500 hover:bg-indigo-600 text-white'">
|
||||||
|
<i class="fas fa-save mr-2"></i>
|
||||||
|
Änderungen speichern
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Markdown-Buttons für den Beitragseditor
|
||||||
|
document.querySelectorAll('.markdown-button').forEach(button => {
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
const textarea = document.getElementById('content');
|
||||||
|
const format = this.dataset.format;
|
||||||
|
const before = this.dataset.before || '';
|
||||||
|
const after = this.dataset.after || '';
|
||||||
|
|
||||||
|
// Hole die aktuelle Auswahl
|
||||||
|
const start = textarea.selectionStart;
|
||||||
|
const end = textarea.selectionEnd;
|
||||||
|
const selection = textarea.value.substring(start, end);
|
||||||
|
|
||||||
|
// Wende die Formatierung an
|
||||||
|
let formattedText;
|
||||||
|
if (format.includes('\n')) {
|
||||||
|
// Für Formate mit Zeilenumbrüchen (z.B. Code-Blöcke)
|
||||||
|
formattedText = format.replace('Code-Block', selection || 'Code-Block');
|
||||||
|
} else if (format.includes('[Link-Text](URL)')) {
|
||||||
|
formattedText = format.replace('Link-Text', selection || 'Link-Text');
|
||||||
|
} else if (format === '- ' || format === '1. ' || format === '# ' || format === '> ') {
|
||||||
|
// Für Listen und Überschriften: am Anfang der Zeile einfügen
|
||||||
|
const beforeSelection = textarea.value.substring(0, start);
|
||||||
|
const afterSelection = textarea.value.substring(end);
|
||||||
|
|
||||||
|
// Finde den Anfang der aktuellen Zeile
|
||||||
|
const lastNewline = beforeSelection.lastIndexOf('\n');
|
||||||
|
const lineStart = lastNewline === -1 ? 0 : lastNewline + 1;
|
||||||
|
|
||||||
|
// Füge das Format am Zeilenanfang ein
|
||||||
|
formattedText = beforeSelection.substring(0, lineStart) +
|
||||||
|
format +
|
||||||
|
beforeSelection.substring(lineStart) +
|
||||||
|
selection +
|
||||||
|
afterSelection;
|
||||||
|
|
||||||
|
// Setze die neue Cursor-Position
|
||||||
|
const newCursorPos = end + format.length;
|
||||||
|
textarea.value = formattedText;
|
||||||
|
textarea.setSelectionRange(newCursorPos, newCursorPos);
|
||||||
|
|
||||||
|
// Alpine.js Model aktualisieren
|
||||||
|
textarea.dispatchEvent(new Event('input'));
|
||||||
|
return; // Früher zurückkehren, da wir die Formatierung bereits angewendet haben
|
||||||
|
} else {
|
||||||
|
// Für einfache Formatierungen wie fett, kursiv, Code
|
||||||
|
formattedText = before + format + selection + format + after;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ersetze den Text
|
||||||
|
textarea.value = textarea.value.substring(0, start) + formattedText + textarea.value.substring(end);
|
||||||
|
|
||||||
|
// Setze den Fokus zurück auf das Textarea
|
||||||
|
textarea.focus();
|
||||||
|
|
||||||
|
// Alpine.js Model aktualisieren
|
||||||
|
textarea.dispatchEvent(new Event('input'));
|
||||||
|
|
||||||
|
// Setze die Auswahl neu, wenn es eine Auswahl gab
|
||||||
|
if (selection) {
|
||||||
|
const newStart = start + before.length + format.length;
|
||||||
|
const newEnd = newStart + selection.length;
|
||||||
|
textarea.setSelectionRange(newStart, newEnd);
|
||||||
|
} else {
|
||||||
|
// Setze den Cursor in die Mitte von **|** oder `|`
|
||||||
|
const newCursorPos = start + before.length + format.length;
|
||||||
|
textarea.setSelectionRange(newCursorPos, newCursorPos);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
125
templates/community/index.html
Normal file
125
templates/community/index.html
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Community Forum{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<style>
|
||||||
|
.forum-category {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
.forum-category:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
.category-icon {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
width: 2.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mx-auto px-4 py-8">
|
||||||
|
<!-- Seitenüberschrift -->
|
||||||
|
<div class="mb-8 text-center">
|
||||||
|
<h1 class="text-3xl font-bold mb-2 gradient-text">Community Forum</h1>
|
||||||
|
<p class="text-lg opacity-75">Diskutiere mit anderen Nutzern über die Hauptthemenbereiche der Mindmap</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Forumskategorien -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
||||||
|
{% if categories_data %}
|
||||||
|
{% for cat_data in categories_data %}
|
||||||
|
<a href="{{ url_for('forum_category', category_id=cat_data.category.id) }}" class="forum-category block">
|
||||||
|
<div class="rounded-xl p-5 h-full"
|
||||||
|
x-bind:class="darkMode ? 'bg-gray-800/60 hover:bg-gray-800/80 border border-white/10' : 'bg-white hover:bg-gray-50 border border-gray-200 shadow-md'">
|
||||||
|
<div class="flex items-start">
|
||||||
|
<!-- Kategorie-Icon -->
|
||||||
|
<div class="category-icon mr-4 text-white"
|
||||||
|
style="background-color: {{ cat_data.category.node.color_code or '#6d28d9' }}">
|
||||||
|
<i class="fas {{ cat_data.category.node.icon or 'fa-folder' }}"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Kategorie-Info -->
|
||||||
|
<div class="flex-grow">
|
||||||
|
<h3 class="text-xl font-semibold mb-2">{{ cat_data.category.title }}</h3>
|
||||||
|
<p class="opacity-75 text-sm mb-3">{{ cat_data.category.description }}</p>
|
||||||
|
|
||||||
|
<!-- Statistik -->
|
||||||
|
<div class="flex flex-wrap gap-4 text-sm opacity-80">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fas fa-comment-alt mr-2"></i>
|
||||||
|
<span>{{ cat_data.total_posts }} Themen</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fas fa-reply mr-2"></i>
|
||||||
|
<span>{{ cat_data.total_replies }} Antworten</span>
|
||||||
|
</div>
|
||||||
|
{% if cat_data.latest_post %}
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fas fa-clock mr-2"></i>
|
||||||
|
<span>Neuster Beitrag: {{ cat_data.latest_post.created_at.strftime('%d.%m.%Y, %H:%M') }}</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pfeil-Icon -->
|
||||||
|
<div class="ml-2">
|
||||||
|
<i class="fas fa-chevron-right opacity-50"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<div class="col-span-2 text-center py-8">
|
||||||
|
<div class="text-3xl mb-3 opacity-30"><i class="fas fa-exclamation-circle"></i></div>
|
||||||
|
<h3 class="text-xl font-semibold mb-2">Keine Forum-Kategorien gefunden</h3>
|
||||||
|
<p class="opacity-75">Es sind derzeit keine Kategorien für Diskussionen verfügbar.</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Hinweis zur Nutzung -->
|
||||||
|
<div class="rounded-xl p-6 text-center mb-8"
|
||||||
|
x-bind:class="darkMode ? 'bg-indigo-900/30 border border-indigo-700/30' : 'bg-indigo-50 border border-indigo-100'">
|
||||||
|
<h3 class="text-xl font-semibold mb-3">
|
||||||
|
<i class="fas fa-lightbulb mr-2 text-yellow-500"></i>
|
||||||
|
So funktioniert das Forum
|
||||||
|
</h3>
|
||||||
|
<p class="mb-4">Das Community-Forum ist nach den Hauptknotenpunkten der Systades-Mindmap strukturiert.
|
||||||
|
In deinen Beiträgen kannst du Knotenpunkte mit <code>@Knotenname</code> verlinken.</p>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mt-6">
|
||||||
|
<div class="p-4 rounded-lg"
|
||||||
|
x-bind:class="darkMode ? 'bg-indigo-800/40' : 'bg-white border border-indigo-100'">
|
||||||
|
<div class="text-2xl mb-2"><i class="fas fa-users text-indigo-400"></i></div>
|
||||||
|
<h4 class="font-medium mb-1">Fachliche Diskussionen</h4>
|
||||||
|
<p class="text-sm opacity-75">Tausche dich mit anderen zu spezifischen Themen aus</p>
|
||||||
|
</div>
|
||||||
|
<div class="p-4 rounded-lg"
|
||||||
|
x-bind:class="darkMode ? 'bg-indigo-800/40' : 'bg-white border border-indigo-100'">
|
||||||
|
<div class="text-2xl mb-2"><i class="fas fa-link text-indigo-400"></i></div>
|
||||||
|
<h4 class="font-medium mb-1">Wissensvernetzung</h4>
|
||||||
|
<p class="text-sm opacity-75">Verknüpfe Inhalte durch Knotenreferenzen</p>
|
||||||
|
</div>
|
||||||
|
<div class="p-4 rounded-lg"
|
||||||
|
x-bind:class="darkMode ? 'bg-indigo-800/40' : 'bg-white border border-indigo-100'">
|
||||||
|
<div class="text-2xl mb-2"><i class="fas fa-markdown text-indigo-400"></i></div>
|
||||||
|
<h4 class="font-medium mb-1">Markdown Support</h4>
|
||||||
|
<p class="text-sm opacity-75">Formatiere deine Beiträge mit Markdown</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
// Hier können bei Bedarf forumspezifische Scripts eingefügt werden
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
355
templates/community/new_post.html
Normal file
355
templates/community/new_post.html
Normal file
@@ -0,0 +1,355 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Neues Thema - {{ category.title }}{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<style>
|
||||||
|
.markdown-preview {
|
||||||
|
min-height: 200px;
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.markdown-preview p {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.markdown-preview h1, .markdown-preview h2, .markdown-preview h3,
|
||||||
|
.markdown-preview h4, .markdown-preview h5, .markdown-preview h6 {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.markdown-preview h1 { font-size: 1.8rem; }
|
||||||
|
.markdown-preview h2 { font-size: 1.5rem; }
|
||||||
|
.markdown-preview h3 { font-size: 1.3rem; }
|
||||||
|
.markdown-preview h4 { font-size: 1.1rem; }
|
||||||
|
.markdown-preview ul, .markdown-preview ol {
|
||||||
|
margin-left: 1.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.markdown-preview ul { list-style-type: disc; }
|
||||||
|
.markdown-preview ol { list-style-type: decimal; }
|
||||||
|
.markdown-preview pre {
|
||||||
|
background-color: rgba(0,0,0,0.05);
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
.markdown-preview code {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 0.9em;
|
||||||
|
padding: 0.1em 0.3em;
|
||||||
|
border-radius: 0.3em;
|
||||||
|
background-color: rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
.markdown-preview pre code {
|
||||||
|
padding: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
.markdown-preview blockquote {
|
||||||
|
border-left: 4px solid;
|
||||||
|
padding-left: 1rem;
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
.dark .markdown-preview code {
|
||||||
|
background-color: rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
.dark .markdown-preview blockquote {
|
||||||
|
border-color: rgba(255,255,255,0.2);
|
||||||
|
}
|
||||||
|
.node-mention {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
background-color: rgba(109, 40, 217, 0.1);
|
||||||
|
color: #6d28d9;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 1px 6px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
margin: 0 2px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.dark .node-mention {
|
||||||
|
background-color: rgba(167, 139, 250, 0.2);
|
||||||
|
color: #a78bfa;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mx-auto px-4 py-8">
|
||||||
|
<!-- Breadcrumb Navigation -->
|
||||||
|
<div class="mb-6 flex items-center text-sm">
|
||||||
|
<a href="{{ url_for('community') }}" class="opacity-75 hover:opacity-100 transition-opacity">
|
||||||
|
<i class="fas fa-home mr-1"></i> Forum
|
||||||
|
</a>
|
||||||
|
<span class="mx-2 opacity-50">/</span>
|
||||||
|
<a href="{{ url_for('forum_category', category_id=category.id) }}" class="opacity-75 hover:opacity-100 transition-opacity">
|
||||||
|
{{ category.title }}
|
||||||
|
</a>
|
||||||
|
<span class="mx-2 opacity-50">/</span>
|
||||||
|
<span class="font-medium">Neues Thema</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Formular-Header -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<h1 class="text-2xl font-bold mb-2">Neues Thema erstellen</h1>
|
||||||
|
<p class="opacity-75">in der Kategorie <span class="font-medium">{{ category.title }}</span></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Formular -->
|
||||||
|
<div class="mb-8 rounded-xl overflow-hidden"
|
||||||
|
x-bind:class="darkMode ? 'bg-gray-800/60 border border-white/10' : 'bg-white border border-gray-200'">
|
||||||
|
<div class="p-4 border-b font-medium" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
|
||||||
|
<i class="fas fa-plus-circle mr-2"></i>
|
||||||
|
Neues Thema
|
||||||
|
</div>
|
||||||
|
<div class="p-6">
|
||||||
|
<form action="{{ url_for('new_post', category_id=category.id) }}" method="POST" x-data="{
|
||||||
|
title: '',
|
||||||
|
content: '',
|
||||||
|
showPreview: false,
|
||||||
|
previewHtml: '',
|
||||||
|
|
||||||
|
updatePreview() {
|
||||||
|
// Verarbeite den Inhalt
|
||||||
|
if (this.content.trim() === '') {
|
||||||
|
this.previewHtml = '<div class=\'opacity-50 italic\'>Die Vorschau wird hier angezeigt...</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verarbeite Markdown
|
||||||
|
let html = marked.parse(this.content);
|
||||||
|
|
||||||
|
// Ersetze @Knotenname mit entsprechenden Links
|
||||||
|
html = html.replace(/@([a-zA-Z0-9äöüÄÖÜß_-]+)/g, '<span class=\'node-mention\'><i class=\'fas fa-diagram-project fa-xs mr-1\'></i>$1</span>');
|
||||||
|
|
||||||
|
this.previewHtml = html;
|
||||||
|
}
|
||||||
|
}">
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="title" class="block mb-2 font-medium">Titel des Themas</label>
|
||||||
|
<div class="rounded-lg overflow-hidden"
|
||||||
|
x-bind:class="darkMode ? 'border border-white/20' : 'border border-gray-300'">
|
||||||
|
<input type="text" id="title" name="title"
|
||||||
|
class="w-full px-4 py-3"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-gray-700/50 text-white placeholder-gray-400 focus:border-indigo-500'
|
||||||
|
: 'bg-white text-gray-700 placeholder-gray-400 focus:border-indigo-500'"
|
||||||
|
placeholder="Ein prägnanter Titel für dein Thema"
|
||||||
|
x-model="title"
|
||||||
|
required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<div class="flex justify-between items-center mb-2">
|
||||||
|
<label for="content" class="font-medium">Inhalt</label>
|
||||||
|
<div class="flex space-x-2">
|
||||||
|
<button type="button"
|
||||||
|
class="px-3 py-1 rounded text-sm flex items-center"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-gray-700 hover:bg-gray-600 text-white'
|
||||||
|
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'"
|
||||||
|
@click="showPreview = false"
|
||||||
|
x-bind:disabled="!showPreview"
|
||||||
|
x-bind:class="{'opacity-50': !showPreview}">
|
||||||
|
<i class="fas fa-edit mr-1"></i> Bearbeiten
|
||||||
|
</button>
|
||||||
|
<button type="button"
|
||||||
|
class="px-3 py-1 rounded text-sm flex items-center"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-gray-700 hover:bg-gray-600 text-white'
|
||||||
|
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'"
|
||||||
|
@click="updatePreview(); showPreview = true"
|
||||||
|
x-bind:disabled="showPreview"
|
||||||
|
x-bind:class="{'opacity-50': showPreview}">
|
||||||
|
<i class="fas fa-eye mr-1"></i> Vorschau
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Editor -->
|
||||||
|
<div class="rounded-lg overflow-hidden mb-2"
|
||||||
|
x-bind:class="darkMode ? 'border border-white/20' : 'border border-gray-300'"
|
||||||
|
x-show="!showPreview">
|
||||||
|
<textarea id="content" name="content" rows="12"
|
||||||
|
class="w-full p-3 resize-y"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-gray-700/50 text-white placeholder-gray-400 focus:border-indigo-500'
|
||||||
|
: 'bg-white text-gray-700 placeholder-gray-400 focus:border-indigo-500'"
|
||||||
|
placeholder="Schreibe deinen Beitrag hier (unterstützt Markdown und @Knotenname-Erwähnungen)..."
|
||||||
|
x-model="content"
|
||||||
|
required></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Preview -->
|
||||||
|
<div class="rounded-lg overflow-hidden mb-2 p-4 markdown-preview"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'border border-white/20 bg-gray-700/30'
|
||||||
|
: 'border border-gray-300 bg-gray-50'"
|
||||||
|
x-show="showPreview"
|
||||||
|
x-html="previewHtml">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Markdown-Hilfsmittel -->
|
||||||
|
<div class="mb-4" x-show="!showPreview">
|
||||||
|
<div class="text-xs opacity-70">
|
||||||
|
<p>Du kannst Knotenpunkte der Mindmap durch <code>@Knotenname</code> verlinken.</p>
|
||||||
|
<p>Dieser Editor unterstützt Markdown-Formatierung:</p>
|
||||||
|
<div class="flex flex-wrap gap-2 mt-1">
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="**" data-before="" data-after="" title="Fett">
|
||||||
|
<i class="fas fa-bold"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="*" data-before="" data-after="" title="Kursiv">
|
||||||
|
<i class="fas fa-italic"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="`" data-before="" data-after="" title="Code">
|
||||||
|
<i class="fas fa-code"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="[Link-Text](URL)" data-before="" data-after="" title="Link">
|
||||||
|
<i class="fas fa-link"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="\n```\nCode-Block\n```" data-before="" data-after="" title="Code-Block">
|
||||||
|
<i class="fas fa-file-code"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format=">" data-before="" data-after="" title="Zitat">
|
||||||
|
<i class="fas fa-quote-right"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="- " data-before="" data-after="" title="Liste">
|
||||||
|
<i class="fas fa-list-ul"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="1. " data-before="" data-after="" title="Nummerierte Liste">
|
||||||
|
<i class="fas fa-list-ol"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="# " data-before="" data-after="" title="Überschrift">
|
||||||
|
<i class="fas fa-heading"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<a href="{{ url_for('forum_category', category_id=category.id) }}"
|
||||||
|
class="px-5 py-2.5 rounded-lg transition-all duration-300 flex items-center"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-gray-700 hover:bg-gray-600 text-white'
|
||||||
|
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'">
|
||||||
|
Abbrechen
|
||||||
|
</a>
|
||||||
|
<button type="submit"
|
||||||
|
class="px-5 py-2.5 rounded-lg transition-all duration-300 flex items-center"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-indigo-700 hover:bg-indigo-600 text-white'
|
||||||
|
: 'bg-indigo-500 hover:bg-indigo-600 text-white'">
|
||||||
|
<i class="fas fa-paper-plane mr-2"></i>
|
||||||
|
Thema erstellen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Link zur Mindmap -->
|
||||||
|
<div class="rounded-xl p-5 mb-4 flex items-center"
|
||||||
|
x-bind:class="darkMode ? 'bg-purple-900/20 border border-purple-800/30' : 'bg-purple-50 border border-purple-100'">
|
||||||
|
<div class="text-3xl mr-4 opacity-80">
|
||||||
|
<i class="fas fa-diagram-project" style="color: {{ category.node.color_code }}"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-medium mb-1">Mindmap-Knotenpunkt: {{ category.node.name }}</h3>
|
||||||
|
<p class="text-sm opacity-75">Dieser Diskussionsbereich ist mit dem Mindmap-Knotenpunkt "{{ category.node.name }}" verknüpft.</p>
|
||||||
|
</div>
|
||||||
|
<div class="ml-auto">
|
||||||
|
<a href="{{ url_for('mindmap') }}"
|
||||||
|
class="px-4 py-2 rounded-lg inline-block text-sm transition-all"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-purple-800/60 hover:bg-purple-700/60 text-white'
|
||||||
|
: 'bg-white hover:bg-purple-100 text-purple-800 border border-purple-200'">
|
||||||
|
Zur Mindmap
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Markdown-Buttons für den Beitragseditor
|
||||||
|
document.querySelectorAll('.markdown-button').forEach(button => {
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
const textarea = document.getElementById('content');
|
||||||
|
const format = this.dataset.format;
|
||||||
|
const before = this.dataset.before || '';
|
||||||
|
const after = this.dataset.after || '';
|
||||||
|
|
||||||
|
// Hole die aktuelle Auswahl
|
||||||
|
const start = textarea.selectionStart;
|
||||||
|
const end = textarea.selectionEnd;
|
||||||
|
const selection = textarea.value.substring(start, end);
|
||||||
|
|
||||||
|
// Wende die Formatierung an
|
||||||
|
let formattedText;
|
||||||
|
if (format.includes('\n')) {
|
||||||
|
// Für Formate mit Zeilenumbrüchen (z.B. Code-Blöcke)
|
||||||
|
formattedText = format.replace('Code-Block', selection || 'Code-Block');
|
||||||
|
} else if (format.includes('[Link-Text](URL)')) {
|
||||||
|
formattedText = format.replace('Link-Text', selection || 'Link-Text');
|
||||||
|
} else if (format === '- ' || format === '1. ' || format === '# ' || format === '> ') {
|
||||||
|
// Für Listen und Überschriften: am Anfang der Zeile einfügen
|
||||||
|
const beforeSelection = textarea.value.substring(0, start);
|
||||||
|
const afterSelection = textarea.value.substring(end);
|
||||||
|
|
||||||
|
// Finde den Anfang der aktuellen Zeile
|
||||||
|
const lastNewline = beforeSelection.lastIndexOf('\n');
|
||||||
|
const lineStart = lastNewline === -1 ? 0 : lastNewline + 1;
|
||||||
|
|
||||||
|
// Füge das Format am Zeilenanfang ein
|
||||||
|
formattedText = beforeSelection.substring(0, lineStart) +
|
||||||
|
format +
|
||||||
|
beforeSelection.substring(lineStart) +
|
||||||
|
selection +
|
||||||
|
afterSelection;
|
||||||
|
|
||||||
|
// Setze die neue Cursor-Position
|
||||||
|
const newCursorPos = end + format.length;
|
||||||
|
textarea.value = formattedText;
|
||||||
|
textarea.setSelectionRange(newCursorPos, newCursorPos);
|
||||||
|
|
||||||
|
// Alpine.js Model aktualisieren
|
||||||
|
textarea.dispatchEvent(new Event('input'));
|
||||||
|
return; // Früher zurückkehren, da wir die Formatierung bereits angewendet haben
|
||||||
|
} else {
|
||||||
|
// Für einfache Formatierungen wie fett, kursiv, Code
|
||||||
|
formattedText = before + format + selection + format + after;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ersetze den Text
|
||||||
|
textarea.value = textarea.value.substring(0, start) + formattedText + textarea.value.substring(end);
|
||||||
|
|
||||||
|
// Setze den Fokus zurück auf das Textarea
|
||||||
|
textarea.focus();
|
||||||
|
|
||||||
|
// Alpine.js Model aktualisieren
|
||||||
|
textarea.dispatchEvent(new Event('input'));
|
||||||
|
|
||||||
|
// Setze die Auswahl neu, wenn es eine Auswahl gab
|
||||||
|
if (selection) {
|
||||||
|
const newStart = start + before.length + format.length;
|
||||||
|
const newEnd = newStart + selection.length;
|
||||||
|
textarea.setSelectionRange(newStart, newEnd);
|
||||||
|
} else {
|
||||||
|
// Setze den Cursor in die Mitte von **|** oder `|`
|
||||||
|
const newCursorPos = start + before.length + format.length;
|
||||||
|
textarea.setSelectionRange(newCursorPos, newCursorPos);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
491
templates/community/post.html
Normal file
491
templates/community/post.html
Normal file
@@ -0,0 +1,491 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}{{ post.title }} - Forum{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<style>
|
||||||
|
.post-content {
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
.post-content p {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.post-content h1, .post-content h2, .post-content h3,
|
||||||
|
.post-content h4, .post-content h5, .post-content h6 {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.post-content h1 { font-size: 1.8rem; }
|
||||||
|
.post-content h2 { font-size: 1.5rem; }
|
||||||
|
.post-content h3 { font-size: 1.3rem; }
|
||||||
|
.post-content h4 { font-size: 1.1rem; }
|
||||||
|
.post-content ul, .post-content ol {
|
||||||
|
margin-left: 1.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.post-content ul { list-style-type: disc; }
|
||||||
|
.post-content ol { list-style-type: decimal; }
|
||||||
|
.post-content pre {
|
||||||
|
background-color: rgba(0,0,0,0.05);
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
.post-content code {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 0.9em;
|
||||||
|
padding: 0.1em 0.3em;
|
||||||
|
border-radius: 0.3em;
|
||||||
|
background-color: rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
.post-content pre code {
|
||||||
|
padding: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
.post-content blockquote {
|
||||||
|
border-left: 4px solid;
|
||||||
|
padding-left: 1rem;
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
.post-content img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
.post-content table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
.post-content th, .post-content td {
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 1px solid;
|
||||||
|
border-color: rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
.post-content th {
|
||||||
|
background-color: rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
.post-content a {
|
||||||
|
color: #6d28d9;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.post-content a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.dark .post-content code {
|
||||||
|
background-color: rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
.dark .post-content th, .dark .post-content td {
|
||||||
|
border-color: rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
.dark .post-content th {
|
||||||
|
background-color: rgba(255,255,255,0.05);
|
||||||
|
}
|
||||||
|
.dark .post-content a {
|
||||||
|
color: #a78bfa;
|
||||||
|
}
|
||||||
|
.node-mention {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
background-color: rgba(109, 40, 217, 0.1);
|
||||||
|
color: #6d28d9;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 1px 6px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
margin: 0 2px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.dark .node-mention {
|
||||||
|
background-color: rgba(167, 139, 250, 0.2);
|
||||||
|
color: #a78bfa;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mx-auto px-4 py-8">
|
||||||
|
<!-- Breadcrumb Navigation -->
|
||||||
|
<div class="mb-6 flex items-center text-sm">
|
||||||
|
<a href="{{ url_for('community') }}" class="opacity-75 hover:opacity-100 transition-opacity">
|
||||||
|
<i class="fas fa-home mr-1"></i> Forum
|
||||||
|
</a>
|
||||||
|
<span class="mx-2 opacity-50">/</span>
|
||||||
|
<a href="{{ url_for('forum_category', category_id=category.id) }}" class="opacity-75 hover:opacity-100 transition-opacity">
|
||||||
|
{{ category.title }}
|
||||||
|
</a>
|
||||||
|
<span class="mx-2 opacity-50">/</span>
|
||||||
|
<span class="font-medium truncate max-w-[300px]">{{ post.title }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Beitrags-Header -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<h1 class="text-2xl font-bold mb-2">{{ post.title }}</h1>
|
||||||
|
<div class="flex flex-wrap items-center gap-3 text-sm opacity-75">
|
||||||
|
<span><i class="fas fa-calendar-alt mr-1"></i> {{ post.created_at.strftime('%d.%m.%Y, %H:%M') }}</span>
|
||||||
|
<span><i class="fas fa-eye mr-1"></i> {{ post.view_count }} Aufrufe</span>
|
||||||
|
<span><i class="fas fa-reply mr-1"></i> {{ replies|length }} Antworten</span>
|
||||||
|
|
||||||
|
{% if post.is_pinned or post.is_locked %}
|
||||||
|
<div class="flex gap-2 ml-2">
|
||||||
|
{% if post.is_pinned %}
|
||||||
|
<span class="px-2 py-0.5 text-xs rounded-full"
|
||||||
|
x-bind:class="darkMode ? 'bg-yellow-700/50 text-yellow-300' : 'bg-yellow-100 text-yellow-800'">
|
||||||
|
<i class="fas fa-thumbtack mr-1"></i> Angepinnt
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if post.is_locked %}
|
||||||
|
<span class="px-2 py-0.5 text-xs rounded-full"
|
||||||
|
x-bind:class="darkMode ? 'bg-red-700/50 text-red-300' : 'bg-red-100 text-red-800'">
|
||||||
|
<i class="fas fa-lock mr-1"></i> Gesperrt
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Hauptbeitrag -->
|
||||||
|
<div class="mb-8 rounded-xl overflow-hidden"
|
||||||
|
x-bind:class="darkMode ? 'bg-gray-800/60 border border-white/10' : 'bg-white border border-gray-200 shadow-sm'">
|
||||||
|
<!-- Beitrags-Header -->
|
||||||
|
<div class="p-4 border-b" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<!-- Autor-Avatar -->
|
||||||
|
<div class="w-10 h-10 rounded-full flex items-center justify-center text-white font-medium text-sm overflow-hidden mr-3"
|
||||||
|
style="background: linear-gradient(135deg, #8b5cf6, #6366f1);">
|
||||||
|
{% if post.author.avatar %}
|
||||||
|
<img src="{{ post.author.avatar }}" alt="{{ post.author.username }}" class="w-full h-full object-cover">
|
||||||
|
{% else %}
|
||||||
|
{{ post.author.username[0].upper() }}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Autor-Info -->
|
||||||
|
<div>
|
||||||
|
<div class="font-medium">{{ post.author.username }}</div>
|
||||||
|
<div class="text-xs opacity-70">Erstellt am {{ post.created_at.strftime('%d.%m.%Y, %H:%M') }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Aktionen -->
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
{% if current_user.id == post.user_id or current_user.role == 'admin' %}
|
||||||
|
<a href="{{ url_for('edit_post', post_id=post.id) }}"
|
||||||
|
class="p-2 rounded transition-colors"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'hover:bg-gray-700/50 text-gray-300'
|
||||||
|
: 'hover:bg-gray-100 text-gray-600'">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</a>
|
||||||
|
<form action="{{ url_for('delete_post', post_id=post.id) }}" method="POST" class="inline" onsubmit="return confirm('Möchtest du diesen Beitrag wirklich löschen?');">
|
||||||
|
<button type="submit"
|
||||||
|
class="p-2 rounded transition-colors"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'hover:bg-red-800/50 text-red-300'
|
||||||
|
: 'hover:bg-red-100 text-red-600'">
|
||||||
|
<i class="fas fa-trash-alt"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Moderation-Optionen -->
|
||||||
|
{% if current_user.role in ['admin', 'moderator'] %}
|
||||||
|
<div class="ml-2 border-l" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'"></div>
|
||||||
|
<form action="{{ url_for('toggle_pin_post', post_id=post.id) }}" method="POST" class="inline">
|
||||||
|
<button type="submit"
|
||||||
|
class="p-2 rounded transition-colors"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'hover:bg-yellow-800/50 text-yellow-300'
|
||||||
|
: 'hover:bg-yellow-100 text-yellow-600'"
|
||||||
|
title="{% if post.is_pinned %}Nicht mehr anpinnen{% else %}Anpinnen{% endif %}">
|
||||||
|
<i class="fas fa-thumbtack"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<form action="{{ url_for('toggle_lock_post', post_id=post.id) }}" method="POST" class="inline">
|
||||||
|
<button type="submit"
|
||||||
|
class="p-2 rounded transition-colors"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'hover:bg-blue-800/50 text-blue-300'
|
||||||
|
: 'hover:bg-blue-100 text-blue-600'"
|
||||||
|
title="{% if post.is_locked %}Entsperren{% else %}Sperren{% endif %}">
|
||||||
|
<i class="fas {% if post.is_locked %}fa-unlock{% else %}fa-lock{% endif %}"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Beitrags-Inhalt -->
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="post-content markdown-content" id="main-post-content">
|
||||||
|
{{ post.content|safe }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if post.updated_at and post.updated_at != post.created_at %}
|
||||||
|
<div class="mt-6 pt-4 text-xs opacity-60 border-t" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
|
||||||
|
<i class="fas fa-edit mr-1"></i> Zuletzt bearbeitet: {{ post.updated_at.strftime('%d.%m.%Y, %H:%M') }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Antworten-Bereich -->
|
||||||
|
<div class="mb-8">
|
||||||
|
<h2 class="text-xl font-semibold mb-4">
|
||||||
|
<i class="fas fa-reply mr-2 opacity-60"></i>
|
||||||
|
{{ replies|length }} Antworten
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<!-- Antworten-Liste -->
|
||||||
|
{% if replies %}
|
||||||
|
{% for reply in replies %}
|
||||||
|
<div class="mb-5 rounded-xl overflow-hidden"
|
||||||
|
x-bind:class="darkMode ? 'bg-gray-800/40 border border-white/10' : 'bg-white border border-gray-200'">
|
||||||
|
<!-- Antwort-Header -->
|
||||||
|
<div class="p-3 border-b" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<!-- Autor-Avatar -->
|
||||||
|
<div class="w-8 h-8 rounded-full flex items-center justify-center text-white font-medium text-xs overflow-hidden mr-3"
|
||||||
|
style="background: linear-gradient(135deg, #8b5cf6, #6366f1);">
|
||||||
|
{% if reply.author.avatar %}
|
||||||
|
<img src="{{ reply.author.avatar }}" alt="{{ reply.author.username }}" class="w-full h-full object-cover">
|
||||||
|
{% else %}
|
||||||
|
{{ reply.author.username[0].upper() }}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Autor-Info -->
|
||||||
|
<div>
|
||||||
|
<div class="font-medium text-sm">{{ reply.author.username }}</div>
|
||||||
|
<div class="text-xs opacity-70">{{ reply.created_at.strftime('%d.%m.%Y, %H:%M') }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Aktionen -->
|
||||||
|
<div class="flex items-center space-x-1">
|
||||||
|
{% if current_user.id == reply.user_id or current_user.role == 'admin' %}
|
||||||
|
<a href="{{ url_for('edit_post', post_id=reply.id) }}"
|
||||||
|
class="p-1.5 rounded text-sm transition-colors"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'hover:bg-gray-700/50 text-gray-300'
|
||||||
|
: 'hover:bg-gray-100 text-gray-600'">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</a>
|
||||||
|
<form action="{{ url_for('delete_post', post_id=reply.id) }}" method="POST" class="inline" onsubmit="return confirm('Möchtest du diese Antwort wirklich löschen?');">
|
||||||
|
<button type="submit"
|
||||||
|
class="p-1.5 rounded text-sm transition-colors"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'hover:bg-red-800/50 text-red-300'
|
||||||
|
: 'hover:bg-red-100 text-red-600'">
|
||||||
|
<i class="fas fa-trash-alt"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Antwort-Inhalt -->
|
||||||
|
<div class="p-5">
|
||||||
|
<div class="post-content markdown-content reply-content" id="reply-content-{{ reply.id }}">
|
||||||
|
{{ reply.content|safe }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if reply.updated_at and reply.updated_at != reply.created_at %}
|
||||||
|
<div class="mt-4 pt-3 text-xs opacity-60 border-t" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
|
||||||
|
<i class="fas fa-edit mr-1"></i> Zuletzt bearbeitet: {{ reply.updated_at.strftime('%d.%m.%Y, %H:%M') }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<div class="rounded-xl p-6 text-center"
|
||||||
|
x-bind:class="darkMode ? 'bg-gray-800/40 border border-white/10' : 'bg-white border border-gray-200'">
|
||||||
|
<div class="text-3xl mb-3 opacity-30"><i class="fas fa-comments"></i></div>
|
||||||
|
<h3 class="text-lg font-semibold mb-2">Noch keine Antworten</h3>
|
||||||
|
<p class="opacity-75">Sei der Erste, der auf diesen Beitrag antwortet!</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Antwort-Formular -->
|
||||||
|
{% if not post.is_locked %}
|
||||||
|
<div class="mb-8 rounded-xl overflow-hidden"
|
||||||
|
x-bind:class="darkMode ? 'bg-gray-800/60 border border-white/10' : 'bg-white border border-gray-200'">
|
||||||
|
<div class="p-4 border-b font-medium" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
|
||||||
|
<i class="fas fa-reply mr-2"></i>
|
||||||
|
Antworten
|
||||||
|
</div>
|
||||||
|
<div class="p-6">
|
||||||
|
<form action="{{ url_for('reply_to_post', post_id=post.id) }}" method="POST">
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="content" class="block mb-2 font-medium">Deine Antwort</label>
|
||||||
|
<div class="mb-2 rounded-lg overflow-hidden"
|
||||||
|
x-bind:class="darkMode ? 'border border-white/20' : 'border border-gray-300'">
|
||||||
|
<textarea id="content" name="content" rows="6"
|
||||||
|
class="w-full p-3 resize-y"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-gray-700/50 text-white placeholder-gray-400 focus:border-indigo-500'
|
||||||
|
: 'bg-white text-gray-700 placeholder-gray-400 focus:border-indigo-500'"
|
||||||
|
placeholder="Schreibe deine Antwort hier (unterstützt Markdown und @Knotenname-Erwähnungen)..."
|
||||||
|
required></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs opacity-70">
|
||||||
|
<p>Du kannst Knotenpunkte der Mindmap durch <code>@Knotenname</code> verlinken.</p>
|
||||||
|
<p>Dieser Editor unterstützt Markdown-Formatierung:</p>
|
||||||
|
<div class="flex flex-wrap gap-2 mt-1">
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="**" data-before="" data-after="" title="Fett">
|
||||||
|
<i class="fas fa-bold"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="*" data-before="" data-after="" title="Kursiv">
|
||||||
|
<i class="fas fa-italic"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="`" data-before="" data-after="" title="Code">
|
||||||
|
<i class="fas fa-code"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="[Link-Text](URL)" data-before="" data-after="" title="Link">
|
||||||
|
<i class="fas fa-link"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="\n```\nCode-Block\n```" data-before="" data-after="" title="Code-Block">
|
||||||
|
<i class="fas fa-file-code"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format=">" data-before="" data-after="" title="Zitat">
|
||||||
|
<i class="fas fa-quote-right"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="- " data-before="" data-after="" title="Liste">
|
||||||
|
<i class="fas fa-list-ul"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="1. " data-before="" data-after="" title="Nummerierte Liste">
|
||||||
|
<i class="fas fa-list-ol"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="# " data-before="" data-after="" title="Überschrift">
|
||||||
|
<i class="fas fa-heading"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button type="submit"
|
||||||
|
class="px-5 py-2.5 rounded-lg transition-all duration-300 flex items-center"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-indigo-700 hover:bg-indigo-600 text-white'
|
||||||
|
: 'bg-indigo-500 hover:bg-indigo-600 text-white'">
|
||||||
|
<i class="fas fa-paper-plane mr-2"></i>
|
||||||
|
Antwort senden
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="rounded-xl p-5 text-center mb-6"
|
||||||
|
x-bind:class="darkMode ? 'bg-red-900/20 border border-red-800/30' : 'bg-red-50 border border-red-100'">
|
||||||
|
<i class="fas fa-lock mr-2 text-red-500"></i>
|
||||||
|
<span>Dieser Beitrag ist geschlossen. Es können keine neuen Antworten mehr verfasst werden.</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Markdown und Knotenerwähnungen verarbeiten
|
||||||
|
const processContent = (content) => {
|
||||||
|
// Verarbeite Markdown mit marked.js
|
||||||
|
let html = marked.parse(content);
|
||||||
|
|
||||||
|
// Ersetze @Knotenname mit entsprechenden Links
|
||||||
|
html = html.replace(/@([a-zA-Z0-9äöüÄÖÜß_-]+)/g, '<span class="node-mention"><i class="fas fa-diagram-project fa-xs mr-1"></i>$1</span>');
|
||||||
|
|
||||||
|
return html;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Markdown-Inhalt für Hauptbeitrag rendern
|
||||||
|
const mainPostContent = document.getElementById('main-post-content');
|
||||||
|
if (mainPostContent) {
|
||||||
|
mainPostContent.innerHTML = processContent(mainPostContent.textContent.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Markdown-Inhalt für Antworten rendern
|
||||||
|
document.querySelectorAll('.reply-content').forEach(reply => {
|
||||||
|
reply.innerHTML = processContent(reply.textContent.trim());
|
||||||
|
});
|
||||||
|
|
||||||
|
// Markdown-Buttons für das Antwortformular
|
||||||
|
document.querySelectorAll('.markdown-button').forEach(button => {
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
const textarea = document.getElementById('content');
|
||||||
|
const format = this.dataset.format;
|
||||||
|
const before = this.dataset.before || '';
|
||||||
|
const after = this.dataset.after || '';
|
||||||
|
|
||||||
|
// Hole die aktuelle Auswahl
|
||||||
|
const start = textarea.selectionStart;
|
||||||
|
const end = textarea.selectionEnd;
|
||||||
|
const selection = textarea.value.substring(start, end);
|
||||||
|
|
||||||
|
// Wende die Formatierung an
|
||||||
|
let formattedText;
|
||||||
|
if (format.includes('\n')) {
|
||||||
|
// Für Formate mit Zeilenumbrüchen (z.B. Code-Blöcke)
|
||||||
|
formattedText = format.replace('Code-Block', selection || 'Code-Block');
|
||||||
|
} else if (format.includes('[Link-Text](URL)')) {
|
||||||
|
formattedText = format.replace('Link-Text', selection || 'Link-Text');
|
||||||
|
} else if (format === '- ' || format === '1. ' || format === '# ' || format === '> ') {
|
||||||
|
// Für Listen und Überschriften: am Anfang der Zeile einfügen
|
||||||
|
const beforeSelection = textarea.value.substring(0, start);
|
||||||
|
const afterSelection = textarea.value.substring(end);
|
||||||
|
|
||||||
|
// Finde den Anfang der aktuellen Zeile
|
||||||
|
const lastNewline = beforeSelection.lastIndexOf('\n');
|
||||||
|
const lineStart = lastNewline === -1 ? 0 : lastNewline + 1;
|
||||||
|
|
||||||
|
// Füge das Format am Zeilenanfang ein
|
||||||
|
formattedText = beforeSelection.substring(0, lineStart) +
|
||||||
|
format +
|
||||||
|
beforeSelection.substring(lineStart) +
|
||||||
|
selection +
|
||||||
|
afterSelection;
|
||||||
|
|
||||||
|
// Setze die neue Cursor-Position
|
||||||
|
const newCursorPos = end + format.length;
|
||||||
|
textarea.value = formattedText;
|
||||||
|
textarea.setSelectionRange(newCursorPos, newCursorPos);
|
||||||
|
return; // Früher zurückkehren, da wir die Formatierung bereits angewendet haben
|
||||||
|
} else {
|
||||||
|
// Für einfache Formatierungen wie fett, kursiv, Code
|
||||||
|
formattedText = before + format + selection + format + after;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ersetze den Text
|
||||||
|
textarea.value = textarea.value.substring(0, start) + formattedText + textarea.value.substring(end);
|
||||||
|
|
||||||
|
// Setze den Fokus zurück auf das Textarea
|
||||||
|
textarea.focus();
|
||||||
|
|
||||||
|
// Setze die Auswahl neu, wenn es eine Auswahl gab
|
||||||
|
if (selection) {
|
||||||
|
const newStart = start + before.length + format.length;
|
||||||
|
const newEnd = newStart + selection.length;
|
||||||
|
textarea.setSelectionRange(newStart, newEnd);
|
||||||
|
} else {
|
||||||
|
// Setze den Cursor in die Mitte von **|** oder `|`
|
||||||
|
const newCursorPos = start + before.length + format.length;
|
||||||
|
textarea.setSelectionRange(newCursorPos, newCursorPos);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
137
templates/community/preview.html
Normal file
137
templates/community/preview.html
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Community Forum Vorschau{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<style>
|
||||||
|
.forum-category {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
.forum-category:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
.category-icon {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
width: 2.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mx-auto px-4 py-8">
|
||||||
|
<!-- Seitenüberschrift -->
|
||||||
|
<div class="mb-8 text-center">
|
||||||
|
<h1 class="text-3xl font-bold mb-2 gradient-text">Community Forum</h1>
|
||||||
|
<p class="text-lg opacity-75">Diskutiere mit anderen Nutzern über die Hauptthemenbereiche der Mindmap</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Login-Aufforderung -->
|
||||||
|
<div class="rounded-xl p-6 text-center mb-8 bg-indigo-50 dark:bg-indigo-900/30 border border-indigo-100 dark:border-indigo-700/30">
|
||||||
|
<h3 class="text-xl font-semibold mb-3">
|
||||||
|
<i class="fas fa-lock mr-2 text-indigo-500"></i>
|
||||||
|
Anmeldung erforderlich
|
||||||
|
</h3>
|
||||||
|
<p class="mb-4">Um am Community-Forum teilzunehmen und alle Funktionen nutzen zu können, musst du dich anmelden oder registrieren.</p>
|
||||||
|
<div class="flex justify-center gap-4 mt-4">
|
||||||
|
<a href="{{ url_for('login', next=url_for('forum')) }}" class="px-4 py-2 bg-indigo-500 hover:bg-indigo-600 text-white rounded-lg transition-colors">
|
||||||
|
<i class="fas fa-sign-in-alt mr-2"></i>Anmelden
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('register') }}" class="px-4 py-2 bg-purple-500 hover:bg-purple-600 text-white rounded-lg transition-colors">
|
||||||
|
<i class="fas fa-user-plus mr-2"></i>Registrieren
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Forumskategorien Vorschau -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
||||||
|
{% if categories_data %}
|
||||||
|
{% for cat_data in categories_data %}
|
||||||
|
<div class="forum-category block">
|
||||||
|
<div class="rounded-xl p-5 h-full"
|
||||||
|
x-bind:class="darkMode ? 'bg-gray-800/60 border border-white/10' : 'bg-white border border-gray-200 shadow-md'">
|
||||||
|
<div class="flex items-start">
|
||||||
|
<!-- Kategorie-Icon -->
|
||||||
|
<div class="category-icon mr-4 text-white"
|
||||||
|
style="background-color: {{ cat_data.category.node.color_code or '#6d28d9' }}">
|
||||||
|
<i class="fas {{ cat_data.category.node.icon or 'fa-folder' }}"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Kategorie-Info -->
|
||||||
|
<div class="flex-grow">
|
||||||
|
<h3 class="text-xl font-semibold mb-2">{{ cat_data.category.title }}</h3>
|
||||||
|
<p class="opacity-75 text-sm mb-3">{{ cat_data.category.description }}</p>
|
||||||
|
|
||||||
|
<!-- Statistik -->
|
||||||
|
<div class="flex flex-wrap gap-4 text-sm opacity-80">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fas fa-comment-alt mr-2"></i>
|
||||||
|
<span>{{ cat_data.total_posts }} Themen</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fas fa-reply mr-2"></i>
|
||||||
|
<span>{{ cat_data.total_replies }} Antworten</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pfeil-Icon -->
|
||||||
|
<div class="ml-2">
|
||||||
|
<i class="fas fa-lock opacity-50"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<div class="col-span-2 text-center py-8">
|
||||||
|
<div class="text-3xl mb-3 opacity-30"><i class="fas fa-exclamation-circle"></i></div>
|
||||||
|
<h3 class="text-xl font-semibold mb-2">Keine Forum-Kategorien gefunden</h3>
|
||||||
|
<p class="opacity-75">Es sind derzeit keine Kategorien für Diskussionen verfügbar.</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Hinweis zur Nutzung -->
|
||||||
|
<div class="rounded-xl p-6 text-center mb-8"
|
||||||
|
x-bind:class="darkMode ? 'bg-indigo-900/30 border border-indigo-700/30' : 'bg-indigo-50 border border-indigo-100'">
|
||||||
|
<h3 class="text-xl font-semibold mb-3">
|
||||||
|
<i class="fas fa-lightbulb mr-2 text-yellow-500"></i>
|
||||||
|
So funktioniert das Forum
|
||||||
|
</h3>
|
||||||
|
<p class="mb-4">Das Community-Forum ist nach den Hauptknotenpunkten der Systades-Mindmap strukturiert.
|
||||||
|
In deinen Beiträgen kannst du Knotenpunkte mit <code>@Knotenname</code> verlinken.</p>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mt-6">
|
||||||
|
<div class="p-4 rounded-lg"
|
||||||
|
x-bind:class="darkMode ? 'bg-indigo-800/40' : 'bg-white border border-indigo-100'">
|
||||||
|
<div class="text-2xl mb-2"><i class="fas fa-users text-indigo-400"></i></div>
|
||||||
|
<h4 class="font-medium mb-1">Fachliche Diskussionen</h4>
|
||||||
|
<p class="text-sm opacity-75">Tausche dich mit anderen zu spezifischen Themen aus</p>
|
||||||
|
</div>
|
||||||
|
<div class="p-4 rounded-lg"
|
||||||
|
x-bind:class="darkMode ? 'bg-indigo-800/40' : 'bg-white border border-indigo-100'">
|
||||||
|
<div class="text-2xl mb-2"><i class="fas fa-link text-indigo-400"></i></div>
|
||||||
|
<h4 class="font-medium mb-1">Wissensvernetzung</h4>
|
||||||
|
<p class="text-sm opacity-75">Verknüpfe Inhalte durch Knotenreferenzen</p>
|
||||||
|
</div>
|
||||||
|
<div class="p-4 rounded-lg"
|
||||||
|
x-bind:class="darkMode ? 'bg-indigo-800/40' : 'bg-white border border-indigo-100'">
|
||||||
|
<div class="text-2xl mb-2"><i class="fas fa-markdown text-indigo-400"></i></div>
|
||||||
|
<h4 class="font-medium mb-1">Markdown Support</h4>
|
||||||
|
<p class="text-sm opacity-75">Formatiere deine Beiträge mit Markdown</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
// Hier können bei Bedarf forumspezifische Scripts eingefügt werden
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -3,21 +3,31 @@
|
|||||||
{% block title %}403 - Zugriff verweigert{% endblock %}
|
{% block title %}403 - Zugriff verweigert{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="min-h-[65vh] flex flex-col items-center justify-center px-4 py-12">
|
<div class="min-h-[75vh] flex flex-col items-center justify-center px-4 py-12 bg-gradient-to-b from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800">
|
||||||
<div class="glass-effect max-w-2xl w-full p-8 rounded-lg border border-gray-300/20 dark:border-gray-700/30 shadow-lg">
|
<div class="glass-effect max-w-2xl w-full p-6 md:p-10 rounded-xl border border-gray-300/20 dark:border-gray-700/30 shadow-xl transform transition-all duration-300 hover:shadow-2xl">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<h1 class="text-6xl font-bold text-primary-600 dark:text-primary-400 mb-4">403</h1>
|
<div class="flex justify-center mb-6">
|
||||||
<h2 class="text-2xl font-semibold mb-4">Zugriff verweigert</h2>
|
<div class="relative">
|
||||||
<p class="text-gray-600 dark:text-gray-300 mb-8">Sie haben nicht die erforderlichen Berechtigungen, um auf diese Seite zuzugreifen. Bitte melden Sie sich an oder nutzen Sie ein Konto mit entsprechenden Rechten.</p>
|
<h1 class="text-7xl md:text-8xl font-extrabold text-primary-600 dark:text-primary-400 opacity-90">403</h1>
|
||||||
|
<div class="absolute -top-4 -right-4 w-12 h-12 bg-red-500 rounded-full flex items-center justify-center animate-pulse">
|
||||||
|
<i class="fa-solid fa-lock text-white text-xl"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h2 class="text-2xl md:text-3xl font-semibold mb-4 text-gray-800 dark:text-gray-200">Zugriff verweigert</h2>
|
||||||
|
<p class="text-gray-600 dark:text-gray-300 mb-8 max-w-lg mx-auto text-base md:text-lg">Sie haben nicht die erforderlichen Berechtigungen, um auf diese Seite zuzugreifen. Bitte melden Sie sich an oder nutzen Sie ein Konto mit entsprechenden Rechten.</p>
|
||||||
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||||
<a href="{{ url_for('index') }}" class="btn-primary">
|
<a href="{{ url_for('index') }}" class="btn-primary transform transition-transform duration-300 hover:scale-105 px-6 py-3 rounded-lg">
|
||||||
<i class="fa-solid fa-home mr-2"></i>Zur Startseite
|
<i class="fa-solid fa-home mr-2"></i>Zur Startseite
|
||||||
</a>
|
</a>
|
||||||
<a href="javascript:history.back()" class="btn-secondary">
|
<a href="javascript:history.back()" class="btn-secondary transform transition-transform duration-300 hover:scale-105 px-6 py-3 rounded-lg">
|
||||||
<i class="fa-solid fa-arrow-left mr-2"></i>Zurück
|
<i class="fa-solid fa-arrow-left mr-2"></i>Zurück
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mt-8 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
<p>Benötigen Sie Hilfe? <a href="#" class="text-primary-600 dark:text-primary-400 hover:underline">Kontaktieren Sie uns</a></p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -3,21 +3,31 @@
|
|||||||
{% block title %}404 - Seite nicht gefunden{% endblock %}
|
{% block title %}404 - Seite nicht gefunden{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="min-h-[65vh] flex flex-col items-center justify-center px-4 py-12">
|
<div class="min-h-[75vh] flex flex-col items-center justify-center px-4 py-12 bg-gradient-to-b from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800">
|
||||||
<div class="glass-effect max-w-2xl w-full p-8 rounded-lg border border-gray-300/20 dark:border-gray-700/30 shadow-lg">
|
<div class="glass-effect max-w-2xl w-full p-6 md:p-10 rounded-xl border border-gray-300/20 dark:border-gray-700/30 shadow-xl transform transition-all duration-300 hover:shadow-2xl">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<h1 class="text-6xl font-bold text-primary-600 dark:text-primary-400 mb-4">404</h1>
|
<div class="flex justify-center mb-6">
|
||||||
<h2 class="text-2xl font-semibold mb-4">Seite nicht gefunden</h2>
|
<div class="relative">
|
||||||
<p class="text-gray-600 dark:text-gray-300 mb-8">Die gesuchte Seite existiert nicht oder wurde verschoben. Bitte prüfen Sie die URL oder nutzen Sie die Navigation.</p>
|
<h1 class="text-7xl md:text-8xl font-extrabold text-primary-600 dark:text-primary-400 opacity-90">404</h1>
|
||||||
|
<div class="absolute -top-4 -right-4 w-12 h-12 bg-yellow-500 rounded-full flex items-center justify-center animate-pulse">
|
||||||
|
<i class="fa-solid fa-question text-white text-xl"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h2 class="text-2xl md:text-3xl font-semibold mb-4 text-gray-800 dark:text-gray-200">Seite nicht gefunden</h2>
|
||||||
|
<p class="text-gray-600 dark:text-gray-300 mb-8 max-w-lg mx-auto text-base md:text-lg">Die gesuchte Seite existiert nicht oder wurde verschoben. Bitte prüfen Sie die URL oder nutzen Sie die Navigation.</p>
|
||||||
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||||
<a href="{{ url_for('index') }}" class="btn-primary">
|
<a href="{{ url_for('index') }}" class="btn-primary transform transition-transform duration-300 hover:scale-105 px-6 py-3 rounded-lg">
|
||||||
<i class="fa-solid fa-home mr-2"></i>Zur Startseite
|
<i class="fa-solid fa-home mr-2"></i>Zur Startseite
|
||||||
</a>
|
</a>
|
||||||
<a href="javascript:history.back()" class="btn-secondary">
|
<a href="javascript:history.back()" class="btn-secondary transform transition-transform duration-300 hover:scale-105 px-6 py-3 rounded-lg">
|
||||||
<i class="fa-solid fa-arrow-left mr-2"></i>Zurück
|
<i class="fa-solid fa-arrow-left mr-2"></i>Zurück
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mt-8 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
<p>Benötigen Sie Hilfe? <a href="#" class="text-primary-600 dark:text-primary-400 hover:underline">Kontaktieren Sie uns</a></p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -3,21 +3,31 @@
|
|||||||
{% block title %}429 - Zu viele Anfragen{% endblock %}
|
{% block title %}429 - Zu viele Anfragen{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="min-h-[65vh] flex flex-col items-center justify-center px-4 py-12">
|
<div class="min-h-[75vh] flex flex-col items-center justify-center px-4 py-12 bg-gradient-to-b from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800">
|
||||||
<div class="glass-effect max-w-2xl w-full p-8 rounded-lg border border-gray-300/20 dark:border-gray-700/30 shadow-lg">
|
<div class="glass-effect max-w-2xl w-full p-6 md:p-10 rounded-xl border border-gray-300/20 dark:border-gray-700/30 shadow-xl transform transition-all duration-300 hover:shadow-2xl">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<h1 class="text-6xl font-bold text-primary-600 dark:text-primary-400 mb-4">429</h1>
|
<div class="flex justify-center mb-6">
|
||||||
<h2 class="text-2xl font-semibold mb-4">Zu viele Anfragen</h2>
|
<div class="relative">
|
||||||
<p class="text-gray-600 dark:text-gray-300 mb-8">Sie haben zu viele Anfragen in kurzer Zeit gestellt. Bitte warten Sie einen Moment und versuchen Sie es dann erneut.</p>
|
<h1 class="text-7xl md:text-8xl font-extrabold text-primary-600 dark:text-primary-400 opacity-90">429</h1>
|
||||||
|
<div class="absolute -top-4 -right-4 w-12 h-12 bg-orange-500 rounded-full flex items-center justify-center animate-pulse">
|
||||||
|
<i class="fa-solid fa-hourglass-half text-white text-xl"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h2 class="text-2xl md:text-3xl font-semibold mb-4 text-gray-800 dark:text-gray-200">Zu viele Anfragen</h2>
|
||||||
|
<p class="text-gray-600 dark:text-gray-300 mb-8 max-w-lg mx-auto text-base md:text-lg">Sie haben zu viele Anfragen in kurzer Zeit gestellt. Bitte warten Sie einen Moment und versuchen Sie es dann erneut.</p>
|
||||||
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||||
<a href="{{ url_for('index') }}" class="btn-primary">
|
<a href="{{ url_for('index') }}" class="btn-primary transform transition-transform duration-300 hover:scale-105 px-6 py-3 rounded-lg">
|
||||||
<i class="fa-solid fa-home mr-2"></i>Zur Startseite
|
<i class="fa-solid fa-home mr-2"></i>Zur Startseite
|
||||||
</a>
|
</a>
|
||||||
<a href="javascript:history.back()" class="btn-secondary">
|
<a href="javascript:history.back()" class="btn-secondary transform transition-transform duration-300 hover:scale-105 px-6 py-3 rounded-lg">
|
||||||
<i class="fa-solid fa-arrow-left mr-2"></i>Zurück
|
<i class="fa-solid fa-arrow-left mr-2"></i>Zurück
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mt-8 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
<p>Benötigen Sie Hilfe? <a href="#" class="text-primary-600 dark:text-primary-400 hover:underline">Kontaktieren Sie uns</a></p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -3,21 +3,31 @@
|
|||||||
{% block title %}500 - Serverfehler{% endblock %}
|
{% block title %}500 - Serverfehler{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="min-h-[65vh] flex flex-col items-center justify-center px-4 py-12">
|
<div class="min-h-[75vh] flex flex-col items-center justify-center px-4 py-12 bg-gradient-to-b from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800">
|
||||||
<div class="glass-effect max-w-2xl w-full p-8 rounded-lg border border-gray-300/20 dark:border-gray-700/30 shadow-lg">
|
<div class="glass-effect max-w-2xl w-full p-6 md:p-10 rounded-xl border border-gray-300/20 dark:border-gray-700/30 shadow-xl transform transition-all duration-300 hover:shadow-2xl">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<h1 class="text-6xl font-bold text-primary-600 dark:text-primary-400 mb-4">500</h1>
|
<div class="flex justify-center mb-6">
|
||||||
<h2 class="text-2xl font-semibold mb-4">Interner Serverfehler</h2>
|
<div class="relative">
|
||||||
<p class="text-gray-600 dark:text-gray-300 mb-8">Es ist ein Fehler auf unserem Server aufgetreten. Unser Team wurde informiert und arbeitet bereits an einer Lösung. Bitte versuchen Sie es später erneut.</p>
|
<h1 class="text-7xl md:text-8xl font-extrabold text-primary-600 dark:text-primary-400 opacity-90">500</h1>
|
||||||
|
<div class="absolute -top-4 -right-4 w-12 h-12 bg-red-600 rounded-full flex items-center justify-center animate-pulse">
|
||||||
|
<i class="fa-solid fa-exclamation-triangle text-white text-xl"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h2 class="text-2xl md:text-3xl font-semibold mb-4 text-gray-800 dark:text-gray-200">Interner Serverfehler</h2>
|
||||||
|
<p class="text-gray-600 dark:text-gray-300 mb-8 max-w-lg mx-auto text-base md:text-lg">Es ist ein Fehler auf unserem Server aufgetreten. Unser Team wurde informiert und arbeitet bereits an einer Lösung. Bitte versuchen Sie es später erneut.</p>
|
||||||
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||||
<a href="{{ url_for('index') }}" class="btn-primary">
|
<a href="{{ url_for('index') }}" class="btn-primary transform transition-transform duration-300 hover:scale-105 px-6 py-3 rounded-lg">
|
||||||
<i class="fa-solid fa-home mr-2"></i>Zur Startseite
|
<i class="fa-solid fa-home mr-2"></i>Zur Startseite
|
||||||
</a>
|
</a>
|
||||||
<a href="javascript:history.back()" class="btn-secondary">
|
<a href="javascript:history.back()" class="btn-secondary transform transition-transform duration-300 hover:scale-105 px-6 py-3 rounded-lg">
|
||||||
<i class="fa-solid fa-arrow-left mr-2"></i>Zurück
|
<i class="fa-solid fa-arrow-left mr-2"></i>Zurück
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mt-8 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
<p>Benötigen Sie Hilfe? <a href="#" class="text-primary-600 dark:text-primary-400 hover:underline">Kontaktieren Sie uns</a></p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -8,56 +8,56 @@
|
|||||||
<h1 class="text-3xl font-bold mb-6 gradient-text">Impressum</h1>
|
<h1 class="text-3xl font-bold mb-6 gradient-text">Impressum</h1>
|
||||||
|
|
||||||
<section class="mb-8">
|
<section class="mb-8">
|
||||||
<h2 class="text-xl font-bold mb-4">Angaben gemäß § 5 TMG</h2>
|
<h2 class="text-xl font-bold mb-4">Angaben gemäß § 5 TMG und § 55 RStV</h2>
|
||||||
<p class="mb-4">MindMap GmbH<br>
|
|
||||||
Musterstraße 123<br>
|
|
||||||
12345 Musterstadt<br>
|
|
||||||
Deutschland</p>
|
|
||||||
|
|
||||||
<p class="mb-4">
|
<p class="mb-4">
|
||||||
<strong>Vertreten durch:</strong><br>
|
Diese Website wird privat betrieben von:<br>
|
||||||
Max Mustermann, Geschäftsführer
|
Marwin Medczinski<br>
|
||||||
|
Fasanenstraße 30<br>
|
||||||
|
16761 Hennigsdorf<br>
|
||||||
|
Deutschland
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p class="mb-4">
|
<p class="mb-4">
|
||||||
<strong>Kontakt:</strong><br>
|
<strong>Kontakt:</strong><br>
|
||||||
Telefon: +49 (0) 123 456789<br>
|
Telefon: +49 (0) 173 8041824<br>
|
||||||
E-Mail: info@mindmap-example.com
|
E-Mail: medczinski.marwin@gmx.de
|
||||||
</p>
|
|
||||||
|
|
||||||
<p class="mb-4">
|
|
||||||
<strong>Registereintrag:</strong><br>
|
|
||||||
Eintragung im Handelsregister.<br>
|
|
||||||
Registergericht: Amtsgericht Musterstadt<br>
|
|
||||||
Registernummer: HRB 12345
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p class="mb-4">
|
|
||||||
<strong>Umsatzsteuer-ID:</strong><br>
|
|
||||||
Umsatzsteuer-Identifikationsnummer gemäß §27 a Umsatzsteuergesetz:<br>
|
|
||||||
DE 123456789
|
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="mb-8">
|
<section class="mb-8">
|
||||||
<h2 class="text-xl font-bold mb-4">Redaktionell verantwortlich</h2>
|
<h2 class="text-xl font-bold mb-4">Inhaltlich Verantwortlicher gemäß § 55 Abs. 2 RStV</h2>
|
||||||
<p>
|
<p class="mb-4">
|
||||||
Max Mustermann<br>
|
Marwin Medczinski<br>
|
||||||
Musterstraße 123<br>
|
Fasanenstraße 30<br>
|
||||||
12345 Musterstadt
|
16761 Hennigsdorf
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</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">
|
<section class="mb-8">
|
||||||
<h2 class="text-xl font-bold mb-4">Haftungsausschluss</h2>
|
<h2 class="text-xl font-bold mb-4">Haftungsausschluss</h2>
|
||||||
|
|
||||||
<h3 class="text-lg font-bold mb-2">Haftung für Inhalte</h3>
|
<h3 class="text-lg font-bold mb-2">Haftung für Inhalte</h3>
|
||||||
<p class="mb-4">Als Diensteanbieter sind wir gemäß § 7 Abs.1 TMG für eigene Inhalte auf diesen Seiten nach den allgemeinen Gesetzen verantwortlich. Nach §§ 8 bis 10 TMG sind wir 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">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 werden wir diese Inhalte umgehend entfernen.</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>
|
<h3 class="text-lg font-bold mb-2">Haftung für Links</h3>
|
||||||
<p class="mb-4">Unser Angebot enthält Links zu externen Websites Dritter, auf deren Inhalte wir keinen Einfluss haben. Deshalb können wir 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">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>Eine permanente inhaltliche Kontrolle der verlinkten Seiten ist jedoch ohne konkrete Anhaltspunkte einer Rechtsverletzung nicht zumutbar. Bei Bekanntwerden von Rechtsverletzungen werden wir derartige Links umgehend entfernen.</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>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
{% block extra_css %}
|
{% block extra_css %}
|
||||||
<style>
|
<style>
|
||||||
/* Hintergrund über die gesamte Seite erstrecken */
|
/* Full height and width for the page */
|
||||||
html, body {
|
html, body {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -13,152 +13,146 @@
|
|||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Entferne den Gradient-Hintergrund vollständig */
|
/* Remove gradient backgrounds */
|
||||||
.hero-gradient, .bg-fade {
|
.hero-gradient, .bg-fade {
|
||||||
background: none !important;
|
background: none !important;
|
||||||
clip-path: none !important;
|
clip-path: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tech-line {
|
/* Style elements */
|
||||||
|
.mystical-line {
|
||||||
height: 1px;
|
height: 1px;
|
||||||
background: linear-gradient(to right, transparent, rgba(100, 100, 100, 0.1), transparent);
|
background: linear-gradient(to right, transparent, rgba(109, 40, 217, 0.2), transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tech-dot {
|
.dark .mystical-line {
|
||||||
width: 4px;
|
background: linear-gradient(to right, transparent, rgba(139, 92, 246, 0.2), transparent);
|
||||||
height: 4px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: rgba(100, 100, 100, 0.2);
|
|
||||||
position: absolute;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .tech-line {
|
/* Text reveal animation */
|
||||||
background: linear-gradient(to right, transparent, rgba(255, 255, 255, 0.1), transparent);
|
@keyframes textReveal {
|
||||||
|
0% { clip-path: polygon(0 0, 0 0, 0 100%, 0 100%); }
|
||||||
|
100% { clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%); }
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .tech-dot {
|
.text-reveal {
|
||||||
background-color: rgba(255, 255, 255, 0.3);
|
animation: textReveal 1s cubic-bezier(0.77, 0, 0.18, 1) forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulse {
|
.delay-1 { animation-delay: 0.2s; }
|
||||||
0% { r: 10; opacity: 0.7; }
|
.delay-2 { animation-delay: 0.4s; }
|
||||||
50% { r: 12; opacity: 1; }
|
.delay-3 { animation-delay: 0.6s; }
|
||||||
100% { r: 10; opacity: 0.7; }
|
|
||||||
|
/* Home page specific styles */
|
||||||
|
.featured-card {
|
||||||
|
transition: transform 0.5s ease, box-shadow 0.5s ease;
|
||||||
|
border: 1px solid;
|
||||||
|
border-color: rgba(139, 92, 246, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate-pulse {
|
.dark .featured-card {
|
||||||
animation: pulse 3s ease-in-out infinite;
|
border-color: rgba(109, 40, 217, 0.2);
|
||||||
|
background-color: rgba(17, 24, 39, 0.7);
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes iconPulse {
|
.featured-card:hover {
|
||||||
0% { transform: scale(1); }
|
transform: translateY(-5px);
|
||||||
50% { transform: scale(1.1); }
|
|
||||||
100% { transform: scale(1); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-pulse {
|
.dark .featured-card:hover {
|
||||||
animation: iconPulse 3s ease-in-out infinite;
|
box-shadow: 0 5px 15px rgba(109, 40, 217, 0.2);
|
||||||
display: inline-block;
|
border-color: rgba(109, 40, 217, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Volle Seitenbreite für Container */
|
.featured-card:hover {
|
||||||
#app-container, .container, main, .mx-auto, .py-12 {
|
box-shadow: 0 5px 15px rgba(139, 92, 246, 0.1);
|
||||||
width: 100%;
|
border-color: rgba(139, 92, 246, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Sicherstellen dass der Hintergrund die ganze Seite abdeckt */
|
/* Chat section styles */
|
||||||
.full-page-bg {
|
.embedded-chat {
|
||||||
position: fixed;
|
height: 350px;
|
||||||
top: 0;
|
border-radius: 1rem;
|
||||||
left: 0;
|
overflow: hidden;
|
||||||
width: 100vw;
|
transition: all 0.3s ease;
|
||||||
height: 100vh;
|
border: 1px solid;
|
||||||
z-index: -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Chat-Animationen */
|
.dark .embedded-chat {
|
||||||
.typing-dots span {
|
background-color: rgba(17, 24, 39, 0.7);
|
||||||
animation-duration: 1.2s;
|
border-color: rgba(109, 40, 217, 0.2);
|
||||||
animation-iteration-count: infinite;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Chat-Nachrichten-Animation */
|
.embedded-chat {
|
||||||
@keyframes fadeInUp {
|
background-color: rgba(255, 255, 255, 0.7);
|
||||||
from {
|
border-color: rgba(139, 92, 246, 0.1);
|
||||||
opacity: 0;
|
|
||||||
transform: translate3d(0, 10px, 0);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translate3d(0, 0, 0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#embedded-chat-messages > div {
|
|
||||||
animation: fadeInUp 0.3s ease-out forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Sanftes Scrollen im Chat */
|
|
||||||
#embedded-chat-messages {
|
#embedded-chat-messages {
|
||||||
scroll-behavior: smooth;
|
height: 250px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Benutzerdefinierter Scrollbar für den Chat */
|
/* Chat typing indicator */
|
||||||
#embedded-chat-messages::-webkit-scrollbar {
|
.typing-dots span {
|
||||||
width: 6px;
|
display: inline-block;
|
||||||
|
width: 5px;
|
||||||
|
height: 5px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-right: 3px;
|
||||||
|
background-color: currentColor;
|
||||||
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
#embedded-chat-messages::-webkit-scrollbar-track {
|
.typing-dots span:nth-child(1) {
|
||||||
background-color: rgba(0, 0, 0, 0.05);
|
animation: dot-pulse 1.2s infinite ease-in-out;
|
||||||
border-radius: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#embedded-chat-messages::-webkit-scrollbar-thumb {
|
.typing-dots span:nth-child(2) {
|
||||||
background-color: rgba(139, 92, 246, 0.3);
|
animation: dot-pulse 1.2s infinite ease-in-out 0.2s;
|
||||||
border-radius: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark #embedded-chat-messages::-webkit-scrollbar-thumb {
|
.typing-dots span:nth-child(3) {
|
||||||
background-color: rgba(139, 92, 246, 0.5);
|
animation: dot-pulse 1.2s infinite ease-in-out 0.4s;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hover-Effekt für Quick-Query-Buttons */
|
@keyframes dot-pulse {
|
||||||
.quick-query-btn:hover {
|
0%, 100% { transform: scale(1); opacity: 0.5; }
|
||||||
cursor: pointer;
|
50% { transform: scale(1.3); opacity: 1; }
|
||||||
background: linear-gradient(to right, rgba(139, 92, 246, 0.1), rgba(96, 165, 250, 0.1));
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .quick-query-btn:hover {
|
|
||||||
background: linear-gradient(to right, rgba(139, 92, 246, 0.2), rgba(96, 165, 250, 0.2));
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<!-- Hintergrund für die gesamte Seite -->
|
|
||||||
<div class="full-page-bg gradient-background"></div>
|
|
||||||
|
|
||||||
<!-- Hero Section -->
|
<!-- Hero Section -->
|
||||||
<section class="relative pt-20 pb-32">
|
<section class="relative pt-20 pb-24">
|
||||||
<!-- Hero Content -->
|
<!-- Hero Content -->
|
||||||
<div class="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
<div class="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||||
<div class="text-center mb-16">
|
<div class="text-center mb-16">
|
||||||
<h1 class="hero-heading mb-8 text-gray-900 dark:text-white">
|
<h1 class="hero-heading mb-8 text-gray-900 dark:text-white">
|
||||||
<span class="gradient-text inline-block transform transition-all duration-700 hover:scale-105">Wissen</span> neu
|
<div class="overflow-hidden">
|
||||||
<div class="mt-2 relative">
|
<span class="gradient-text inline-block text-reveal">Wissen</span>
|
||||||
<span class="relative inline-block">vernetzen
|
</div>
|
||||||
|
<div class="overflow-hidden mt-2">
|
||||||
|
<span class="inline-block text-reveal delay-1">neu</span>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 relative overflow-hidden">
|
||||||
|
<span class="relative inline-block text-reveal delay-2">vernetzen
|
||||||
<div class="absolute -bottom-2 left-0 right-0 h-1 bg-gradient-to-r from-purple-500/0 via-purple-500/70 to-purple-500/0 rounded-full"></div>
|
<div class="absolute -bottom-2 left-0 right-0 h-1 bg-gradient-to-r from-purple-500/0 via-purple-500/70 to-purple-500/0 rounded-full"></div>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-xl md:text-2xl text-gray-700 dark:text-gray-300 max-w-3xl mx-auto mb-12">
|
<div class="overflow-hidden">
|
||||||
Erkunde komplexe Ideen visuell, schaffe Verbindungen und teile deine Gedanken
|
<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">
|
||||||
in einem interaktiven Wissensnetzwerk.
|
Erkunde komplexe Ideen visuell, schaffe Verbindungen und teile deine Gedanken
|
||||||
</p>
|
in einem interaktiven Wissensnetzwerk.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<div class="flex flex-col sm:flex-row gap-5 justify-center">
|
<div class="flex flex-col sm:flex-row gap-5 justify-center">
|
||||||
<a href="{{ url_for('mindmap') }}" class="group transition-all duration-300 bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-700 hover:to-indigo-700 text-white font-medium text-lg px-8 py-4 rounded-2xl shadow-lg hover:shadow-xl hover:shadow-purple-500/20 transform hover:-translate-y-1">
|
<a href="{{ url_for('mindmap') }}" class="mystical-button mystical-button-primary group transition-all duration-300">
|
||||||
<span class="flex items-center justify-center">
|
<span class="flex items-center justify-center">
|
||||||
<i class="fa-solid fa-diagram-project mr-3 text-purple-200 group-hover:text-white transition-all duration-300 animate-pulse"></i>
|
<i class="fa-solid fa-diagram-project mr-3 text-purple-200 group-hover:text-white transition-all duration-300"></i>
|
||||||
<span class="relative">
|
<span class="relative">
|
||||||
Mindmap erkunden
|
Mindmap erkunden
|
||||||
<span class="absolute -bottom-1 left-0 w-0 h-0.5 bg-white group-hover:w-full transition-all duration-300"></span>
|
<span class="absolute -bottom-1 left-0 w-0 h-0.5 bg-white group-hover:w-full transition-all duration-300"></span>
|
||||||
@@ -166,12 +160,12 @@
|
|||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
{% if not current_user.is_authenticated %}
|
{% if not current_user.is_authenticated %}
|
||||||
<a href="{{ url_for('register') }}" class="group transition-all duration-300 bg-gradient-to-r from-blue-500 to-cyan-500 hover:from-blue-600 hover:to-cyan-600 text-white font-medium text-lg px-8 py-4 rounded-2xl shadow-lg hover:shadow-xl hover:shadow-blue-500/20 transform hover:-translate-y-1">
|
<a href="{{ url_for('register') }}" class="mystical-button mystical-button-secondary group transition-all duration-300">
|
||||||
<span class="flex items-center justify-center">
|
<span class="flex items-center justify-center">
|
||||||
<i class="fa-solid fa-user-plus mr-3 text-blue-200 group-hover:text-white transition-all duration-300 icon-pulse"></i>
|
<i class="fa-solid fa-user-plus mr-3 group-hover:text-accent-secondary-dark dark:group-hover:text-accent-secondary-light transition-all duration-300"></i>
|
||||||
<span class="relative">
|
<span class="relative">
|
||||||
Konto erstellen
|
Konto erstellen
|
||||||
<span class="absolute -bottom-1 left-0 w-0 h-0.5 bg-white group-hover:w-full transition-all duration-300"></span>
|
<span class="absolute -bottom-1 left-0 w-0 h-0.5 bg-accent-primary-light dark:bg-accent-primary-dark group-hover:w-full transition-all duration-300"></span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
@@ -179,61 +173,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tech illustration -->
|
<!-- Central logo and name -->
|
||||||
<div class="relative w-full max-w-4xl mx-auto h-80 sm:h-96">
|
<div class="relative w-full max-w-4xl mx-auto h-40 sm:h-60 mb-16">
|
||||||
<div class="absolute inset-0 flex items-center justify-center">
|
<div class="absolute inset-0 flex items-center justify-center">
|
||||||
<div class="hidden md:block text-center">
|
<div class="text-center">
|
||||||
<div class="text-3xl font-bold gradient-text mb-2 animate-float">Systades</div>
|
<div class="text-3xl font-bold gradient-text mb-2 animate-float">Systades</div>
|
||||||
<div class="text-lg text-gray-700 dark:text-gray-300">WISSEN VERNETZEN</div>
|
<div class="text-lg text-gray-700 dark:text-gray-300">WISSEN VERNETZEN</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Network Visualization with SVG -->
|
|
||||||
<svg class="absolute inset-0 w-full h-full" viewBox="0 0 800 600" preserveAspectRatio="xMidYMid meet">
|
|
||||||
<!-- Glossy Nodes and Lines -->
|
|
||||||
<defs>
|
|
||||||
<radialGradient id="nodeGradient" cx="50%" cy="50%" r="50%" fx="50%" fy="50%">
|
|
||||||
<stop offset="0%" stop-color="rgba(139, 92, 246, 0.9)" />
|
|
||||||
<stop offset="100%" stop-color="rgba(79, 70, 229, 0.5)" />
|
|
||||||
</radialGradient>
|
|
||||||
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
|
|
||||||
<feGaussianBlur stdDeviation="5" result="blur" />
|
|
||||||
<feComposite in="SourceGraphic" in2="blur" operator="over" />
|
|
||||||
</filter>
|
|
||||||
</defs>
|
|
||||||
|
|
||||||
<!-- Network Lines -->
|
|
||||||
<g class="lines">
|
|
||||||
<!-- Connection network -->
|
|
||||||
<line x1="200" y1="250" x2="400" y2="150" stroke="rgba(0,0,0,0.1)" stroke-width="1" class="dark:hidden" />
|
|
||||||
<line x1="400" y1="150" x2="600" y2="250" stroke="rgba(0,0,0,0.1)" stroke-width="1" class="dark:hidden" />
|
|
||||||
<line x1="600" y1="250" x2="400" y2="350" stroke="rgba(0,0,0,0.1)" stroke-width="1" class="dark:hidden" />
|
|
||||||
<line x1="400" y1="350" x2="200" y2="250" stroke="rgba(0,0,0,0.1)" stroke-width="1" class="dark:hidden" />
|
|
||||||
<line x1="400" y1="150" x2="400" y2="350" stroke="rgba(0,0,0,0.1)" stroke-width="1" class="dark:hidden" />
|
|
||||||
<line x1="200" y1="250" x2="600" y2="250" stroke="rgba(0,0,0,0.1)" stroke-width="1" class="dark:hidden" />
|
|
||||||
|
|
||||||
<!-- Dark mode connections -->
|
|
||||||
<line x1="200" y1="250" x2="400" y2="150" stroke="rgba(255,255,255,0.1)" stroke-width="1" class="hidden dark:inline" />
|
|
||||||
<line x1="400" y1="150" x2="600" y2="250" stroke="rgba(255,255,255,0.1)" stroke-width="1" class="hidden dark:inline" />
|
|
||||||
<line x1="600" y1="250" x2="400" y2="350" stroke="rgba(255,255,255,0.1)" stroke-width="1" class="hidden dark:inline" />
|
|
||||||
<line x1="400" y1="350" x2="200" y2="250" stroke="rgba(255,255,255,0.1)" stroke-width="1" class="hidden dark:inline" />
|
|
||||||
<line x1="400" y1="150" x2="400" y2="350" stroke="rgba(255,255,255,0.1)" stroke-width="1" class="hidden dark:inline" />
|
|
||||||
<line x1="200" y1="250" x2="600" y2="250" stroke="rgba(255,255,255,0.1)" stroke-width="1" class="hidden dark:inline" />
|
|
||||||
|
|
||||||
<!-- Pulse animation for some lines -->
|
|
||||||
<line class="animate-pulse" x1="400" y1="150" x2="300" y2="200" stroke="rgba(139, 92, 246, 0.5)" stroke-width="2" />
|
|
||||||
<line class="animate-pulse" x1="400" y1="350" x2="500" y2="300" stroke="rgba(168, 85, 247, 0.5)" stroke-width="2" />
|
|
||||||
</g>
|
|
||||||
|
|
||||||
<!-- Network Nodes -->
|
|
||||||
<g class="nodes">
|
|
||||||
<circle cx="400" cy="150" r="15" fill="url(#nodeGradient)" filter="url(#glow)" class="animate-pulse float-animation" />
|
|
||||||
<circle cx="200" cy="250" r="10" fill="url(#nodeGradient)" class="float-animation" />
|
|
||||||
<circle cx="600" cy="250" r="10" fill="url(#nodeGradient)" class="float-animation" />
|
|
||||||
<circle cx="400" cy="350" r="15" fill="url(#nodeGradient)" filter="url(#glow)" class="animate-pulse float-animation" />
|
|
||||||
<circle cx="300" cy="200" r="8" fill="url(#nodeGradient)" class="float-animation" />
|
|
||||||
<circle cx="500" cy="300" r="8" fill="url(#nodeGradient)" class="float-animation" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -241,363 +187,334 @@
|
|||||||
|
|
||||||
<!-- Features Section -->
|
<!-- Features Section -->
|
||||||
<section class="py-20 relative">
|
<section class="py-20 relative">
|
||||||
<div class="tech-line absolute top-0 left-1/2 transform -translate-x-1/2 w-1/3"></div>
|
<div class="mystical-line absolute top-0 left-1/2 transform -translate-x-1/2 w-1/3"></div>
|
||||||
|
|
||||||
<div class="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div class="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div class="text-center mb-16">
|
<div class="text-center mb-16">
|
||||||
<h2 class="section-heading mb-4 text-gray-900 dark:text-white">Was ist <span class="gradient-text">Systades?</span></h2>
|
<h2 class="section-heading mb-4 text-gray-900 dark:text-white">Was ist <span class="gradient-text">Systades?</span></h2>
|
||||||
<p class="text-lg text-gray-700 dark:text-gray-300 max-w-3xl mx-auto">
|
<p class="text-lg text-gray-700 dark:text-gray-300 max-w-3xl mx-auto">
|
||||||
Ein modernes Werkzeug zum Visualisieren, Erforschen und Teilen von Wissen
|
Ein modernes Werkzeug zum Visualisieren, Erforschen und Teilen von Wissen in einem interaktiven
|
||||||
in einer intuitiven, interaktiven Umgebung.
|
Netzwerk, das dir hilft, Verbindungen zu entdecken und dein Wissen zu organisieren.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
<!-- Feature Cards -->
|
||||||
<!-- Feature Card 1 -->
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-8 mb-12">
|
||||||
<div class="feature-card p-8 rounded-3xl hover:-translate-y-3 transform transition-all duration-300">
|
<!-- Feature 1: Visualize -->
|
||||||
<div class="icon mb-6 rounded-2xl shadow-lg">
|
<div class="featured-card rounded-2xl p-6 bg-white/80 dark:bg-gray-800/30 backdrop-blur-sm">
|
||||||
<i class="fa-solid fa-brain"></i>
|
<div class="mb-4 text-purple-600 dark:text-purple-400">
|
||||||
|
<i class="fa-solid fa-diagram-project text-3xl"></i>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-xl font-semibold mb-3">Visualisiere Wissen</h3>
|
<h3 class="text-xl font-semibold mb-2 text-gray-900 dark:text-white">Visualisiere Wissen</h3>
|
||||||
<p>
|
<p class="text-gray-700 dark:text-gray-300">
|
||||||
Sieh Wissen als vernetztes System, entdecke Zusammenhänge und erkenne überraschende
|
Organisiere Gedanken und Ideen in einem interaktiven Netzwerk, das komplexe Beziehungen
|
||||||
Verbindungen zwischen verschiedenen Themengebieten.
|
visuell darstellt und neue Verbindungen offenbart.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Feature Card 2 -->
|
<!-- Feature 2: Connect -->
|
||||||
<div class="feature-card p-8 rounded-3xl hover:-translate-y-3 transform transition-all duration-300">
|
<div class="featured-card rounded-2xl p-6 bg-white/80 dark:bg-gray-800/30 backdrop-blur-sm">
|
||||||
<div class="icon mb-6 rounded-2xl shadow-lg">
|
<div class="mb-4 text-indigo-600 dark:text-indigo-400">
|
||||||
<i class="fa-solid fa-lightbulb"></i>
|
<i class="fa-solid fa-network-wired text-3xl"></i>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-xl font-semibold mb-3">Teile Gedanken</h3>
|
<h3 class="text-xl font-semibold mb-2 text-gray-900 dark:text-white">Verknüpfe Gedanken</h3>
|
||||||
<p>
|
<p class="text-gray-700 dark:text-gray-300">
|
||||||
Füge deine eigenen Ideen und Perspektiven hinzu. Erstelle Verbindungen zu
|
Entdecke Zusammenhänge zwischen scheinbar unzusammenhängenden Ideen und schaffe dein
|
||||||
vorhandenen Gedanken und bereichere die wachsende Wissensbasis.
|
persönliches Wissensnetzwerk über verschiedene Bereiche hinweg.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Feature Card 3 -->
|
<!-- Feature 3: Share -->
|
||||||
<div class="feature-card p-8 rounded-3xl hover:-translate-y-3 transform transition-all duration-300">
|
<div class="featured-card rounded-2xl p-6 bg-white/80 dark:bg-gray-800/30 backdrop-blur-sm">
|
||||||
<div class="icon mb-6 rounded-2xl shadow-lg">
|
<div class="mb-4 text-purple-600 dark:text-purple-400">
|
||||||
<i class="fa-solid fa-users"></i>
|
<i class="fa-solid fa-share-nodes text-3xl"></i>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-xl font-semibold mb-3">Community</h3>
|
<h3 class="text-xl font-semibold mb-2 text-gray-900 dark:text-white">Teile und Entdecke</h3>
|
||||||
<p>
|
<p class="text-gray-700 dark:text-gray-300">
|
||||||
Sei Teil einer Gemeinschaft, die gemeinsam ein verteiltes Wissensarchiv aufbaut
|
Tausche Erkenntnisse mit anderen aus und erweitere dein Wissen durch die
|
||||||
und sich in thematisch fokussierten Bereichen austauscht.
|
Perspektiven und Gedanken der Community.
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Feature Card 4 -->
|
|
||||||
<div class="feature-card p-8 rounded-3xl hover:-translate-y-3 transform transition-all duration-300">
|
|
||||||
<div class="icon mb-6 rounded-2xl shadow-lg">
|
|
||||||
<i class="fa-solid fa-robot"></i>
|
|
||||||
</div>
|
|
||||||
<h3 class="text-xl font-semibold mb-3">KI-Assistenz</h3>
|
|
||||||
<p>
|
|
||||||
Lass dir von künstlicher Intelligenz helfen, neue Zusammenhänge zu entdecken,
|
|
||||||
Inhalte zusammenzufassen und Fragen zu beantworten.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Feature Card 5 -->
|
|
||||||
<div class="feature-card p-8 rounded-3xl hover:-translate-y-3 transform transition-all duration-300">
|
|
||||||
<div class="icon mb-6 rounded-2xl shadow-lg">
|
|
||||||
<i class="fa-solid fa-search"></i>
|
|
||||||
</div>
|
|
||||||
<h3 class="text-xl font-semibold mb-3">Intelligente Suche</h3>
|
|
||||||
<p>
|
|
||||||
Finde genau die Informationen, die du suchst, mit fortschrittlichen Such- und
|
|
||||||
Filterfunktionen für eine präzise Navigation durch das Wissen.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Feature Card 6 -->
|
|
||||||
<div class="feature-card p-8 rounded-3xl hover:-translate-y-3 transform transition-all duration-300">
|
|
||||||
<div class="icon mb-6 rounded-2xl shadow-lg">
|
|
||||||
<i class="fa-solid fa-route"></i>
|
|
||||||
</div>
|
|
||||||
<h3 class="text-xl font-semibold mb-3">Geführte Pfade</h3>
|
|
||||||
<p>
|
|
||||||
Folge kuratierten Lernpfaden durch komplexe Themen oder erschaffe selbst
|
|
||||||
Routen für andere, die deinen Gedankengängen folgen möchten.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Call to Action Section -->
|
<!-- AI Assistant Preview -->
|
||||||
<section class="py-16 sm:py-20 md:py-24 relative overflow-hidden">
|
<section class="py-20 relative">
|
||||||
<div class="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
<div class="mystical-line absolute top-0 left-1/2 transform -translate-x-1/2 w-1/3"></div>
|
||||||
<div class="glass-effect p-6 sm:p-8 md:p-12 rounded-3xl transform transition-all duration-500 hover:-translate-y-2 hover:shadow-2xl bg-gradient-to-br from-purple-500/15 to-blue-500/15 backdrop-blur-xl border border-white/10 shadow-lg">
|
|
||||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-6">
|
|
||||||
<div class="md:w-2/3">
|
|
||||||
<h2 class="text-2xl sm:text-3xl lg:text-4xl font-bold mb-3 text-gray-900 dark:text-white leading-tight">
|
|
||||||
Bereit, <span class="gradient-text bg-clip-text text-transparent bg-gradient-to-r from-purple-500 to-blue-500">Wissen</span> neu zu entdecken?
|
|
||||||
</h2>
|
|
||||||
<p class="text-gray-700 dark:text-gray-300 text-base sm:text-lg mb-6 md:mb-0 max-w-2xl">
|
|
||||||
Starte jetzt deine Reise durch das Wissensnetzwerk und erschließe neue Perspektiven.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="md:w-1/3 text-center md:text-right">
|
|
||||||
<a href="{{ url_for('mindmap') }}" class="inline-flex items-center justify-center w-full md:w-auto btn-primary font-bold py-3 sm:py-3.5 px-6 sm:px-8 rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1 hover:scale-105 bg-gradient-to-r from-purple-600 to-blue-600 text-white">
|
|
||||||
<span class="flex items-center justify-center">
|
|
||||||
<i class="fa-solid fa-arrow-right mr-2"></i>
|
|
||||||
<span>Zur Mindmap</span>
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Quick Access Section -->
|
|
||||||
<section class="py-16 sm:py-20">
|
|
||||||
<div class="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div class="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 lg:gap-8">
|
<div class="text-center mb-12">
|
||||||
<!-- Themen-Übersicht -->
|
<h2 class="section-heading mb-4 text-gray-900 dark:text-white">Dein <span class="gradient-text">KI-Assistent</span></h2>
|
||||||
<div class="glass-morphism p-6 sm:p-8 rounded-3xl transition-all duration-500 hover:-translate-y-3 hover:shadow-xl border border-white/10 backdrop-blur-md">
|
<p class="text-lg text-gray-700 dark:text-gray-300 max-w-3xl mx-auto">
|
||||||
<h3 class="text-xl font-bold mb-4 flex items-center text-gray-800 dark:text-white">
|
Unser integrierter KI-Assistent hilft dir, Wissen zu organisieren, Verbindungen zu finden und
|
||||||
<div class="w-10 h-10 sm:w-12 sm:h-12 rounded-2xl bg-gradient-to-r from-violet-500 to-fuchsia-500 flex items-center justify-center mr-3 sm:mr-4 shadow-md transform transition-transform duration-300 hover:scale-110">
|
Einsichten zu gewinnen.
|
||||||
<i class="fa-solid fa-fire text-white text-base sm:text-lg"></i>
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 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>
|
||||||
<span class="text-lg sm:text-xl md:text-2xl">Themen-Übersicht</span>
|
|
||||||
</h3>
|
|
||||||
<div class="space-y-3 sm:space-y-4 mb-6">
|
|
||||||
<a href="{{ url_for('mindmap') }}" class="flex items-center p-3 sm:p-3.5 rounded-xl hover:bg-gray-100/50 dark:hover:bg-white/5 transition-all duration-200 group">
|
|
||||||
<div class="w-3 h-3 rounded-full bg-purple-400 mr-3 group-hover:scale-125 transition-transform"></div>
|
|
||||||
<div class="flex-grow">
|
|
||||||
<p class="font-medium text-gray-800 dark:text-gray-200">Wissensbereiche <span class="text-xs text-gray-500">(12)</span></p>
|
|
||||||
<p class="text-xs sm:text-sm text-gray-500 dark:text-gray-400">Überblick über Themenbereiche</p>
|
|
||||||
</div>
|
|
||||||
<i class="fa-solid fa-chevron-right text-gray-500 group-hover:translate-x-1 transition-transform"></i>
|
|
||||||
</a>
|
|
||||||
<a href="{{ url_for('search_thoughts_page') }}" class="flex items-center p-3 sm:p-3.5 rounded-xl hover:bg-gray-100/50 dark:hover:bg-white/5 transition-all duration-200 group">
|
|
||||||
<div class="w-3 h-3 rounded-full bg-blue-400 mr-3 group-hover:scale-125 transition-transform"></div>
|
|
||||||
<div class="flex-grow">
|
|
||||||
<p class="font-medium text-gray-800 dark:text-gray-200">Gedanken <span class="text-xs text-gray-500">(87)</span></p>
|
|
||||||
<p class="text-xs sm:text-sm text-gray-500 dark:text-gray-400">Konkrete Einträge durchsuchen</p>
|
|
||||||
</div>
|
|
||||||
<i class="fa-solid fa-chevron-right text-gray-500 group-hover:translate-x-1 transition-transform"></i>
|
|
||||||
</a>
|
|
||||||
<a href="#" class="flex items-center p-3 sm:p-3.5 rounded-xl hover:bg-gray-100/50 dark:hover:bg-white/5 transition-all duration-200 group">
|
|
||||||
<div class="w-3 h-3 rounded-full bg-green-400 mr-3 group-hover:scale-125 transition-transform"></div>
|
|
||||||
<div class="flex-grow">
|
|
||||||
<p class="font-medium text-gray-800 dark:text-gray-200">Verbindungen <span class="text-xs text-gray-500">(34)</span></p>
|
|
||||||
<p class="text-xs sm:text-sm text-gray-500 dark:text-gray-400">Beziehungen zwischen Gedanken</p>
|
|
||||||
</div>
|
|
||||||
<i class="fa-solid fa-chevron-right text-gray-500 group-hover:translate-x-1 transition-transform"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
<a href="{{ url_for('search_thoughts_page') }}" class="btn-primary w-full text-center rounded-xl py-3 sm:py-3.5 transform transition-all duration-300 hover:-translate-y-1 hover:shadow-lg flex items-center justify-center">
|
|
||||||
<span>Alle Themen entdecken</span>
|
<!-- Chat Messages -->
|
||||||
<i class="fa-solid fa-arrow-right ml-2"></i>
|
<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>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- KI-Assistent mit eingebettetem Chat -->
|
<!-- Step 3 -->
|
||||||
<div class="glass-morphism p-6 sm:p-8 rounded-3xl transition-all duration-500 hover:-translate-y-1 hover:shadow-xl backdrop-blur-md border border-white/10">
|
<div class="featured-card rounded-2xl p-6 bg-white/80 dark:bg-gray-800/30 backdrop-blur-sm relative overflow-hidden">
|
||||||
<h3 class="text-xl md:text-2xl font-bold mb-4 flex flex-wrap sm:flex-nowrap items-center text-gray-800 dark:text-white">
|
<div class="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">
|
||||||
<div class="w-10 h-10 sm:w-12 sm:h-12 rounded-2xl bg-gradient-to-r from-purple-600 to-blue-600 flex items-center justify-center mr-3 sm:mr-4 shadow-lg transform transition-transform duration-300 hover:scale-110">
|
3
|
||||||
<i class="fa-solid fa-robot text-white text-base sm:text-lg"></i>
|
|
||||||
</div>
|
|
||||||
<span class="mt-1 sm:mt-0">KI-Assistent</span>
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<!-- Eingebettetes Chat-Interface -->
|
|
||||||
<div id="embedded-assistant" class="rounded-xl border border-gray-200/50 dark:border-gray-700/50 overflow-hidden flex flex-col h-[300px]">
|
|
||||||
<!-- Chat Verlauf -->
|
|
||||||
<div id="embedded-chat-messages" class="flex-grow p-4 overflow-y-auto space-y-3 bg-white/70 dark:bg-gray-800/70">
|
|
||||||
<!-- Begrüßungsnachricht -->
|
|
||||||
<div class="flex items-start space-x-2">
|
|
||||||
<div class="w-8 h-8 rounded-full bg-gradient-to-r from-purple-600 to-blue-600 flex items-center justify-center flex-shrink-0">
|
|
||||||
<i class="fa-solid fa-robot text-white text-xs"></i>
|
|
||||||
</div>
|
|
||||||
<div class="max-w-[85%] bg-purple-100 dark:bg-gray-700 p-3 rounded-xl rounded-tl-none shadow-sm">
|
|
||||||
<p class="text-sm text-gray-700 dark:text-gray-200">Hallo! Ich bin dein KI-Assistent. Wie kann ich dir helfen?</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Chat Eingabe -->
|
|
||||||
<div class="p-3 border-t border-gray-200/70 dark:border-gray-700/70 bg-gray-50/90 dark:bg-gray-800/90">
|
|
||||||
<form id="embedded-chat-form" class="flex items-center space-x-2">
|
|
||||||
<input type="text" id="embedded-chat-input"
|
|
||||||
placeholder="Stelle eine Frage..."
|
|
||||||
class="flex-grow px-4 py-2 rounded-xl border bg-white/90 dark:bg-gray-700/90 border-gray-300 dark:border-gray-600 shadow-sm focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-all duration-200 placeholder-gray-400 dark:placeholder-gray-500 text-gray-700 dark:text-gray-200">
|
|
||||||
<button type="submit"
|
|
||||||
class="p-2 rounded-xl bg-gradient-to-r from-purple-600 to-blue-600 text-white shadow-md hover:shadow-lg transition-all duration-200 hover:-translate-y-0.5">
|
|
||||||
<i class="fa-solid fa-paper-plane"></i>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<h3 class="text-xl font-semibold mb-4 text-gray-900 dark:text-white">Gedanken teilen</h3>
|
||||||
<!-- Schnelllinks unter dem Chat -->
|
<p class="text-gray-700 dark:text-gray-300 mb-4">
|
||||||
<div class="mt-4 flex flex-wrap gap-2">
|
Teile deine eigenen Gedanken, verbinde sie mit vorhandenen Knoten und baue das kollektive Wissen aus.
|
||||||
<button class="quick-query-btn px-2 sm:px-3 py-1 sm:py-1.5 bg-gray-100 hover:bg-gray-200 dark:bg-gray-800/70 dark:hover:bg-gray-700/80 rounded-lg sm:rounded-xl text-xs text-gray-700 dark:text-gray-300 transition-all duration-200 hover:-translate-y-0.5 shadow-sm hover:shadow">
|
</p>
|
||||||
Was ist Systades?
|
{% if current_user.is_authenticated %}
|
||||||
</button>
|
<a href="{{ url_for('profile') }}" class="text-purple-600 dark:text-purple-400 hover:underline">
|
||||||
<button class="quick-query-btn px-2 sm:px-3 py-1 sm:py-1.5 bg-gray-100 hover:bg-gray-200 dark:bg-gray-800/70 dark:hover:bg-gray-700/80 rounded-lg sm:rounded-xl text-xs text-gray-700 dark:text-gray-300 transition-all duration-200 hover:-translate-y-0.5 shadow-sm hover:shadow">
|
Zum Profil <i class="fa-solid fa-arrow-right ml-1"></i>
|
||||||
Wie erstelle ich eine Mindmap?
|
</a>
|
||||||
</button>
|
{% else %}
|
||||||
<button class="quick-query-btn px-2 sm:px-3 py-1 sm:py-1.5 bg-gray-100 hover:bg-gray-200 dark:bg-gray-800/70 dark:hover:bg-gray-700/80 rounded-lg sm:rounded-xl text-xs text-gray-700 dark:text-gray-300 transition-all duration-200 hover:-translate-y-0.5 shadow-sm hover:shadow">
|
<a href="{{ url_for('login') }}" class="text-purple-600 dark:text-purple-400 hover:underline">
|
||||||
Zeige neueste Gedanken
|
Jetzt anmelden <i class="fa-solid fa-arrow-right ml-1"></i>
|
||||||
</button>
|
</a>
|
||||||
</div>
|
{% endif %}
|
||||||
|
|
||||||
<!-- Vollständigen KI-Chat öffnen -->
|
|
||||||
<button onclick="window.MindMap.assistant.toggleAssistant(true)" class="mt-4 btn-primary w-full text-center rounded-xl py-2 sm:py-2.5 shadow-md hover:shadow-lg transition-all duration-300 hover:-translate-y-1 flex items-center justify-center">
|
|
||||||
<i class="fa-solid fa-expand mr-2"></i>
|
|
||||||
<span>Chat in Vollansicht öffnen</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- Call to Action -->
|
||||||
|
<section class="py-20 relative">
|
||||||
|
<div class="mystical-line absolute top-0 left-1/2 transform -translate-x-1/2 w-1/3"></div>
|
||||||
|
|
||||||
|
<div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||||
|
<h2 class="section-heading mb-6 text-gray-900 dark:text-white">Bereit, dein <span class="gradient-text">Wissen zu vernetzen</span>?</h2>
|
||||||
|
<p class="text-lg text-gray-700 dark:text-gray-300 mb-8">
|
||||||
|
Tritt unserer wachsenden Community bei und entdecke eine neue Art, Wissen zu organisieren und zu teilen.
|
||||||
|
</p>
|
||||||
|
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||||
|
<a href="{{ url_for('mindmap') }}" class="mystical-button mystical-button-primary">
|
||||||
|
<i class="fa-solid fa-diagram-project mr-2"></i> Mindmap erkunden
|
||||||
|
</a>
|
||||||
|
{% if not current_user.is_authenticated %}
|
||||||
|
<a href="{{ url_for('register') }}" class="mystical-button mystical-button-secondary">
|
||||||
|
<i class="fa-solid fa-user-plus mr-2"></i> Konto erstellen
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
<!-- JavaScript für eingebetteten Chat -->
|
{% block extra_js %}
|
||||||
{% block scripts %}
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
// Warten bis MindMap und der Assistent initialisiert sind
|
// Expand-Button mit dem echten Assistenten verknüpfen
|
||||||
const waitForAssistant = setInterval(() => {
|
const openRealAssistantBtn = document.getElementById('open-real-assistant');
|
||||||
if (window.MindMap && window.MindMap.assistant) {
|
if (openRealAssistantBtn) {
|
||||||
clearInterval(waitForAssistant);
|
openRealAssistantBtn.addEventListener('click', function() {
|
||||||
initEmbeddedChat();
|
if (window.MindMap && window.MindMap.assistant) {
|
||||||
}
|
window.MindMap.assistant.toggleAssistant(true);
|
||||||
}, 200);
|
|
||||||
|
|
||||||
function initEmbeddedChat() {
|
|
||||||
const chatForm = document.getElementById('embedded-chat-form');
|
|
||||||
const chatInput = document.getElementById('embedded-chat-input');
|
|
||||||
const messagesContainer = document.getElementById('embedded-chat-messages');
|
|
||||||
const quickQueryBtns = document.querySelectorAll('.quick-query-btn');
|
|
||||||
|
|
||||||
// Event-Listener für das Chat-Formular
|
|
||||||
chatForm.addEventListener('submit', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
const userMessage = chatInput.value.trim();
|
|
||||||
if (!userMessage) return;
|
|
||||||
|
|
||||||
// Nachricht des Benutzers anzeigen
|
|
||||||
appendMessage('user', userMessage);
|
|
||||||
chatInput.value = '';
|
|
||||||
|
|
||||||
// Anzeigen, dass der Assistent antwortet
|
|
||||||
const typingIndicator = appendTypingIndicator();
|
|
||||||
|
|
||||||
// API-Anfrage an den Assistenten senden
|
|
||||||
sendToAssistant(userMessage)
|
|
||||||
.then(response => {
|
|
||||||
// Entferne Tipp-Indikator
|
|
||||||
typingIndicator.remove();
|
|
||||||
// Zeige Antwort des Assistenten an
|
|
||||||
appendMessage('assistant', response);
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
typingIndicator.remove();
|
|
||||||
appendMessage('assistant', 'Es tut mir leid, ich konnte deine Nachricht nicht verarbeiten. Bitte versuche es später noch einmal.');
|
|
||||||
console.error('Fehler bei der Kommunikation mit dem Assistenten:', error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Schnellabfragen-Buttons
|
|
||||||
quickQueryBtns.forEach(btn => {
|
|
||||||
btn.addEventListener('click', function() {
|
|
||||||
const query = this.textContent.trim();
|
|
||||||
chatInput.value = query;
|
|
||||||
chatForm.dispatchEvent(new Event('submit'));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Funktion zum Hinzufügen einer Nachricht zum Chat
|
|
||||||
function appendMessage(sender, message) {
|
|
||||||
const messageElement = document.createElement('div');
|
|
||||||
messageElement.className = 'flex items-start space-x-2';
|
|
||||||
|
|
||||||
if (sender === 'user') {
|
|
||||||
messageElement.innerHTML = `
|
|
||||||
<div class="flex-grow"></div>
|
|
||||||
<div class="max-w-[85%] bg-blue-100 dark:bg-blue-900/40 p-3 rounded-xl rounded-tr-none shadow-sm">
|
|
||||||
<p class="text-sm text-gray-700 dark:text-gray-200">${message}</p>
|
|
||||||
</div>
|
|
||||||
<div class="w-8 h-8 rounded-full bg-gradient-to-r from-blue-500 to-indigo-500 flex items-center justify-center flex-shrink-0">
|
|
||||||
<i class="fa-solid fa-user text-white text-xs"></i>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
} else {
|
|
||||||
messageElement.innerHTML = `
|
|
||||||
<div class="w-8 h-8 rounded-full bg-gradient-to-r from-purple-600 to-blue-600 flex items-center justify-center flex-shrink-0">
|
|
||||||
<i class="fa-solid fa-robot text-white text-xs"></i>
|
|
||||||
</div>
|
|
||||||
<div class="max-w-[85%] bg-purple-100 dark:bg-gray-700 p-3 rounded-xl rounded-tl-none shadow-sm">
|
|
||||||
<p class="text-sm text-gray-700 dark:text-gray-200">${message}</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
messagesContainer.appendChild(messageElement);
|
|
||||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tipp-Indikator für "Assistent schreibt..."
|
|
||||||
function appendTypingIndicator() {
|
|
||||||
const indicatorElement = document.createElement('div');
|
|
||||||
indicatorElement.className = 'flex items-start space-x-2 typing-indicator';
|
|
||||||
indicatorElement.innerHTML = `
|
|
||||||
<div class="w-8 h-8 rounded-full bg-gradient-to-r from-purple-600 to-blue-600 flex items-center justify-center flex-shrink-0">
|
|
||||||
<i class="fa-solid fa-robot text-white text-xs"></i>
|
|
||||||
</div>
|
|
||||||
<div class="max-w-[85%] bg-purple-100 dark:bg-gray-700 p-3 rounded-xl rounded-tl-none shadow-sm">
|
|
||||||
<p class="text-sm text-gray-500 dark:text-gray-400 flex items-center">
|
|
||||||
<span class="mr-1">Tipp</span>
|
|
||||||
<span class="typing-dots flex space-x-1">
|
|
||||||
<span class="w-1.5 h-1.5 bg-gray-500 dark:bg-gray-400 rounded-full animate-bounce" style="animation-delay: 0ms;"></span>
|
|
||||||
<span class="w-1.5 h-1.5 bg-gray-500 dark:bg-gray-400 rounded-full animate-bounce" style="animation-delay: 150ms;"></span>
|
|
||||||
<span class="w-1.5 h-1.5 bg-gray-500 dark:bg-gray-400 rounded-full animate-bounce" style="animation-delay: 300ms;"></span>
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
messagesContainer.appendChild(indicatorElement);
|
|
||||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
|
||||||
return indicatorElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sende Nachricht an den Assistenten und erhalte Antwort
|
|
||||||
async function sendToAssistant(message) {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/assistant', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
messages: [
|
|
||||||
{ role: "system", content: "Du bist ein hilfreicher Assistent für das Wissensnetzwerk Systades." },
|
|
||||||
{ role: "user", content: message }
|
|
||||||
]
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(data.error || 'Unbekannter Fehler');
|
|
||||||
}
|
|
||||||
|
|
||||||
return data.response || data.answer || 'Ich habe keine Antwort erhalten.';
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Fehler bei der API-Anfrage:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -32,7 +32,7 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-avatar {
|
.avatar-container {
|
||||||
width: 180px;
|
width: 180px;
|
||||||
height: 180px;
|
height: 180px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
@@ -50,40 +50,48 @@
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-avatar:hover {
|
.avatar-container:hover {
|
||||||
transform: scale(1.05);
|
transform: scale(1.05);
|
||||||
border: 3px solid rgba(179, 143, 255, 0.5);
|
border: 3px solid rgba(179, 143, 255, 0.5);
|
||||||
box-shadow: 0 12px 45px rgba(0, 0, 0, 0.3), 0 0 25px rgba(179, 143, 255, 0.35);
|
box-shadow: 0 12px 45px rgba(0, 0, 0, 0.3), 0 0 25px rgba(179, 143, 255, 0.35);
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-avatar img {
|
.avatar-container img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
transition: filter 0.3s ease;
|
transition: filter 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-avatar:hover img {
|
.avatar-container:hover img {
|
||||||
filter: brightness(1.1);
|
filter: brightness(1.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-avatar-placeholder {
|
.avatar-edit {
|
||||||
font-size: 5rem;
|
position: absolute;
|
||||||
color: rgba(255, 255, 255, 0.6);
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-avatar:hover .profile-avatar-placeholder {
|
.avatar-edit:hover {
|
||||||
color: rgba(255, 255, 255, 0.9);
|
background: rgba(255, 255, 255, 0.3);
|
||||||
transform: scale(1.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-info {
|
.user-info {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding-top: 0.5rem;
|
padding-top: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-name {
|
.user-info h1 {
|
||||||
font-size: 2.75rem;
|
font-size: 2.75rem;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
margin-bottom: 0.75rem;
|
margin-bottom: 0.75rem;
|
||||||
@@ -96,33 +104,7 @@
|
|||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-username {
|
.user-bio {
|
||||||
font-size: 1.35rem;
|
|
||||||
color: rgba(255, 255, 255, 0.85);
|
|
||||||
margin-bottom: 1.25rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.username-badge {
|
|
||||||
background: rgba(179, 143, 255, 0.2);
|
|
||||||
border: 1px solid rgba(179, 143, 255, 0.3);
|
|
||||||
color: #b38fff;
|
|
||||||
padding: 0.3rem 1rem;
|
|
||||||
border-radius: 20px;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-weight: 600;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.username-badge:hover {
|
|
||||||
background: rgba(179, 143, 255, 0.3);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2), 0 0 8px rgba(179, 143, 255, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-bio {
|
|
||||||
font-size: 1.15rem;
|
font-size: 1.15rem;
|
||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
color: rgba(255, 255, 255, 0.9);
|
color: rgba(255, 255, 255, 0.9);
|
||||||
@@ -131,7 +113,7 @@
|
|||||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-meta {
|
.user-meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 1.5rem;
|
gap: 1.5rem;
|
||||||
@@ -140,118 +122,22 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-meta-item {
|
.user-meta span {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
transition: all 0.25s ease;
|
transition: all 0.25s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-meta-item:hover {
|
.user-meta span:hover {
|
||||||
color: rgba(255, 255, 255, 1);
|
color: rgba(255, 255, 255, 1);
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-meta-icon {
|
.user-meta i {
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Verbesserte Statistik-Karten mit interaktiven Effekten */
|
|
||||||
.profile-stats {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
||||||
gap: 1.5rem;
|
|
||||||
margin-top: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-item {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
padding: 1.75rem 1.25rem;
|
|
||||||
background: rgba(32, 36, 55, 0.7);
|
|
||||||
backdrop-filter: blur(15px);
|
|
||||||
-webkit-backdrop-filter: blur(15px);
|
|
||||||
border-radius: 20px;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
text-align: center;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-item::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: radial-gradient(circle at center, rgba(179, 143, 255, 0.15) 0%, transparent 70%);
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.5s ease;
|
|
||||||
z-index: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-item:hover::before {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-item:hover {
|
|
||||||
transform: translateY(-5px);
|
|
||||||
background: rgba(32, 36, 55, 0.8);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
|
||||||
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.25), 0 0 20px rgba(179, 143, 255, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-value {
|
|
||||||
font-size: 2.25rem;
|
|
||||||
font-weight: 800;
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
background: linear-gradient(135deg, #b38fff, #58a9ff);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
background-clip: text;
|
|
||||||
color: transparent;
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-item:hover .stat-value {
|
|
||||||
transform: scale(1.1);
|
|
||||||
text-shadow: 0 0 15px rgba(179, 143, 255, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-label {
|
|
||||||
font-size: 0.95rem;
|
|
||||||
color: rgba(255, 255, 255, 0.75);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 1px;
|
|
||||||
font-weight: 600;
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-item:hover .stat-label {
|
|
||||||
color: rgba(255, 255, 255, 0.95);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Stat-Icon für visuelle Verstärkung */
|
|
||||||
.stat-icon {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
color: rgba(179, 143, 255, 0.7);
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-item:hover .stat-icon {
|
|
||||||
transform: scale(1.2) translateY(-3px);
|
|
||||||
color: rgba(179, 143, 255, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Benutzer-Aktionsbereich */
|
/* Benutzer-Aktionsbereich */
|
||||||
.profile-actions {
|
.profile-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -504,31 +390,24 @@
|
|||||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
html.light .profile-avatar {
|
html.light .avatar-container {
|
||||||
background: rgba(255, 255, 255, 0.9);
|
background: rgba(255, 255, 255, 0.9);
|
||||||
border: 3px solid rgba(126, 63, 242, 0.3);
|
border: 3px solid rgba(126, 63, 242, 0.3);
|
||||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1), 0 0 15px rgba(126, 63, 242, 0.15);
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1), 0 0 15px rgba(126, 63, 242, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
html.light .profile-name {
|
html.light .user-info h1 {
|
||||||
background: linear-gradient(135deg, #7e3ff2, #3282f6);
|
background: linear-gradient(135deg, #7e3ff2, #3282f6);
|
||||||
text-shadow: none;
|
text-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
html.light .profile-username,
|
html.light .user-bio,
|
||||||
html.light .profile-bio,
|
|
||||||
html.light .activity-content {
|
html.light .activity-content {
|
||||||
color: #1a202c;
|
color: #1a202c;
|
||||||
text-shadow: none;
|
text-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
html.light .username-badge {
|
html.light .user-meta span {
|
||||||
background: rgba(126, 63, 242, 0.15);
|
|
||||||
border: 1px solid rgba(126, 63, 242, 0.3);
|
|
||||||
color: #7e3ff2;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.light .profile-meta {
|
|
||||||
color: #4a5568;
|
color: #4a5568;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -606,63 +485,22 @@
|
|||||||
<div class="container mx-auto px-4 py-10">
|
<div class="container mx-auto px-4 py-10">
|
||||||
<!-- Profil-Container -->
|
<!-- Profil-Container -->
|
||||||
<div class="profile-container">
|
<div class="profile-container">
|
||||||
<!-- Profil-Header mit Benutzerinformationen -->
|
<!-- User Info Section -->
|
||||||
<div class="profile-header">
|
<div class="profile-header">
|
||||||
<!-- Profilbild -->
|
<div class="avatar-container">
|
||||||
<div class="profile-avatar">
|
<img src="{{ user.avatar if user.avatar else url_for('static', filename='img/default-avatar.png') }}" alt="Profilbild" class="avatar">
|
||||||
{% if user.profile_image %}
|
<div class="avatar-edit">
|
||||||
<img src="{{ user.profile_image }}" alt="{{ user.name }}" />
|
<i class="fas fa-camera"></i>
|
||||||
{% else %}
|
</div>
|
||||||
<div class="profile-avatar-placeholder">
|
|
||||||
<i class="fas fa-user"></i>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="user-info">
|
||||||
<!-- Profilinformationen -->
|
<h1>{{ user.username }}</h1>
|
||||||
<div class="profile-info">
|
<p class="user-bio">{{ user.bio if user.bio else 'Keine Bio vorhanden. Klicke auf bearbeiten, um eine hinzuzufügen.' }}</p>
|
||||||
<h1 class="profile-name">{{ user.name|default('Max Mustermann') }}</h1>
|
<div class="user-meta">
|
||||||
<div class="profile-username">
|
<span><i class="fas fa-map-marker-alt"></i> {{ user.location if user.location else 'Kein Standort angegeben' }}</span>
|
||||||
@{{ user.username|default('maxmustermann') }}
|
<span><i class="fas fa-calendar-alt"></i> Mitglied seit {{ user.created_at.strftime('%d.%m.%Y') }}</span>
|
||||||
{% if user.verified %}
|
|
||||||
<span class="username-badge">
|
|
||||||
<i class="fas fa-check-circle mr-1"></i> Verifiziert
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="profile-bio">
|
|
||||||
{{ user.bio|default('Willkommen auf meinem Profil! Ich bin daran interessiert, Wissen zu vernetzen und neue Verbindungen zwischen verschiedenen Themengebieten zu entdecken. Mein Ziel ist es, ein tieferes Verständnis für komplexe Zusammenhänge zu entwickeln.') }}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<!-- Meta-Informationen -->
|
|
||||||
<div class="profile-meta">
|
|
||||||
<div class="profile-meta-item">
|
|
||||||
<i class="fas fa-map-marker-alt profile-meta-icon"></i>
|
|
||||||
<span>{{ user.location|default('Berlin, Deutschland') }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="profile-meta-item">
|
|
||||||
<i class="fas fa-calendar-alt profile-meta-icon"></i>
|
|
||||||
<span>Mitglied seit {{ user.joined_date|default('Januar 2023') }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="profile-meta-item">
|
|
||||||
<i class="fas fa-globe profile-meta-icon"></i>
|
|
||||||
<span>{{ user.website|default('www.beispiel.de') }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Profil-Aktionen -->
|
|
||||||
<div class="profile-actions">
|
|
||||||
<button class="profile-action-btn primary">
|
|
||||||
<i class="fas fa-user-plus mr-1"></i> Folgen
|
|
||||||
</button>
|
|
||||||
<button class="profile-action-btn">
|
|
||||||
<i class="fas fa-comment mr-1"></i> Nachricht
|
|
||||||
</button>
|
|
||||||
<button class="profile-action-btn">
|
|
||||||
<i class="fas fa-share-alt mr-1"></i> Teilen
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<button class="edit-profile-btn">Profil bearbeiten</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -673,7 +511,7 @@
|
|||||||
<div class="stat-icon">
|
<div class="stat-icon">
|
||||||
<i class="fas fa-lightbulb"></i>
|
<i class="fas fa-lightbulb"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-value">{{ user.thoughts_count|default('42') }}</div>
|
<div class="stat-value">{{ stats.thought_count if stats and stats.thought_count else 0 }}</div>
|
||||||
<div class="stat-label">Gedanken</div>
|
<div class="stat-label">Gedanken</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -682,7 +520,7 @@
|
|||||||
<div class="stat-icon">
|
<div class="stat-icon">
|
||||||
<i class="fas fa-project-diagram"></i>
|
<i class="fas fa-project-diagram"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-value">{{ user.connections_count|default('128') }}</div>
|
<div class="stat-value">{{ stats.connections_count if stats and stats.connections_count else 0 }}</div>
|
||||||
<div class="stat-label">Verbindungen</div>
|
<div class="stat-label">Verbindungen</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -691,7 +529,7 @@
|
|||||||
<div class="stat-icon">
|
<div class="stat-icon">
|
||||||
<i class="fas fa-users"></i>
|
<i class="fas fa-users"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-value">{{ user.followers_count|default('567') }}</div>
|
<div class="stat-value">{{ stats.followers_count if stats and stats.followers_count else 0 }}</div>
|
||||||
<div class="stat-label">Follower</div>
|
<div class="stat-label">Follower</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -700,7 +538,7 @@
|
|||||||
<div class="stat-icon">
|
<div class="stat-icon">
|
||||||
<i class="fas fa-comment-dots"></i>
|
<i class="fas fa-comment-dots"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-value">{{ user.contributions_count|default('89') }}</div>
|
<div class="stat-value">{{ stats.contributions_count if stats and stats.contributions_count else 0 }}</div>
|
||||||
<div class="stat-label">Beiträge</div>
|
<div class="stat-label">Beiträge</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -709,7 +547,7 @@
|
|||||||
<div class="stat-icon">
|
<div class="stat-icon">
|
||||||
<i class="fas fa-star"></i>
|
<i class="fas fa-star"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-value">{{ user.rating|default('4.8') }}</div>
|
<div class="stat-value">{{ stats.rating if stats and stats.rating else '0.0' }}</div>
|
||||||
<div class="stat-label">Bewertung</div>
|
<div class="stat-label">Bewertung</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -731,115 +569,124 @@
|
|||||||
<!-- Aktivitäts-Tab (Standardmäßig angezeigt) -->
|
<!-- Aktivitäts-Tab (Standardmäßig angezeigt) -->
|
||||||
<div class="tab-content" id="activity-tab">
|
<div class="tab-content" id="activity-tab">
|
||||||
<div class="activity-feed">
|
<div class="activity-feed">
|
||||||
<!-- Aktivität 1 -->
|
{% if activities %}
|
||||||
<div class="activity-card">
|
{% for activity in activities %}
|
||||||
<div class="activity-header">
|
<div class="activity-card">
|
||||||
<div class="activity-title">Neuer Gedanke hinzugefügt</div>
|
<div class="activity-header">
|
||||||
<div class="activity-date">vor 2 Stunden</div>
|
<div class="activity-title">{{ activity.title }}</div>
|
||||||
</div>
|
<div class="activity-date">{{ activity.date }}</div>
|
||||||
<div class="activity-content">
|
</div>
|
||||||
<p>Ich habe einen neuen Gedanken zum Thema "Künstliche Intelligenz und Kreativität" hinzugefügt. Wie können KI-Tools uns dabei helfen, kreativer zu denken?</p>
|
<div class="activity-content">
|
||||||
</div>
|
<p>{{ activity.content }}</p>
|
||||||
<div class="activity-footer">
|
</div>
|
||||||
<div class="activity-reactions">
|
<div class="activity-footer">
|
||||||
<button class="reaction-button">
|
<div class="activity-reactions">
|
||||||
<i class="fas fa-thumbs-up"></i> <span>24</span>
|
<button class="reaction-button {% if activity.user_liked %}active{% endif %}">
|
||||||
</button>
|
<i class="fas fa-thumbs-up"></i> <span>{{ activity.likes }}</span>
|
||||||
<button class="reaction-button">
|
</button>
|
||||||
<i class="fas fa-comment"></i> <span>8</span>
|
<button class="reaction-button">
|
||||||
</button>
|
<i class="fas fa-comment"></i> <span>{{ activity.comments }}</span>
|
||||||
<button class="reaction-button">
|
</button>
|
||||||
<i class="fas fa-share"></i> <span>3</span>
|
<button class="reaction-button">
|
||||||
</button>
|
<i class="fas fa-share"></i> <span>{{ activity.shares }}</span>
|
||||||
</div>
|
</button>
|
||||||
<div class="activity-actions">
|
</div>
|
||||||
<button class="action-button">
|
<div class="activity-actions">
|
||||||
Ansehen
|
<button class="action-button">
|
||||||
</button>
|
Ansehen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</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>
|
</div>
|
||||||
</div>
|
{% endif %}
|
||||||
|
|
||||||
<!-- Aktivität 2 -->
|
|
||||||
<div class="activity-card">
|
|
||||||
<div class="activity-header">
|
|
||||||
<div class="activity-title">Verbindung erstellt</div>
|
|
||||||
<div class="activity-date">vor 1 Tag</div>
|
|
||||||
</div>
|
|
||||||
<div class="activity-content">
|
|
||||||
<p>Ich habe eine neue Verbindung zwischen "Nachhaltige Entwicklung" und "Digitale Transformation" hergestellt. Es gibt interessante Überschneidungen in diesen Bereichen.</p>
|
|
||||||
</div>
|
|
||||||
<div class="activity-footer">
|
|
||||||
<div class="activity-reactions">
|
|
||||||
<button class="reaction-button active">
|
|
||||||
<i class="fas fa-thumbs-up"></i> <span>42</span>
|
|
||||||
</button>
|
|
||||||
<button class="reaction-button">
|
|
||||||
<i class="fas fa-comment"></i> <span>12</span>
|
|
||||||
</button>
|
|
||||||
<button class="reaction-button">
|
|
||||||
<i class="fas fa-share"></i> <span>7</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="activity-actions">
|
|
||||||
<button class="action-button">
|
|
||||||
Ansehen
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Aktivität 3 -->
|
|
||||||
<div class="activity-card">
|
|
||||||
<div class="activity-header">
|
|
||||||
<div class="activity-title">Sammlung erstellt</div>
|
|
||||||
<div class="activity-date">vor 3 Tagen</div>
|
|
||||||
</div>
|
|
||||||
<div class="activity-content">
|
|
||||||
<p>Ich habe eine neue Sammlung zum Thema "Zukunftstechnologien" erstellt. Diese Sammlung enthält Gedanken zu KI, Quantencomputing, Biotechnologie und mehr.</p>
|
|
||||||
</div>
|
|
||||||
<div class="activity-footer">
|
|
||||||
<div class="activity-reactions">
|
|
||||||
<button class="reaction-button">
|
|
||||||
<i class="fas fa-thumbs-up"></i> <span>17</span>
|
|
||||||
</button>
|
|
||||||
<button class="reaction-button">
|
|
||||||
<i class="fas fa-comment"></i> <span>4</span>
|
|
||||||
</button>
|
|
||||||
<button class="reaction-button">
|
|
||||||
<i class="fas fa-share"></i> <span>2</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="activity-actions">
|
|
||||||
<button class="action-button">
|
|
||||||
Ansehen
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Weitere Tab-Inhalte (werden per JavaScript angezeigt) -->
|
<!-- Weitere Tab-Inhalte (werden per JavaScript angezeigt) -->
|
||||||
<div class="tab-content hidden" id="thoughts-tab">
|
<div class="tab-content hidden" id="thoughts-tab">
|
||||||
<p class="text-center text-gray-400 py-12">
|
<div id="thoughts-container">
|
||||||
<i class="fas fa-spinner fa-spin text-3xl mb-4 block"></i>
|
{% if thoughts %}
|
||||||
Gedanken werden geladen...
|
{% for thought in thoughts %}
|
||||||
</p>
|
<div class="thought-item">
|
||||||
|
<h3>{{ thought.title }}</h3>
|
||||||
|
<p>{{ thought.content }}</p>
|
||||||
|
<div class="thought-meta">
|
||||||
|
<span>{{ thought.date }}</span>
|
||||||
|
<span>{{ thought.category }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-12">
|
||||||
|
<i class="fas fa-lightbulb text-5xl text-gray-400 mb-4"></i>
|
||||||
|
<p class="text-gray-500">Noch keine Gedanken erstellt</p>
|
||||||
|
<a href="{{ url_for('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>
|
||||||
|
|
||||||
<div class="tab-content hidden" id="collections-tab">
|
<div class="tab-content hidden" id="collections-tab">
|
||||||
<p class="text-center text-gray-400 py-12">
|
<div id="collections-container">
|
||||||
<i class="fas fa-spinner fa-spin text-3xl mb-4 block"></i>
|
{% if collections %}
|
||||||
Sammlungen werden geladen...
|
{% for collection in collections %}
|
||||||
</p>
|
<div class="collection-item">
|
||||||
|
<h3>{{ collection.title }}</h3>
|
||||||
|
<p>{{ collection.description }}</p>
|
||||||
|
<div class="collection-meta">
|
||||||
|
<span>{{ collection.thoughts_count }} Gedanken</span>
|
||||||
|
<span>{{ collection.date }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-12">
|
||||||
|
<i class="fas fa-folder-open text-5xl text-gray-400 mb-4"></i>
|
||||||
|
<p class="text-gray-500">Noch keine Sammlungen erstellt</p>
|
||||||
|
<a href="{{ url_for('create_collection') }}" class="mt-4 inline-block px-4 py-2 bg-purple-600 text-white rounded-lg">Erste Sammlung erstellen</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tab-content hidden" id="connections-tab">
|
<div class="tab-content hidden" id="connections-tab">
|
||||||
<p class="text-center text-gray-400 py-12">
|
<div id="connections-container">
|
||||||
<i class="fas fa-spinner fa-spin text-3xl mb-4 block"></i>
|
{% if connections %}
|
||||||
Verbindungen werden geladen...
|
{% for connection in connections %}
|
||||||
</p>
|
<div class="connection-item">
|
||||||
|
<div class="connection-nodes">
|
||||||
|
<div class="connection-node">
|
||||||
|
<h4>{{ connection.source.title }}</h4>
|
||||||
|
<p>{{ connection.source.excerpt }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="connection-type">
|
||||||
|
<i class="fas fa-arrow-right"></i>
|
||||||
|
<span>{{ connection.relation_type }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="connection-node">
|
||||||
|
<h4>{{ connection.target.title }}</h4>
|
||||||
|
<p>{{ connection.target.excerpt }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="connection-meta">
|
||||||
|
<span>{{ connection.date }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-12">
|
||||||
|
<i class="fas fa-project-diagram text-5xl text-gray-400 mb-4"></i>
|
||||||
|
<p class="text-gray-500">Noch keine Verbindungen erstellt</p>
|
||||||
|
<a href="{{ url_for('mindmap') }}" class="mt-4 inline-block px-4 py-2 bg-purple-600 text-white rounded-lg">Verbindungen in der Mindmap erstellen</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tab-content hidden" id="settings-tab">
|
<div class="tab-content hidden" id="settings-tab">
|
||||||
@@ -849,22 +696,22 @@
|
|||||||
<div class="settings-card-body">
|
<div class="settings-card-body">
|
||||||
<div class="settings-group">
|
<div class="settings-group">
|
||||||
<label class="settings-label" for="name">Name</label>
|
<label class="settings-label" for="name">Name</label>
|
||||||
<input type="text" id="name" class="settings-input" value="{{ user.name|default('Max Mustermann') }}" />
|
<input type="text" id="name" class="settings-input" value="{{ user.name }}" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-group">
|
<div class="settings-group">
|
||||||
<label class="settings-label" for="bio">Über mich</label>
|
<label class="settings-label" for="bio">Über mich</label>
|
||||||
<textarea id="bio" class="settings-input" rows="4">{{ user.bio|default('Willkommen auf meinem Profil! Ich bin daran interessiert, Wissen zu vernetzen und neue Verbindungen zwischen verschiedenen Themengebieten zu entdecken.') }}</textarea>
|
<textarea id="bio" class="settings-input" rows="4">{{ user.bio }}</textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-group">
|
<div class="settings-group">
|
||||||
<label class="settings-label" for="location">Standort</label>
|
<label class="settings-label" for="location">Standort</label>
|
||||||
<input type="text" id="location" class="settings-input" value="{{ user.location|default('Berlin, Deutschland') }}" />
|
<input type="text" id="location" class="settings-input" value="{{ user.location }}" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-group">
|
<div class="settings-group">
|
||||||
<label class="settings-label" for="website">Website</label>
|
<label class="settings-label" for="website">Website</label>
|
||||||
<input type="url" id="website" class="settings-input" value="{{ user.website|default('https://www.beispiel.de') }}" />
|
<input type="url" id="website" class="settings-input" value="{{ user.website }}" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="profile-action-btn primary mt-4">
|
<button class="profile-action-btn primary mt-4">
|
||||||
@@ -878,7 +725,7 @@
|
|||||||
<div class="settings-card-body">
|
<div class="settings-card-body">
|
||||||
<div class="settings-group">
|
<div class="settings-group">
|
||||||
<label class="settings-label" for="email">E-Mail-Adresse</label>
|
<label class="settings-label" for="email">E-Mail-Adresse</label>
|
||||||
<input type="email" id="email" class="settings-input" value="{{ user.email|default('beispiel@email.com') }}" />
|
<input type="email" id="email" class="settings-input" value="{{ user.email }}" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-group">
|
<div class="settings-group">
|
||||||
@@ -903,7 +750,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
<script>
|
<script nonce="{{ csp_nonce }}">
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
// Profil-Tab-Funktionalität
|
// Profil-Tab-Funktionalität
|
||||||
const tabs = document.querySelectorAll('.profile-tab');
|
const tabs = document.querySelectorAll('.profile-tab');
|
||||||
|
|||||||
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 %}
|
||||||
19
utils/__init__.py
Executable file → Normal file
19
utils/__init__.py
Executable file → Normal file
@@ -7,14 +7,14 @@ This package contains various utilities for database management,
|
|||||||
user management, and server administration.
|
user management, and server administration.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from .db_fix import fix_database_schema
|
# Import utilities that don't depend on app.py first
|
||||||
from .db_rebuild import rebuild_database
|
from .db_check import check_db_connection, initialize_db_if_needed
|
||||||
from .db_test import test_database_connection, test_models, print_database_stats, run_all_tests
|
|
||||||
from .user_manager import list_users, create_user, reset_password, delete_user, create_admin_user
|
|
||||||
from .server import run_development_server
|
|
||||||
|
|
||||||
|
# Define the list of all available utilities
|
||||||
__all__ = [
|
__all__ = [
|
||||||
# Database utilities
|
# Database utilities
|
||||||
|
'check_db_connection',
|
||||||
|
'initialize_db_if_needed',
|
||||||
'fix_database_schema',
|
'fix_database_schema',
|
||||||
'rebuild_database',
|
'rebuild_database',
|
||||||
'test_database_connection',
|
'test_database_connection',
|
||||||
@@ -31,4 +31,11 @@ __all__ = [
|
|||||||
|
|
||||||
# Server management
|
# Server management
|
||||||
'run_development_server',
|
'run_development_server',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Import remaining modules that might depend on app
|
||||||
|
from .db_fix import fix_database_schema
|
||||||
|
from .db_rebuild import rebuild_database
|
||||||
|
from .db_test import test_database_connection, test_models, print_database_stats, run_all_tests
|
||||||
|
from .user_manager import list_users, create_user, reset_password, delete_user, create_admin_user
|
||||||
|
from .server import run_development_server
|
||||||
Binary file not shown.
BIN
utils/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
utils/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/db_check.cpython-311.pyc
Normal file
BIN
utils/__pycache__/db_check.cpython-311.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/db_check.cpython-313.pyc
Normal file
BIN
utils/__pycache__/db_check.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
utils/__pycache__/db_fix.cpython-313.pyc
Normal file
BIN
utils/__pycache__/db_fix.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
utils/__pycache__/db_rebuild.cpython-313.pyc
Normal file
BIN
utils/__pycache__/db_rebuild.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user