Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b8ad3aea13 | |||
| edf3049e42 | |||
| d117978005 | |||
| 48d8463481 | |||
| 08314ec703 | |||
| 0bb7d8d0dc | |||
| 4a28c2c453 | |||
| 66d987857a | |||
| d58aba26c2 | |||
| 8f0a6d4372 | |||
| 5372fe220e | |||
| 11ab15127c | |||
| 0705ecce59 | |||
| 1c59b0b616 | |||
| d42c43db50 | |||
| e46264b201 | |||
| 74307ba345 | |||
| 14474c4eab | |||
| 4797cc3b72 | |||
| ab280b55af | |||
| 84b492d8d2 | |||
| b0db3398f2 |
@@ -1,34 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,36 +0,0 @@
|
||||
---
|
||||
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.)
|
||||
@@ -1,31 +0,0 @@
|
||||
---
|
||||
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)
|
||||
@@ -1,31 +0,0 @@
|
||||
---
|
||||
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)
|
||||
@@ -1,32 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,41 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,27 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,43 +0,0 @@
|
||||
---
|
||||
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,14 +2,12 @@
|
||||
# Kopiere diese Datei zu .env und passe die Werte an
|
||||
|
||||
# Flask
|
||||
FLASK_APP=app.py
|
||||
FLASK_ENV=development
|
||||
SECRET_KEY=your-secret-key-replace-in-production
|
||||
SECRET_KEY=dein-geheimer-schluessel-hier
|
||||
|
||||
# OpenAI API
|
||||
OPENAI_API_KEY=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
|
||||
OPENAI_API_KEY=sk-dein-openai-api-schluessel-hier
|
||||
|
||||
# Datenbank
|
||||
# Bei Bedarf kann hier eine andere Datenbank-URL angegeben werden
|
||||
# Der Pfad wird relativ zum Projektverzeichnis angegeben
|
||||
# SQLALCHEMY_DATABASE_URI=sqlite:////absoluter/pfad/zu/database/systades.db
|
||||
# SQLALCHEMY_DATABASE_URI=sqlite:////absoluter/pfad/zu/database/systades.db OPENAI_API_KEY=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
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +0,0 @@
|
||||
logs/app.log
|
||||
8
.vscode/jsconfig.json
vendored
8
.vscode/jsconfig.json
vendored
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"lib": [
|
||||
"esnext"
|
||||
]
|
||||
}
|
||||
}
|
||||
68
.vscode/main.js
vendored
68
.vscode/main.js
vendored
@@ -1,68 +0,0 @@
|
||||
/// <reference types="vscode" />
|
||||
// @ts-check
|
||||
// API: https://code.visualstudio.com/api/references/vscode-api
|
||||
// @ts-ignore
|
||||
const vscode = require('vscode');
|
||||
* @typedef {import('vscode').ExtensionContext} ExtensionContext
|
||||
* @typedef {import('vscode').commands} commands
|
||||
* @typedef {import('vscode').window} window
|
||||
* @typedef {import('vscode').TextEditor} TextEditor
|
||||
* @typedef {import('vscode').TextDocument} TextDocument
|
||||
*/
|
||||
|
||||
/**
|
||||
* Aktiviert die Erweiterung und registriert den Auto-Resume-Befehl
|
||||
* @param {vscode.ExtensionContext} context - Der Erweiterungskontext
|
||||
*/
|
||||
function activate(context) {
|
||||
const disposable = vscode.commands.registerCommand('extension.autoResume', () => {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (!editor) return;
|
||||
|
||||
const document = editor.document;
|
||||
const text = document.getText();
|
||||
|
||||
// Track last click time to avoid multiple clicks
|
||||
let lastClickTime = 0;
|
||||
|
||||
// Main function that looks for and clicks the resume link
|
||||
function clickResumeLink() {
|
||||
// Prevent clicking too frequently (3 second cooldown)
|
||||
const now = Date.now();
|
||||
if (now - lastClickTime < 3000) return;
|
||||
|
||||
// Check if text contains rate limit text
|
||||
if (text.includes('stop the agent after 25 tool calls') ||
|
||||
text.includes('Note: we default stop')) {
|
||||
|
||||
// Find the resume link position
|
||||
const resumePos = text.indexOf('resume the conversation');
|
||||
if (resumePos !== -1) {
|
||||
vscode.window.showInformationMessage('Auto-resuming conversation...');
|
||||
lastClickTime = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Führe periodisch aus
|
||||
const interval = global.setInterval(clickResumeLink, 1000);
|
||||
|
||||
// Speichere das Intervall in den Subscriptions
|
||||
context.subscriptions.push({
|
||||
dispose: () => global.clearInterval(interval)
|
||||
});
|
||||
// Führe die Funktion sofort aus
|
||||
clickResumeLink();
|
||||
});
|
||||
|
||||
context.subscriptions.push(disposable);
|
||||
}
|
||||
|
||||
function deactivate() {
|
||||
// Cleanup if needed
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
activate,
|
||||
deactivate
|
||||
}
|
||||
1274
COMMON_ERRORS.md
1274
COMMON_ERRORS.md
File diff suppressed because it is too large
Load Diff
33
Dockerfile
33
Dockerfile
@@ -1,33 +0,0 @@
|
||||
# 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,10 +6,9 @@ Das MindMapProjekt ist eine interaktive Plattform zum Visualisieren, Erforschen
|
||||
## Technischer Stack
|
||||
- **Backend**: Python/Flask
|
||||
- **Frontend**:
|
||||
- Tailwind CSS (via CDN) für moderne UI
|
||||
- Tailwind CSS für moderne UI
|
||||
- SVG-Bibliotheken für Visualisierungen (D3.js)
|
||||
- JavaScript/Alpine.js für interaktive Komponenten
|
||||
- WebGL für animierte Hintergrundeffekte
|
||||
- **Datenbank**: SQLite mit SQLAlchemy
|
||||
- **KI-Integration**: OpenAI API für intelligente Assistenz
|
||||
|
||||
@@ -62,20 +61,16 @@ Für detaillierte Hilfe: `python TOOLS.py -h`
|
||||
- [x] Favicon erstellen
|
||||
- [x] Setup-Skript für einfache Installation
|
||||
|
||||
### Phase 2: Design-Überarbeitung ✅
|
||||
### Phase 2: Design-Überarbeitung 🔄
|
||||
- [x] Implementierung des Dark Mode
|
||||
- [x] Erstellung eines modernen, minimalistischen UI mit Tech-Ästhetik
|
||||
- [x] Responsive Design für alle Geräte
|
||||
- [x] Gestaltung der Landing Page mit großer Typografie
|
||||
- [x] Animierter Neurales Netzwerk-Hintergrund mit WebGL
|
||||
- [ ] Gestaltung der Landing Page mit großer Typografie
|
||||
|
||||
### Phase 3: Mindmap-Funktionalitäten 🔄
|
||||
- [x] Verbesserte Visualisierung mit SVG und D3.js
|
||||
- [x] Implementierung der Mouseover-Funktion
|
||||
- [x] Entwicklung der Suchfunktion für Knoten
|
||||
- [x] Clustertopologie für neuronale Netzwerkdarstellung
|
||||
- [x] Fehlerbehandlung für robuste Datenverarbeitung und Knotenverweise
|
||||
- [x] Verbesserte Verbindungserkennung zwischen Knoten
|
||||
- [ ] Tagging-System für Inhalte
|
||||
- [ ] Quellenmanagement und -verlinkung
|
||||
- [ ] Upload-Funktionalität an Knotenpunkten
|
||||
@@ -120,8 +115,8 @@ Für detaillierte Hilfe: `python TOOLS.py -h`
|
||||
|
||||
## Aktueller Status
|
||||
- **Phase 1**: ✅ Abgeschlossen
|
||||
- **Phase 2**: ✅ Abgeschlossen
|
||||
- **Phase 3**: 🔄 In Bearbeitung (75% abgeschlossen)
|
||||
- **Phase 2**: 🔄 In Bearbeitung (75% abgeschlossen)
|
||||
- **Phase 3**: 🔄 In Bearbeitung (50% abgeschlossen)
|
||||
|
||||
## Aktuelle Fortschritte
|
||||
- Grundlegende UI modernisiert mit Tailwind CSS und Dark Mode
|
||||
@@ -129,68 +124,11 @@ Für detaillierte Hilfe: `python TOOLS.py -h`
|
||||
- Setup-Prozess vereinfacht mit einem Shell-Skript
|
||||
- Mindmap-Visualisierung komplett überarbeitet mit D3.js für eine interaktivere Erfahrung
|
||||
- Responsive Design für optimale Darstellung auf allen Geräten
|
||||
- Animierter neuronaler Netzwerk-Hintergrund mit WebGL implementiert
|
||||
- Verbesserte neuronale Cluster-Darstellung in der MindMap-Ansicht
|
||||
- Behebung von kritischen Fehlern in der Knotenvisualisierung und Verbindungserkennung
|
||||
- Robustere Datenverarbeitung für Mindmap-Knoten implementiert
|
||||
- Fehlerbehandlung für verschiedene API-Datenformate verbessert
|
||||
|
||||
## Neuronaler Netzwerk-Hintergrund
|
||||
|
||||
Ein wesentliches neues Feature ist der animierte Hintergrund, der ein neuronales Netzwerk simuliert:
|
||||
|
||||
- **WebGL-basierte Rendering-Engine** für hohe Performance
|
||||
- **Dynamische Knoten und Verbindungen** mit realistischem Bewegungsverhalten
|
||||
- **Neuronenfeuer-Simulation** mit Signalweiterleitung zwischen Knoten
|
||||
- **Clustertopologie** für realistisches Erscheinungsbild
|
||||
- **Anpassbare Farbgebung und Animationsparameter**
|
||||
- **Flüssige Animationen** mit über 100 Knotenpunkten
|
||||
|
||||
Die Animation ist vollständig responsiv und passt sich automatisch an verschiedene Bildschirmgrößen an, ohne die Browser-Performance zu beeinträchtigen.
|
||||
|
||||
## Mindmap-Verbesserungen
|
||||
|
||||
Die Mindmap-Darstellung wurde grundlegend überarbeitet:
|
||||
|
||||
- **D3.js Force-Directed Graph** für intuitive Knotenpositionierung
|
||||
- **Verbesserte Fehlerbehandlung** für robustere Datenverarbeitung
|
||||
- **Neuronale Cluster-Gruppierung** von thematisch zusammengehörigen Inhalten
|
||||
- **Glasmorphismus-Effekte** für moderne visuelle Darstellung
|
||||
- **Verbesserte Hover- und Selektionseffekte**
|
||||
- **Flüssige Animationen** bei Knotenauswahl und -fokussierung
|
||||
|
||||
## Nächste Schritte
|
||||
- Fertigstellung des Tagging-Systems für Gedanken
|
||||
- Fertigstellung der Landing Page
|
||||
- Erstellung der "Wer sind wir?"-Seite
|
||||
- Implementierung des Tagging-Systems für Gedanken
|
||||
- Verbesserung der Gedankenansicht im Mindmap-Bereich
|
||||
- Implementierung von Quellenmanagement
|
||||
- Überarbeitung der Startseite mit neuen Features
|
||||
|
||||
## Content Security Policy (CSP)
|
||||
|
||||
Die Anwendung implementiert eine Content Security Policy, um die Sicherheit zu erhöhen und unerwünschte externe Ressourcen zu blockieren. CSP wird in `app.py` konfiguriert und schränkt ein, welche Ressourcen geladen werden dürfen.
|
||||
|
||||
### Aktualisierung (06.06.2024)
|
||||
Die Anwendung verwendet nun die Tailwind CSS CDN für vereinfachte Entwicklung. Die CSP wurde entsprechend angepasst, um die Domain `cdn.tailwindcss.com` zu erlauben.
|
||||
|
||||
### Lokale und CDN-Ressourcen
|
||||
|
||||
Die Anwendung nutzt eine Mischung aus lokalen Ressourcen und CDNs:
|
||||
- **CDN-Ressourcen**:
|
||||
- Tailwind CSS (cdn.tailwindcss.com)
|
||||
- **Lokale Ressourcen**:
|
||||
- Alpine.js
|
||||
- Font Awesome
|
||||
- Google Fonts (Inter und JetBrains Mono)
|
||||
- WebGL-Animation (neural-network-background.js)
|
||||
|
||||
### CSP-Nonces
|
||||
|
||||
Die Anwendung verwendet Nonces für Inline-Skripte. In den Templates wird `{{ csp_nonce }}` verwendet, um den Nonce-Wert einzufügen:
|
||||
|
||||
```html
|
||||
<script nonce="{{ csp_nonce }}">
|
||||
// JavaScript Code
|
||||
</script>
|
||||
```
|
||||
|
||||
*Zuletzt aktualisiert: 15.06.2024*
|
||||
*Zuletzt aktualisiert: 01.06.2024*
|
||||
510
ROADMAP.md
510
ROADMAP.md
@@ -1,464 +1,102 @@
|
||||
# 🚀 SysTades Social Network - Entwicklungsroadmap
|
||||
# Systades Mindmap - Entwicklungs-Roadmap
|
||||
|
||||
## 📋 Überblick
|
||||
SysTades ist jetzt ein vollwertiges Social Network für Wissensaustausch, Mindmapping und Community-Building.
|
||||
Diese Roadmap beschreibt die geplante Entwicklung der dynamischen, benutzerorientierten Mindmap-Funktionalität für das Systades-Projekt.
|
||||
|
||||
## ✅ Abgeschlossene Phasen
|
||||
## Phase 1: Grundlegendes Datenmodell und Backend (Abgeschlossen)
|
||||
|
||||
### Phase 1: Basis Social Network ✅
|
||||
- ✅ Erweiterte Benutzermodelle mit Social Features
|
||||
- ✅ Posts, Kommentare, Likes, Follows System
|
||||
- ✅ Benachrichtigungssystem
|
||||
- ✅ Benutzerprofile mit Statistiken
|
||||
- ✅ Erweiterte Navigation und UI
|
||||
- ✅ **Verbessertes Logging-System mit visuellen Enhancements**
|
||||
- ✅ Social Feed mit Filtering
|
||||
- ✅ Mobile-responsive Design
|
||||
- [x] Entwurf des Datenbankschemas für benutzerorientierte Mindmaps
|
||||
- [x] Implementierung der Modelle in models.py
|
||||
- [x] Erstellung der API-Endpunkte für CRUD-Operationen
|
||||
- [x] Integration mit der bestehenden Benutzerauthentifizierung
|
||||
- [x] Seed-Daten für die Entwicklung und Tests
|
||||
|
||||
### Phase 2: Core Features ✅
|
||||
- ✅ Mindmap-Integration in Social Posts
|
||||
- ✅ Gedanken-Sharing System
|
||||
- ✅ Bookmark-System für Posts
|
||||
- ✅ Analytics Dashboard für Benutzer
|
||||
- ✅ Erweiterte Suche (Benutzer, Posts, Gedanken)
|
||||
- ✅ Real-time Benachrichtigungen
|
||||
- ✅ Post-Sharing und Engagement Metrics
|
||||
## Phase 2: Dynamische Mindmap-Visualisierung (Aktuell)
|
||||
|
||||
### Phase 3: Erweiterte Social Features ✅
|
||||
- ✅ Benutzerprofile mit Tabs (Posts, Gedanken, Mindmaps, Aktivität)
|
||||
- ✅ Follow/Unfollow System mit UI
|
||||
- ✅ Notification Center mit Filtering
|
||||
- ✅ Post-Typen (Text, Gedanke, Frage, Erkenntnis)
|
||||
- ✅ Sichtbarkeitseinstellungen (Öffentlich, Follower, Privat)
|
||||
- ✅ Quick-Create Post Modal
|
||||
- [ ] Anpassung des Frontend-Codes zur Verwendung der DB-Daten anstelle des SVG
|
||||
- [ ] Implementierung von AJAX-Anfragen zum Laden der Mindmap-Daten
|
||||
- [ ] Dynamisches Rendering der Knoten, Verbindungen und Labels
|
||||
- [ ] Drag-and-Drop-Funktionalität für die Bewegung von Knoten
|
||||
- [ ] Zoom- und Pan-Funktionalität mit Persistenz der Ansicht
|
||||
|
||||
### Phase 3.5: Logging & Monitoring System ✅ (NEU)
|
||||
- ✅ **Erweiterte SocialNetworkLogger Klasse mit visuellen Features**
|
||||
- ✅ **Farbige Konsolen-Ausgabe mit ANSI-Codes**
|
||||
- ✅ **Emoji-basierte Kategorisierung für bessere Übersicht**
|
||||
- ✅ **Component-spezifisches Logging (AUTH, API, DB, ERROR, etc.)**
|
||||
- ✅ **Performance-Monitoring mit Zeitstempel**
|
||||
- ✅ **Strukturierte JSON-Logs für externe Analyse**
|
||||
- ✅ **Decorator-basierte Instrumentierung**
|
||||
- ✅ **Vollständige Integration in alle App-Komponenten**
|
||||
- ✅ **Ersetzung aller print-Statements durch strukturierte Logs**
|
||||
## Phase 3: Benutzerdefinierte Mindmaps
|
||||
|
||||
## 🔄 Aktuelle Phase 4: UI/UX Verbesserungen (In Arbeit)
|
||||
- [ ] UI für das Erstellen, Bearbeiten und Löschen eigener Mindmaps
|
||||
- [ ] Funktion zum Hinzufügen/Entfernen von Knoten aus der öffentlichen Mindmap
|
||||
- [ ] Speichern der Knotenpositionen und Ansichtseinstellungen
|
||||
- [ ] Benutzerspezifische Visualisierungseinstellungen
|
||||
- [ ] Dashboard mit Übersicht aller Mindmaps des Benutzers
|
||||
|
||||
### UI/UX Komponenten
|
||||
- ✅ Moderne Navigation mit Icons und Badges
|
||||
- ✅ Dark/Light Mode Toggle
|
||||
- ✅ Responsive Mobile Navigation
|
||||
- ✅ Glassmorphism Design Elements
|
||||
- ✅ Gradient Themes und Farbsystem
|
||||
- ✅ Toast Notification System
|
||||
- ⏳ Chat/Messaging System
|
||||
- ⏳ Story/Status Features
|
||||
- ⏳ Advanced Image/Video Upload
|
||||
## Phase 4: Notizen und Annotationen
|
||||
|
||||
### Performance Optimierungen
|
||||
- ⏳ Lazy Loading für Posts
|
||||
- ⏳ Image Optimization
|
||||
- ⏳ Caching System
|
||||
- ⏳ API Rate Limiting
|
||||
- ⏳ Database Indexing
|
||||
- [ ] UI für das Hinzufügen privater Notizen zu Knoten
|
||||
- [ ] Visuelle Anzeige von Notizen in der Mindmap
|
||||
- [ ] Texteditor mit Markdown-Unterstützung für Notizen
|
||||
- [ ] Kategorisierung und Farbkodierung von Notizen
|
||||
- [ ] Suchfunktion für Notizen
|
||||
|
||||
## 📈 Kommende Phasen
|
||||
## Phase 5: Integrationen und Erweiterungen
|
||||
|
||||
### Phase 5: Community Features
|
||||
- 🔲 Gruppen/Communities System
|
||||
- 🔲 Events und Kalenderfunktion
|
||||
- 🔲 Live Discussions/Chats
|
||||
- 🔲 Trending Topics/Hashtags
|
||||
- 🔲 User Verification System
|
||||
- 🔲 Moderation Tools
|
||||
- [ ] Import/Export-Funktionalität für Mindmaps (JSON, PNG)
|
||||
- [ ] Teilen von Mindmaps (öffentlich/privat/mit bestimmten Benutzern)
|
||||
- [ ] Kollaborative Bearbeitung von Mindmaps
|
||||
- [ ] Verknüpfung mit externen Ressourcen (Links, Dateien)
|
||||
- [ ] Versionierung von Mindmaps
|
||||
|
||||
### Phase 6: Advanced Features
|
||||
- 🔲 AI-basierte Content Empfehlungen
|
||||
- 🔲 Voice Notes und Audio Posts
|
||||
- 🔲 Video Sharing und Streaming
|
||||
- 🔲 Collaborative Mindmaps
|
||||
- 🔲 Knowledge Graph Visualisierung
|
||||
- 🔲 Advanced Analytics
|
||||
## Phase 6: KI-Integration und Analyse
|
||||
|
||||
### Phase 7: Monetarisierung & Skalierung
|
||||
- 🔲 Premium Features
|
||||
- 🔲 Creator Economy Tools
|
||||
- 🔲 API für Drittanbieter
|
||||
- 🔲 Mobile Apps (iOS/Android)
|
||||
- 🔲 Enterprise Features
|
||||
- 🔲 Advanced Security Features
|
||||
- [ ] KI-gestützte Vorschläge für Verbindungen zwischen Knoten
|
||||
- [ ] Automatische Kategorisierung von Inhalten
|
||||
- [ ] Visualisierung von Beziehungsstärken und -typen
|
||||
- [ ] Mindmap-Statistiken und Analysen
|
||||
- [ ] KI-basierte Zusammenfassung von Teilbereichen der Mindmap
|
||||
|
||||
### Phase 8: Integration & Ecosystem
|
||||
- 🔲 External Tool Integrations
|
||||
- 🔲 Learning Management System
|
||||
- 🔲 Knowledge Base Integration
|
||||
- 🔲 Research Tools
|
||||
- 🔲 Publication System
|
||||
- 🔲 Academic Collaboration Tools
|
||||
## Phase 7: Optimierung und Skalierung
|
||||
|
||||
## 🏗️ Technische Architektur
|
||||
- [ ] Performance-Optimierung für große Mindmaps
|
||||
- [ ] Verbesserung der Benutzerfreundlichkeit basierend auf Feedback
|
||||
- [ ] Erweiterte Such- und Filterfunktionen
|
||||
- [ ] Mobile Optimierung
|
||||
- [ ] Offline-Funktionalität mit Synchronisierung
|
||||
|
||||
### Backend Stack ✅
|
||||
- **Framework**: Flask mit SQLAlchemy
|
||||
- **Datenbank**: SQLite (PostgreSQL für Produktion)
|
||||
- **Authentifizierung**: Flask-Login
|
||||
- **API**: RESTful JSON APIs
|
||||
- **Logging**: **Erweiterte SocialNetworkLogger mit visuellen Features**
|
||||
- **Farbige Konsolen-Ausgabe mit ANSI-Codes**
|
||||
- **Emoji-basierte Kategorisierung (🔐 AUTH, 🌐 API, 💾 DB, etc.)**
|
||||
- **Component-spezifisches Logging mit Performance-Monitoring**
|
||||
- **JSON-strukturierte Logs für externe Analyse**
|
||||
- **Decorator-basierte automatische Instrumentierung**
|
||||
- **Performance**: Pagination, Caching
|
||||
## Technische Schulden und Refactoring
|
||||
|
||||
### Frontend Stack ✅
|
||||
- **Styling**: TailwindCSS mit Custom Themes
|
||||
- **JavaScript**: Vanilla JS mit ES6+ Features
|
||||
- **Icons**: Font Awesome 6
|
||||
- **Responsive**: Mobile-First Design
|
||||
- **Interaktivität**: Alpine.js für reaktive Komponenten
|
||||
|
||||
### Database Schema ✅
|
||||
```sql
|
||||
-- Core Tables
|
||||
users (erweitert mit Social Features)
|
||||
social_posts (Posts System)
|
||||
social_comments (Kommentar System)
|
||||
notifications (Benachrichtigungssystem)
|
||||
user_settings (Benutzereinstellungen)
|
||||
activities (Aktivitätsverfolgung)
|
||||
|
||||
-- Relationship Tables
|
||||
user_friendships (Freundschaftssystem)
|
||||
user_follows (Follow System)
|
||||
post_likes (Like System)
|
||||
comment_likes (Comment Likes)
|
||||
user_thought_bookmark (Bookmark System)
|
||||
```
|
||||
|
||||
## 📊 API Endpunkte
|
||||
|
||||
### Social Feed APIs ✅
|
||||
- `GET /api/social/posts` - Feed Posts abrufen
|
||||
- `POST /api/social/posts` - Neuen Post erstellen
|
||||
- `POST /api/social/posts/{id}/like` - Post liken/unliken
|
||||
- `POST /api/social/posts/{id}/share` - Post teilen
|
||||
- `POST /api/social/posts/{id}/bookmark` - Post bookmarken
|
||||
|
||||
### User Management APIs ✅
|
||||
- `GET /api/social/users/{id}` - Benutzerprofil abrufen
|
||||
- `GET /api/social/users/search` - Benutzer suchen
|
||||
- `POST /api/social/users/{id}/follow` - Benutzer folgen/entfolgen
|
||||
|
||||
### Notification APIs ✅
|
||||
- `GET /api/social/notifications` - Benachrichtigungen abrufen
|
||||
- `POST /api/social/notifications/{id}/read` - Als gelesen markieren
|
||||
- `POST /api/social/notifications/mark-all-read` - Alle als gelesen
|
||||
- `DELETE /api/social/notifications/{id}` - Benachrichtigung löschen
|
||||
|
||||
### Analytics APIs ✅
|
||||
- `GET /api/social/analytics/dashboard` - Benutzer-Analytics
|
||||
- `GET /api/social/bookmarks` - Gebookmarkte Posts
|
||||
|
||||
## 🔒 Sicherheit & Datenschutz
|
||||
|
||||
### Implementierte Features ✅
|
||||
- CSRF Protection
|
||||
- SQL Injection Prevention
|
||||
- Input Validation & Sanitization
|
||||
- Session Management
|
||||
- Password Hashing
|
||||
- Privacy Controls (Post Visibility)
|
||||
|
||||
### Geplante Features
|
||||
- 2FA Authentication
|
||||
- Advanced Privacy Settings
|
||||
- Data Export/Import
|
||||
- GDPR Compliance Tools
|
||||
- Content Moderation AI
|
||||
|
||||
## 📱 Mobile Support
|
||||
|
||||
### Aktuelle Features ✅
|
||||
- Responsive Design
|
||||
- Touch-Friendly Interface
|
||||
- Mobile Navigation
|
||||
- Optimized Loading
|
||||
|
||||
### Geplante Features
|
||||
- PWA Support
|
||||
- Offline Capabilities
|
||||
- Push Notifications
|
||||
- Native Mobile Apps
|
||||
|
||||
## 🎯 Leistungsziele
|
||||
|
||||
### Aktueller Status
|
||||
- ✅ Grundlegende Performance
|
||||
- ✅ Database Queries optimiert
|
||||
- ✅ Frontend Responsiveness
|
||||
- ✅ Strukturiertes Logging System
|
||||
|
||||
### Ziele für nächste Phase
|
||||
- 🎯 < 200ms API Response Zeit
|
||||
- 🎯 90+ Lighthouse Score
|
||||
- 🎯 Skalierung auf 10k+ Benutzer
|
||||
- 🎯 99.9% Uptime
|
||||
|
||||
## 🧪 Testing & Quality
|
||||
|
||||
### Implementiert
|
||||
- ✅ Manuelle Testing
|
||||
- ✅ Error Handling
|
||||
- ✅ **Erweiterte Logging & Monitoring mit visuellen Features**
|
||||
- ✅ **Farbige, kategorisierte Logs für bessere Debugging-Erfahrung**
|
||||
- ✅ **Performance-Monitoring mit Zeitstempel**
|
||||
- ✅ **Component-spezifische Fehlerbehandlung**
|
||||
- ✅ **Strukturierte JSON-Logs für Analyse**
|
||||
|
||||
### Geplant
|
||||
- 🔲 Automatisierte Unit Tests
|
||||
- 🔲 Integration Tests
|
||||
- 🔲 Performance Tests
|
||||
- 🔲 Security Audits
|
||||
- 🔲 Load Testing
|
||||
- 🔲 **Log-basierte Alerting System**
|
||||
- 🔲 **Automated Error Reporting**
|
||||
|
||||
## 📈 Metriken & Analytics
|
||||
|
||||
### User Engagement
|
||||
- Posts pro Tag
|
||||
- Kommentare und Likes
|
||||
- Follow/Unfollow Raten
|
||||
- Session Dauer
|
||||
- Return User Rate
|
||||
|
||||
### System Performance
|
||||
- API Response Zeiten
|
||||
- Database Performance
|
||||
- Error Rates
|
||||
- User Activity Patterns
|
||||
|
||||
## 🛠️ Entwicklungsumgebung
|
||||
|
||||
### Setup Requirements ✅
|
||||
```bash
|
||||
# Virtual Environment
|
||||
python3.11 -m venv venv
|
||||
source venv/bin/activate
|
||||
|
||||
# Dependencies
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Database Migration
|
||||
flask db upgrade
|
||||
|
||||
# Development Server
|
||||
python3.11 app.py
|
||||
```
|
||||
|
||||
### Development Tools ✅
|
||||
- **IDE**: Cursor/VS Code
|
||||
- **Version Control**: Git
|
||||
- **Database**: SQLite (dev), PostgreSQL (prod)
|
||||
- **Logging**: Colored Console + File Logging
|
||||
- **Debug**: Flask Debug Mode
|
||||
|
||||
## 🌟 Innovation Features
|
||||
|
||||
### Einzigartige Aspekte
|
||||
- 🧠 **Knowledge-First Design**: Fokus auf Wissensaustausch
|
||||
- 🎨 **Mindmap Integration**: Visuelle Gedankenlandkarten
|
||||
- 🔍 **Deep Search**: Semantic Search durch Inhalte
|
||||
- 📊 **Learning Analytics**: Fortschritt und Erkenntnisse
|
||||
- 🤝 **Collaborative Learning**: Gemeinsam Wissen erschaffen
|
||||
|
||||
### Zukünftige Innovationen
|
||||
- 🤖 AI-Powered Knowledge Extraction
|
||||
- 🎬 Interactive Learning Experiences
|
||||
- 🌐 Cross-Platform Knowledge Sync
|
||||
- 📚 Dynamic Knowledge Graphs
|
||||
- 🧮 Algorithmic Learning Paths
|
||||
- [ ] Trennung der Datenbank-Logik vom Flask-App-Code
|
||||
- [ ] Einführung von Unit-Tests und Integration-Tests
|
||||
- [ ] Überarbeitung der API-Dokumentation
|
||||
- [ ] Caching-Strategien für bessere Performance
|
||||
- [ ] Verbesserte Fehlerbehandlung und Logging
|
||||
|
||||
---
|
||||
|
||||
## 📝 Aktuelle Tasks
|
||||
## Implementierungsdetails
|
||||
|
||||
### Hohe Priorität
|
||||
1. ⏳ Chat/Messaging System implementieren
|
||||
2. ⏳ Advanced Image Upload mit Preview
|
||||
3. ⏳ Performance Optimierungen
|
||||
4. ⏳ Mobile App Prototyp
|
||||
### Datenbankschema
|
||||
|
||||
### Mittlere Priorität
|
||||
1. 🔲 Gruppen/Communities Feature
|
||||
2. 🔲 Advanced Analytics Dashboard
|
||||
3. 🔲 Content Moderation Tools
|
||||
4. 🔲 API Rate Limiting
|
||||
Das Datenbankschema umfasst folgende Hauptentitäten:
|
||||
|
||||
### Niedrige Priorität
|
||||
1. 🔲 Email Benachrichtigungen
|
||||
2. 🔲 Export/Import Features
|
||||
3. 🔲 Advanced Search Filters
|
||||
4. 🔲 Theming System
|
||||
1. **Category** - Wissenschaftliche Kategorien für die öffentliche Mindmap
|
||||
2. **MindMapNode** - Öffentliche Mindmap-Knoten mit Metadaten
|
||||
3. **UserMindmap** - Benutzerdefinierte Mindmaps
|
||||
4. **UserMindmapNode** - Verknüpfung zwischen Benutzermindmaps und öffentlichen Knoten
|
||||
5. **MindmapNote** - Benutzerspezifische Notizen
|
||||
6. **Thought** - Gedanken und Inhalte, die Knoten zugeordnet sind
|
||||
7. **ThoughtRelation** - Beziehungen zwischen Gedanken
|
||||
|
||||
---
|
||||
### Frontend-Technologien
|
||||
|
||||
**Letzte Aktualisierung**: {{ current_date }}
|
||||
**Version**: 2.0.0 - Social Network Release
|
||||
**Status**: ✅ Fully Functional Social Platform
|
||||
- D3.js für die Visualisierung der Mindmap
|
||||
- AJAX für dynamisches Laden von Daten
|
||||
- Interaktive Bedienelemente mit JavaScript
|
||||
- Responsive Design mit Tailwind CSS
|
||||
|
||||
# 🗺️ SysTades Roadmap
|
||||
### Backend-APIs
|
||||
|
||||
## ✅ Abgeschlossen (v1.0 - v1.3)
|
||||
Die implementierten API-Endpunkte umfassen:
|
||||
|
||||
### 🎯 Grundfunktionen
|
||||
- [x] **Benutzerauthentifizierung** - Registrierung, Login, Logout
|
||||
- [x] **Interaktive Mindmap** - Cytoscape.js-basierte Visualisierung
|
||||
- [x] **Gedankenverwaltung** - CRUD-Operationen für Thoughts
|
||||
- [x] **Kategoriesystem** - Hierarchische Wissensorganisation
|
||||
- [x] **Responsive Design** - Mobile-first Ansatz
|
||||
- [x] **Dark/Light Mode** - Benutzerfreundliche Themes
|
||||
|
||||
### 🤖 KI-Integration
|
||||
- [x] **ChatGPT-Assistent** - Integrierter AI-Chat
|
||||
- [x] **Intelligente Suche** - KI-gestützte Inhaltssuche
|
||||
- [x] **Automatische Kategorisierung** - AI-basierte Thought-Klassifizierung
|
||||
|
||||
### 🎨 UI/UX Verbesserungen
|
||||
- [x] **Moderne Navigation** - Glassmorphism-Design
|
||||
- [x] **Animationen** - Smooth Transitions und Hover-Effekte
|
||||
- [x] **Accessibility** - ARIA-Labels und Keyboard-Navigation
|
||||
- [x] **Performance-Optimierung** - Lazy Loading und Caching
|
||||
|
||||
## 🚀 Neu implementiert (v1.4 - Social Network Update)
|
||||
|
||||
### 📱 Social Network Features
|
||||
- [x] **Social Feed** - Instagram/Twitter-ähnlicher Feed
|
||||
- [x] **Post-System** - Erstellen, Liken, Kommentieren von Posts
|
||||
- [x] **Follow-System** - Benutzer folgen und entfolgen
|
||||
- [x] **Discover-Seite** - Trending Posts und empfohlene Benutzer
|
||||
- [x] **Benutzerprofile** - Erweiterte Profile mit Posts, Mindmaps, Gedanken
|
||||
- [x] **Benachrichtigungssystem** - Likes, Kommentare, Follows
|
||||
- [x] **Community-Statistiken** - Aktive Benutzer, Posts, Mindmaps
|
||||
|
||||
### 🧠 Erweiterte Mindmap-Features
|
||||
- [x] **Kollaborative Bearbeitung** - Vorbereitung für Echtzeit-Kollaboration
|
||||
- [x] **Mindmap-Export** - JSON-Export mit geplanten weiteren Formaten
|
||||
- [x] **Mindmap-Sharing** - Teilen von Mindmaps in sozialen Netzwerken
|
||||
- [x] **Erweiterte Toolbar** - Neue Bearbeitungsoptionen
|
||||
- [x] **Vollbild-Modus** - Immersive Mindmap-Bearbeitung
|
||||
- [x] **Schnelle Knoten-/Gedanken-Erstellung** - Direkt aus der Mindmap
|
||||
|
||||
### 🔗 Integration & Vernetzung
|
||||
- [x] **Gedanken in Posts teilen** - Wissenschaftliche Inhalte im Feed
|
||||
- [x] **Mindmap-Knoten teilen** - Wissensbausteine verbreiten
|
||||
- [x] **Cross-Platform Navigation** - Nahtlose Übergänge zwischen Features
|
||||
- [x] **Unified Search** - Suche über alle Inhaltstypen
|
||||
|
||||
## 🔄 In Entwicklung (v1.5)
|
||||
|
||||
### 🔄 Echtzeit-Features
|
||||
- [ ] **Live-Kollaboration** - Mehrere Benutzer bearbeiten gleichzeitig Mindmaps
|
||||
- [ ] **WebSocket-Integration** - Echtzeit-Updates für Feed und Benachrichtigungen
|
||||
- [ ] **Live-Cursor** - Sehen wo andere Benutzer arbeiten
|
||||
- [ ] **Änderungshistorie** - Versionskontrolle für Mindmaps
|
||||
|
||||
### 💬 Erweiterte Kommunikation
|
||||
- [ ] **Direktnachrichten** - Private Nachrichten zwischen Benutzern
|
||||
- [ ] **Gruppen-Chats** - Themenbasierte Diskussionsgruppen
|
||||
- [ ] **Video-Calls** - Integrierte Videokonferenzen für Kollaboration
|
||||
- [ ] **Screen-Sharing** - Bildschirm teilen während Kollaboration
|
||||
|
||||
## 📋 Geplant (v1.6 - v2.0)
|
||||
|
||||
### 📊 Analytics & Insights
|
||||
- [ ] **Lernfortschritt-Tracking** - Persönliche Wissensstatistiken
|
||||
- [ ] **Mindmap-Analytics** - Nutzungsstatistiken und Hotspots
|
||||
- [ ] **Community-Insights** - Trending-Themen und beliebte Inhalte
|
||||
- [ ] **Empfehlungsalgorithmus** - Personalisierte Inhaltsvorschläge
|
||||
|
||||
### 🎓 Bildungsfeatures
|
||||
- [ ] **Kurssystem** - Strukturierte Lernpfade
|
||||
- [ ] **Quizzes & Tests** - Wissensüberprüfung
|
||||
- [ ] **Zertifikate** - Digitale Abschlüsse
|
||||
- [ ] **Mentoring-System** - Experten-Schüler-Verbindungen
|
||||
|
||||
### 🔧 Erweiterte Tools
|
||||
- [ ] **PDF-Import** - Automatische Mindmap-Generierung aus Dokumenten
|
||||
- [ ] **LaTeX-Support** - Mathematische Formeln in Gedanken
|
||||
- [ ] **Multimedia-Integration** - Videos, Audio, Bilder in Mindmaps
|
||||
- [ ] **API für Drittanbieter** - Integration mit anderen Tools
|
||||
|
||||
### 🌐 Skalierung & Performance
|
||||
- [ ] **Microservices-Architektur** - Bessere Skalierbarkeit
|
||||
- [ ] **CDN-Integration** - Globale Content-Delivery
|
||||
- [ ] **Caching-Optimierung** - Redis für bessere Performance
|
||||
- [ ] **Load Balancing** - Hochverfügbarkeit
|
||||
|
||||
## 🔮 Vision (v2.0+)
|
||||
|
||||
### 🤖 Erweiterte KI
|
||||
- [ ] **Personalisierte KI-Tutoren** - Individuelle Lernbegleitung
|
||||
- [ ] **Automatische Mindmap-Generierung** - KI erstellt Mindmaps aus Text
|
||||
- [ ] **Intelligente Verbindungen** - KI schlägt Gedankenverknüpfungen vor
|
||||
- [ ] **Adaptive Lernpfade** - KI passt Inhalte an Lernstil an
|
||||
|
||||
### 🌍 Globale Community
|
||||
- [ ] **Mehrsprachigkeit** - Internationale Benutzergemeinschaft
|
||||
- [ ] **Kultureller Austausch** - Globale Wissensnetzwerke
|
||||
- [ ] **Übersetzungsfeatures** - Automatische Inhaltsübersetzung
|
||||
- [ ] **Regionale Communities** - Lokale Wissensgruppen
|
||||
|
||||
### 🔬 Forschungstools
|
||||
- [ ] **Literaturverwaltung** - Integration mit wissenschaftlichen Datenbanken
|
||||
- [ ] **Zitiersystem** - Automatische Quellenangaben
|
||||
- [ ] **Peer-Review-System** - Wissenschaftliche Qualitätskontrolle
|
||||
- [ ] **Publikationstools** - Direkte Veröffentlichung von Forschungsergebnissen
|
||||
|
||||
---
|
||||
|
||||
## 📈 Metriken & Ziele
|
||||
|
||||
### Technische Ziele
|
||||
- **Performance**: < 2s Ladezeit für alle Seiten
|
||||
- **Verfügbarkeit**: 99.9% Uptime
|
||||
- **Skalierbarkeit**: 10.000+ gleichzeitige Benutzer
|
||||
- **Sicherheit**: Zero-Trust-Architektur
|
||||
|
||||
### Community-Ziele
|
||||
- **Benutzer**: 1.000+ aktive Benutzer bis Ende 2024
|
||||
- **Inhalte**: 10.000+ Gedanken und 1.000+ Mindmaps
|
||||
- **Engagement**: 70%+ monatliche Aktivitätsrate
|
||||
- **Zufriedenheit**: 4.5+ Sterne Bewertung
|
||||
|
||||
---
|
||||
|
||||
## 🤝 Beitragen
|
||||
|
||||
Interessiert an der Mitarbeit? Hier sind die Bereiche, in denen wir Unterstützung suchen:
|
||||
|
||||
### 👨💻 Entwicklung
|
||||
- **Frontend**: React/Vue.js Komponenten
|
||||
- **Backend**: Python/Flask API-Entwicklung
|
||||
- **Mobile**: React Native App
|
||||
- **DevOps**: Docker, Kubernetes, CI/CD
|
||||
|
||||
### 🎨 Design
|
||||
- **UI/UX**: Benutzeroberflächen-Design
|
||||
- **Grafik**: Icons, Illustrationen, Branding
|
||||
- **Animation**: Micro-Interactions und Transitions
|
||||
- **Accessibility**: Barrierefreie Gestaltung
|
||||
|
||||
### 📝 Content
|
||||
- **Dokumentation**: Technische und Benutzer-Dokumentation
|
||||
- **Tutorials**: Video- und Text-Anleitungen
|
||||
- **Übersetzungen**: Mehrsprachige Inhalte
|
||||
- **Community**: Moderation und Support
|
||||
|
||||
---
|
||||
|
||||
*Letzte Aktualisierung: Januar 2024*
|
||||
*Version: 1.4.0 - Social Network Update*
|
||||
- `/api/mindmap/public` - Abrufen der öffentlichen Mindmap-Struktur
|
||||
- `/api/mindmap/user/<id>` - Abrufen benutzerdefinierter Mindmaps
|
||||
- `/api/mindmap/<id>/add_node` - Hinzufügen eines Knotens zur Benutzer-Mindmap
|
||||
- `/api/mindmap/<id>/remove_node/<node_id>` - Entfernen eines Knotens
|
||||
- `/api/mindmap/<id>/update_node_position` - Aktualisierung von Knotenpositionen
|
||||
- `/api/mindmap/<id>/notes` - Verwaltung von Notizen
|
||||
- `/api/nodes/<id>/thoughts` - Abrufen und Hinzufügen von Gedanken zu Knoten
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
2526
app.py.bak
2526
app.py.bak
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -2,4 +2,3 @@
|
||||
# https://curl.se/docs/http-cookies.html
|
||||
# This file was generated by libcurl! Edit at your own risk.
|
||||
|
||||
#HttpOnly_127.0.0.1 FALSE / FALSE 0 session .eJwlzjEOwjAMQNG7ZGaIYztJe5nKjm2BACG1dELcnUiMf3n6n7TF7sc1re_99EvabpbWZI7cikB4dsoylLrmcKXSormH-OhKoQRSAy0v3kEzDqJlSNFCg8NIW25sfYChAgryFIWxdqyskqFWtNIdiF3awiZRaq9TzGmOnIfv_xuYabLft-fLPK0hj8O_P-1dNpA.aDdoog.bmKi2y6o3HQgIk4gwDvhirnxuoM
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,17 +0,0 @@
|
||||
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,14 +2,12 @@
|
||||
# Kopiere diese Datei zu .env und passe die Werte an
|
||||
|
||||
# Flask
|
||||
FLASK_APP=app.py
|
||||
FLASK_ENV=development
|
||||
SECRET_KEY=mein-sicherer-schluessel-fuer-entwicklung
|
||||
SECRET_KEY=dein-geheimer-schluessel-hier
|
||||
|
||||
# OpenAI API
|
||||
OPENAI_API_KEY=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
|
||||
OPENAI_API_KEY=sk-dein-openai-api-schluessel-hier
|
||||
|
||||
# Datenbank
|
||||
# Bei Bedarf kann hier eine andere Datenbank-URL angegeben werden
|
||||
# Der Pfad wird relativ zum Projektverzeichnis angegeben
|
||||
SQLALCHEMY_DATABASE_URI=sqlite:///database/systades.db
|
||||
# SQLALCHEMY_DATABASE_URI=sqlite:////absoluter/pfad/zu/database/systades.db
|
||||
550
init_db.py
Normal file → Executable file
550
init_db.py
Normal file → Executable file
@@ -1,320 +1,256 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
import sqlite3
|
||||
from flask import Flask
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from datetime import datetime
|
||||
|
||||
# Pfad zur Datenbank
|
||||
basedir = os.path.abspath(os.path.dirname(__file__))
|
||||
db_path = os.path.join(basedir, 'database', 'systades.db')
|
||||
|
||||
# Stelle sicher, dass das Verzeichnis existiert
|
||||
db_dir = os.path.dirname(db_path)
|
||||
os.makedirs(db_dir, exist_ok=True)
|
||||
|
||||
# Erstelle eine temporäre Flask-App, um die Datenbank zu initialisieren
|
||||
app = Flask(__name__)
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{db_path}'
|
||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||
|
||||
# Importiere die Modelle nach der App-Initialisierung
|
||||
from app import app, initialize_database, db_path
|
||||
from models import db, User, Thought, Comment, MindMapNode, ThoughtRelation, ThoughtRating, RelationType
|
||||
from models import Category, UserMindmap, UserMindmapNode, MindmapNote
|
||||
import os
|
||||
|
||||
db.init_app(app)
|
||||
def init_database():
|
||||
"""Initialisiert die Datenbank mit Beispieldaten."""
|
||||
with app.app_context():
|
||||
# Datenbank löschen und neu erstellen
|
||||
if os.path.exists(db_path):
|
||||
os.remove(db_path)
|
||||
|
||||
# Stellen Sie sicher, dass das Verzeichnis existiert
|
||||
os.makedirs(os.path.dirname(db_path), exist_ok=True)
|
||||
|
||||
db.create_all()
|
||||
|
||||
# Admin-Benutzer erstellen
|
||||
admin = User(username='admin', email='admin@example.com', is_admin=True)
|
||||
admin.set_password('admin')
|
||||
db.session.add(admin)
|
||||
|
||||
# Beispiel-Benutzer erstellen
|
||||
user = User(username='user', email='user@example.com')
|
||||
user.set_password('user')
|
||||
db.session.add(user)
|
||||
|
||||
# Commit, um IDs zu generieren
|
||||
db.session.commit()
|
||||
|
||||
# Wissenschaftliche Kategorien erstellen
|
||||
science = Category(name='Wissenschaft', description='Wissenschaftliche Erkenntnisse',
|
||||
color_code='#4CAF50', icon='flask')
|
||||
db.session.add(science)
|
||||
|
||||
philosophy = Category(name='Philosophie', description='Philosophische Theorien und Gedanken',
|
||||
color_code='#9C27B0', icon='lightbulb')
|
||||
db.session.add(philosophy)
|
||||
|
||||
technology = Category(name='Technologie', description='Technologische Entwicklungen',
|
||||
color_code='#FF9800', icon='microchip')
|
||||
db.session.add(technology)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
# Wissenschaftliche Unterkategorien
|
||||
physics = Category(name='Physik', description='Studium der Materie und Energie',
|
||||
color_code='#81C784', icon='atom', parent_id=science.id)
|
||||
biology = Category(name='Biologie', description='Studium lebender Organismen',
|
||||
color_code='#66BB6A', icon='leaf', parent_id=science.id)
|
||||
chemistry = Category(name='Chemie', description='Studium der Stoffe und ihrer Reaktionen',
|
||||
color_code='#A5D6A7', icon='vial', parent_id=science.id)
|
||||
|
||||
db.session.add_all([physics, biology, chemistry])
|
||||
|
||||
# Technologie-Unterkategorien
|
||||
informatics = Category(name='Informatik', description='Studium der Informationsverarbeitung',
|
||||
color_code='#FFB74D', icon='laptop-code', parent_id=technology.id)
|
||||
ai = Category(name='Künstliche Intelligenz', description='Entwicklung intelligenter Systeme',
|
||||
color_code='#FFA726', icon='robot', parent_id=technology.id)
|
||||
|
||||
db.session.add_all([informatics, ai])
|
||||
|
||||
# Philosophie-Unterkategorien
|
||||
ethics = Category(name='Ethik', description='Moralphilosophie und Wertesysteme',
|
||||
color_code='#BA68C8', icon='balance-scale', parent_id=philosophy.id)
|
||||
logic = Category(name='Logik', description='Studie der gültigen Schlussfolgerungen',
|
||||
color_code='#AB47BC', icon='project-diagram', parent_id=philosophy.id)
|
||||
|
||||
db.session.add_all([ethics, logic])
|
||||
|
||||
db.session.commit()
|
||||
|
||||
# Knoten für die öffentliche Mindmap erstellen
|
||||
nodes = {
|
||||
'quantenmechanik': MindMapNode(
|
||||
name='Quantenmechanik',
|
||||
description='Physikalische Theorie zur Beschreibung der Materie auf atomarer Ebene',
|
||||
color_code='#81C784',
|
||||
category_id=physics.id,
|
||||
created_by_id=admin.id
|
||||
),
|
||||
'relativitaetstheorie': MindMapNode(
|
||||
name='Relativitätstheorie',
|
||||
description='Einsteins Theorien zur Raumzeit und Gravitation',
|
||||
color_code='#81C784',
|
||||
category_id=physics.id,
|
||||
created_by_id=admin.id
|
||||
),
|
||||
'genetik': MindMapNode(
|
||||
name='Genetik',
|
||||
description='Wissenschaft der Gene und Vererbung',
|
||||
color_code='#66BB6A',
|
||||
category_id=biology.id,
|
||||
created_by_id=admin.id
|
||||
),
|
||||
'machine_learning': MindMapNode(
|
||||
name='Machine Learning',
|
||||
description='Algorithmen, die aus Daten lernen können',
|
||||
color_code='#FFA726',
|
||||
category_id=ai.id,
|
||||
created_by_id=admin.id
|
||||
),
|
||||
'ki_ethik': MindMapNode(
|
||||
name='KI-Ethik',
|
||||
description='Moralische Implikationen künstlicher Intelligenz',
|
||||
color_code='#BA68C8',
|
||||
category_id=ethics.id,
|
||||
created_by_id=user.id
|
||||
)
|
||||
}
|
||||
|
||||
for node in nodes.values():
|
||||
db.session.add(node)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
# Verknüpfungen zwischen Knoten herstellen (Hierarchie)
|
||||
nodes['machine_learning'].parents.append(nodes['ki_ethik'])
|
||||
db.session.commit()
|
||||
|
||||
# Gedanken erstellen
|
||||
thoughts = [
|
||||
{
|
||||
'title': 'Künstliche Intelligenz und Bewusstsein',
|
||||
'content': 'Die Frage nach maschinellem Bewusstsein ist fundamental für die KI-Ethik. Aktuelle KI-Systeme haben kein Bewusstsein, aber fortschrittliche KI könnte in Zukunft Eigenschaften entwickeln, die diesem nahekommen.',
|
||||
'abstract': 'Eine Untersuchung der philosophischen Implikationen von KI-Bewusstsein.',
|
||||
'keywords': 'KI, Bewusstsein, Ethik, Philosophie',
|
||||
'branch': 'Philosophie',
|
||||
'color_code': '#BA68C8',
|
||||
'source_type': 'Markdown',
|
||||
'user_id': user.id,
|
||||
'node': nodes['ki_ethik']
|
||||
},
|
||||
{
|
||||
'title': 'Quantenmechanik und Realität',
|
||||
'content': 'Die Kopenhagener Deutung und ihre Auswirkungen auf unser Verständnis der Realität. Quantenmechanik stellt grundlegende Annahmen über Determinismus und Lokalität in Frage.',
|
||||
'abstract': 'Eine Analyse verschiedener Interpretationen der Quantenmechanik.',
|
||||
'keywords': 'Quantenmechanik, Physik, Realität',
|
||||
'branch': 'Physik',
|
||||
'color_code': '#81C784',
|
||||
'source_type': 'PDF',
|
||||
'user_id': admin.id,
|
||||
'node': nodes['quantenmechanik']
|
||||
},
|
||||
{
|
||||
'title': 'Deep Learning Fortschritte',
|
||||
'content': 'Die neuesten Fortschritte im Deep Learning haben zu beeindruckenden Ergebnissen in Bereichen wie Computer Vision, Natural Language Processing und Reinforcement Learning geführt.',
|
||||
'abstract': 'Überblick über aktuelle Deep Learning-Techniken und ihre Anwendungen.',
|
||||
'keywords': 'Deep Learning, Neural Networks, AI',
|
||||
'branch': 'Technologie',
|
||||
'color_code': '#FFA726',
|
||||
'source_type': 'Webpage',
|
||||
'user_id': admin.id,
|
||||
'node': nodes['machine_learning']
|
||||
}
|
||||
]
|
||||
|
||||
thought_objects = []
|
||||
for t_data in thoughts:
|
||||
node = t_data.pop('node')
|
||||
thought = Thought(**t_data)
|
||||
node.thoughts.append(thought)
|
||||
thought_objects.append(thought)
|
||||
db.session.add(thought)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
# Beziehungen zwischen Gedanken
|
||||
relation = ThoughtRelation(
|
||||
source_id=thought_objects[0].id,
|
||||
target_id=thought_objects[2].id,
|
||||
relation_type=RelationType.INSPIRES,
|
||||
created_by_id=user.id
|
||||
)
|
||||
db.session.add(relation)
|
||||
|
||||
# Bewertungen erstellen
|
||||
rating1 = ThoughtRating(
|
||||
thought_id=thought_objects[0].id,
|
||||
user_id=admin.id,
|
||||
relevance_score=5
|
||||
)
|
||||
rating2 = ThoughtRating(
|
||||
thought_id=thought_objects[2].id,
|
||||
user_id=user.id,
|
||||
relevance_score=4
|
||||
)
|
||||
db.session.add_all([rating1, rating2])
|
||||
|
||||
# Kommentare erstellen
|
||||
for thought in thought_objects:
|
||||
comment = Comment(
|
||||
content=f'Interessante Perspektive zu {thought.title}!',
|
||||
thought_id=thought.id,
|
||||
user_id=admin.id if thought.user_id != admin.id else user.id
|
||||
)
|
||||
db.session.add(comment)
|
||||
|
||||
# Benutzer-Mindmaps erstellen
|
||||
user_mindmap = UserMindmap(
|
||||
name='Meine KI-Forschung',
|
||||
description='Meine persönliche Sammlung zu KI und Ethik',
|
||||
user_id=user.id
|
||||
)
|
||||
db.session.add(user_mindmap)
|
||||
db.session.commit()
|
||||
|
||||
# Knoten zur Benutzer-Mindmap hinzufügen
|
||||
user_mindmap_nodes = [
|
||||
UserMindmapNode(
|
||||
user_mindmap_id=user_mindmap.id,
|
||||
node_id=nodes['machine_learning'].id,
|
||||
x_position=200,
|
||||
y_position=300
|
||||
),
|
||||
UserMindmapNode(
|
||||
user_mindmap_id=user_mindmap.id,
|
||||
node_id=nodes['ki_ethik'].id,
|
||||
x_position=500,
|
||||
y_position=200
|
||||
)
|
||||
]
|
||||
db.session.add_all(user_mindmap_nodes)
|
||||
|
||||
# Private Notizen
|
||||
note = MindmapNote(
|
||||
user_id=user.id,
|
||||
mindmap_id=user_mindmap.id,
|
||||
node_id=nodes['ki_ethik'].id,
|
||||
content="Recherchiere mehr über aktuelle ethische Richtlinien für KI-Entwicklung!",
|
||||
color_code="#FFF59D"
|
||||
)
|
||||
db.session.add(note)
|
||||
|
||||
# Gedanken zu Bookmarks hinzufügen
|
||||
user.bookmarked_thoughts.append(thought_objects[0])
|
||||
admin.bookmarked_thoughts.append(thought_objects[1])
|
||||
|
||||
# Finaler Commit
|
||||
db.session.commit()
|
||||
|
||||
print("Datenbank wurde erfolgreich initialisiert!")
|
||||
|
||||
def init_db():
|
||||
with app.app_context():
|
||||
print("Initialisiere Datenbank...")
|
||||
|
||||
# Tabellen erstellen
|
||||
db.create_all()
|
||||
print("Tabellen wurden erstellt.")
|
||||
|
||||
# Standardbenutzer erstellen, falls keine vorhanden sind
|
||||
if User.query.count() == 0:
|
||||
print("Erstelle Standardbenutzer...")
|
||||
create_default_users()
|
||||
|
||||
# Standardkategorien erstellen, falls keine vorhanden sind
|
||||
if Category.query.count() == 0:
|
||||
print("Erstelle Standardkategorien...")
|
||||
create_default_categories()
|
||||
|
||||
# Beispiel-Mindmap erstellen, falls keine Knoten vorhanden sind
|
||||
if MindMapNode.query.count() == 0:
|
||||
print("Erstelle Beispiel-Mindmap...")
|
||||
create_sample_mindmap()
|
||||
|
||||
print("Datenbankinitialisierung abgeschlossen.")
|
||||
|
||||
def create_default_users():
|
||||
"""Erstellt Standardbenutzer für die Anwendung"""
|
||||
users = [
|
||||
{
|
||||
'username': 'admin',
|
||||
'email': 'admin@example.com',
|
||||
'password': 'admin',
|
||||
'role': 'admin'
|
||||
},
|
||||
{
|
||||
'username': 'user',
|
||||
'email': 'user@example.com',
|
||||
'password': 'user',
|
||||
'role': 'user'
|
||||
}
|
||||
]
|
||||
|
||||
for user_data in users:
|
||||
password = user_data.pop('password')
|
||||
user = User(**user_data)
|
||||
user.set_password(password)
|
||||
db.session.add(user)
|
||||
|
||||
db.session.commit()
|
||||
print(f"{len(users)} Benutzer wurden erstellt.")
|
||||
|
||||
def create_default_categories():
|
||||
"""Erstellt die Standardkategorien für die Mindmap"""
|
||||
# Hauptkategorien
|
||||
main_categories = [
|
||||
{
|
||||
"name": "Philosophie",
|
||||
"description": "Philosophisches Denken und Konzepte",
|
||||
"color_code": "#9F7AEA",
|
||||
"icon": "fa-brain"
|
||||
},
|
||||
{
|
||||
"name": "Wissenschaft",
|
||||
"description": "Wissenschaftliche Disziplinen und Erkenntnisse",
|
||||
"color_code": "#60A5FA",
|
||||
"icon": "fa-flask"
|
||||
},
|
||||
{
|
||||
"name": "Technologie",
|
||||
"description": "Technologische Entwicklungen und Anwendungen",
|
||||
"color_code": "#10B981",
|
||||
"icon": "fa-microchip"
|
||||
},
|
||||
{
|
||||
"name": "Künste",
|
||||
"description": "Künstlerische Ausdrucksformen und Werke",
|
||||
"color_code": "#F59E0B",
|
||||
"icon": "fa-palette"
|
||||
},
|
||||
{
|
||||
"name": "Psychologie",
|
||||
"description": "Mentale Prozesse und Verhaltensweisen",
|
||||
"color_code": "#EF4444",
|
||||
"icon": "fa-brain"
|
||||
}
|
||||
]
|
||||
|
||||
# Hauptkategorien erstellen
|
||||
category_map = {}
|
||||
for cat_data in main_categories:
|
||||
category = Category(**cat_data)
|
||||
db.session.add(category)
|
||||
db.session.flush() # ID generieren
|
||||
category_map[cat_data["name"]] = category
|
||||
|
||||
# Unterkategorien für Philosophie
|
||||
philosophy_subcategories = [
|
||||
{"name": "Ethik", "description": "Moralische Grundsätze", "icon": "fa-balance-scale", "color_code": "#8B5CF6"},
|
||||
{"name": "Logik", "description": "Gesetze des Denkens", "icon": "fa-project-diagram", "color_code": "#8B5CF6"},
|
||||
{"name": "Erkenntnistheorie", "description": "Natur des Wissens", "icon": "fa-lightbulb", "color_code": "#8B5CF6"}
|
||||
]
|
||||
|
||||
# Unterkategorien für Wissenschaft
|
||||
science_subcategories = [
|
||||
{"name": "Physik", "description": "Studie der Materie und Energie", "icon": "fa-atom", "color_code": "#3B82F6"},
|
||||
{"name": "Biologie", "description": "Studie des Lebens", "icon": "fa-dna", "color_code": "#3B82F6"},
|
||||
{"name": "Mathematik", "description": "Studie der Zahlen und Strukturen", "icon": "fa-square-root-alt", "color_code": "#3B82F6"}
|
||||
]
|
||||
|
||||
# Unterkategorien für Technologie
|
||||
tech_subcategories = [
|
||||
{"name": "Software", "description": "Computerprogramme und Anwendungen", "icon": "fa-code", "color_code": "#059669"},
|
||||
{"name": "Hardware", "description": "Physische Komponenten der Technik", "icon": "fa-microchip", "color_code": "#059669"},
|
||||
{"name": "Internet", "description": "Globales Netzwerk und Web", "icon": "fa-globe", "color_code": "#059669"}
|
||||
]
|
||||
|
||||
# Unterkategorien für Künste
|
||||
arts_subcategories = [
|
||||
{"name": "Musik", "description": "Klangkunst", "icon": "fa-music", "color_code": "#D97706"},
|
||||
{"name": "Literatur", "description": "Geschriebene Kunst", "icon": "fa-book", "color_code": "#D97706"},
|
||||
{"name": "Bildende Kunst", "description": "Visuelle Kunst", "icon": "fa-paint-brush", "color_code": "#D97706"}
|
||||
]
|
||||
|
||||
# Unterkategorien für Psychologie
|
||||
psychology_subcategories = [
|
||||
{"name": "Kognition", "description": "Gedächtnisprozesse und Denken", "icon": "fa-brain", "color_code": "#DC2626"},
|
||||
{"name": "Emotionen", "description": "Gefühle und emotionale Prozesse", "icon": "fa-heart", "color_code": "#DC2626"},
|
||||
{"name": "Verhalten", "description": "Beobachtbares Verhalten und Reaktionen", "icon": "fa-user", "color_code": "#DC2626"}
|
||||
]
|
||||
|
||||
# Alle Unterkategorien zu ihren Hauptkategorien hinzufügen
|
||||
for subcat_data in philosophy_subcategories:
|
||||
subcat = Category(**subcat_data)
|
||||
subcat.parent_id = category_map["Philosophie"].id
|
||||
db.session.add(subcat)
|
||||
|
||||
for subcat_data in science_subcategories:
|
||||
subcat = Category(**subcat_data)
|
||||
subcat.parent_id = category_map["Wissenschaft"].id
|
||||
db.session.add(subcat)
|
||||
|
||||
for subcat_data in tech_subcategories:
|
||||
subcat = Category(**subcat_data)
|
||||
subcat.parent_id = category_map["Technologie"].id
|
||||
db.session.add(subcat)
|
||||
|
||||
for subcat_data in arts_subcategories:
|
||||
subcat = Category(**subcat_data)
|
||||
subcat.parent_id = category_map["Künste"].id
|
||||
db.session.add(subcat)
|
||||
|
||||
for subcat_data in psychology_subcategories:
|
||||
subcat = Category(**subcat_data)
|
||||
subcat.parent_id = category_map["Psychologie"].id
|
||||
db.session.add(subcat)
|
||||
|
||||
db.session.commit()
|
||||
print(f"{len(main_categories)} Hauptkategorien und {len(philosophy_subcategories + science_subcategories + tech_subcategories + arts_subcategories + psychology_subcategories)} Unterkategorien wurden erstellt.")
|
||||
|
||||
def create_sample_mindmap():
|
||||
"""Erstellt eine Beispiel-Mindmap mit Knoten und Beziehungen"""
|
||||
|
||||
# Kategorien für die Zuordnung
|
||||
categories = Category.query.all()
|
||||
category_map = {cat.name: cat for cat in categories}
|
||||
|
||||
# Beispielknoten erstellen
|
||||
nodes = [
|
||||
{
|
||||
'name': 'Wissensmanagement',
|
||||
'description': 'Systematische Erfassung, Speicherung und Nutzung von Wissen in Organisationen.',
|
||||
'color_code': '#6366f1',
|
||||
'icon': 'database',
|
||||
'category': category_map.get('Konzept'),
|
||||
'x': 0,
|
||||
'y': 0
|
||||
},
|
||||
{
|
||||
'name': 'Mind-Mapping',
|
||||
'description': 'Technik zur visuellen Darstellung von Informationen und Zusammenhängen.',
|
||||
'color_code': '#10b981',
|
||||
'icon': 'git-branch',
|
||||
'category': category_map.get('Prozess'),
|
||||
'x': 200,
|
||||
'y': -150
|
||||
},
|
||||
{
|
||||
'name': 'Cytoscape.js',
|
||||
'description': 'JavaScript-Bibliothek für die Visualisierung und Manipulation von Graphen.',
|
||||
'color_code': '#3b82f6',
|
||||
'icon': 'code',
|
||||
'category': category_map.get('Technologie'),
|
||||
'x': 350,
|
||||
'y': -50
|
||||
},
|
||||
{
|
||||
'name': 'Socket.IO',
|
||||
'description': 'Bibliothek für Echtzeit-Kommunikation zwischen Client und Server.',
|
||||
'color_code': '#3b82f6',
|
||||
'icon': 'zap',
|
||||
'category': category_map.get('Technologie'),
|
||||
'x': 350,
|
||||
'y': 100
|
||||
},
|
||||
{
|
||||
'name': 'Kollaboration',
|
||||
'description': 'Zusammenarbeit mehrerer Benutzer an gemeinsamen Inhalten.',
|
||||
'color_code': '#f59e0b',
|
||||
'icon': 'users',
|
||||
'category': category_map.get('Prozess'),
|
||||
'x': 200,
|
||||
'y': 150
|
||||
},
|
||||
{
|
||||
'name': 'SQLite',
|
||||
'description': 'Leichtgewichtige relationale Datenbank, die ohne Server-Prozess auskommt.',
|
||||
'color_code': '#3b82f6',
|
||||
'icon': 'database',
|
||||
'category': category_map.get('Technologie'),
|
||||
'x': 0,
|
||||
'y': 200
|
||||
},
|
||||
{
|
||||
'name': 'Flask',
|
||||
'description': 'Leichtgewichtiges Python-Webframework für die Entwicklung von Webanwendungen.',
|
||||
'color_code': '#3b82f6',
|
||||
'icon': 'server',
|
||||
'category': category_map.get('Technologie'),
|
||||
'x': -200,
|
||||
'y': 150
|
||||
},
|
||||
{
|
||||
'name': 'REST API',
|
||||
'description': 'Architekturstil für verteilte Systeme, insbesondere Webanwendungen.',
|
||||
'color_code': '#10b981',
|
||||
'icon': 'link',
|
||||
'category': category_map.get('Konzept'),
|
||||
'x': -200,
|
||||
'y': -150
|
||||
},
|
||||
{
|
||||
'name': 'Dokumentation',
|
||||
'description': 'Strukturierte Erfassung und Beschreibung von Informationen und Prozessen.',
|
||||
'color_code': '#ec4899',
|
||||
'icon': 'file-text',
|
||||
'category': category_map.get('Dokument'),
|
||||
'x': -350,
|
||||
'y': 0
|
||||
}
|
||||
]
|
||||
|
||||
# Knoten in die Datenbank einfügen
|
||||
node_objects = {}
|
||||
for node_data in nodes:
|
||||
category = node_data.pop('category', None)
|
||||
x = node_data.pop('x', 0)
|
||||
y = node_data.pop('y', 0)
|
||||
node = MindMapNode(**node_data)
|
||||
if category:
|
||||
node.category_id = category.id
|
||||
db.session.add(node)
|
||||
db.session.flush() # Generiert IDs für neue Objekte
|
||||
node_objects[node.name] = node
|
||||
|
||||
# Beziehungen erstellen
|
||||
relationships = [
|
||||
('Wissensmanagement', 'Mind-Mapping'),
|
||||
('Wissensmanagement', 'Kollaboration'),
|
||||
('Wissensmanagement', 'Dokumentation'),
|
||||
('Mind-Mapping', 'Cytoscape.js'),
|
||||
('Kollaboration', 'Socket.IO'),
|
||||
('Wissensmanagement', 'SQLite'),
|
||||
('SQLite', 'Flask'),
|
||||
('Flask', 'REST API'),
|
||||
('REST API', 'Socket.IO'),
|
||||
('REST API', 'Dokumentation')
|
||||
]
|
||||
|
||||
for parent_name, child_name in relationships:
|
||||
parent = node_objects.get(parent_name)
|
||||
child = node_objects.get(child_name)
|
||||
if parent and child:
|
||||
parent.children.append(child)
|
||||
|
||||
db.session.commit()
|
||||
print(f"{len(nodes)} Knoten und {len(relationships)} Beziehungen wurden erstellt.")
|
||||
"""Alias für Kompatibilität mit älteren Scripts."""
|
||||
init_database()
|
||||
|
||||
if __name__ == '__main__':
|
||||
init_db()
|
||||
init_database()
|
||||
print("Datenbank wurde erfolgreich initialisiert!")
|
||||
print("Sie können die Anwendung jetzt mit 'python app.py' starten")
|
||||
print("Anmelden mit:")
|
||||
|
||||
2419
logs/app.log
2419
logs/app.log
File diff suppressed because it is too large
Load Diff
424
logs/errors.log
424
logs/errors.log
@@ -1,424 +0,0 @@
|
||||
2025-05-28 21:29:08 | ERROR | SysTades | ERROR | Fehler 500: 405 Method Not Allowed: The method is not allowed for the requested URL.
|
||||
Endpoint: /api/thoughts, Method: GET, IP: 127.0.0.1
|
||||
User: 1 (admin)
|
||||
Traceback (most recent call last):
|
||||
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 867, in full_dispatch_request
|
||||
rv = self.dispatch_request()
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 841, in dispatch_request
|
||||
self.raise_routing_exception(req)
|
||||
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 450, in raise_routing_exception
|
||||
raise request.routing_exception # type: ignore
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/core/.local/lib/python3.11/site-packages/flask/ctx.py", line 353, in match_request
|
||||
result = self.url_adapter.match(return_rule=True) # type: ignore
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/core/.local/lib/python3.11/site-packages/werkzeug/routing/map.py", line 619, in match
|
||||
raise MethodNotAllowed(valid_methods=list(e.have_match_for)) from None
|
||||
werkzeug.exceptions.MethodNotAllowed: 405 Method Not Allowed: The method is not allowed for the requested URL.
|
||||
|
||||
2025-05-28 21:43:40 | ERROR | SysTades | ERROR | Fehler in social_feed nach 2.83ms - Exception: AttributeError: followed_id
|
||||
Traceback (most recent call last):
|
||||
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/sql/base.py", line 1633, in __getattr__
|
||||
return self._index[key][1]
|
||||
~~~~~~~~~~~^^^^^
|
||||
KeyError: 'followed_id'
|
||||
|
||||
The above exception was the direct cause of the following exception:
|
||||
|
||||
Traceback (most recent call last):
|
||||
File "/home/core/dev/website/utils/logger.py", line 586, in wrapper
|
||||
result = func(*args, **kwargs)
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/core/dev/website/app.py", line 2774, in social_feed
|
||||
followed_posts = current_user.get_feed_posts(limit=100)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/core/dev/website/models.py", line 193, in get_feed_posts
|
||||
followed_users, SocialPost.user_id == followed_users.c.followed_id
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/sql/base.py", line 1635, in __getattr__
|
||||
raise AttributeError(key) from err
|
||||
AttributeError: followed_id
|
||||
|
||||
2025-05-28 21:43:40 | ERROR | SysTades | ERROR | Fehler 500: followed_id
|
||||
Endpoint: /feed, Method: GET, IP: 127.0.0.1
|
||||
User: 1 (admin)
|
||||
Traceback (most recent call last):
|
||||
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/sql/base.py", line 1633, in __getattr__
|
||||
return self._index[key][1]
|
||||
~~~~~~~~~~~^^^^^
|
||||
KeyError: 'followed_id'
|
||||
|
||||
The above exception was the direct cause of the following exception:
|
||||
|
||||
Traceback (most recent call last):
|
||||
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 867, in full_dispatch_request
|
||||
rv = self.dispatch_request()
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 852, in dispatch_request
|
||||
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/core/.local/lib/python3.11/site-packages/flask_login/utils.py", line 290, in decorated_view
|
||||
return current_app.ensure_sync(func)(*args, **kwargs)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/core/dev/website/utils/logger.py", line 586, in wrapper
|
||||
result = func(*args, **kwargs)
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/core/dev/website/app.py", line 2774, in social_feed
|
||||
followed_posts = current_user.get_feed_posts(limit=100)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/core/dev/website/models.py", line 193, in get_feed_posts
|
||||
followed_users, SocialPost.user_id == followed_users.c.followed_id
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/sql/base.py", line 1635, in __getattr__
|
||||
raise AttributeError(key) from err
|
||||
AttributeError: followed_id
|
||||
|
||||
2025-05-28 21:43:59 | ERROR | SysTades | ERROR | Fehler in discover nach 16.89ms - Exception: AttributeError: 'AppenderQuery' object has no attribute 'contains'
|
||||
Traceback (most recent call last):
|
||||
File "/home/core/dev/website/utils/logger.py", line 586, in wrapper
|
||||
result = func(*args, **kwargs)
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/core/dev/website/app.py", line 2800, in discover
|
||||
~current_user.following.contains(User.id)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
AttributeError: 'AppenderQuery' object has no attribute 'contains'
|
||||
|
||||
2025-05-28 21:43:59 | ERROR | SysTades | ERROR | Fehler 500: 'AppenderQuery' object has no attribute 'contains'
|
||||
Endpoint: /discover, Method: GET, IP: 127.0.0.1
|
||||
User: 1 (admin)
|
||||
Traceback (most recent call last):
|
||||
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 867, in full_dispatch_request
|
||||
rv = self.dispatch_request()
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 852, in dispatch_request
|
||||
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/core/.local/lib/python3.11/site-packages/flask_login/utils.py", line 290, in decorated_view
|
||||
return current_app.ensure_sync(func)(*args, **kwargs)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/core/dev/website/utils/logger.py", line 586, in wrapper
|
||||
result = func(*args, **kwargs)
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/core/dev/website/app.py", line 2800, in discover
|
||||
~current_user.following.contains(User.id)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
AttributeError: 'AppenderQuery' object has no attribute 'contains'
|
||||
|
||||
2025-05-28 21:46:15 | ERROR | SysTades | ERROR | Fehler in social_feed nach 54.92ms - Exception: OperationalError: (sqlite3.OperationalError) near "UNION": syntax error
|
||||
[SQL: SELECT anon_1.social_post_id AS anon_1_social_post_id, anon_1.social_post_content AS anon_1_social_post_content, anon_1.social_post_image_url AS anon_1_social_post_image_url, anon_1.social_post_video_url AS anon_1_social_post_video_url, anon_1.social_post_link_url AS anon_1_social_post_link_url, anon_1.social_post_link_title AS anon_1_social_post_link_title, anon_1.social_post_link_description AS anon_1_social_post_link_description, anon_1.social_post_post_type AS anon_1_social_post_post_type, anon_1.social_post_visibility AS anon_1_social_post_visibility, anon_1.social_post_is_pinned AS anon_1_social_post_is_pinned, anon_1.social_post_like_count AS anon_1_social_post_like_count, anon_1.social_post_comment_count AS anon_1_social_post_comment_count, anon_1.social_post_share_count AS anon_1_social_post_share_count, anon_1.social_post_view_count AS anon_1_social_post_view_count, anon_1.social_post_created_at AS anon_1_social_post_created_at, anon_1.social_post_updated_at AS anon_1_social_post_updated_at, anon_1.social_post_user_id AS anon_1_social_post_user_id, anon_1.social_post_shared_thought_id AS anon_1_social_post_shared_thought_id, anon_1.social_post_shared_node_id AS anon_1_social_post_shared_node_id
|
||||
FROM ((SELECT social_post.id AS social_post_id, social_post.content AS social_post_content, social_post.image_url AS social_post_image_url, social_post.video_url AS social_post_video_url, social_post.link_url AS social_post_link_url, social_post.link_title AS social_post_link_title, social_post.link_description AS social_post_link_description, social_post.post_type AS social_post_post_type, social_post.visibility AS social_post_visibility, social_post.is_pinned AS social_post_is_pinned, social_post.like_count AS social_post_like_count, social_post.comment_count AS social_post_comment_count, social_post.share_count AS social_post_share_count, social_post.view_count AS social_post_view_count, social_post.created_at AS social_post_created_at, social_post.updated_at AS social_post_updated_at, social_post.user_id AS social_post_user_id, social_post.shared_thought_id AS social_post_shared_thought_id, social_post.shared_node_id AS social_post_shared_node_id
|
||||
FROM social_post
|
||||
WHERE social_post.user_id IN (?) ORDER BY social_post.created_at DESC
|
||||
LIMIT ? OFFSET ?) UNION SELECT social_post.id AS social_post_id, social_post.content AS social_post_content, social_post.image_url AS social_post_image_url, social_post.video_url AS social_post_video_url, social_post.link_url AS social_post_link_url, social_post.link_title AS social_post_link_title, social_post.link_description AS social_post_link_description, social_post.post_type AS social_post_post_type, social_post.visibility AS social_post_visibility, social_post.is_pinned AS social_post_is_pinned, social_post.like_count AS social_post_like_count, social_post.comment_count AS social_post_comment_count, social_post.share_count AS social_post_share_count, social_post.view_count AS social_post_view_count, social_post.created_at AS social_post_created_at, social_post.updated_at AS social_post_updated_at, social_post.user_id AS social_post_user_id, social_post.shared_thought_id AS social_post_shared_thought_id, social_post.shared_node_id AS social_post_shared_node_id
|
||||
FROM social_post
|
||||
WHERE social_post.user_id = ?) AS anon_1 ORDER BY anon_1.social_post_created_at DESC
|
||||
LIMIT ? OFFSET ?]
|
||||
[parameters: (1, 100, 0, 1, 10, 0)]
|
||||
(Background on this error at: https://sqlalche.me/e/20/e3q8)
|
||||
Traceback (most recent call last):
|
||||
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1969, in _exec_single_context
|
||||
self.dialect.do_execute(
|
||||
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/default.py", line 922, in do_execute
|
||||
cursor.execute(statement, parameters)
|
||||
sqlite3.OperationalError: near "UNION": syntax error
|
||||
|
||||
The above exception was the direct cause of the following exception:
|
||||
|
||||
Traceback (most recent call last):
|
||||
File "/home/core/dev/website/utils/logger.py", line 586, in wrapper
|
||||
result = func(*args, **kwargs)
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/core/dev/website/app.py", line 2782, in social_feed
|
||||
posts = all_posts.paginate(
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/core/.local/lib/python3.11/site-packages/flask_sqlalchemy/query.py", line 98, in paginate
|
||||
return QueryPagination(
|
||||
^^^^^^^^^^^^^^^^
|
||||
File "/home/core/.local/lib/python3.11/site-packages/flask_sqlalchemy/pagination.py", line 72, in __init__
|
||||
items = self._query_items()
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/core/.local/lib/python3.11/site-packages/flask_sqlalchemy/pagination.py", line 358, in _query_items
|
||||
out = query.limit(self.per_page).offset(self._query_offset).all()
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/query.py", line 2693, in all
|
||||
return self._iter().all() # type: ignore
|
||||
^^^^^^^^^^^^
|
||||
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/query.py", line 2847, in _iter
|
||||
result: Union[ScalarResult[_T], Result[_T]] = self.session.execute(
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/session.py", line 2308, in execute
|
||||
return self._execute_internal(
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/session.py", line 2190, in _execute_internal
|
||||
result: Result[Any] = compile_state_cls.orm_execute_statement(
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/context.py", line 293, in orm_execute_statement
|
||||
result = conn.execute(
|
||||
^^^^^^^^^^^^^
|
||||
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1416, in execute
|
||||
return meth(
|
||||
^^^^^
|
||||
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/sql/elements.py", line 516, in _execute_on_connection
|
||||
return connection._execute_clauseelement(
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1639, in _execute_clauseelement
|
||||
ret = self._execute_context(
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1848, in _execute_context
|
||||
return self._exec_single_context(
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1988, in _exec_single_context
|
||||
self._handle_dbapi_exception(
|
||||
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 2343, in _handle_dbapi_exception
|
||||
raise sqlalchemy_exception.with_traceback(exc_info[2]) from e
|
||||
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1969, in _exec_single_context
|
||||
self.dialect.do_execute(
|
||||
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/default.py", line 922, in do_execute
|
||||
cursor.execute(statement, parameters)
|
||||
sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) near "UNION": syntax error
|
||||
[SQL: SELECT anon_1.social_post_id AS anon_1_social_post_id, anon_1.social_post_content AS anon_1_social_post_content, anon_1.social_post_image_url AS anon_1_social_post_image_url, anon_1.social_post_video_url AS anon_1_social_post_video_url, anon_1.social_post_link_url AS anon_1_social_post_link_url, anon_1.social_post_link_title AS anon_1_social_post_link_title, anon_1.social_post_link_description AS anon_1_social_post_link_description, anon_1.social_post_post_type AS anon_1_social_post_post_type, anon_1.social_post_visibility AS anon_1_social_post_visibility, anon_1.social_post_is_pinned AS anon_1_social_post_is_pinned, anon_1.social_post_like_count AS anon_1_social_post_like_count, anon_1.social_post_comment_count AS anon_1_social_post_comment_count, anon_1.social_post_share_count AS anon_1_social_post_share_count, anon_1.social_post_view_count AS anon_1_social_post_view_count, anon_1.social_post_created_at AS anon_1_social_post_created_at, anon_1.social_post_updated_at AS anon_1_social_post_updated_at, anon_1.social_post_user_id AS anon_1_social_post_user_id, anon_1.social_post_shared_thought_id AS anon_1_social_post_shared_thought_id, anon_1.social_post_shared_node_id AS anon_1_social_post_shared_node_id
|
||||
FROM ((SELECT social_post.id AS social_post_id, social_post.content AS social_post_content, social_post.image_url AS social_post_image_url, social_post.video_url AS social_post_video_url, social_post.link_url AS social_post_link_url, social_post.link_title AS social_post_link_title, social_post.link_description AS social_post_link_description, social_post.post_type AS social_post_post_type, social_post.visibility AS social_post_visibility, social_post.is_pinned AS social_post_is_pinned, social_post.like_count AS social_post_like_count, social_post.comment_count AS social_post_comment_count, social_post.share_count AS social_post_share_count, social_post.view_count AS social_post_view_count, social_post.created_at AS social_post_created_at, social_post.updated_at AS social_post_updated_at, social_post.user_id AS social_post_user_id, social_post.shared_thought_id AS social_post_shared_thought_id, social_post.shared_node_id AS social_post_shared_node_id
|
||||
FROM social_post
|
||||
WHERE social_post.user_id IN (?) ORDER BY social_post.created_at DESC
|
||||
LIMIT ? OFFSET ?) UNION SELECT social_post.id AS social_post_id, social_post.content AS social_post_content, social_post.image_url AS social_post_image_url, social_post.video_url AS social_post_video_url, social_post.link_url AS social_post_link_url, social_post.link_title AS social_post_link_title, social_post.link_description AS social_post_link_description, social_post.post_type AS social_post_post_type, social_post.visibility AS social_post_visibility, social_post.is_pinned AS social_post_is_pinned, social_post.like_count AS social_post_like_count, social_post.comment_count AS social_post_comment_count, social_post.share_count AS social_post_share_count, social_post.view_count AS social_post_view_count, social_post.created_at AS social_post_created_at, social_post.updated_at AS social_post_updated_at, social_post.user_id AS social_post_user_id, social_post.shared_thought_id AS social_post_shared_thought_id, social_post.shared_node_id AS social_post_shared_node_id
|
||||
FROM social_post
|
||||
WHERE social_post.user_id = ?) AS anon_1 ORDER BY anon_1.social_post_created_at DESC
|
||||
LIMIT ? OFFSET ?]
|
||||
[parameters: (1, 100, 0, 1, 10, 0)]
|
||||
(Background on this error at: https://sqlalche.me/e/20/e3q8)
|
||||
|
||||
2025-05-28 21:46:15 | ERROR | SysTades | ERROR | Fehler 500: (sqlite3.OperationalError) near "UNION": syntax error
|
||||
[SQL: SELECT anon_1.social_post_id AS anon_1_social_post_id, anon_1.social_post_content AS anon_1_social_post_content, anon_1.social_post_image_url AS anon_1_social_post_image_url, anon_1.social_post_video_url AS anon_1_social_post_video_url, anon_1.social_post_link_url AS anon_1_social_post_link_url, anon_1.social_post_link_title AS anon_1_social_post_link_title, anon_1.social_post_link_description AS anon_1_social_post_link_description, anon_1.social_post_post_type AS anon_1_social_post_post_type, anon_1.social_post_visibility AS anon_1_social_post_visibility, anon_1.social_post_is_pinned AS anon_1_social_post_is_pinned, anon_1.social_post_like_count AS anon_1_social_post_like_count, anon_1.social_post_comment_count AS anon_1_social_post_comment_count, anon_1.social_post_share_count AS anon_1_social_post_share_count, anon_1.social_post_view_count AS anon_1_social_post_view_count, anon_1.social_post_created_at AS anon_1_social_post_created_at, anon_1.social_post_updated_at AS anon_1_social_post_updated_at, anon_1.social_post_user_id AS anon_1_social_post_user_id, anon_1.social_post_shared_thought_id AS anon_1_social_post_shared_thought_id, anon_1.social_post_shared_node_id AS anon_1_social_post_shared_node_id
|
||||
FROM ((SELECT social_post.id AS social_post_id, social_post.content AS social_post_content, social_post.image_url AS social_post_image_url, social_post.video_url AS social_post_video_url, social_post.link_url AS social_post_link_url, social_post.link_title AS social_post_link_title, social_post.link_description AS social_post_link_description, social_post.post_type AS social_post_post_type, social_post.visibility AS social_post_visibility, social_post.is_pinned AS social_post_is_pinned, social_post.like_count AS social_post_like_count, social_post.comment_count AS social_post_comment_count, social_post.share_count AS social_post_share_count, social_post.view_count AS social_post_view_count, social_post.created_at AS social_post_created_at, social_post.updated_at AS social_post_updated_at, social_post.user_id AS social_post_user_id, social_post.shared_thought_id AS social_post_shared_thought_id, social_post.shared_node_id AS social_post_shared_node_id
|
||||
FROM social_post
|
||||
WHERE social_post.user_id IN (?) ORDER BY social_post.created_at DESC
|
||||
LIMIT ? OFFSET ?) UNION SELECT social_post.id AS social_post_id, social_post.content AS social_post_content, social_post.image_url AS social_post_image_url, social_post.video_url AS social_post_video_url, social_post.link_url AS social_post_link_url, social_post.link_title AS social_post_link_title, social_post.link_description AS social_post_link_description, social_post.post_type AS social_post_post_type, social_post.visibility AS social_post_visibility, social_post.is_pinned AS social_post_is_pinned, social_post.like_count AS social_post_like_count, social_post.comment_count AS social_post_comment_count, social_post.share_count AS social_post_share_count, social_post.view_count AS social_post_view_count, social_post.created_at AS social_post_created_at, social_post.updated_at AS social_post_updated_at, social_post.user_id AS social_post_user_id, social_post.shared_thought_id AS social_post_shared_thought_id, social_post.shared_node_id AS social_post_shared_node_id
|
||||
FROM social_post
|
||||
WHERE social_post.user_id = ?) AS anon_1 ORDER BY anon_1.social_post_created_at DESC
|
||||
LIMIT ? OFFSET ?]
|
||||
[parameters: (1, 100, 0, 1, 10, 0)]
|
||||
(Background on this error at: https://sqlalche.me/e/20/e3q8)
|
||||
Endpoint: /feed, Method: GET, IP: 127.0.0.1
|
||||
User: 1 (admin)
|
||||
Traceback (most recent call last):
|
||||
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1969, in _exec_single_context
|
||||
self.dialect.do_execute(
|
||||
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/default.py", line 922, in do_execute
|
||||
cursor.execute(statement, parameters)
|
||||
sqlite3.OperationalError: near "UNION": syntax error
|
||||
|
||||
The above exception was the direct cause of the following exception:
|
||||
|
||||
Traceback (most recent call last):
|
||||
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 867, in full_dispatch_request
|
||||
rv = self.dispatch_request()
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 852, in dispatch_request
|
||||
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/core/.local/lib/python3.11/site-packages/flask_login/utils.py", line 290, in decorated_view
|
||||
return current_app.ensure_sync(func)(*args, **kwargs)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/core/dev/website/utils/logger.py", line 586, in wrapper
|
||||
result = func(*args, **kwargs)
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/core/dev/website/app.py", line 2782, in social_feed
|
||||
posts = all_posts.paginate(
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/core/.local/lib/python3.11/site-packages/flask_sqlalchemy/query.py", line 98, in paginate
|
||||
return QueryPagination(
|
||||
^^^^^^^^^^^^^^^^
|
||||
File "/home/core/.local/lib/python3.11/site-packages/flask_sqlalchemy/pagination.py", line 72, in __init__
|
||||
items = self._query_items()
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/core/.local/lib/python3.11/site-packages/flask_sqlalchemy/pagination.py", line 358, in _query_items
|
||||
out = query.limit(self.per_page).offset(self._query_offset).all()
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/query.py", line 2693, in all
|
||||
return self._iter().all() # type: ignore
|
||||
^^^^^^^^^^^^
|
||||
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/query.py", line 2847, in _iter
|
||||
result: Union[ScalarResult[_T], Result[_T]] = self.session.execute(
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/session.py", line 2308, in execute
|
||||
return self._execute_internal(
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/session.py", line 2190, in _execute_internal
|
||||
result: Result[Any] = compile_state_cls.orm_execute_statement(
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/context.py", line 293, in orm_execute_statement
|
||||
result = conn.execute(
|
||||
^^^^^^^^^^^^^
|
||||
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1416, in execute
|
||||
return meth(
|
||||
^^^^^
|
||||
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/sql/elements.py", line 516, in _execute_on_connection
|
||||
return connection._execute_clauseelement(
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1639, in _execute_clauseelement
|
||||
ret = self._execute_context(
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1848, in _execute_context
|
||||
return self._exec_single_context(
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1988, in _exec_single_context
|
||||
self._handle_dbapi_exception(
|
||||
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 2343, in _handle_dbapi_exception
|
||||
raise sqlalchemy_exception.with_traceback(exc_info[2]) from e
|
||||
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1969, in _exec_single_context
|
||||
self.dialect.do_execute(
|
||||
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/default.py", line 922, in do_execute
|
||||
cursor.execute(statement, parameters)
|
||||
sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) near "UNION": syntax error
|
||||
[SQL: SELECT anon_1.social_post_id AS anon_1_social_post_id, anon_1.social_post_content AS anon_1_social_post_content, anon_1.social_post_image_url AS anon_1_social_post_image_url, anon_1.social_post_video_url AS anon_1_social_post_video_url, anon_1.social_post_link_url AS anon_1_social_post_link_url, anon_1.social_post_link_title AS anon_1_social_post_link_title, anon_1.social_post_link_description AS anon_1_social_post_link_description, anon_1.social_post_post_type AS anon_1_social_post_post_type, anon_1.social_post_visibility AS anon_1_social_post_visibility, anon_1.social_post_is_pinned AS anon_1_social_post_is_pinned, anon_1.social_post_like_count AS anon_1_social_post_like_count, anon_1.social_post_comment_count AS anon_1_social_post_comment_count, anon_1.social_post_share_count AS anon_1_social_post_share_count, anon_1.social_post_view_count AS anon_1_social_post_view_count, anon_1.social_post_created_at AS anon_1_social_post_created_at, anon_1.social_post_updated_at AS anon_1_social_post_updated_at, anon_1.social_post_user_id AS anon_1_social_post_user_id, anon_1.social_post_shared_thought_id AS anon_1_social_post_shared_thought_id, anon_1.social_post_shared_node_id AS anon_1_social_post_shared_node_id
|
||||
FROM ((SELECT social_post.id AS social_post_id, social_post.content AS social_post_content, social_post.image_url AS social_post_image_url, social_post.video_url AS social_post_video_url, social_post.link_url AS social_post_link_url, social_post.link_title AS social_post_link_title, social_post.link_description AS social_post_link_description, social_post.post_type AS social_post_post_type, social_post.visibility AS social_post_visibility, social_post.is_pinned AS social_post_is_pinned, social_post.like_count AS social_post_like_count, social_post.comment_count AS social_post_comment_count, social_post.share_count AS social_post_share_count, social_post.view_count AS social_post_view_count, social_post.created_at AS social_post_created_at, social_post.updated_at AS social_post_updated_at, social_post.user_id AS social_post_user_id, social_post.shared_thought_id AS social_post_shared_thought_id, social_post.shared_node_id AS social_post_shared_node_id
|
||||
FROM social_post
|
||||
WHERE social_post.user_id IN (?) ORDER BY social_post.created_at DESC
|
||||
LIMIT ? OFFSET ?) UNION SELECT social_post.id AS social_post_id, social_post.content AS social_post_content, social_post.image_url AS social_post_image_url, social_post.video_url AS social_post_video_url, social_post.link_url AS social_post_link_url, social_post.link_title AS social_post_link_title, social_post.link_description AS social_post_link_description, social_post.post_type AS social_post_post_type, social_post.visibility AS social_post_visibility, social_post.is_pinned AS social_post_is_pinned, social_post.like_count AS social_post_like_count, social_post.comment_count AS social_post_comment_count, social_post.share_count AS social_post_share_count, social_post.view_count AS social_post_view_count, social_post.created_at AS social_post_created_at, social_post.updated_at AS social_post_updated_at, social_post.user_id AS social_post_user_id, social_post.shared_thought_id AS social_post_shared_thought_id, social_post.shared_node_id AS social_post_shared_node_id
|
||||
FROM social_post
|
||||
WHERE social_post.user_id = ?) AS anon_1 ORDER BY anon_1.social_post_created_at DESC
|
||||
LIMIT ? OFFSET ?]
|
||||
[parameters: (1, 100, 0, 1, 10, 0)]
|
||||
(Background on this error at: https://sqlalche.me/e/20/e3q8)
|
||||
|
||||
2025-05-28 21:48:48 | ERROR | SysTades | ERROR | Fehler 404: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||
Endpoint: /sw.js, Method: GET, IP: 127.0.0.1
|
||||
Nicht angemeldet
|
||||
Traceback (most recent call last):
|
||||
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 867, in full_dispatch_request
|
||||
rv = self.dispatch_request()
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 841, in dispatch_request
|
||||
self.raise_routing_exception(req)
|
||||
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 450, in raise_routing_exception
|
||||
raise request.routing_exception # type: ignore
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/core/.local/lib/python3.11/site-packages/flask/ctx.py", line 353, in match_request
|
||||
result = self.url_adapter.match(return_rule=True) # type: ignore
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/core/.local/lib/python3.11/site-packages/werkzeug/routing/map.py", line 624, in match
|
||||
raise NotFound() from None
|
||||
werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||
|
||||
2025-05-28 21:48:54 | ERROR | SysTades | ERROR | Fehler 404: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||
Endpoint: /static/fonts/inter-regular.woff2, Method: GET, IP: 127.0.0.1
|
||||
User: 1 (admin)
|
||||
Traceback (most recent call last):
|
||||
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 867, in full_dispatch_request
|
||||
rv = self.dispatch_request()
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 852, in dispatch_request
|
||||
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 257, in <lambda>
|
||||
view_func=lambda **kw: self_ref().send_static_file(**kw), # type: ignore # noqa: B950
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 305, in send_static_file
|
||||
return send_from_directory(
|
||||
^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/core/.local/lib/python3.11/site-packages/flask/helpers.py", line 554, in send_from_directory
|
||||
return werkzeug.utils.send_from_directory( # type: ignore[return-value]
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/core/.local/lib/python3.11/site-packages/werkzeug/utils.py", line 574, in send_from_directory
|
||||
raise NotFound()
|
||||
werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||
|
||||
2025-05-28 21:49:17 | ERROR | SysTades | ERROR | Fehler 404: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||
Endpoint: /sw.js, Method: GET, IP: 127.0.0.1
|
||||
Nicht angemeldet
|
||||
Traceback (most recent call last):
|
||||
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 867, in full_dispatch_request
|
||||
rv = self.dispatch_request()
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 841, in dispatch_request
|
||||
self.raise_routing_exception(req)
|
||||
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 450, in raise_routing_exception
|
||||
raise request.routing_exception # type: ignore
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/core/.local/lib/python3.11/site-packages/flask/ctx.py", line 353, in match_request
|
||||
result = self.url_adapter.match(return_rule=True) # type: ignore
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/core/.local/lib/python3.11/site-packages/werkzeug/routing/map.py", line 624, in match
|
||||
raise NotFound() from None
|
||||
werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||
|
||||
2025-05-28 21:55:55 | ERROR | SysTades | ERROR | Fehler 500: name 'follows' is not defined
|
||||
Endpoint: /api/discover/users, Method: GET, IP: 127.0.0.1
|
||||
User: 1 (admin)
|
||||
Traceback (most recent call last):
|
||||
File "/home/core/dev/website/app.py", line 424, in wrapper
|
||||
return func(*args, **kwargs)
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/core/dev/website/app.py", line 3141, in discover_users
|
||||
not_following_subquery = db.session.query(follows.c.followed_id).filter(
|
||||
^^^^^^^
|
||||
NameError: name 'follows' is not defined
|
||||
|
||||
2025-05-28 21:55:55 | ERROR | SysTades | ERROR | Fehler 500: name 'follows' is not defined
|
||||
Endpoint: /api/discover/users, Method: GET, IP: 127.0.0.1
|
||||
User: 1 (admin)
|
||||
Traceback (most recent call last):
|
||||
File "/home/core/dev/website/app.py", line 424, in wrapper
|
||||
return func(*args, **kwargs)
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/core/dev/website/app.py", line 3141, in discover_users
|
||||
not_following_subquery = db.session.query(follows.c.followed_id).filter(
|
||||
^^^^^^^
|
||||
NameError: name 'follows' is not defined
|
||||
|
||||
2025-05-28 21:56:25 | ERROR | SysTades | ERROR | Fehler 404: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||
Endpoint: /auth/login, Method: GET, IP: 127.0.0.1
|
||||
Nicht angemeldet
|
||||
Traceback (most recent call last):
|
||||
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 867, in full_dispatch_request
|
||||
rv = self.dispatch_request()
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 841, in dispatch_request
|
||||
self.raise_routing_exception(req)
|
||||
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 450, in raise_routing_exception
|
||||
raise request.routing_exception # type: ignore
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/core/.local/lib/python3.11/site-packages/flask/ctx.py", line 353, in match_request
|
||||
result = self.url_adapter.match(return_rule=True) # type: ignore
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/core/.local/lib/python3.11/site-packages/werkzeug/routing/map.py", line 624, in match
|
||||
raise NotFound() from None
|
||||
werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||||
|
||||
2025-05-28 21:56:41 | ERROR | SysTades | ERROR | Fehler 500: name 'follows' is not defined
|
||||
Endpoint: /api/discover/users, Method: GET, IP: 127.0.0.1
|
||||
User: 1 (admin)
|
||||
Traceback (most recent call last):
|
||||
File "/home/core/dev/website/app.py", line 424, in wrapper
|
||||
return func(*args, **kwargs)
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/core/dev/website/app.py", line 3141, in discover_users
|
||||
not_following_subquery = db.session.query(follows.c.followed_id).filter(
|
||||
^^^^^^^
|
||||
NameError: name 'follows' is not defined
|
||||
|
||||
2025-05-28 21:57:25 | ERROR | SysTades | ERROR | Fehler 500: name 'follows' is not defined
|
||||
Endpoint: /api/discover/users, Method: GET, IP: 127.0.0.1
|
||||
User: 1 (admin)
|
||||
Traceback (most recent call last):
|
||||
File "/home/core/dev/website/app.py", line 424, in wrapper
|
||||
return func(*args, **kwargs)
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/core/dev/website/app.py", line 3141, in discover_users
|
||||
not_following_subquery = db.session.query(user_follows.c.followed_id).filter(
|
||||
^^^^^^^
|
||||
NameError: name 'follows' is not defined
|
||||
|
||||
2025-05-28 21:58:02 | ERROR | SysTades | ERROR | Fehler 500: name 'follows' is not defined
|
||||
Endpoint: /api/discover/users, Method: GET, IP: 127.0.0.1
|
||||
User: 1 (admin)
|
||||
Traceback (most recent call last):
|
||||
File "/home/core/dev/website/app.py", line 424, in wrapper
|
||||
return func(*args, **kwargs)
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/core/dev/website/app.py", line 3141, in discover_users
|
||||
users = User.query.filter(
|
||||
|
||||
NameError: name 'follows' is not defined
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
Single-database configuration for Flask.
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,50 +0,0 @@
|
||||
# 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
|
||||
@@ -1,113 +0,0 @@
|
||||
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()
|
||||
@@ -1,24 +0,0 @@
|
||||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = ${repr(up_revision)}
|
||||
down_revision = ${repr(down_revision)}
|
||||
branch_labels = ${repr(branch_labels)}
|
||||
depends_on = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade():
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade():
|
||||
${downgrades if downgrades else "pass"}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,38 +0,0 @@
|
||||
"""add mindmap shares table
|
||||
|
||||
Revision ID: add_mindmap_shares
|
||||
Revises: add_missing_user_fields
|
||||
Create Date: 2025-05-10 23:20:00.000000
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import sqlite
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'add_mindmap_shares'
|
||||
down_revision = 'add_missing_user_fields'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# Erstelle PermissionType Enum
|
||||
op.create_table('mindmap_share',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('mindmap_id', sa.Integer(), nullable=False),
|
||||
sa.Column('shared_by_id', sa.Integer(), nullable=False),
|
||||
sa.Column('shared_with_id', sa.Integer(), nullable=False),
|
||||
sa.Column('permission_type', sa.String(20), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('last_accessed', sa.DateTime(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['mindmap_id'], ['user_mindmap.id'], ),
|
||||
sa.ForeignKeyConstraint(['shared_by_id'], ['user.id'], ),
|
||||
sa.ForeignKeyConstraint(['shared_with_id'], ['user.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('mindmap_id', 'shared_with_id', name='unique_mindmap_share')
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_table('mindmap_share')
|
||||
@@ -1,40 +0,0 @@
|
||||
"""Add missing user fields
|
||||
|
||||
Revision ID: 5a23f8c6db37
|
||||
Revises: d4406f5b12f7
|
||||
Create Date: 2025-05-02 10:45:00.000000
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '5a23f8c6db37'
|
||||
down_revision = 'd4406f5b12f7'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('user', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('bio', sa.Text(), nullable=True))
|
||||
batch_op.add_column(sa.Column('location', sa.String(length=100), nullable=True))
|
||||
batch_op.add_column(sa.Column('website', sa.String(length=200), nullable=True))
|
||||
batch_op.add_column(sa.Column('avatar', sa.String(length=200), nullable=True))
|
||||
batch_op.add_column(sa.Column('last_login', sa.DateTime(), nullable=True))
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('user', schema=None) as batch_op:
|
||||
batch_op.drop_column('last_login')
|
||||
batch_op.drop_column('avatar')
|
||||
batch_op.drop_column('website')
|
||||
batch_op.drop_column('location')
|
||||
batch_op.drop_column('bio')
|
||||
|
||||
# ### end Alembic commands ###
|
||||
@@ -1,46 +0,0 @@
|
||||
"""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 ###
|
||||
493
models.py
Normal file → Executable file
493
models.py
Normal file → Executable file
@@ -6,8 +6,6 @@ from flask_login import UserMixin
|
||||
from datetime import datetime
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
from enum import Enum
|
||||
import uuid as uuid_pkg
|
||||
import os
|
||||
|
||||
db = SQLAlchemy()
|
||||
|
||||
@@ -45,157 +43,30 @@ user_thought_bookmark = db.Table('user_thought_bookmark',
|
||||
db.Column('created_at', db.DateTime, default=datetime.utcnow)
|
||||
)
|
||||
|
||||
# Beziehungstabelle für Benutzer-Freundschaften
|
||||
user_friendships = db.Table('user_friendships',
|
||||
db.Column('user_id', db.Integer, db.ForeignKey('user.id'), primary_key=True),
|
||||
db.Column('friend_id', db.Integer, db.ForeignKey('user.id'), primary_key=True),
|
||||
db.Column('created_at', db.DateTime, default=datetime.utcnow),
|
||||
db.Column('status', db.String(20), default='pending') # pending, accepted, blocked
|
||||
)
|
||||
|
||||
# Beziehungstabelle für Benutzer-Follows
|
||||
user_follows = db.Table('user_follows',
|
||||
db.Column('follower_id', db.Integer, db.ForeignKey('user.id'), primary_key=True),
|
||||
db.Column('followed_id', db.Integer, db.ForeignKey('user.id'), primary_key=True),
|
||||
db.Column('created_at', db.DateTime, default=datetime.utcnow)
|
||||
)
|
||||
|
||||
# Beziehungstabelle für Post-Likes
|
||||
post_likes = db.Table('post_likes',
|
||||
db.Column('user_id', db.Integer, db.ForeignKey('user.id'), primary_key=True),
|
||||
db.Column('post_id', db.Integer, db.ForeignKey('social_post.id'), primary_key=True),
|
||||
db.Column('created_at', db.DateTime, default=datetime.utcnow)
|
||||
)
|
||||
|
||||
# Beziehungstabelle für Comment-Likes
|
||||
comment_likes = db.Table('comment_likes',
|
||||
db.Column('user_id', db.Integer, db.ForeignKey('user.id'), primary_key=True),
|
||||
db.Column('comment_id', db.Integer, db.ForeignKey('social_comment.id'), primary_key=True),
|
||||
db.Column('created_at', db.DateTime, default=datetime.utcnow)
|
||||
)
|
||||
|
||||
class User(db.Model, UserMixin):
|
||||
class User(UserMixin, db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
username = db.Column(db.String(80), unique=True, nullable=False)
|
||||
email = db.Column(db.String(120), unique=True, nullable=False)
|
||||
password = db.Column(db.String(512), nullable=False)
|
||||
password_hash = db.Column(db.String(128))
|
||||
is_admin = db.Column(db.Boolean, default=False)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
is_active = db.Column(db.Boolean, default=True)
|
||||
role = db.Column(db.String(20), default="user") # 'user', 'admin', 'moderator'
|
||||
bio = db.Column(db.Text, nullable=True) # Profil-Bio
|
||||
location = db.Column(db.String(100), nullable=True) # Standort
|
||||
website = db.Column(db.String(200), nullable=True) # Website
|
||||
avatar = db.Column(db.String(200), nullable=True) # Profilbild-URL
|
||||
last_login = db.Column(db.DateTime, nullable=True) # Letzter Login
|
||||
|
||||
# Social Network Felder
|
||||
display_name = db.Column(db.String(100), nullable=True) # Anzeigename
|
||||
birth_date = db.Column(db.Date, nullable=True) # Geburtsdatum
|
||||
gender = db.Column(db.String(20), nullable=True) # Geschlecht
|
||||
phone = db.Column(db.String(20), nullable=True) # Telefonnummer
|
||||
is_verified = db.Column(db.Boolean, default=False) # Verifizierter Account
|
||||
is_private = db.Column(db.Boolean, default=False) # Privater Account
|
||||
follower_count = db.Column(db.Integer, default=0) # Follower-Anzahl
|
||||
following_count = db.Column(db.Integer, default=0) # Following-Anzahl
|
||||
post_count = db.Column(db.Integer, default=0) # Post-Anzahl
|
||||
online_status = db.Column(db.String(20), default='offline') # online, offline, away
|
||||
last_seen = db.Column(db.DateTime, nullable=True) # Zuletzt gesehen
|
||||
last_login = db.Column(db.DateTime)
|
||||
avatar = db.Column(db.String(200))
|
||||
bio = db.Column(db.Text)
|
||||
|
||||
# Beziehungen
|
||||
threads = db.relationship('Thread', backref='creator', lazy=True)
|
||||
messages = db.relationship('Message', backref='author', lazy=True)
|
||||
projects = db.relationship('Project', backref='owner', lazy=True)
|
||||
mindmaps = db.relationship('UserMindmap', backref='user', lazy=True)
|
||||
thoughts = db.relationship('Thought', backref='author', lazy=True)
|
||||
comments = db.relationship('Comment', backref='author', lazy=True)
|
||||
user_mindmaps = db.relationship('UserMindmap', backref='user', lazy=True)
|
||||
mindmap_notes = db.relationship('MindmapNote', backref='user', lazy=True)
|
||||
bookmarked_thoughts = db.relationship('Thought', secondary=user_thought_bookmark,
|
||||
lazy='dynamic', backref=db.backref('bookmarked_by', lazy='dynamic'))
|
||||
backref=db.backref('bookmarked_by', lazy='dynamic'))
|
||||
|
||||
# Social Network Beziehungen
|
||||
posts = db.relationship('SocialPost', backref='author', lazy=True, cascade="all, delete-orphan")
|
||||
comments = db.relationship('SocialComment', backref='author', lazy=True, cascade="all, delete-orphan")
|
||||
notifications = db.relationship('Notification', foreign_keys='Notification.user_id', backref='user', lazy=True, cascade="all, delete-orphan")
|
||||
|
||||
# Freundschaften (bidirektional)
|
||||
friends = db.relationship(
|
||||
'User',
|
||||
secondary=user_friendships,
|
||||
primaryjoin=id == user_friendships.c.user_id,
|
||||
secondaryjoin=id == user_friendships.c.friend_id,
|
||||
backref='friend_of',
|
||||
lazy='dynamic'
|
||||
)
|
||||
|
||||
# Following/Followers
|
||||
following = db.relationship(
|
||||
'User',
|
||||
secondary=user_follows,
|
||||
primaryjoin=id == user_follows.c.follower_id,
|
||||
secondaryjoin=id == user_follows.c.followed_id,
|
||||
backref=db.backref('followers', lazy='dynamic'),
|
||||
lazy='dynamic'
|
||||
)
|
||||
|
||||
# Liked Posts und Comments
|
||||
liked_posts = db.relationship('SocialPost', secondary=post_likes,
|
||||
backref=db.backref('liked_by', lazy='dynamic'), lazy='dynamic')
|
||||
liked_comments = db.relationship('SocialComment', secondary=comment_likes,
|
||||
backref=db.backref('liked_by', lazy='dynamic'), lazy='dynamic')
|
||||
|
||||
def __repr__(self):
|
||||
return f'<User {self.username}>'
|
||||
|
||||
def set_password(self, password):
|
||||
self.password = generate_password_hash(password)
|
||||
self.password_hash = generate_password_hash(password)
|
||||
|
||||
def check_password(self, password):
|
||||
return check_password_hash(self.password, password)
|
||||
|
||||
@property
|
||||
def is_admin(self):
|
||||
return self.role == 'admin'
|
||||
|
||||
@is_admin.setter
|
||||
def is_admin(self, value):
|
||||
self.role = 'admin' if value else 'user'
|
||||
|
||||
# Social Network Methoden
|
||||
def follow(self, user):
|
||||
"""Folgt einem anderen Benutzer"""
|
||||
if not self.is_following(user):
|
||||
self.following.append(user)
|
||||
user.follower_count += 1
|
||||
user.following_count += 1
|
||||
|
||||
# Notification erstellen
|
||||
notification = Notification(
|
||||
user_id=user.id,
|
||||
type='follow',
|
||||
message=f'{self.username} folgt dir jetzt',
|
||||
related_user_id=self.id
|
||||
)
|
||||
db.session.add(notification)
|
||||
|
||||
def unfollow(self, user):
|
||||
"""Entfolgt einem Benutzer"""
|
||||
if self.is_following(user):
|
||||
self.following.remove(user)
|
||||
user.follower_count -= 1
|
||||
user.following_count -= 1
|
||||
|
||||
def is_following(self, user):
|
||||
"""Prüft ob der Benutzer einem anderen folgt"""
|
||||
return self.following.filter(user_follows.c.followed_id == user.id).count() > 0
|
||||
|
||||
def get_feed_posts(self, limit=20):
|
||||
"""Holt Posts für den Feed (von gefolgten Benutzern)"""
|
||||
# Hole alle User-IDs von Benutzern, denen ich folge + meine eigene
|
||||
followed_user_ids = [user.id for user in self.following]
|
||||
all_user_ids = followed_user_ids + [self.id]
|
||||
|
||||
# Hole Posts von diesen Benutzern
|
||||
return SocialPost.query.filter(
|
||||
SocialPost.user_id.in_(all_user_ids)
|
||||
).order_by(SocialPost.created_at.desc()).limit(limit)
|
||||
return check_password_hash(self.password_hash, password)
|
||||
|
||||
class Category(db.Model):
|
||||
"""Wissenschaftliche Kategorien für die Gliederung der öffentlichen Mindmap"""
|
||||
@@ -210,9 +81,6 @@ class Category(db.Model):
|
||||
children = db.relationship('Category', backref=db.backref('parent', remote_side=[id]))
|
||||
nodes = db.relationship('MindMapNode', backref='category', lazy=True)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Category {self.name}>'
|
||||
|
||||
class MindMapNode(db.Model):
|
||||
"""Öffentliche Mindmap-Knoten, die für alle Benutzer sichtbar sind"""
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
@@ -224,9 +92,7 @@ class MindMapNode(db.Model):
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
created_by_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True)
|
||||
category_id = db.Column(db.Integer, db.ForeignKey('category.id'), nullable=True)
|
||||
|
||||
__table_args__ = {'extend_existing': True}
|
||||
|
||||
|
||||
# Beziehungen für Baumstruktur (mehrere Eltern möglich)
|
||||
parents = db.relationship(
|
||||
'MindMapNode',
|
||||
@@ -245,20 +111,6 @@ class MindMapNode(db.Model):
|
||||
# Beziehung zum Ersteller
|
||||
created_by = db.relationship('User', backref='created_nodes')
|
||||
|
||||
def __repr__(self):
|
||||
return f'<MindMapNode {self.name}>'
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'name': self.name,
|
||||
'description': self.description,
|
||||
'color_code': self.color_code,
|
||||
'icon': self.icon,
|
||||
'category_id': self.category_id,
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None
|
||||
}
|
||||
|
||||
class UserMindmap(db.Model):
|
||||
"""Benutzerspezifische Mindmap, die vom Benutzer personalisierbar ist"""
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
@@ -375,321 +227,4 @@ class Comment(db.Model):
|
||||
thought_id = db.Column(db.Integer, db.ForeignKey('thought.id'), nullable=False)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
last_modified = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
# Thread model
|
||||
class Thread(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
title = db.Column(db.String(200), nullable=False)
|
||||
description = db.Column(db.Text)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||
|
||||
# Relationships
|
||||
messages = db.relationship('Message', backref='thread', lazy=True, cascade="all, delete-orphan")
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Thread {self.title}>'
|
||||
|
||||
# Message model
|
||||
class Message(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
content = db.Column(db.Text, nullable=False)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||
thread_id = db.Column(db.Integer, db.ForeignKey('thread.id'), nullable=False)
|
||||
role = db.Column(db.String(20), default="user") # 'user', 'assistant', 'system'
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Message {self.id} by {self.user_id}>'
|
||||
|
||||
# Project model
|
||||
class Project(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
title = db.Column(db.String(150), nullable=False)
|
||||
description = db.Column(db.Text)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||
category_id = db.Column(db.Integer, db.ForeignKey('category.id'))
|
||||
|
||||
# Relationships
|
||||
documents = db.relationship('Document', backref='project', lazy=True, cascade="all, delete-orphan")
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Project {self.title}>'
|
||||
|
||||
# Document model
|
||||
class Document(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
title = db.Column(db.String(150), nullable=False)
|
||||
content = db.Column(db.Text)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
project_id = db.Column(db.Integer, db.ForeignKey('project.id'), nullable=False)
|
||||
filename = db.Column(db.String(150), nullable=True)
|
||||
file_path = db.Column(db.String(300), nullable=True)
|
||||
file_type = db.Column(db.String(50), nullable=True)
|
||||
file_size = db.Column(db.Integer, nullable=True)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Document {self.title}>'
|
||||
|
||||
# Forum-Kategorie-Modell - entspricht den Hauptknotenpunkten der Mindmap
|
||||
class ForumCategory(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
node_id = db.Column(db.Integer, db.ForeignKey('mind_map_node.id'), nullable=False)
|
||||
title = db.Column(db.String(200), nullable=False)
|
||||
description = db.Column(db.Text)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
is_active = db.Column(db.Boolean, default=True)
|
||||
|
||||
# Beziehungen
|
||||
node = db.relationship('MindMapNode', backref='forum_category')
|
||||
posts = db.relationship('ForumPost', backref='category', lazy=True, cascade="all, delete-orphan")
|
||||
|
||||
def __repr__(self):
|
||||
return f'<ForumCategory {self.title}>'
|
||||
|
||||
# Forum-Beitrag-Modell
|
||||
class ForumPost(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
title = db.Column(db.String(200), nullable=False)
|
||||
content = db.Column(db.Text, nullable=False)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||
category_id = db.Column(db.Integer, db.ForeignKey('forum_category.id'), nullable=False)
|
||||
parent_id = db.Column(db.Integer, db.ForeignKey('forum_post.id'), nullable=True)
|
||||
is_pinned = db.Column(db.Boolean, default=False)
|
||||
is_locked = db.Column(db.Boolean, default=False)
|
||||
view_count = db.Column(db.Integer, default=0)
|
||||
|
||||
# Beziehungen
|
||||
author = db.relationship('User', backref='forum_posts')
|
||||
replies = db.relationship('ForumPost', backref=db.backref('parent', remote_side=[id]), lazy=True)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<ForumPost {self.title}>'
|
||||
|
||||
# Berechtigungstypen für Mindmap-Freigaben
|
||||
class PermissionType(Enum):
|
||||
READ = "Nur-Lesen"
|
||||
EDIT = "Bearbeiten"
|
||||
ADMIN = "Administrator"
|
||||
|
||||
# Freigabemodell für Mindmaps
|
||||
class MindmapShare(db.Model):
|
||||
"""Speichert Informationen über freigegebene Mindmaps und Berechtigungen"""
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
mindmap_id = db.Column(db.Integer, db.ForeignKey('user_mindmap.id'), nullable=False)
|
||||
shared_by_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||
shared_with_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||
permission_type = db.Column(db.Enum(PermissionType), nullable=False, default=PermissionType.READ)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
last_accessed = db.Column(db.DateTime, nullable=True)
|
||||
|
||||
# Beziehungen
|
||||
mindmap = db.relationship('UserMindmap', backref=db.backref('shares', lazy='dynamic'))
|
||||
shared_by = db.relationship('User', foreign_keys=[shared_by_id], backref=db.backref('shared_mindmaps', lazy='dynamic'))
|
||||
shared_with = db.relationship('User', foreign_keys=[shared_with_id], backref=db.backref('accessible_mindmaps', lazy='dynamic'))
|
||||
|
||||
__table_args__ = (
|
||||
db.UniqueConstraint('mindmap_id', 'shared_with_id', name='unique_mindmap_share'),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<MindmapShare: {self.mindmap_id} - {self.shared_with_id} - {self.permission_type.name}>'
|
||||
|
||||
class SocialPost(db.Model):
|
||||
"""Posts im Social Network"""
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
content = db.Column(db.Text, nullable=False)
|
||||
image_url = db.Column(db.String(500), nullable=True) # Bild-URL
|
||||
video_url = db.Column(db.String(500), nullable=True) # Video-URL
|
||||
link_url = db.Column(db.String(500), nullable=True) # Link-URL
|
||||
link_title = db.Column(db.String(200), nullable=True) # Link-Titel
|
||||
link_description = db.Column(db.Text, nullable=True) # Link-Beschreibung
|
||||
post_type = db.Column(db.String(20), default='text') # text, image, video, link, thought_share
|
||||
visibility = db.Column(db.String(20), default='public') # public, friends, private
|
||||
is_pinned = db.Column(db.Boolean, default=False)
|
||||
like_count = db.Column(db.Integer, default=0)
|
||||
comment_count = db.Column(db.Integer, default=0)
|
||||
share_count = db.Column(db.Integer, default=0)
|
||||
view_count = db.Column(db.Integer, default=0)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||
|
||||
# Verknüpfung zu Gedanken (falls der Post einen Gedanken teilt)
|
||||
shared_thought_id = db.Column(db.Integer, db.ForeignKey('thought.id'), nullable=True)
|
||||
shared_thought = db.relationship('Thought', backref='shared_in_posts')
|
||||
|
||||
# Verknüpfung zu Mindmap-Knoten
|
||||
shared_node_id = db.Column(db.Integer, db.ForeignKey('mind_map_node.id'), nullable=True)
|
||||
shared_node = db.relationship('MindMapNode', backref='shared_in_posts')
|
||||
|
||||
# Kommentare zu diesem Post
|
||||
comments = db.relationship('SocialComment', backref='post', lazy=True, cascade="all, delete-orphan")
|
||||
|
||||
def __repr__(self):
|
||||
return f'<SocialPost {self.id} by {self.author.username}>'
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'content': self.content,
|
||||
'post_type': self.post_type,
|
||||
'image_url': self.image_url,
|
||||
'video_url': self.video_url,
|
||||
'link_url': self.link_url,
|
||||
'link_title': self.link_title,
|
||||
'link_description': self.link_description,
|
||||
'visibility': self.visibility,
|
||||
'is_pinned': self.is_pinned,
|
||||
'like_count': self.like_count,
|
||||
'comment_count': self.comment_count,
|
||||
'share_count': self.share_count,
|
||||
'view_count': self.view_count,
|
||||
'created_at': self.created_at.isoformat(),
|
||||
'updated_at': self.updated_at.isoformat(),
|
||||
'author': {
|
||||
'id': self.author.id,
|
||||
'username': self.author.username,
|
||||
'display_name': self.author.display_name or self.author.username,
|
||||
'avatar': self.author.avatar,
|
||||
'is_verified': self.author.is_verified
|
||||
},
|
||||
'shared_thought': self.shared_thought.to_dict() if self.shared_thought else None,
|
||||
'shared_node': self.shared_node.to_dict() if self.shared_node else None
|
||||
}
|
||||
|
||||
class SocialComment(db.Model):
|
||||
"""Kommentare zu Posts"""
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
content = db.Column(db.Text, nullable=False)
|
||||
like_count = db.Column(db.Integer, default=0)
|
||||
reply_count = db.Column(db.Integer, default=0)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||
post_id = db.Column(db.Integer, db.ForeignKey('social_post.id'), nullable=False)
|
||||
parent_id = db.Column(db.Integer, db.ForeignKey('social_comment.id'), nullable=True)
|
||||
|
||||
# Antworten auf diesen Kommentar
|
||||
replies = db.relationship('SocialComment', backref=db.backref('parent', remote_side=[id]), lazy=True)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<SocialComment {self.id} by {self.author.username}>'
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'content': self.content,
|
||||
'like_count': self.like_count,
|
||||
'reply_count': self.reply_count,
|
||||
'created_at': self.created_at.isoformat(),
|
||||
'updated_at': self.updated_at.isoformat(),
|
||||
'author': {
|
||||
'id': self.author.id,
|
||||
'username': self.author.username,
|
||||
'display_name': self.author.display_name or self.author.username,
|
||||
'avatar': self.author.avatar,
|
||||
'is_verified': self.author.is_verified
|
||||
},
|
||||
'parent_id': self.parent_id
|
||||
}
|
||||
|
||||
class Notification(db.Model):
|
||||
"""Benachrichtigungen für Benutzer"""
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
type = db.Column(db.String(50), nullable=False) # follow, like, comment, mention, friend_request, etc.
|
||||
message = db.Column(db.String(500), nullable=False)
|
||||
is_read = db.Column(db.Boolean, default=False)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||
|
||||
# Verknüpfungen zu anderen Entitäten
|
||||
related_user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True)
|
||||
related_post_id = db.Column(db.Integer, db.ForeignKey('social_post.id'), nullable=True)
|
||||
related_comment_id = db.Column(db.Integer, db.ForeignKey('social_comment.id'), nullable=True)
|
||||
related_thought_id = db.Column(db.Integer, db.ForeignKey('thought.id'), nullable=True)
|
||||
|
||||
# Beziehungen
|
||||
related_user = db.relationship('User', foreign_keys=[related_user_id])
|
||||
related_post = db.relationship('SocialPost', foreign_keys=[related_post_id])
|
||||
related_comment = db.relationship('SocialComment', foreign_keys=[related_comment_id])
|
||||
related_thought = db.relationship('Thought', foreign_keys=[related_thought_id])
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Notification {self.id} for {self.user.username}>'
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'type': self.type,
|
||||
'message': self.message,
|
||||
'is_read': self.is_read,
|
||||
'created_at': self.created_at.isoformat(),
|
||||
'related_user': self.related_user.username if self.related_user else None,
|
||||
'related_post_id': self.related_post_id,
|
||||
'related_comment_id': self.related_comment_id,
|
||||
'related_thought_id': self.related_thought_id
|
||||
}
|
||||
|
||||
class UserSettings(db.Model):
|
||||
"""Benutzereinstellungen"""
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False, unique=True)
|
||||
|
||||
# Datenschutz-Einstellungen
|
||||
profile_visibility = db.Column(db.String(20), default='public') # public, friends, private
|
||||
show_email = db.Column(db.Boolean, default=False)
|
||||
show_birth_date = db.Column(db.Boolean, default=False)
|
||||
show_location = db.Column(db.Boolean, default=True)
|
||||
allow_friend_requests = db.Column(db.Boolean, default=True)
|
||||
allow_messages = db.Column(db.String(20), default='everyone') # everyone, friends, none
|
||||
|
||||
# Benachrichtigungs-Einstellungen
|
||||
email_notifications = db.Column(db.Boolean, default=True)
|
||||
push_notifications = db.Column(db.Boolean, default=True)
|
||||
notify_on_follow = db.Column(db.Boolean, default=True)
|
||||
notify_on_like = db.Column(db.Boolean, default=True)
|
||||
notify_on_comment = db.Column(db.Boolean, default=True)
|
||||
notify_on_mention = db.Column(db.Boolean, default=True)
|
||||
notify_on_friend_request = db.Column(db.Boolean, default=True)
|
||||
|
||||
# Interface-Einstellungen
|
||||
dark_mode = db.Column(db.Boolean, default=False)
|
||||
language = db.Column(db.String(10), default='de')
|
||||
|
||||
# Beziehung
|
||||
user = db.relationship('User', backref=db.backref('settings', uselist=False))
|
||||
|
||||
def __repr__(self):
|
||||
return f'<UserSettings for {self.user.username}>'
|
||||
|
||||
class Activity(db.Model):
|
||||
"""Aktivitätsprotokoll für Benutzer"""
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||
action = db.Column(db.String(100), nullable=False) # login, logout, post_created, thought_shared, etc.
|
||||
description = db.Column(db.String(500), nullable=True)
|
||||
ip_address = db.Column(db.String(45), nullable=True) # IPv4/IPv6
|
||||
user_agent = db.Column(db.String(500), nullable=True)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
# Verknüpfungen zu anderen Entitäten
|
||||
related_post_id = db.Column(db.Integer, db.ForeignKey('social_post.id'), nullable=True)
|
||||
related_thought_id = db.Column(db.Integer, db.ForeignKey('thought.id'), nullable=True)
|
||||
related_mindmap_id = db.Column(db.Integer, db.ForeignKey('user_mindmap.id'), nullable=True)
|
||||
|
||||
# Beziehungen
|
||||
user = db.relationship('User', backref='activities')
|
||||
related_post = db.relationship('SocialPost')
|
||||
related_thought = db.relationship('Thought')
|
||||
related_mindmap = db.relationship('UserMindmap')
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Activity {self.action} by {self.user.username}>'
|
||||
last_modified = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
@@ -5,13 +5,10 @@ email-validator
|
||||
python-dotenv
|
||||
werkzeug==2.2.3
|
||||
flask-sqlalchemy==3.0.5
|
||||
openai
|
||||
openai==1.3.0
|
||||
requests==2.31.0
|
||||
flask-cors==4.0.0
|
||||
gunicorn==21.2.0
|
||||
#pillow==10.0.1
|
||||
pytest==7.4.0
|
||||
pytest-flask==1.2.0
|
||||
Flask-Migrate
|
||||
flask-socketio==5.3.6
|
||||
python-engineio==4.8.2
|
||||
python-socketio==5.11.1
|
||||
pytest-flask==1.2.0
|
||||
22
server.log
22
server.log
@@ -1,22 +0,0 @@
|
||||
[2m⏰ 21:58:48.486[0m [2m│[0m [92m✅ INFO [0m [2m│[0m [33m⚙️ [SYSTEM ][0m [2m│[0m 📝 🚀 SysTades Social Network gestartet
|
||||
[2m⏰ 21:58:48.486[0m [2m│[0m [92m✅ INFO [0m [2m│[0m [33m⚙️ [SYSTEM ][0m [2m│[0m 📝 🚀 SysTades Social Network gestartet (v1.0.0) in development Umgebung auf Port 5000
|
||||
[2m⏰ 21:58:49.951[0m [2m│[0m [92m✅ INFO [0m [2m│[0m [33m⚙️ [SYSTEM ][0m [2m│[0m 📝 OpenAI API-Verbindung erfolgreich hergestellt
|
||||
[2m⏰ 21:58:50.122[0m [2m│[0m [92m✅ INFO [0m [2m│[0m [35m🗄️ [DB ][0m [2m│[0m 🚫 Datenbank erfolgreich initialisiert
|
||||
[2m⏰ 21:58:50.132[0m [2m│[0m [92m✅ INFO [0m [2m│[0m [35m🗄️ [DB ][0m [2m│[0m 🚫 Datenbanktabellen erstellt/aktualisiert
|
||||
[2m⏰ 21:58:50.134[0m [2m│[0m [92m✅ INFO [0m [2m│[0m [33m⚙️ [SYSTEM ][0m [2m│[0m 📝 Starte Flask-Entwicklungsserver auf http://localhost:5000
|
||||
* Serving Flask app 'app'
|
||||
* Debug mode: on
|
||||
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
|
||||
* Running on all addresses (0.0.0.0)
|
||||
* Running on http://127.0.0.1:5000
|
||||
* Running on http://127.0.0.1:5000
|
||||
Press CTRL+C to quit
|
||||
* Restarting with watchdog (inotify)
|
||||
[2m⏰ 21:58:52.225[0m [2m│[0m [92m✅ INFO [0m [2m│[0m [33m⚙️ [SYSTEM ][0m [2m│[0m 📝 🚀 SysTades Social Network gestartet
|
||||
[2m⏰ 21:58:52.226[0m [2m│[0m [92m✅ INFO [0m [2m│[0m [33m⚙️ [SYSTEM ][0m [2m│[0m 📝 🚀 SysTades Social Network gestartet (v1.0.0) in development Umgebung auf Port 5000
|
||||
[2m⏰ 21:58:53.848[0m [2m│[0m [92m✅ INFO [0m [2m│[0m [33m⚙️ [SYSTEM ][0m [2m│[0m 📝 OpenAI API-Verbindung erfolgreich hergestellt
|
||||
[2m⏰ 21:58:53.997[0m [2m│[0m [92m✅ INFO [0m [2m│[0m [35m🗄️ [DB ][0m [2m│[0m 🚫 Datenbank erfolgreich initialisiert
|
||||
[2m⏰ 21:58:54.002[0m [2m│[0m [92m✅ INFO [0m [2m│[0m [35m🗄️ [DB ][0m [2m│[0m 🚫 Datenbanktabellen erstellt/aktualisiert
|
||||
[2m⏰ 21:58:54.006[0m [2m│[0m [92m✅ INFO [0m [2m│[0m [33m⚙️ [SYSTEM ][0m [2m│[0m 📝 Starte Flask-Entwicklungsserver auf http://localhost:5000
|
||||
* Debugger is active!
|
||||
* Debugger PIN: 114-005-893
|
||||
53
start.sh
53
start.sh
@@ -1,53 +0,0 @@
|
||||
#!/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 +0,0 @@
|
||||
|
||||
108
static/background.js
Normal file
108
static/background.js
Normal file
@@ -0,0 +1,108 @@
|
||||
// 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
static/background.mp4
Normal file
1
static/background.mp4
Normal file
@@ -0,0 +1 @@
|
||||
C:\Users\firem\Downloads\background.mp4
|
||||
27
static/css/all.min.css
vendored
27
static/css/all.min.css
vendored
@@ -1,27 +0,0 @@
|
||||
/*
|
||||
* 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,37 +1,26 @@
|
||||
/* ChatGPT Assistent Styles - Verbesserte Version */
|
||||
/* ChatGPT Assistent Styles */
|
||||
#chatgpt-assistant {
|
||||
font-family: 'Inter', sans-serif;
|
||||
bottom: 5.5rem;
|
||||
z-index: 100;
|
||||
max-height: 85vh;
|
||||
}
|
||||
|
||||
#assistant-chat {
|
||||
transition: max-height 0.4s cubic-bezier(0.25, 0.1, 0.25, 1),
|
||||
opacity 0.3s cubic-bezier(0.25, 0.1, 0.25, 1);
|
||||
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.25);
|
||||
transition: max-height 0.3s ease, opacity 0.3s ease;
|
||||
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.2);
|
||||
border-radius: 0.75rem;
|
||||
overflow: hidden;
|
||||
max-width: calc(100vw - 2rem);
|
||||
max-height: 80vh !important;
|
||||
}
|
||||
|
||||
#assistant-history {
|
||||
max-height: calc(80vh - 150px);
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(156, 163, 175, 0.5) transparent;
|
||||
padding-bottom: 2rem; /* Zusätzlicher Abstand unten */
|
||||
}
|
||||
|
||||
#assistant-toggle {
|
||||
transition: transform 0.3s ease, background-color 0.2s ease;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
z-index: 60;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
#assistant-toggle:hover {
|
||||
transform: scale(1.1) rotate(10deg);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
#assistant-history {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(156, 163, 175, 0.5) transparent;
|
||||
}
|
||||
|
||||
#assistant-history::-webkit-scrollbar {
|
||||
@@ -51,74 +40,27 @@
|
||||
background-color: rgba(156, 163, 175, 0.3);
|
||||
}
|
||||
|
||||
/* Verbesserte Message-Bubbles mit Schatten und Animation */
|
||||
#assistant-history .flex > div {
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
|
||||
animation: messageAppear 0.3s ease-out forwards;
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
@keyframes messageAppear {
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Verzögerte Animation für Messages */
|
||||
#assistant-history .flex:nth-child(1) > div { animation-delay: 0.05s; }
|
||||
#assistant-history .flex:nth-child(2) > div { animation-delay: 0.1s; }
|
||||
#assistant-history .flex:nth-child(3) > div { animation-delay: 0.15s; }
|
||||
#assistant-history .flex:nth-child(4) > div { animation-delay: 0.2s; }
|
||||
#assistant-history .flex:nth-child(5) > div { animation-delay: 0.25s; }
|
||||
|
||||
/* Vorschläge styling */
|
||||
#assistant-suggestions {
|
||||
padding: 0.5rem 0.75rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.suggestion-pill {
|
||||
animation: pillAppear 0.4s ease forwards;
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
@keyframes pillAppear {
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Styling für verschiedene Verzögerungen bei Vorschlägen */
|
||||
#assistant-suggestions button:nth-child(1) { animation-delay: 0.1s; }
|
||||
#assistant-suggestions button:nth-child(2) { animation-delay: 0.2s; }
|
||||
#assistant-suggestions button:nth-child(3) { animation-delay: 0.3s; }
|
||||
|
||||
/* Mach Platz für Notifications, damit sie nicht mit dem Assistenten überlappen */
|
||||
.notification-area {
|
||||
bottom: 5rem;
|
||||
}
|
||||
|
||||
/* Verbesserte Glassmorphism-Effekt */
|
||||
/* Verbesserter Glassmorphism-Effekt */
|
||||
.glass-morphism {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.15);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
|
||||
.dark .glass-morphism {
|
||||
background: rgba(15, 23, 42, 0.35);
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
background: rgba(15, 23, 42, 0.3);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
/* Verbesserte Farbpalette für Dark Theme */
|
||||
/* Dunkleres Dark Theme */
|
||||
.dark {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgba(10, 15, 25, var(--tw-bg-opacity)) !important;
|
||||
@@ -140,62 +82,6 @@
|
||||
background-color: rgba(23, 33, 64, var(--tw-bg-opacity)) !important;
|
||||
}
|
||||
|
||||
/* Typing Indicator Animation Styles */
|
||||
.typing-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.typing-indicator span {
|
||||
height: 8px;
|
||||
width: 8px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin: 0 2px;
|
||||
opacity: 0.6;
|
||||
animation: bounce 1.4s infinite ease-in-out;
|
||||
}
|
||||
|
||||
body.dark .typing-indicator span {
|
||||
background-color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
body:not(.dark) .typing-indicator span {
|
||||
background-color: rgba(107, 114, 128, 0.8);
|
||||
}
|
||||
|
||||
.typing-indicator span:nth-child(1) { animation-delay: 0s; }
|
||||
.typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
|
||||
.typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 80%, 100% { transform: translateY(0); }
|
||||
40% { transform: translateY(-8px); }
|
||||
}
|
||||
|
||||
/* Chat Input Fokus-Effekt */
|
||||
#assistant-chat input:focus {
|
||||
border-color: var(--primary-500, #3B82F6);
|
||||
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.25);
|
||||
}
|
||||
|
||||
.dark #assistant-chat input:focus {
|
||||
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.4);
|
||||
}
|
||||
|
||||
/* Verbesserte Responsive Layouts */
|
||||
@media (max-width: 640px) {
|
||||
#assistant-chat {
|
||||
width: calc(100vw - 2rem) !important;
|
||||
max-height: 65vh !important;
|
||||
}
|
||||
|
||||
#chatgpt-assistant {
|
||||
right: 1rem;
|
||||
bottom: 6rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Footer immer unten */
|
||||
html, body {
|
||||
height: 100%;
|
||||
@@ -215,38 +101,4 @@ main {
|
||||
|
||||
footer {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Verbesserte Farbkontraste für Nachrichtenblasen */
|
||||
.user-message {
|
||||
background-color: rgba(124, 58, 237, 0.1) !important;
|
||||
color: #4B5563 !important;
|
||||
}
|
||||
|
||||
body.dark .user-message {
|
||||
background-color: rgba(124, 58, 237, 0.2) !important;
|
||||
color: #F9FAFB !important;
|
||||
}
|
||||
|
||||
.assistant-message {
|
||||
background-color: #F3F4F6 !important;
|
||||
color: #1F2937 !important;
|
||||
border-left: 3px solid #8B5CF6;
|
||||
}
|
||||
|
||||
body.dark .assistant-message {
|
||||
background-color: rgba(31, 41, 55, 0.5) !important;
|
||||
color: #F9FAFB !important;
|
||||
border-left: 3px solid #8B5CF6;
|
||||
}
|
||||
|
||||
/* Chat-Assistent-Position im Footer-Bereich anpassen */
|
||||
.chat-assistant {
|
||||
max-height: 75vh;
|
||||
bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.chat-assistant .chat-messages {
|
||||
max-height: calc(75vh - 180px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,437 +0,0 @@
|
||||
/* Mindmap Container Styles */
|
||||
.mindmap-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 600px;
|
||||
background: var(--bg-primary);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Cytoscape Container für die Hauptmindmap */
|
||||
#cy {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: transparent;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Subpage Styles - Identisches Design wie Hauptmindmap */
|
||||
.mindmap-subpage {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--bg-primary);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 10;
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Subpage Header */
|
||||
.subpage-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
background: var(--bg-secondary);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.dark .subpage-header {
|
||||
background: rgba(30, 41, 59, 0.8);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* Zurück-Button */
|
||||
.back-button {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: none;
|
||||
color: white;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.back-button:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Subpage Titel */
|
||||
.subpage-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
margin: 0;
|
||||
background: linear-gradient(90deg, #60a5fa, #8b5cf6);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
/* Subpage Cytoscape Container */
|
||||
.subpage-cy-container {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
height: calc(100% - 72px);
|
||||
overflow: hidden;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Toolbar für Zoom-Kontrollen */
|
||||
.mindmap-toolbar {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
background: rgba(30, 41, 59, 0.8);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
|
||||
z-index: 20;
|
||||
backdrop-filter: blur(8px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.mindmap-toolbar button {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: none;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: white;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.mindmap-toolbar button:hover {
|
||||
background: rgba(139, 92, 246, 0.5);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.mindmap-toolbar button i {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* Mindmap Header */
|
||||
.mindmap-header {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 1.5rem;
|
||||
background: rgba(15, 23, 42, 0.8);
|
||||
backdrop-filter: blur(10px);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Dark Mode spezifische Stile */
|
||||
.dark .mindmap-subpage {
|
||||
background: linear-gradient(135deg, #0f172a 0%, #0c1221 100%);
|
||||
}
|
||||
|
||||
/* Fix für Zoom-Buttons */
|
||||
body.dark .mindmap-toolbar button {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
body:not(.dark) .mindmap-toolbar button {
|
||||
background: rgba(30, 41, 59, 0.2);
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
/* Kontext-Menü-Anpassungen */
|
||||
.context-menu {
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
/* Export Group Styles */
|
||||
.export-group {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.export-options {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
margin-top: 8px;
|
||||
padding: 8px;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 160px;
|
||||
}
|
||||
|
||||
.dark .export-options {
|
||||
background: rgba(30, 41, 59, 0.9);
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.export-group:hover .export-options {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.export-options button {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
padding: 8px 12px;
|
||||
justify-content: flex-start;
|
||||
font-size: 14px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/* Context Menu Styles */
|
||||
.mindmap-context-menu {
|
||||
position: fixed;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
padding: 8px;
|
||||
z-index: 1000;
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.dark .mindmap-context-menu {
|
||||
background: rgba(30, 41, 59, 0.9);
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.mindmap-context-menu button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.mindmap-context-menu button:hover {
|
||||
background: var(--accent-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.mindmap-context-menu button i {
|
||||
margin-right: 8px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
/* Node Styles */
|
||||
.mindmap-node {
|
||||
background-color: var(--bg-secondary);
|
||||
border: 2px solid var(--accent-primary);
|
||||
border-radius: 8px;
|
||||
padding: 8px 12px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.mindmap-node:hover {
|
||||
box-shadow: 0 0 0 2px var(--accent-primary);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.mindmap-node.selected {
|
||||
border-color: var(--accent-secondary);
|
||||
box-shadow: 0 0 0 3px var(--accent-secondary);
|
||||
}
|
||||
|
||||
/* Edge Styles */
|
||||
.mindmap-edge {
|
||||
width: 2px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.dark .mindmap-edge {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.mindmap-edge:hover {
|
||||
width: 3px;
|
||||
background-color: var(--accent-primary);
|
||||
}
|
||||
|
||||
/* Animation Styles */
|
||||
@keyframes nodeAppear {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.mindmap-node-new {
|
||||
animation: nodeAppear 0.3s ease forwards;
|
||||
}
|
||||
|
||||
/* Responsive Styles */
|
||||
@media (max-width: 768px) {
|
||||
.mindmap-toolbar {
|
||||
flex-wrap: wrap;
|
||||
width: calc(100% - 32px);
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.export-options {
|
||||
left: 0;
|
||||
right: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* Loading State */
|
||||
.mindmap-loading {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.mindmap-loading-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid var(--bg-secondary);
|
||||
border-top-color: var(--accent-primary);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Tooltip Styles */
|
||||
.mindmap-tooltip {
|
||||
position: absolute;
|
||||
background: var(--bg-secondary);
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
pointer-events: none;
|
||||
z-index: 1000;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
max-width: 200px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.dark .mindmap-tooltip {
|
||||
background: rgba(30, 41, 59, 0.9);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* Kategorien-Panel */
|
||||
.categories-panel {
|
||||
position: absolute;
|
||||
top: 80px;
|
||||
left: 20px;
|
||||
width: 300px;
|
||||
max-height: calc(100vh - 120px);
|
||||
background: rgba(15, 23, 42, 0.95);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
z-index: 1000;
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
transform: translateX(-320px);
|
||||
transition: transform 0.3s ease-in-out;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.categories-panel.visible {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.categories-panel h3 {
|
||||
color: white;
|
||||
font-size: 1.2rem;
|
||||
margin: 0 0 16px 0;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.category-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.category-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
margin: 4px 0;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.category-item:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.category-color {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
margin-right: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.category-name {
|
||||
flex-grow: 1;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.category-count {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.8rem;
|
||||
margin-left: 8px;
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
/* 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,915 +0,0 @@
|
||||
/* ================================
|
||||
SysTades Social Network Styles
|
||||
================================ */
|
||||
|
||||
:root {
|
||||
/* Primary Colors */
|
||||
--primary-50: #f0f9ff;
|
||||
--primary-100: #e0f2fe;
|
||||
--primary-200: #bae6fd;
|
||||
--primary-300: #7dd3fc;
|
||||
--primary-400: #38bdf8;
|
||||
--primary-500: #0ea5e9;
|
||||
--primary-600: #0284c7;
|
||||
--primary-700: #0369a1;
|
||||
--primary-800: #075985;
|
||||
--primary-900: #0c4a6e;
|
||||
|
||||
/* Neutral Colors */
|
||||
--gray-50: #f9fafb;
|
||||
--gray-100: #f3f4f6;
|
||||
--gray-200: #e5e7eb;
|
||||
--gray-300: #d1d5db;
|
||||
--gray-400: #9ca3af;
|
||||
--gray-500: #6b7280;
|
||||
--gray-600: #4b5563;
|
||||
--gray-700: #374151;
|
||||
--gray-800: #1f2937;
|
||||
--gray-900: #111827;
|
||||
|
||||
/* Semantic Colors */
|
||||
--success: #10b981;
|
||||
--warning: #f59e0b;
|
||||
--error: #ef4444;
|
||||
--info: #3b82f6;
|
||||
|
||||
/* Social Media Colors */
|
||||
--like-color: #ec4899;
|
||||
--share-color: #8b5cf6;
|
||||
--bookmark-color: #f59e0b;
|
||||
--comment-color: var(--primary-500);
|
||||
|
||||
/* Glassmorphism */
|
||||
--glass-bg: rgba(255, 255, 255, 0.1);
|
||||
--glass-border: rgba(255, 255, 255, 0.2);
|
||||
--glass-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
|
||||
|
||||
/* Animations */
|
||||
--transition-fast: 0.15s ease-out;
|
||||
--transition-normal: 0.3s ease-out;
|
||||
--transition-slow: 0.6s ease-out;
|
||||
|
||||
/* Shadows */
|
||||
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
||||
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
|
||||
|
||||
/* Border Radius */
|
||||
--radius-sm: 0.375rem;
|
||||
--radius-md: 0.5rem;
|
||||
--radius-lg: 0.75rem;
|
||||
--radius-xl: 1rem;
|
||||
--radius-2xl: 1.5rem;
|
||||
}
|
||||
|
||||
/* Dark Mode Variables */
|
||||
[data-theme="dark"] {
|
||||
--glass-bg: rgba(0, 0, 0, 0.1);
|
||||
--glass-border: rgba(255, 255, 255, 0.1);
|
||||
--glass-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
|
||||
}
|
||||
|
||||
/* ================================
|
||||
Performance Optimizations
|
||||
================================ */
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* GPU Acceleration for animations */
|
||||
.accelerated {
|
||||
transform: translateZ(0);
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
/* Smooth scrolling */
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* ================================
|
||||
Social Feed Styles
|
||||
================================ */
|
||||
|
||||
.social-feed {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.post-card {
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: var(--radius-xl);
|
||||
margin-bottom: 1.5rem;
|
||||
padding: 1.5rem;
|
||||
box-shadow: var(--shadow-lg);
|
||||
transition: all var(--transition-normal);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.post-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-xl);
|
||||
border-color: var(--primary-300);
|
||||
}
|
||||
|
||||
.post-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, var(--primary-500), var(--primary-600));
|
||||
opacity: 0;
|
||||
transition: opacity var(--transition-normal);
|
||||
}
|
||||
|
||||
.post-card:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Post Header */
|
||||
.post-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.post-avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid var(--primary-500);
|
||||
transition: all var(--transition-fast);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.post-avatar:hover {
|
||||
transform: scale(1.1);
|
||||
border-color: var(--primary-400);
|
||||
}
|
||||
|
||||
.post-author {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.post-author-name {
|
||||
font-weight: 600;
|
||||
color: var(--gray-800);
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.post-author-username {
|
||||
color: var(--gray-500);
|
||||
font-size: 0.875rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.post-time {
|
||||
color: var(--gray-400);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* Post Content */
|
||||
.post-content {
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.6;
|
||||
color: var(--gray-700);
|
||||
}
|
||||
|
||||
.post-type-badge {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.post-type-text { background: var(--gray-100); color: var(--gray-600); }
|
||||
.post-type-thought { background: var(--primary-100); color: var(--primary-600); }
|
||||
.post-type-question { background: var(--warning); color: white; }
|
||||
.post-type-insight { background: var(--success); color: white; }
|
||||
|
||||
/* Post Actions */
|
||||
.post-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid var(--gray-200);
|
||||
}
|
||||
|
||||
.action-group {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
font-size: 0.875rem;
|
||||
color: var(--gray-500);
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
background: var(--gray-100);
|
||||
color: var(--gray-700);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.action-btn.active {
|
||||
color: var(--primary-600);
|
||||
background: var(--primary-50);
|
||||
}
|
||||
|
||||
.action-btn i {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* Specific action colors */
|
||||
.action-btn.like-btn.active {
|
||||
color: var(--like-color);
|
||||
background: rgba(236, 72, 153, 0.1);
|
||||
}
|
||||
|
||||
.action-btn.share-btn:hover {
|
||||
color: var(--share-color);
|
||||
background: rgba(139, 92, 246, 0.1);
|
||||
}
|
||||
|
||||
.action-btn.bookmark-btn.active {
|
||||
color: var(--bookmark-color);
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
}
|
||||
|
||||
/* ================================
|
||||
Comments Section
|
||||
================================ */
|
||||
|
||||
.comments-section {
|
||||
border-top: 1px solid var(--gray-200);
|
||||
padding-top: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.comment-item {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
padding: 0.75rem;
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--gray-50);
|
||||
transition: background var(--transition-fast);
|
||||
}
|
||||
|
||||
.comment-item:hover {
|
||||
background: var(--gray-100);
|
||||
}
|
||||
|
||||
.comment-avatar {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid var(--primary-400);
|
||||
}
|
||||
|
||||
.comment-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.comment-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.comment-author {
|
||||
font-weight: 600;
|
||||
color: var(--gray-800);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.comment-time {
|
||||
color: var(--gray-400);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.comment-text {
|
||||
color: var(--gray-700);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.comment-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.comment-action {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--gray-400);
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: color var(--transition-fast);
|
||||
}
|
||||
|
||||
.comment-action:hover {
|
||||
color: var(--primary-500);
|
||||
}
|
||||
|
||||
/* Comment Form */
|
||||
.comment-form {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.comment-form textarea {
|
||||
flex: 1;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid var(--gray-300);
|
||||
border-radius: var(--radius-lg);
|
||||
resize: none;
|
||||
min-height: 80px;
|
||||
transition: border-color var(--transition-fast);
|
||||
}
|
||||
|
||||
.comment-form textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-500);
|
||||
box-shadow: 0 0 0 3px rgba(14, 165, 233, 0.1);
|
||||
}
|
||||
|
||||
.comment-submit {
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: var(--primary-500);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: var(--radius-lg);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.comment-submit:hover {
|
||||
background: var(--primary-600);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* ================================
|
||||
Create Post Form
|
||||
================================ */
|
||||
|
||||
.create-post-form {
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: var(--radius-xl);
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.create-post-textarea {
|
||||
width: 100%;
|
||||
min-height: 120px;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--gray-300);
|
||||
border-radius: var(--radius-lg);
|
||||
resize: vertical;
|
||||
font-family: inherit;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
transition: all var(--transition-normal);
|
||||
}
|
||||
|
||||
.create-post-textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-500);
|
||||
box-shadow: 0 0 0 3px rgba(14, 165, 233, 0.1);
|
||||
}
|
||||
|
||||
.create-post-options {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 1rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.post-type-select,
|
||||
.post-visibility-select {
|
||||
padding: 0.5rem 1rem;
|
||||
border: 1px solid var(--gray-300);
|
||||
border-radius: var(--radius-md);
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
transition: border-color var(--transition-fast);
|
||||
}
|
||||
|
||||
.post-type-select:focus,
|
||||
.post-visibility-select:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-500);
|
||||
}
|
||||
|
||||
.create-post-btn {
|
||||
padding: 0.75rem 2rem;
|
||||
background: linear-gradient(135deg, var(--primary-500), var(--primary-600));
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: var(--radius-lg);
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition: all var(--transition-normal);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.create-post-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.create-post-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
/* ================================
|
||||
Filter Tabs
|
||||
================================ */
|
||||
|
||||
.feed-filters {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 2rem;
|
||||
padding: 0.5rem;
|
||||
background: var(--gray-100);
|
||||
border-radius: var(--radius-xl);
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.filter-tab {
|
||||
padding: 0.75rem 1.5rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: var(--radius-lg);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
color: var(--gray-600);
|
||||
}
|
||||
|
||||
.filter-tab:hover {
|
||||
background: var(--gray-200);
|
||||
color: var(--gray-800);
|
||||
}
|
||||
|
||||
.filter-tab.active {
|
||||
background: var(--primary-500);
|
||||
color: white;
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
/* ================================
|
||||
Notifications
|
||||
================================ */
|
||||
|
||||
.notification-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
border-radius: var(--radius-lg);
|
||||
transition: all var(--transition-fast);
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.notification-item:hover {
|
||||
background: var(--gray-50);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.notification-item.unread {
|
||||
background: var(--primary-50);
|
||||
border-left: 4px solid var(--primary-500);
|
||||
}
|
||||
|
||||
.notification-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.2rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.notification-like { background: var(--like-color); }
|
||||
.notification-comment { background: var(--comment-color); }
|
||||
.notification-follow { background: var(--success); }
|
||||
.notification-share { background: var(--share-color); }
|
||||
|
||||
.notification-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.notification-text {
|
||||
margin: 0 0 0.25rem 0;
|
||||
color: var(--gray-800);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.notification-time {
|
||||
color: var(--gray-500);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.notification-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.notification-delete {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--gray-400);
|
||||
cursor: pointer;
|
||||
padding: 0.5rem;
|
||||
border-radius: var(--radius-md);
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.notification-delete:hover {
|
||||
background: var(--error);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* ================================
|
||||
User Profile
|
||||
================================ */
|
||||
|
||||
.profile-header {
|
||||
background: linear-gradient(135deg, var(--primary-500), var(--primary-600));
|
||||
color: white;
|
||||
padding: 2rem;
|
||||
border-radius: var(--radius-2xl);
|
||||
margin-bottom: 2rem;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.profile-header::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="grain" width="100" height="100" patternUnits="userSpaceOnUse"><circle cx="25" cy="25" r="1" fill="rgba(255,255,255,0.1)"/><circle cx="75" cy="75" r="1" fill="rgba(255,255,255,0.1)"/></pattern></defs><rect width="100" height="100" fill="url(%23grain)"/></svg>');
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.profile-info {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.profile-avatar {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 50%;
|
||||
border: 4px solid rgba(255, 255, 255, 0.3);
|
||||
box-shadow: var(--shadow-xl);
|
||||
}
|
||||
|
||||
.profile-details h1 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.profile-username {
|
||||
opacity: 0.9;
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.profile-bio {
|
||||
opacity: 0.8;
|
||||
line-height: 1.5;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.profile-stats {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
display: block;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.875rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.follow-btn {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
color: white;
|
||||
padding: 0.75rem 2rem;
|
||||
border-radius: var(--radius-lg);
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-normal);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.follow-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.follow-btn.following {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
color: var(--primary-600);
|
||||
}
|
||||
|
||||
/* Profile Tabs */
|
||||
.profile-tabs {
|
||||
display: flex;
|
||||
border-bottom: 1px solid var(--gray-200);
|
||||
margin-bottom: 2rem;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.profile-tab {
|
||||
padding: 1rem 2rem;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
color: var(--gray-600);
|
||||
font-weight: 500;
|
||||
transition: all var(--transition-fast);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.profile-tab:hover {
|
||||
color: var(--primary-600);
|
||||
}
|
||||
|
||||
.profile-tab.active {
|
||||
color: var(--primary-600);
|
||||
}
|
||||
|
||||
.profile-tab.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background: var(--primary-500);
|
||||
border-radius: 2px 2px 0 0;
|
||||
}
|
||||
|
||||
/* ================================
|
||||
Responsive Design
|
||||
================================ */
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.social-feed {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.post-card {
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.post-actions {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.action-group {
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.create-post-options {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.feed-filters {
|
||||
padding: 0.25rem;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.filter-tab {
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.profile-header {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.profile-info {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.profile-avatar {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.profile-details h1 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.profile-stats {
|
||||
justify-content: center;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.profile-tabs {
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.profile-tab {
|
||||
flex: 1;
|
||||
padding: 0.75rem 1rem;
|
||||
text-align: center;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* ================================
|
||||
Loading & Animations
|
||||
================================ */
|
||||
|
||||
.loading-spinner {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid var(--gray-300);
|
||||
border-radius: 50%;
|
||||
border-top-color: var(--primary-500);
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation: fadeIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.slide-up {
|
||||
animation: slideUp 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from { transform: translateY(100%); opacity: 0; }
|
||||
to { transform: translateY(0); opacity: 1; }
|
||||
}
|
||||
|
||||
.pulse {
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
/* ================================
|
||||
Toast Notifications
|
||||
================================ */
|
||||
|
||||
.toast-container {
|
||||
position: fixed;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.toast {
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 1rem 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
box-shadow: var(--shadow-lg);
|
||||
pointer-events: all;
|
||||
max-width: 400px;
|
||||
animation: slideInRight 0.3s ease-out;
|
||||
}
|
||||
|
||||
.toast.success {
|
||||
border-left: 4px solid var(--success);
|
||||
}
|
||||
|
||||
.toast.error {
|
||||
border-left: 4px solid var(--error);
|
||||
}
|
||||
|
||||
.toast.warning {
|
||||
border-left: 4px solid var(--warning);
|
||||
}
|
||||
|
||||
.toast.info {
|
||||
border-left: 4px solid var(--info);
|
||||
}
|
||||
|
||||
@keyframes slideInRight {
|
||||
from { transform: translateX(100%); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
|
||||
/* ================================
|
||||
Utilities
|
||||
================================ */
|
||||
|
||||
.text-gradient {
|
||||
background: linear-gradient(135deg, var(--primary-500), var(--primary-600));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.glass-effect {
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid var(--glass-border);
|
||||
}
|
||||
|
||||
.shadow-glow {
|
||||
box-shadow: 0 0 20px rgba(14, 165, 233, 0.3);
|
||||
}
|
||||
|
||||
.scrollbar-hide {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
125
static/css/src/cybernetwork-bg.css
Normal file
125
static/css/src/cybernetwork-bg.css
Normal file
@@ -0,0 +1,125 @@
|
||||
/* 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,211 +1434,16 @@ html, body {
|
||||
overflow-x: hidden;
|
||||
background: linear-gradient(135deg, var(--background-start), var(--background-end));
|
||||
background-attachment: fixed;
|
||||
scroll-behavior: smooth;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Sticky navbar */
|
||||
.navbar.sticky-top {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
/* Light Mode Optimierungen für wichtige UI-Komponenten */
|
||||
|
||||
/* Buttons im Light Mode */
|
||||
.btn-primary:not(.dark-mode .btn-primary) {
|
||||
background-color: var(--light-primary, #3b82f6);
|
||||
color: white;
|
||||
border: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn-primary:not(.dark-mode .btn-primary):hover {
|
||||
background-color: var(--light-primary-hover, #4f46e5);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.btn-secondary:not(.dark-mode .btn-secondary) {
|
||||
background-color: #f3f4f6;
|
||||
color: #374151;
|
||||
border: 1px solid #d1d5db;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn-secondary:not(.dark-mode .btn-secondary):hover {
|
||||
background-color: #e5e7eb;
|
||||
}
|
||||
|
||||
/* Navbar im Light Mode */
|
||||
.navbar:not(.dark-mode .navbar),
|
||||
.nav:not(.dark-mode .nav) {
|
||||
background-color: rgba(255, 255, 255, 0.95);
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.navbar:not(.dark-mode .navbar) .nav-link,
|
||||
.nav:not(.dark-mode .nav) .nav-link {
|
||||
color: #1e3a8a;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.navbar:not(.dark-mode .navbar) .nav-link:hover,
|
||||
.nav:not(.dark-mode .nav) .nav-link:hover {
|
||||
color: #4f46e5;
|
||||
}
|
||||
|
||||
.navbar:not(.dark-mode .navbar) .navbar-brand,
|
||||
.nav:not(.dark-mode .nav) .navbar-brand {
|
||||
color: #0f172a;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Dropdown Menüs im Light Mode */
|
||||
.dropdown-menu:not(.dark-mode .dropdown-menu) {
|
||||
background-color: white;
|
||||
border: 1px solid #e5e7eb;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05), 0 10px 15px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.dropdown-item:not(.dark-mode .dropdown-item) {
|
||||
color: #1e293b;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.dropdown-item:not(.dark-mode .dropdown-item):hover {
|
||||
background-color: #f1f5f9;
|
||||
color: #4f46e5;
|
||||
}
|
||||
|
||||
/* Karten im Light Mode */
|
||||
.card:not(.dark-mode .card) {
|
||||
background-color: white;
|
||||
border: 1px solid #e5e7eb;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.03), 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-header:not(.dark-mode .card-header) {
|
||||
background-color: #f8fafc;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
padding: 1rem 1.5rem;
|
||||
}
|
||||
|
||||
.card-footer:not(.dark-mode .card-footer) {
|
||||
background-color: #f8fafc;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
/* Formulare im Light Mode */
|
||||
.form-control:not(.dark-mode .form-control) {
|
||||
background-color: white;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 0.375rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.form-control:not(.dark-mode .form-control):focus {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.25);
|
||||
}
|
||||
|
||||
/* Tabs im Light Mode */
|
||||
.nav-tabs:not(.dark-mode .nav-tabs) {
|
||||
border-bottom-color: #e5e7eb;
|
||||
}
|
||||
|
||||
.nav-tabs:not(.dark-mode .nav-tabs) .nav-link {
|
||||
color: #64748b;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.nav-tabs:not(.dark-mode .nav-tabs) .nav-link:hover {
|
||||
border-color: #e5e7eb #e5e7eb #e5e7eb;
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.nav-tabs:not(.dark-mode .nav-tabs) .nav-link.active {
|
||||
color: #0f172a;
|
||||
background-color: white;
|
||||
border-color: #e5e7eb #e5e7eb white;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Alerts im Light Mode */
|
||||
.alert:not(.dark-mode .alert) {
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.alert-primary:not(.dark-mode .alert-primary) {
|
||||
background-color: #eff6ff;
|
||||
border-color: #bfdbfe;
|
||||
color: #1e40af;
|
||||
}
|
||||
|
||||
.alert-success:not(.dark-mode .alert-success) {
|
||||
background-color: #f0fdf4;
|
||||
border-color: #bbf7d0;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.alert-warning:not(.dark-mode .alert-warning) {
|
||||
background-color: #fffbeb;
|
||||
border-color: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.alert-danger:not(.dark-mode .alert-danger) {
|
||||
background-color: #fef2f2;
|
||||
border-color: #fecaca;
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
/* Badges im Light Mode */
|
||||
.badge:not(.dark-mode .badge) {
|
||||
font-weight: 500;
|
||||
padding: 0.25em 0.6em;
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.badge-primary:not(.dark-mode .badge-primary) {
|
||||
background-color: #3b82f6;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.badge-secondary:not(.dark-mode .badge-secondary) {
|
||||
background-color: #f3f4f6;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
/* Tabellen im Light Mode */
|
||||
table:not(.dark-mode table) {
|
||||
background-color: white;
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
table:not(.dark-mode table) th {
|
||||
background-color: #f8fafc;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
color: #0f172a;
|
||||
font-weight: 600;
|
||||
padding: 0.75rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
table:not(.dark-mode table) td {
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
padding: 0.75rem;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
table:not(.dark-mode table) tr:hover {
|
||||
background-color: #f8fafc;
|
||||
}
|
||||
/* Importiere das Cyber-Network CSS */
|
||||
@import url('/static/css/src/cybernetwork-bg.css');
|
||||
6
static/css/tailwind.min.css
vendored
6
static/css/tailwind.min.css
vendored
@@ -1,6 +0,0 @@
|
||||
/**
|
||||
* 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,76 +450,6 @@ class D3Extensions {
|
||||
// Pulsanimation starten
|
||||
pulse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verarbeitet Daten aus der Datenbank für die Mindmap-Visualisierung
|
||||
* @param {Array} databaseNodes - Knotendaten aus der Datenbank
|
||||
* @param {Array} links - Verbindungsdaten oder null für automatische Extraktion
|
||||
* @returns {Object} Aufbereitete Daten für D3.js
|
||||
*/
|
||||
static processDbNodesForVisualization(databaseNodes, links = null) {
|
||||
// Überprüfe, ob Daten vorhanden sind
|
||||
if (!databaseNodes || databaseNodes.length === 0) {
|
||||
console.warn('Keine Knotendaten zum Verarbeiten vorhanden');
|
||||
return { nodes: [], links: [] };
|
||||
}
|
||||
|
||||
// Knoten mit D3-Kompatiblem Format erstellen
|
||||
const nodes = databaseNodes.map(node => {
|
||||
// Farbgenerierung, falls keine vorhanden
|
||||
const nodeColor = node.color_code ||
|
||||
node.color ||
|
||||
D3Extensions.stringToColor(node.name || 'default');
|
||||
|
||||
return {
|
||||
id: node.id,
|
||||
name: node.name,
|
||||
description: node.description || '',
|
||||
thought_count: node.thought_count || 0,
|
||||
color: nodeColor,
|
||||
// Zusätzliche Attribute
|
||||
category_id: node.category_id,
|
||||
is_public: node.is_public !== undefined ? node.is_public : true,
|
||||
// Position, falls vorhanden
|
||||
x: node.x_position,
|
||||
y: node.y_position,
|
||||
// Größe, falls vorhanden
|
||||
scale: node.scale || 1.0
|
||||
};
|
||||
});
|
||||
|
||||
// Verbindungen verarbeiten
|
||||
let processedLinks = [];
|
||||
|
||||
if (links && Array.isArray(links)) {
|
||||
// Verwende übergebene Verbindungen
|
||||
processedLinks = links.map(link => {
|
||||
return {
|
||||
source: link.source,
|
||||
target: link.target,
|
||||
// Zusätzliche Attribute
|
||||
type: link.type || 'default',
|
||||
strength: link.strength || 1
|
||||
};
|
||||
});
|
||||
} else {
|
||||
// Extrahiere Verbindungen aus den Knoten
|
||||
databaseNodes.forEach(node => {
|
||||
if (node.connections && Array.isArray(node.connections)) {
|
||||
node.connections.forEach(conn => {
|
||||
processedLinks.push({
|
||||
source: node.id,
|
||||
target: conn.target,
|
||||
type: conn.type || 'default',
|
||||
strength: conn.strength || 1
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return { nodes, links: processedLinks };
|
||||
}
|
||||
}
|
||||
|
||||
// Globale Verfügbarkeit sicherstellen
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.3 MiB |
@@ -1,35 +0,0 @@
|
||||
/* 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');
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
/* 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');
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
<svg width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="100" cy="100" r="98" fill="url(#gradient)" stroke="#7C3AED" stroke-width="4"/>
|
||||
<circle cx="100" cy="80" r="36" fill="white"/>
|
||||
<path d="M100 140C77.9086 140 60 157.909 60 180H140C140 157.909 122.091 140 100 140Z" fill="white"/>
|
||||
<defs>
|
||||
<linearGradient id="gradient" x1="0" y1="0" x2="200" y2="200" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#8B5CF6"/>
|
||||
<stop offset="1" stop-color="#3B82F6"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 583 B |
@@ -1,54 +1,25 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate favicon.ico from SVG using cairosvg and PIL
|
||||
"""
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
import io
|
||||
from cairosvg import svg2png
|
||||
from PIL import Image
|
||||
import cairosvg
|
||||
|
||||
# Verzeichnis dieses Skripts
|
||||
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
# Pfad zum SVG-Favicon
|
||||
svg_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'favicon.svg')
|
||||
# Ausgabepfad für das PNG
|
||||
png_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'favicon.png')
|
||||
# Ausgabepfad für das ICO
|
||||
ico_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'favicon.ico')
|
||||
|
||||
def svg_to_ico(svg_path, ico_path, sizes=[16, 32, 48, 64, 128, 256]):
|
||||
"""Convert SVG to multi-size ICO file"""
|
||||
img_io = io.BytesIO()
|
||||
|
||||
# Höchste Auflösung für Zwischenspeicherung
|
||||
max_size = max(sizes)
|
||||
|
||||
# SVG in PNG konvertieren
|
||||
with open(svg_path, 'rb') as svg_file:
|
||||
svg_data = svg_file.read()
|
||||
svg2png(bytestring=svg_data, write_to=img_io, output_width=max_size, output_height=max_size)
|
||||
|
||||
# PNG in verschiedene Größen konvertieren
|
||||
img = Image.open(img_io)
|
||||
|
||||
# Alle Größen für das ICO-Format vorbereiten
|
||||
img_list = []
|
||||
for size in sizes:
|
||||
resized_img = img.resize((size, size), Image.LANCZOS)
|
||||
img_list.append(resized_img)
|
||||
|
||||
# ICO-Datei speichern
|
||||
img_list[0].save(
|
||||
ico_path,
|
||||
format='ICO',
|
||||
sizes=[(img.width, img.height) for img in img_list],
|
||||
append_images=img_list[1:]
|
||||
)
|
||||
print(f"Favicon {ico_path} wurde erstellt!")
|
||||
# SVG zu PNG konvertieren
|
||||
cairosvg.svg2png(url=svg_path, write_to=png_path, output_width=512, output_height=512)
|
||||
|
||||
# Ursprüngliches Favicon konvertieren
|
||||
svg_to_ico(
|
||||
os.path.join(CURRENT_DIR, 'favicon.svg'),
|
||||
os.path.join(CURRENT_DIR, 'favicon.ico')
|
||||
)
|
||||
# PNG zu ICO konvertieren
|
||||
img = Image.open(png_path)
|
||||
img.save(ico_path, sizes=[(16, 16), (32, 32), (48, 48), (64, 64), (128, 128)])
|
||||
|
||||
# Neues Neuron-Favicon konvertieren
|
||||
svg_to_ico(
|
||||
os.path.join(CURRENT_DIR, 'neuron-favicon.svg'),
|
||||
os.path.join(CURRENT_DIR, 'neuron-favicon.ico')
|
||||
)
|
||||
print(f"Favicon erfolgreich erstellt: {ico_path}")
|
||||
|
||||
# Optional: PNG-Datei löschen, wenn nur ICO benötigt wird
|
||||
# os.remove(png_path)
|
||||
@@ -1,29 +0,0 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Hintergrund -->
|
||||
<rect width="32" height="32" rx="8" fill="#6d28d9" />
|
||||
|
||||
<!-- Mindmap-Punkte -->
|
||||
<!-- Zentraler Punkt -->
|
||||
<circle cx="16" cy="16" r="3.5" fill="#a78bfa" />
|
||||
|
||||
<!-- Umgebende Punkte -->
|
||||
<circle cx="8" cy="10" r="2.5" fill="#8b5cf6" />
|
||||
<circle cx="24" cy="10" r="2.5" fill="#8b5cf6" />
|
||||
<circle cx="16" cy="26" r="2.5" fill="#8b5cf6" />
|
||||
|
||||
<!-- Verbindende Linien -->
|
||||
<path d="M16 16 L8 10" stroke="white" stroke-width="1" stroke-linecap="round" />
|
||||
<path d="M16 16 L24 10" stroke="white" stroke-width="1" stroke-linecap="round" />
|
||||
<path d="M16 16 L16 26" stroke="white" stroke-width="1" stroke-linecap="round" />
|
||||
|
||||
<!-- Weitere Verbindungslinien für mehr Komplexität -->
|
||||
<path d="M8 10 L16 26" stroke="#c4b5fd" stroke-width="0.8" stroke-linecap="round" stroke-dasharray="2 1" />
|
||||
<path d="M24 10 L16 26" stroke="#c4b5fd" stroke-width="0.8" stroke-linecap="round" stroke-dasharray="2 1" />
|
||||
<path d="M8 10 L24 10" stroke="#c4b5fd" stroke-width="0.8" stroke-linecap="round" stroke-dasharray="2 1" />
|
||||
|
||||
<!-- Kleine Dekoration-Punkte für Hintergrund-Ähnlichkeit -->
|
||||
<circle cx="5" cy="20" r="0.8" fill="#ddd6fe" opacity="0.7" />
|
||||
<circle cx="27" cy="20" r="0.8" fill="#ddd6fe" opacity="0.7" />
|
||||
<circle cx="20" cy="5" r="0.8" fill="#ddd6fe" opacity="0.7" />
|
||||
<circle cx="12" cy="5" r="0.8" fill="#ddd6fe" opacity="0.7" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB |
@@ -1,59 +0,0 @@
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Hintergrund mit Farbverlauf -->
|
||||
<rect width="64" height="64" rx="16" fill="url(#paint0_linear)" />
|
||||
|
||||
<!-- Mindmap-Punkte -->
|
||||
<!-- Zentraler Punkt -->
|
||||
<circle cx="32" cy="32" r="8" fill="url(#glow_gradient)" filter="url(#glow)" />
|
||||
|
||||
<!-- Umgebende Punkte -->
|
||||
<circle cx="16" cy="20" r="6" fill="#8b5cf6" />
|
||||
<circle cx="48" cy="20" r="6" fill="#8b5cf6" />
|
||||
<circle cx="32" cy="52" r="6" fill="#8b5cf6" />
|
||||
<circle cx="16" cy="48" r="4" fill="#a78bfa" />
|
||||
<circle cx="48" cy="48" r="4" fill="#a78bfa" />
|
||||
|
||||
<!-- Verbindende Linien (Hauptpfade) -->
|
||||
<path d="M32 32 L16 20" stroke="white" stroke-width="2" stroke-linecap="round" />
|
||||
<path d="M32 32 L48 20" stroke="white" stroke-width="2" stroke-linecap="round" />
|
||||
<path d="M32 32 L32 52" stroke="white" stroke-width="2" stroke-linecap="round" />
|
||||
<path d="M32 32 L16 48" stroke="white" stroke-width="2" stroke-linecap="round" />
|
||||
<path d="M32 32 L48 48" stroke="white" stroke-width="2" stroke-linecap="round" />
|
||||
|
||||
<!-- Zusätzliche Verbindungslinien -->
|
||||
<path d="M16 20 L16 48" stroke="#c4b5fd" stroke-width="1.5" stroke-linecap="round" stroke-dasharray="3 2" />
|
||||
<path d="M48 20 L48 48" stroke="#c4b5fd" stroke-width="1.5" stroke-linecap="round" stroke-dasharray="3 2" />
|
||||
<path d="M16 20 L48 20" stroke="#c4b5fd" stroke-width="1.5" stroke-linecap="round" stroke-dasharray="3 2" />
|
||||
<path d="M16 48 L32 52" stroke="#c4b5fd" stroke-width="1.5" stroke-linecap="round" stroke-dasharray="3 2" />
|
||||
<path d="M48 48 L32 52" stroke="#c4b5fd" stroke-width="1.5" stroke-linecap="round" stroke-dasharray="3 2" />
|
||||
|
||||
<!-- Kleine Dekoration-Punkte für Hintergrund-Ähnlichkeit -->
|
||||
<circle cx="10" cy="36" r="1.5" fill="#ddd6fe" opacity="0.7" />
|
||||
<circle cx="54" cy="36" r="1.5" fill="#ddd6fe" opacity="0.7" />
|
||||
<circle cx="40" cy="10" r="1.5" fill="#ddd6fe" opacity="0.7" />
|
||||
<circle cx="24" cy="10" r="1.5" fill="#ddd6fe" opacity="0.7" />
|
||||
<circle cx="20" cy="36" r="1.2" fill="#ddd6fe" opacity="0.5" />
|
||||
<circle cx="44" cy="36" r="1.2" fill="#ddd6fe" opacity="0.5" />
|
||||
<circle cx="32" cy="16" r="1.2" fill="#ddd6fe" opacity="0.5" />
|
||||
|
||||
<!-- Definitionen für Farbverläufe und Effekte -->
|
||||
<defs>
|
||||
<!-- Haupthintergrund-Farbverlauf -->
|
||||
<linearGradient id="paint0_linear" x1="0" y1="0" x2="64" y2="64" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#6d28d9" />
|
||||
<stop offset="1" stop-color="#4c1d95" />
|
||||
</linearGradient>
|
||||
|
||||
<!-- Glüheffekt für den zentralen Punkt -->
|
||||
<filter id="glow" x="20" y="20" width="24" height="24" filterUnits="userSpaceOnUse">
|
||||
<feGaussianBlur stdDeviation="2" result="blur" />
|
||||
<feComposite in="SourceGraphic" in2="blur" operator="over" />
|
||||
</filter>
|
||||
|
||||
<!-- Farbverlauf für den zentralen Punkt -->
|
||||
<linearGradient id="glow_gradient" x1="24" y1="24" x2="40" y2="40" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#a78bfa" />
|
||||
<stop offset="1" stop-color="#8b5cf6" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.0 KiB |
5
static/js/alpine.min.js
vendored
5
static/js/alpine.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -2,11 +2,23 @@
|
||||
* MindMap - Hauptdatei für globale JavaScript-Funktionen
|
||||
*/
|
||||
|
||||
// Import des ChatGPT-Assistenten
|
||||
import ChatGPTAssistant from './modules/chatgpt-assistant.js';
|
||||
|
||||
/**
|
||||
* Hauptmodul für die MindMap-Anwendung
|
||||
* Verwaltet die globale Anwendungslogik
|
||||
*/
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialisiere die Anwendung
|
||||
MindMap.init();
|
||||
|
||||
// Wende Dunkel-/Hellmodus an
|
||||
const isDarkMode = localStorage.getItem('darkMode') === 'dark';
|
||||
document.documentElement.classList.toggle('dark', isDarkMode);
|
||||
});
|
||||
|
||||
/**
|
||||
* Hauptobjekt der MindMap-Anwendung
|
||||
*/
|
||||
@@ -15,7 +27,7 @@ const MindMap = {
|
||||
initialized: false,
|
||||
darkMode: document.documentElement.classList.contains('dark'),
|
||||
pageInitializers: {},
|
||||
currentPage: null,
|
||||
currentPage: document.body.dataset.page,
|
||||
|
||||
/**
|
||||
* Initialisiert die MindMap-Anwendung
|
||||
@@ -23,18 +35,13 @@ const MindMap = {
|
||||
init() {
|
||||
if (this.initialized) return;
|
||||
|
||||
// Setze currentPage erst jetzt, wenn DOM garantiert geladen ist
|
||||
this.currentPage = document.body && document.body.dataset ? document.body.dataset.page : null;
|
||||
|
||||
console.log('MindMap-Anwendung wird initialisiert...');
|
||||
|
||||
// Initialisiere den ChatGPT-Assistenten
|
||||
if (typeof ChatGPTAssistant !== 'undefined') {
|
||||
const assistant = new ChatGPTAssistant();
|
||||
assistant.init();
|
||||
// Speichere als Teil von MindMap
|
||||
this.assistant = assistant;
|
||||
}
|
||||
const assistant = new ChatGPTAssistant();
|
||||
assistant.init();
|
||||
// Speichere als Teil von MindMap
|
||||
this.assistant = assistant;
|
||||
|
||||
// Seiten-spezifische Initialisierer aufrufen
|
||||
if (this.currentPage && this.pageInitializers[this.currentPage]) {
|
||||
@@ -67,12 +74,6 @@ const MindMap = {
|
||||
try {
|
||||
console.log('Initialisiere Mindmap...');
|
||||
|
||||
// Prüfe, ob MindMapVisualization geladen ist
|
||||
if (typeof MindMapVisualization === 'undefined') {
|
||||
console.error('MindMapVisualization-Klasse ist nicht definiert!');
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialisiere die Mindmap
|
||||
const mindmap = new MindMapVisualization('#mindmap-container', {
|
||||
height: mindmapContainer.clientHeight || 600,
|
||||
@@ -223,13 +224,6 @@ const MindMap = {
|
||||
});
|
||||
}
|
||||
};
|
||||
window.MindMap = MindMap;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialisiere die Anwendung
|
||||
MindMap.init();
|
||||
|
||||
// Wende Dunkel-/Hellmodus an
|
||||
const isDarkMode = localStorage.getItem('darkMode') === 'dark';
|
||||
document.documentElement.classList.toggle('dark', isDarkMode);
|
||||
});
|
||||
// Globale Export für andere Module
|
||||
window.MindMap = MindMap;
|
||||
@@ -1,214 +0,0 @@
|
||||
/**
|
||||
* Mindmap Initialisierung und Event-Handling
|
||||
*/
|
||||
|
||||
// Warte auf die Cytoscape-Instanz
|
||||
document.addEventListener('mindmap-loaded', function() {
|
||||
const cy = window.cy;
|
||||
if (!cy) return;
|
||||
|
||||
// Event-Listener für Knoten-Klicks
|
||||
cy.on('tap', 'node', function(evt) {
|
||||
const node = evt.target;
|
||||
|
||||
// Alle vorherigen Hervorhebungen zurücksetzen
|
||||
cy.nodes().forEach(n => {
|
||||
n.removeStyle();
|
||||
n.connectedEdges().removeStyle();
|
||||
});
|
||||
|
||||
// Speichere ausgewählten Knoten
|
||||
window.mindmapInstance.selectedNode = node;
|
||||
|
||||
// Aktiviere leuchtenden Effekt statt Umkreisung
|
||||
node.style({
|
||||
'background-opacity': 1,
|
||||
'background-color': node.data('color'),
|
||||
'shadow-color': node.data('color'),
|
||||
'shadow-opacity': 1,
|
||||
'shadow-blur': 15,
|
||||
'shadow-offset-x': 0,
|
||||
'shadow-offset-y': 0
|
||||
});
|
||||
|
||||
// Verbundene Kanten und Knoten hervorheben
|
||||
const connectedEdges = node.connectedEdges();
|
||||
const connectedNodes = node.neighborhood('node');
|
||||
|
||||
connectedEdges.style({
|
||||
'line-color': '#a78bfa',
|
||||
'target-arrow-color': '#a78bfa',
|
||||
'source-arrow-color': '#a78bfa',
|
||||
'line-opacity': 0.8,
|
||||
'width': 2
|
||||
});
|
||||
|
||||
connectedNodes.style({
|
||||
'shadow-opacity': 0.7,
|
||||
'shadow-blur': 10,
|
||||
'shadow-color': '#a78bfa'
|
||||
});
|
||||
|
||||
// Info-Panel aktualisieren
|
||||
updateInfoPanel(node);
|
||||
|
||||
// Seitenleiste aktualisieren
|
||||
updateSidebar(node);
|
||||
});
|
||||
|
||||
// Klick auf Hintergrund - Auswahl zurücksetzen
|
||||
cy.on('tap', function(evt) {
|
||||
if (evt.target === cy) {
|
||||
resetSelection(cy);
|
||||
}
|
||||
});
|
||||
|
||||
// Zoom-Controls
|
||||
document.getElementById('zoomIn')?.addEventListener('click', () => {
|
||||
cy.zoom({
|
||||
level: cy.zoom() * 1.2,
|
||||
renderedPosition: { x: cy.width() / 2, y: cy.height() / 2 }
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('zoomOut')?.addEventListener('click', () => {
|
||||
cy.zoom({
|
||||
level: cy.zoom() / 1.2,
|
||||
renderedPosition: { x: cy.width() / 2, y: cy.height() / 2 }
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('resetView')?.addEventListener('click', () => {
|
||||
cy.fit();
|
||||
resetSelection(cy);
|
||||
});
|
||||
|
||||
// Legend-Toggle
|
||||
document.getElementById('toggleLegend')?.addEventListener('click', () => {
|
||||
const legend = document.getElementById('categoryLegend');
|
||||
if (legend) {
|
||||
isLegendVisible = !isLegendVisible;
|
||||
legend.style.display = isLegendVisible ? 'block' : 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// Keyboard-Controls
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === '+' || e.key === '=') {
|
||||
cy.zoom({
|
||||
level: cy.zoom() * 1.2,
|
||||
renderedPosition: { x: cy.width() / 2, y: cy.height() / 2 }
|
||||
});
|
||||
} else if (e.key === '-' || e.key === '_') {
|
||||
cy.zoom({
|
||||
level: cy.zoom() / 1.2,
|
||||
renderedPosition: { x: cy.width() / 2, y: cy.height() / 2 }
|
||||
});
|
||||
} else if (e.key === 'Escape') {
|
||||
resetSelection(cy);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Aktualisiert das Info-Panel mit Knoteninformationen
|
||||
* @param {Object} node - Der ausgewählte Knoten
|
||||
*/
|
||||
function updateInfoPanel(node) {
|
||||
const infoPanel = document.getElementById('infoPanel');
|
||||
if (!infoPanel) return;
|
||||
|
||||
const data = node.data();
|
||||
const connectedNodes = node.neighborhood('node');
|
||||
|
||||
let html = `
|
||||
<h3>${data.label || data.name}</h3>
|
||||
<p class="category">${data.category || 'Keine Kategorie'}</p>
|
||||
${data.description ? `<p class="description">${data.description}</p>` : ''}
|
||||
<div class="connections">
|
||||
<h4>Verbindungen (${connectedNodes.length})</h4>
|
||||
<ul>
|
||||
`;
|
||||
|
||||
connectedNodes.forEach(connectedNode => {
|
||||
const connectedData = connectedNode.data();
|
||||
html += `
|
||||
<li style="color: ${connectedData.color || '#60a5fa'}">
|
||||
${connectedData.label || connectedData.name}
|
||||
</li>
|
||||
`;
|
||||
});
|
||||
|
||||
html += `
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
|
||||
infoPanel.innerHTML = html;
|
||||
infoPanel.style.display = 'block';
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiert die Seitenleiste mit Knoteninformationen
|
||||
* @param {Object} node - Der ausgewählte Knoten
|
||||
*/
|
||||
function updateSidebar(node) {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
if (!sidebar) return;
|
||||
|
||||
const data = node.data();
|
||||
const connectedNodes = node.neighborhood('node');
|
||||
|
||||
let html = `
|
||||
<div class="node-details">
|
||||
<h3>${data.label || data.name}</h3>
|
||||
<p class="category">${data.category || 'Keine Kategorie'}</p>
|
||||
${data.description ? `<p class="description">${data.description}</p>` : ''}
|
||||
<div class="connections">
|
||||
<h4>Verbindungen (${connectedNodes.length})</h4>
|
||||
<ul>
|
||||
`;
|
||||
|
||||
connectedNodes.forEach(connectedNode => {
|
||||
const connectedData = connectedNode.data();
|
||||
html += `
|
||||
<li style="color: ${connectedData.color || '#60a5fa'}">
|
||||
${connectedData.label || connectedData.name}
|
||||
</li>
|
||||
`;
|
||||
});
|
||||
|
||||
html += `
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
sidebar.innerHTML = html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt die Auswahl zurück
|
||||
* @param {Object} cy - Cytoscape-Instanz
|
||||
*/
|
||||
function resetSelection(cy) {
|
||||
window.mindmapInstance.selectedNode = null;
|
||||
|
||||
// Alle Hervorhebungen zurücksetzen
|
||||
cy.nodes().forEach(node => {
|
||||
node.removeStyle();
|
||||
node.connectedEdges().removeStyle();
|
||||
});
|
||||
|
||||
// Info-Panel ausblenden
|
||||
const infoPanel = document.getElementById('infoPanel');
|
||||
if (infoPanel) {
|
||||
infoPanel.style.display = 'none';
|
||||
}
|
||||
|
||||
// Seitenleiste leeren
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
if (sidebar) {
|
||||
sidebar.innerHTML = '';
|
||||
}
|
||||
}
|
||||
@@ -11,62 +11,6 @@ class ChatGPTAssistant {
|
||||
this.container = null;
|
||||
this.chatHistory = null;
|
||||
this.inputField = null;
|
||||
this.suggestionArea = null;
|
||||
this.maxRetries = 2;
|
||||
this.retryCount = 0;
|
||||
this.markdownParser = null;
|
||||
this.initializeMarkdownParser();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialisiert den Markdown-Parser
|
||||
*/
|
||||
async initializeMarkdownParser() {
|
||||
// Dynamisch marked.js laden, wenn noch nicht vorhanden
|
||||
if (!window.marked) {
|
||||
try {
|
||||
// Prüfen, ob marked.js bereits im Dokument geladen ist
|
||||
if (!document.querySelector('script[src*="marked"]')) {
|
||||
// Falls nicht, Script-Tag erstellen und einfügen
|
||||
const script = document.createElement('script');
|
||||
script.src = 'https://cdn.jsdelivr.net/npm/marked/marked.min.js';
|
||||
script.async = true;
|
||||
|
||||
// Promise erstellen, das resolved wird, wenn das Script geladen wurde
|
||||
await new Promise((resolve, reject) => {
|
||||
script.onload = resolve;
|
||||
script.onerror = reject;
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
|
||||
console.log('Marked.js erfolgreich geladen');
|
||||
}
|
||||
|
||||
// Marked konfigurieren
|
||||
this.markdownParser = window.marked;
|
||||
this.markdownParser.setOptions({
|
||||
gfm: true,
|
||||
breaks: true,
|
||||
sanitize: true,
|
||||
smartLists: true,
|
||||
smartypants: true
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden von marked.js:', error);
|
||||
// Fallback-Parser, der nur einfache Absätze erkennt
|
||||
this.markdownParser = {
|
||||
parse: (text) => {
|
||||
return text.split('\n').map(line => {
|
||||
if (line.trim() === '') return '<br>';
|
||||
return `<p>${line}</p>`;
|
||||
}).join('');
|
||||
}
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// Marked ist bereits geladen
|
||||
this.markdownParser = window.marked;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -80,16 +24,7 @@ class ChatGPTAssistant {
|
||||
this.setupEventListeners();
|
||||
|
||||
// Ersten Willkommensnachricht anzeigen
|
||||
this.addMessage("assistant", "Hallo! Ich bin dein KI-Assistent (4o-mini) und habe Zugriff auf die Wissensdatenbank. Wie kann ich dir helfen?\n\nDu kannst mir Fragen über:\n- **Gedanken** in der Datenbank\n- **Kategorien** und Wissenschaftsbereiche\n- **Mindmaps** und Wissensverknüpfungen\n\nstellen.");
|
||||
|
||||
// Vorschläge anzeigen
|
||||
this.showSuggestions([
|
||||
"Zeige mir Gedanken zur künstlichen Intelligenz",
|
||||
"Welche Kategorien gibt es in der Datenbank?",
|
||||
"Suche nach Mindmaps zum Thema Informatik"
|
||||
]);
|
||||
|
||||
console.log('KI-Assistent initialisiert!');
|
||||
this.addMessage("assistant", "Frage den KI-Assistenten");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -110,7 +45,7 @@ class ChatGPTAssistant {
|
||||
// Chat-Container
|
||||
const chatContainer = document.createElement('div');
|
||||
chatContainer.id = 'assistant-chat';
|
||||
chatContainer.className = 'bg-white dark:bg-dark-800 rounded-lg shadow-xl overflow-hidden transition-all duration-300 w-80 md: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 sm:w-96 max-h-0 opacity-0';
|
||||
|
||||
// Chat-Header
|
||||
const header = document.createElement('div');
|
||||
@@ -118,7 +53,7 @@ class ChatGPTAssistant {
|
||||
header.innerHTML = `
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-robot mr-2"></i>
|
||||
<span>KI-Assistent (4o-mini)</span>
|
||||
<span>KI-Assistent</span>
|
||||
</div>
|
||||
<button id="assistant-close" class="text-white hover:text-gray-200">
|
||||
<i class="fas fa-times"></i>
|
||||
@@ -128,12 +63,7 @@ class ChatGPTAssistant {
|
||||
// Chat-Verlauf
|
||||
this.chatHistory = document.createElement('div');
|
||||
this.chatHistory.id = 'assistant-history';
|
||||
this.chatHistory.className = 'p-3 overflow-y-auto max-h-96 space-y-3';
|
||||
|
||||
// Vorschlagsbereich
|
||||
this.suggestionArea = document.createElement('div');
|
||||
this.suggestionArea.id = 'assistant-suggestions';
|
||||
this.suggestionArea.className = 'px-3 pb-2 flex flex-wrap gap-2 overflow-x-auto hidden';
|
||||
this.chatHistory.className = 'p-3 overflow-y-auto max-h-80 space-y-3';
|
||||
|
||||
// Chat-Eingabe
|
||||
const inputContainer = document.createElement('div');
|
||||
@@ -141,7 +71,7 @@ class ChatGPTAssistant {
|
||||
|
||||
this.inputField = document.createElement('input');
|
||||
this.inputField.type = 'text';
|
||||
this.inputField.placeholder = 'Stelle eine Frage zur Wissensdatenbank...';
|
||||
this.inputField.placeholder = 'Frage den KI-Assistenten';
|
||||
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');
|
||||
@@ -155,7 +85,6 @@ class ChatGPTAssistant {
|
||||
|
||||
chatContainer.appendChild(header);
|
||||
chatContainer.appendChild(this.chatHistory);
|
||||
chatContainer.appendChild(this.suggestionArea);
|
||||
chatContainer.appendChild(inputContainer);
|
||||
|
||||
this.container.appendChild(toggleButton);
|
||||
@@ -171,40 +100,22 @@ class ChatGPTAssistant {
|
||||
setupEventListeners() {
|
||||
// Toggle-Button
|
||||
const toggleButton = document.getElementById('assistant-toggle');
|
||||
if (toggleButton) {
|
||||
toggleButton.addEventListener('click', () => this.toggleAssistant());
|
||||
}
|
||||
toggleButton.addEventListener('click', () => this.toggleAssistant());
|
||||
|
||||
// Schließen-Button
|
||||
const closeButton = document.getElementById('assistant-close');
|
||||
if (closeButton) {
|
||||
closeButton.addEventListener('click', () => this.toggleAssistant(false));
|
||||
}
|
||||
closeButton.addEventListener('click', () => this.toggleAssistant(false));
|
||||
|
||||
// Senden-Button
|
||||
const sendButton = document.getElementById('assistant-send');
|
||||
if (sendButton) {
|
||||
sendButton.addEventListener('click', () => this.sendMessage());
|
||||
}
|
||||
sendButton.addEventListener('click', () => this.sendMessage());
|
||||
|
||||
// Enter-Taste im Eingabefeld
|
||||
if (this.inputField) {
|
||||
this.inputField.addEventListener('keyup', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
this.sendMessage();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Vorschläge klickbar machen
|
||||
if (this.suggestionArea) {
|
||||
this.suggestionArea.addEventListener('click', (e) => {
|
||||
if (e.target.classList.contains('suggestion-pill')) {
|
||||
this.inputField.value = e.target.textContent;
|
||||
this.sendMessage();
|
||||
}
|
||||
});
|
||||
}
|
||||
this.inputField.addEventListener('keyup', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
this.sendMessage();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -213,21 +124,14 @@ class ChatGPTAssistant {
|
||||
*/
|
||||
toggleAssistant(state = null) {
|
||||
const chatContainer = document.getElementById('assistant-chat');
|
||||
if (!chatContainer) return;
|
||||
|
||||
this.isOpen = state !== null ? state : !this.isOpen;
|
||||
|
||||
if (this.isOpen) {
|
||||
chatContainer.classList.remove('max-h-0', 'opacity-0');
|
||||
chatContainer.classList.add('max-h-[32rem]', 'opacity-100');
|
||||
if (this.inputField) this.inputField.focus();
|
||||
|
||||
// Zeige Vorschläge wenn verfügbar
|
||||
if (this.suggestionArea && this.suggestionArea.children.length > 0) {
|
||||
this.suggestionArea.classList.remove('hidden');
|
||||
}
|
||||
chatContainer.classList.add('max-h-96', 'opacity-100');
|
||||
this.inputField.focus();
|
||||
} else {
|
||||
chatContainer.classList.remove('max-h-[32rem]', 'opacity-100');
|
||||
chatContainer.classList.remove('max-h-96', 'opacity-100');
|
||||
chatContainer.classList.add('max-h-0', 'opacity-0');
|
||||
}
|
||||
}
|
||||
@@ -247,79 +151,24 @@ class ChatGPTAssistant {
|
||||
|
||||
const bubble = document.createElement('div');
|
||||
bubble.className = sender === 'user'
|
||||
? 'user-message rounded-lg py-2 px-3 max-w-[85%]'
|
||||
: 'assistant-message rounded-lg py-2 px-3 max-w-[85%]';
|
||||
|
||||
// Nachrichtentext einfügen, falls Markdown-Parser verfügbar, nutzen
|
||||
if (this.markdownParser) {
|
||||
bubble.innerHTML = this.markdownParser.parse(text);
|
||||
} else {
|
||||
bubble.textContent = text;
|
||||
}
|
||||
|
||||
// Links in der Nachricht klickbar machen
|
||||
const links = bubble.querySelectorAll('a');
|
||||
links.forEach(link => {
|
||||
link.target = '_blank';
|
||||
link.rel = 'noopener noreferrer';
|
||||
link.className = 'text-primary-600 dark:text-primary-400 underline';
|
||||
});
|
||||
|
||||
// Code-Blöcke stylen
|
||||
const codeBlocks = bubble.querySelectorAll('pre');
|
||||
codeBlocks.forEach(block => {
|
||||
block.className = 'bg-gray-100 dark:bg-dark-900 p-2 rounded my-2 overflow-x-auto';
|
||||
});
|
||||
|
||||
const inlineCode = bubble.querySelectorAll('code:not(pre code)');
|
||||
inlineCode.forEach(code => {
|
||||
code.className = 'bg-gray-100 dark:bg-dark-900 px-1 rounded font-mono text-sm';
|
||||
});
|
||||
? 'bg-primary-100 dark:bg-primary-900 text-gray-800 dark:text-white rounded-lg py-2 px-3 max-w-[85%]'
|
||||
: 'bg-gray-100 dark:bg-dark-700 text-gray-800 dark:text-white rounded-lg py-2 px-3 max-w-[85%]';
|
||||
bubble.textContent = text;
|
||||
|
||||
messageEl.appendChild(bubble);
|
||||
this.chatHistory.appendChild(messageEl);
|
||||
|
||||
// Scrolle zum Ende des Chat-Verlaufs
|
||||
// Scroll zum Ende des Verlaufs
|
||||
this.chatHistory.scrollTop = this.chatHistory.scrollHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeigt Vorschläge für mögliche Fragen an
|
||||
* @param {Array} suggestions - Array von Vorschlägen
|
||||
*/
|
||||
showSuggestions(suggestions) {
|
||||
if (!this.suggestionArea || !suggestions || !suggestions.length) return;
|
||||
|
||||
// Vorherige Vorschläge entfernen
|
||||
this.suggestionArea.innerHTML = '';
|
||||
|
||||
// Neue Vorschläge hinzufügen
|
||||
suggestions.forEach((text, index) => {
|
||||
const pill = document.createElement('button');
|
||||
pill.className = 'suggestion-pill text-sm px-3 py-1.5 rounded-full bg-primary-100 dark:bg-primary-900 text-primary-800 dark:text-primary-200 hover:bg-primary-200 dark:hover:bg-primary-800 transition-all duration-200';
|
||||
pill.style.animationDelay = `${index * 0.1}s`;
|
||||
pill.textContent = text;
|
||||
this.suggestionArea.appendChild(pill);
|
||||
});
|
||||
|
||||
// Vorschlagsbereich anzeigen
|
||||
this.suggestionArea.classList.remove('hidden');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sendet die Benutzernachricht an den Server und zeigt die Antwort an
|
||||
*/
|
||||
async sendMessage() {
|
||||
if (!this.inputField) return;
|
||||
|
||||
const userInput = this.inputField.value.trim();
|
||||
if (!userInput || this.isLoading) return;
|
||||
|
||||
// Vorschläge ausblenden
|
||||
if (this.suggestionArea) {
|
||||
this.suggestionArea.classList.add('hidden');
|
||||
}
|
||||
|
||||
// Benutzernachricht anzeigen
|
||||
this.addMessage('user', userInput);
|
||||
|
||||
@@ -331,7 +180,6 @@ class ChatGPTAssistant {
|
||||
this.showLoadingIndicator();
|
||||
|
||||
try {
|
||||
console.log('Sende Anfrage an KI-Assistent API...');
|
||||
// Anfrage an den Server senden
|
||||
const response = await fetch('/api/assistant', {
|
||||
method: 'POST',
|
||||
@@ -341,206 +189,92 @@ class ChatGPTAssistant {
|
||||
body: JSON.stringify({
|
||||
messages: this.messages
|
||||
}),
|
||||
cache: 'no-cache', // Kein Cache verwenden
|
||||
credentials: 'same-origin', // Cookies senden
|
||||
timeout: 60000 // 60 Sekunden Timeout
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Netzwerkfehler oder Serverproblem');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Ladeindikator entfernen
|
||||
this.removeLoadingIndicator();
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
let errorMessage;
|
||||
|
||||
try {
|
||||
// Versuche, die Fehlermeldung zu parsen
|
||||
const errorData = JSON.parse(errorText);
|
||||
errorMessage = errorData.error || `Serverfehler: ${response.status} ${response.statusText}`;
|
||||
} catch {
|
||||
// Bei Parsing-Fehler verwende Standardfehlermeldung
|
||||
errorMessage = `Serverfehler: ${response.status} ${response.statusText}`;
|
||||
}
|
||||
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('Antwort erhalten:', data);
|
||||
|
||||
// Antwort anzeigen
|
||||
if (data.response) {
|
||||
this.addMessage('assistant', data.response);
|
||||
|
||||
// Neue Vorschläge basierend auf dem aktuellen Kontext anzeigen
|
||||
this.generateContextualSuggestions();
|
||||
|
||||
// Erfolgreiche Anfrage zurücksetzen
|
||||
this.retryCount = 0;
|
||||
} else if (data.error) {
|
||||
this.addMessage('assistant', `Fehler: ${data.error}`);
|
||||
} else {
|
||||
throw new Error('Unerwartetes Antwortformat vom Server');
|
||||
}
|
||||
this.addMessage('assistant', data.response);
|
||||
} catch (error) {
|
||||
console.error('Fehler bei der Kommunikation mit dem Assistenten:', error);
|
||||
|
||||
// Ladeindikator entfernen, falls noch vorhanden
|
||||
// Ladeindikator entfernen
|
||||
this.removeLoadingIndicator();
|
||||
|
||||
// Spezielle Fehlermeldungen für bestimmte Fehlertypen
|
||||
const errorMessage = error.message || '';
|
||||
let userFriendlyMessage = 'Es gab ein Problem mit der Anfrage.';
|
||||
|
||||
if (errorMessage.includes('timeout') || errorMessage.includes('Zeitüberschreitung')) {
|
||||
userFriendlyMessage = 'Die Antwort hat zu lange gedauert. Der Server ist möglicherweise überlastet.';
|
||||
} else if (errorMessage.includes('500') || errorMessage.includes('Internal Server Error')) {
|
||||
userFriendlyMessage = 'Ein Serverfehler ist aufgetreten. Wir arbeiten an einer Lösung.';
|
||||
} else if (errorMessage.includes('429') || errorMessage.includes('rate limit')) {
|
||||
userFriendlyMessage = 'Die API-Anfragelimits wurden erreicht. Bitte warte einen Moment.';
|
||||
}
|
||||
|
||||
// Fehlermeldung anzeigen oder Wiederholungsversuch starten
|
||||
if (this.retryCount < this.maxRetries) {
|
||||
this.retryCount++;
|
||||
this.addMessage('assistant', `${userFriendlyMessage} Ich versuche es erneut... (Versuch ${this.retryCount}/${this.maxRetries})`);
|
||||
|
||||
// Letzte Benutzernachricht speichern für den Wiederholungsversuch
|
||||
const lastUserMessageIndex = this.messages.findLastIndex(msg => msg.role === 'user');
|
||||
if (lastUserMessageIndex >= 0) {
|
||||
const lastUserMessage = this.messages[lastUserMessageIndex].content;
|
||||
|
||||
// Kurze Verzögerung vor dem erneuten Versuch mit exponentieller Backoff-Strategie
|
||||
const retryDelay = 1500 * Math.pow(2, this.retryCount - 1); // 1.5s, 3s, 6s, ...
|
||||
|
||||
setTimeout(() => {
|
||||
// Entferne Fehlermeldung aus dem Messages-Array, behalte aber die Benutzernachricht
|
||||
this.messages = this.messages.filter(msg =>
|
||||
!(msg.role === 'assistant' && msg.content.includes('versuche es erneut'))
|
||||
);
|
||||
|
||||
// Erneuter Versand mit gleicher Nachricht
|
||||
this.inputField.value = lastUserMessage;
|
||||
this.sendMessage();
|
||||
}, retryDelay);
|
||||
}
|
||||
} else {
|
||||
// Maximale Anzahl an Wiederholungsversuchen erreicht
|
||||
this.addMessage('assistant', 'Es tut mir leid, aber es gab ein Problem bei der Verarbeitung deiner Anfrage. Bitte versuche es später noch einmal oder kontaktiere den Support, falls das Problem weiterhin besteht.');
|
||||
this.retryCount = 0; // Zurücksetzen für die nächste Anfrage
|
||||
}
|
||||
// Fehlermeldung anzeigen
|
||||
this.addMessage('assistant', 'Es tut mir leid, aber es gab ein Problem bei der Verarbeitung deiner Anfrage. Bitte versuche es später noch einmal.');
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert kontextbasierte Vorschläge basierend auf dem aktuellen Chat-Verlauf
|
||||
*/
|
||||
generateContextualSuggestions() {
|
||||
// Basierend auf letzter Antwort des Assistenten, verschiedene Vorschläge generieren
|
||||
const lastAssistantMessage = this.messages.findLast(msg => msg.role === 'assistant')?.content || '';
|
||||
|
||||
let suggestions = [];
|
||||
|
||||
// Intelligente Vorschläge basierend auf Kontext
|
||||
if (lastAssistantMessage.includes('Künstliche Intelligenz') ||
|
||||
lastAssistantMessage.includes('KI ') ||
|
||||
lastAssistantMessage.includes('AI ')) {
|
||||
suggestions = [
|
||||
"Wie wird KI in der Wissenschaft eingesetzt?",
|
||||
"Zeige mir Gedanken zum maschinellen Lernen",
|
||||
"Was ist der Unterschied zwischen KI und ML?"
|
||||
];
|
||||
} else if (lastAssistantMessage.includes('Kategorie') ||
|
||||
lastAssistantMessage.includes('Kategorien')) {
|
||||
suggestions = [
|
||||
"Zeige mir die Unterkategorien",
|
||||
"Welche Gedanken gehören zu dieser Kategorie?",
|
||||
"Liste alle Wissenschaftskategorien auf"
|
||||
];
|
||||
} else if (lastAssistantMessage.includes('Mindmap') ||
|
||||
lastAssistantMessage.includes('Visualisierung')) {
|
||||
suggestions = [
|
||||
"Wie kann ich eine eigene Mindmap erstellen?",
|
||||
"Zeige mir Beispiele für Mindmaps",
|
||||
"Wie funktionieren die Verbindungen in Mindmaps?"
|
||||
];
|
||||
} else {
|
||||
// Standardvorschläge
|
||||
suggestions = [
|
||||
"Erzähle mir mehr dazu",
|
||||
"Gibt es Beispiele dafür?",
|
||||
"Wie kann ich diese Information nutzen?"
|
||||
];
|
||||
}
|
||||
|
||||
this.showSuggestions(suggestions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeigt eine Ladeanimation an
|
||||
* Zeigt einen Ladeindikator im Chat an
|
||||
*/
|
||||
showLoadingIndicator() {
|
||||
if (!this.chatHistory) return;
|
||||
|
||||
// Prüfen, ob bereits ein Ladeindikator angezeigt wird
|
||||
if (document.getElementById('assistant-loading-indicator')) return;
|
||||
|
||||
const loadingEl = document.createElement('div');
|
||||
loadingEl.id = 'assistant-loading';
|
||||
loadingEl.className = 'flex justify-start';
|
||||
loadingEl.id = 'assistant-loading-indicator';
|
||||
|
||||
const bubble = document.createElement('div');
|
||||
bubble.className = 'assistant-message rounded-lg py-3 px-4 max-w-[85%] flex items-center';
|
||||
bubble.className = 'bg-gray-100 dark:bg-dark-700 text-gray-800 dark:text-white rounded-lg py-2 px-3';
|
||||
bubble.innerHTML = '<div class="typing-indicator"><span></span><span></span><span></span></div>';
|
||||
|
||||
const typingIndicator = document.createElement('div');
|
||||
typingIndicator.className = 'typing-indicator';
|
||||
typingIndicator.innerHTML = `
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
`;
|
||||
|
||||
bubble.appendChild(typingIndicator);
|
||||
loadingEl.appendChild(bubble);
|
||||
|
||||
this.chatHistory.appendChild(loadingEl);
|
||||
|
||||
// Scroll zum Ende des Verlaufs
|
||||
this.chatHistory.scrollTop = this.chatHistory.scrollHeight;
|
||||
|
||||
// Stil für den Typing-Indikator
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.typing-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.typing-indicator span {
|
||||
height: 8px;
|
||||
width: 8px;
|
||||
background-color: #888;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin: 0 2px;
|
||||
opacity: 0.4;
|
||||
animation: typing 1.5s infinite ease-in-out;
|
||||
}
|
||||
.typing-indicator span:nth-child(2) {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
.typing-indicator span:nth-child(3) {
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
@keyframes typing {
|
||||
0% { transform: translateY(0); }
|
||||
50% { transform: translateY(-5px); }
|
||||
100% { transform: translateY(0); }
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
/**
|
||||
* Entfernt den Ladeindikator aus dem Chat
|
||||
*/
|
||||
removeLoadingIndicator() {
|
||||
const loadingIndicator = document.getElementById('assistant-loading-indicator');
|
||||
if (loadingIndicator) {
|
||||
loadingIndicator.remove();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Öffnet den Assistenten und sendet eine vorgegebene Frage
|
||||
* @param {string} question - Die zu stellende Frage
|
||||
*/
|
||||
async sendQuestion(question) {
|
||||
if (!question || this.isLoading) return;
|
||||
|
||||
// Assistenten öffnen
|
||||
this.toggleAssistant(true);
|
||||
|
||||
// Kurze Verzögerung, um sicherzustellen, dass der UI vollständig geöffnet ist
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
|
||||
// Frage in Eingabefeld setzen
|
||||
if (this.inputField) {
|
||||
this.inputField.value = question;
|
||||
|
||||
// Sende die Frage
|
||||
this.sendMessage();
|
||||
const loadingEl = document.getElementById('assistant-loading');
|
||||
if (loadingEl) {
|
||||
loadingEl.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mache die Klasse global verfügbar
|
||||
window.ChatGPTAssistant = ChatGPTAssistant;
|
||||
// Exportiere die Klasse für die Verwendung in anderen Modulen
|
||||
export default ChatGPTAssistant;
|
||||
95
static/js/modules/cyber-network-init.js
Normal file
95
static/js/modules/cyber-network-init.js
Normal file
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
240
static/js/modules/cyber-network.js
Normal file
240
static/js/modules/cyber-network.js
Normal file
@@ -0,0 +1,240 @@
|
||||
/**
|
||||
* 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
777
static/js/modules/mindmap.js
Normal file
777
static/js/modules/mindmap.js
Normal file
@@ -0,0 +1,777 @@
|
||||
/**
|
||||
* MindMap D3.js Modul
|
||||
* Visualisiert die Mindmap mit D3.js
|
||||
*/
|
||||
|
||||
class MindMapVisualization {
|
||||
constructor(containerSelector, options = {}) {
|
||||
this.containerSelector = containerSelector;
|
||||
this.container = d3.select(containerSelector);
|
||||
this.width = options.width || this.container.node().clientWidth || 800;
|
||||
this.height = options.height || 600;
|
||||
this.nodeRadius = options.nodeRadius || 14;
|
||||
this.selectedNodeRadius = options.selectedNodeRadius || 20;
|
||||
this.linkDistance = options.linkDistance || 150;
|
||||
this.chargeStrength = options.chargeStrength || -900;
|
||||
this.centerForce = options.centerForce || 0.15;
|
||||
this.onNodeClick = options.onNodeClick || ((node) => console.log('Node clicked:', node));
|
||||
|
||||
this.nodes = [];
|
||||
this.links = [];
|
||||
this.simulation = null;
|
||||
this.svg = null;
|
||||
this.linkElements = null;
|
||||
this.nodeElements = null;
|
||||
this.textElements = null;
|
||||
this.tooltipEnabled = options.tooltipEnabled !== undefined ? options.tooltipEnabled : true;
|
||||
|
||||
this.mouseoverNode = null;
|
||||
this.selectedNode = null;
|
||||
|
||||
this.zoomFactor = 1;
|
||||
this.tooltipDiv = null;
|
||||
this.isLoading = true;
|
||||
|
||||
// Lade die gemerkten Knoten
|
||||
this.bookmarkedNodes = this.loadBookmarkedNodes();
|
||||
|
||||
// Sicherstellen, dass der Container bereit ist
|
||||
if (this.container.node()) {
|
||||
this.init();
|
||||
this.setupDefaultNodes();
|
||||
|
||||
// Sofortige Datenladung
|
||||
window.setTimeout(() => {
|
||||
this.loadData();
|
||||
}, 100);
|
||||
} else {
|
||||
console.error('Mindmap-Container nicht gefunden:', containerSelector);
|
||||
}
|
||||
}
|
||||
|
||||
// Standardknoten als Fallback einrichten, falls die API nicht reagiert
|
||||
setupDefaultNodes() {
|
||||
// Basis-Mindmap mit Hauptthemen
|
||||
const defaultNodes = [
|
||||
{ id: "root", name: "Wissen", description: "Zentrale Wissensbasis", thought_count: 0 },
|
||||
{ id: "philosophy", name: "Philosophie", description: "Philosophisches Denken", thought_count: 0 },
|
||||
{ id: "science", name: "Wissenschaft", description: "Wissenschaftliche Erkenntnisse", thought_count: 0 },
|
||||
{ id: "technology", name: "Technologie", description: "Technologische Entwicklungen", thought_count: 0 },
|
||||
{ id: "arts", name: "Künste", description: "Künstlerische Ausdrucksformen", thought_count: 0 }
|
||||
];
|
||||
|
||||
const defaultLinks = [
|
||||
{ source: "root", target: "philosophy" },
|
||||
{ source: "root", target: "science" },
|
||||
{ source: "root", target: "technology" },
|
||||
{ source: "root", target: "arts" }
|
||||
];
|
||||
|
||||
// Als Fallback verwenden, falls die API fehlschlägt
|
||||
this.defaultNodes = defaultNodes;
|
||||
this.defaultLinks = defaultLinks;
|
||||
}
|
||||
|
||||
init() {
|
||||
// SVG erstellen, wenn noch nicht vorhanden
|
||||
if (!this.svg) {
|
||||
// Container zuerst leeren
|
||||
this.container.html('');
|
||||
|
||||
this.svg = this.container
|
||||
.append('svg')
|
||||
.attr('width', '100%')
|
||||
.attr('height', this.height)
|
||||
.attr('viewBox', `0 0 ${this.width} ${this.height}`)
|
||||
.attr('class', 'mindmap-svg')
|
||||
.call(
|
||||
d3.zoom()
|
||||
.scaleExtent([0.1, 5])
|
||||
.on('zoom', (event) => {
|
||||
this.handleZoom(event.transform);
|
||||
})
|
||||
);
|
||||
|
||||
// Hauptgruppe für alles, was zoom-transformierbar ist
|
||||
this.g = this.svg.append('g');
|
||||
|
||||
// Tooltip initialisieren
|
||||
if (!d3.select('body').select('.node-tooltip').size()) {
|
||||
this.tooltipDiv = d3.select('body')
|
||||
.append('div')
|
||||
.attr('class', 'node-tooltip')
|
||||
.style('opacity', 0)
|
||||
.style('position', 'absolute')
|
||||
.style('pointer-events', 'none')
|
||||
.style('background', 'rgba(20, 20, 40, 0.9)')
|
||||
.style('color', '#ffffff')
|
||||
.style('border', '1px solid rgba(160, 80, 255, 0.2)')
|
||||
.style('border-radius', '6px')
|
||||
.style('padding', '8px 12px')
|
||||
.style('font-size', '14px')
|
||||
.style('max-width', '250px')
|
||||
.style('box-shadow', '0 10px 25px rgba(0, 0, 0, 0.5), 0 0 10px rgba(160, 80, 255, 0.2)');
|
||||
} else {
|
||||
this.tooltipDiv = d3.select('body').select('.node-tooltip');
|
||||
}
|
||||
}
|
||||
|
||||
// Force-Simulation initialisieren
|
||||
this.simulation = d3.forceSimulation()
|
||||
.force('link', d3.forceLink().id(d => d.id).distance(this.linkDistance))
|
||||
.force('charge', d3.forceManyBody().strength(this.chargeStrength))
|
||||
.force('center', d3.forceCenter(this.width / 2, this.height / 2).strength(this.centerForce))
|
||||
.force('collision', d3.forceCollide().radius(this.nodeRadius * 2));
|
||||
|
||||
// Globale Mindmap-Instanz für externe Zugriffe setzen
|
||||
window.mindmapInstance = this;
|
||||
}
|
||||
|
||||
handleZoom(transform) {
|
||||
this.g.attr('transform', transform);
|
||||
this.zoomFactor = transform.k;
|
||||
|
||||
// Knotengröße anpassen, um bei Zoom lesbar zu bleiben
|
||||
if (this.nodeElements) {
|
||||
this.nodeElements
|
||||
.attr('r', d => (d === this.selectedNode ? this.selectedNodeRadius : this.nodeRadius) / Math.sqrt(transform.k));
|
||||
}
|
||||
|
||||
// Textgröße anpassen
|
||||
if (this.textElements) {
|
||||
this.textElements
|
||||
.style('font-size', `${12 / Math.sqrt(transform.k)}px`);
|
||||
}
|
||||
}
|
||||
|
||||
async loadData() {
|
||||
try {
|
||||
// Ladeindikator anzeigen
|
||||
this.showLoading();
|
||||
|
||||
// Verwende sofort die Standarddaten für eine schnelle erste Anzeige
|
||||
this.nodes = [...this.defaultNodes];
|
||||
this.links = [...this.defaultLinks];
|
||||
|
||||
// Visualisierung sofort aktualisieren
|
||||
this.isLoading = false;
|
||||
this.updateVisualization();
|
||||
|
||||
// Status auf bereit setzen - don't wait for API
|
||||
this.container.attr('data-status', 'ready');
|
||||
|
||||
// API-Aufruf mit kürzerem Timeout im Hintergrund durchführen
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 Sekunden Timeout - reduced from 10
|
||||
|
||||
const response = await fetch('/api/mindmap', {
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache',
|
||||
'Pragma': 'no-cache'
|
||||
}
|
||||
});
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn(`HTTP Fehler: ${response.status}, verwende Standarddaten`);
|
||||
return; // Keep using default data
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data || !data.nodes || data.nodes.length === 0) {
|
||||
console.warn('Keine Mindmap-Daten vorhanden, verwende weiterhin Standard-Daten.');
|
||||
return; // Keep using default data
|
||||
}
|
||||
|
||||
// Flache Liste von Knoten und Verbindungen erstellen
|
||||
this.nodes = [];
|
||||
this.links = [];
|
||||
this.processHierarchicalData(data.nodes);
|
||||
|
||||
// Visualisierung aktualisieren mit den tatsächlichen Daten
|
||||
this.updateVisualization();
|
||||
|
||||
} catch (error) {
|
||||
console.warn('Fehler beim Laden der Mindmap-Daten, verwende Standarddaten:', error);
|
||||
// Already using default data, no action needed
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Kritischer Fehler bei der Mindmap-Darstellung:', error);
|
||||
this.showError('Fehler beim Laden der Mindmap-Daten. Bitte laden Sie die Seite neu.');
|
||||
this.container.attr('data-status', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
showLoading() {
|
||||
// Element nur leeren, wenn es noch kein SVG enthält
|
||||
if (!this.container.select('svg').size()) {
|
||||
this.container.html(`
|
||||
<div class="flex justify-center items-center h-full">
|
||||
<div class="text-center">
|
||||
<div class="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-primary-400 mx-auto mb-4"></div>
|
||||
<p class="text-lg text-white">Mindmap wird geladen...</p>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
processHierarchicalData(hierarchicalNodes, parentId = null) {
|
||||
hierarchicalNodes.forEach(node => {
|
||||
// Knoten hinzufügen, wenn noch nicht vorhanden
|
||||
if (!this.nodes.find(n => n.id === node.id)) {
|
||||
this.nodes.push({
|
||||
id: node.id,
|
||||
name: node.name,
|
||||
description: node.description || '',
|
||||
thought_count: node.thought_count || 0,
|
||||
color: this.generateColorFromString(node.name),
|
||||
});
|
||||
}
|
||||
|
||||
// Verbindung zum Elternknoten hinzufügen
|
||||
if (parentId !== null) {
|
||||
this.links.push({
|
||||
source: parentId,
|
||||
target: node.id
|
||||
});
|
||||
}
|
||||
|
||||
// Rekursiv für Kindknoten aufrufen
|
||||
if (node.children && node.children.length > 0) {
|
||||
this.processHierarchicalData(node.children, node.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
generateColorFromString(str) {
|
||||
// Erzeugt eine deterministische Farbe basierend auf dem String
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
||||
}
|
||||
|
||||
// Verwende deterministische Farbe aus unserem Farbschema
|
||||
const colors = [
|
||||
'#4080ff', // primary-400
|
||||
'#a040ff', // secondary-400
|
||||
'#205cf5', // primary-500
|
||||
'#8020f5', // secondary-500
|
||||
'#1040e0', // primary-600
|
||||
'#6010e0', // secondary-600
|
||||
];
|
||||
|
||||
return colors[Math.abs(hash) % colors.length];
|
||||
}
|
||||
|
||||
updateVisualization() {
|
||||
// Starte die Visualisierung nur, wenn nicht mehr im Ladezustand
|
||||
if (this.isLoading) return;
|
||||
|
||||
// Container leeren, wenn Diagramm neu erstellt wird
|
||||
if (!this.svg) {
|
||||
this.container.html('');
|
||||
this.init();
|
||||
}
|
||||
|
||||
// Performance-Optimierung: Deaktiviere Transition während des Datenladens
|
||||
const useTransitions = false;
|
||||
|
||||
// Links (Edges) erstellen
|
||||
this.linkElements = this.g.selectAll('.link')
|
||||
.data(this.links)
|
||||
.join(
|
||||
enter => enter.append('line')
|
||||
.attr('class', 'link')
|
||||
.attr('stroke', '#ffffff30')
|
||||
.attr('stroke-width', 2)
|
||||
.attr('stroke-dasharray', d => d.relation_type === 'contradicts' ? '5,5' : null)
|
||||
.attr('marker-end', d => d.relation_type === 'builds_upon' ? 'url(#arrowhead)' : null),
|
||||
update => update
|
||||
.attr('stroke', '#ffffff30')
|
||||
.attr('stroke-dasharray', d => d.relation_type === 'contradicts' ? '5,5' : null)
|
||||
.attr('marker-end', d => d.relation_type === 'builds_upon' ? 'url(#arrowhead)' : null),
|
||||
exit => exit.remove()
|
||||
);
|
||||
|
||||
// Pfeilspitze für gerichtete Beziehungen hinzufügen (falls noch nicht vorhanden)
|
||||
if (!this.svg.select('defs').node()) {
|
||||
const defs = this.svg.append('defs');
|
||||
defs.append('marker')
|
||||
.attr('id', 'arrowhead')
|
||||
.attr('viewBox', '0 -5 10 10')
|
||||
.attr('refX', 20)
|
||||
.attr('refY', 0)
|
||||
.attr('orient', 'auto')
|
||||
.attr('markerWidth', 6)
|
||||
.attr('markerHeight', 6)
|
||||
.append('path')
|
||||
.attr('d', 'M0,-5L10,0L0,5')
|
||||
.attr('fill', '#ffffff50');
|
||||
}
|
||||
|
||||
// Simplified Effekte definieren, falls noch nicht vorhanden
|
||||
if (!this.svg.select('#glow').node()) {
|
||||
const defs = this.svg.select('defs').size() ? this.svg.select('defs') : this.svg.append('defs');
|
||||
|
||||
// Glow-Effekt für Knoten
|
||||
const filter = defs.append('filter')
|
||||
.attr('id', 'glow')
|
||||
.attr('x', '-50%')
|
||||
.attr('y', '-50%')
|
||||
.attr('width', '200%')
|
||||
.attr('height', '200%');
|
||||
|
||||
filter.append('feGaussianBlur')
|
||||
.attr('stdDeviation', '1')
|
||||
.attr('result', 'blur');
|
||||
|
||||
filter.append('feComposite')
|
||||
.attr('in', 'SourceGraphic')
|
||||
.attr('in2', 'blur')
|
||||
.attr('operator', 'over');
|
||||
|
||||
// Blur-Effekt für Schatten
|
||||
const blurFilter = defs.append('filter')
|
||||
.attr('id', 'blur')
|
||||
.attr('x', '-50%')
|
||||
.attr('y', '-50%')
|
||||
.attr('width', '200%')
|
||||
.attr('height', '200%');
|
||||
|
||||
blurFilter.append('feGaussianBlur')
|
||||
.attr('stdDeviation', '1');
|
||||
}
|
||||
|
||||
// Knoten-Gruppe erstellen/aktualisieren
|
||||
const nodeGroups = this.g.selectAll('.node-group')
|
||||
.data(this.nodes)
|
||||
.join(
|
||||
enter => {
|
||||
const group = enter.append('g')
|
||||
.attr('class', 'node-group')
|
||||
.call(d3.drag()
|
||||
.on('start', (event, d) => this.dragStarted(event, d))
|
||||
.on('drag', (event, d) => this.dragged(event, d))
|
||||
.on('end', (event, d) => this.dragEnded(event, d)));
|
||||
|
||||
// Hintergrundschatten für besseren Kontrast
|
||||
group.append('circle')
|
||||
.attr('class', 'node-shadow')
|
||||
.attr('r', d => this.nodeRadius * 1.2)
|
||||
.attr('fill', 'rgba(0, 0, 0, 0.3)')
|
||||
.attr('filter', 'url(#blur)');
|
||||
|
||||
// Kreis für jeden Knoten
|
||||
group.append('circle')
|
||||
.attr('class', d => `node ${this.isNodeBookmarked(d.id) ? 'bookmarked' : ''}`)
|
||||
.attr('r', this.nodeRadius)
|
||||
.attr('fill', d => d.color || this.generateColorFromString(d.name))
|
||||
.attr('stroke', d => this.isNodeBookmarked(d.id) ? '#FFD700' : '#ffffff50')
|
||||
.attr('stroke-width', d => this.isNodeBookmarked(d.id) ? 3 : 2)
|
||||
.attr('filter', 'url(#glow)');
|
||||
|
||||
// Text-Label mit besserem Kontrast
|
||||
group.append('text')
|
||||
.attr('class', 'node-label')
|
||||
.attr('dy', '0.35em')
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('fill', '#ffffff')
|
||||
.attr('stroke', 'rgba(0, 0, 0, 0.4)')
|
||||
.attr('stroke-width', '0.7px')
|
||||
.attr('paint-order', 'stroke')
|
||||
.style('font-size', '12px')
|
||||
.style('font-weight', '500')
|
||||
.style('pointer-events', 'none')
|
||||
.text(d => d.name.length > 12 ? d.name.slice(0, 10) + '...' : d.name);
|
||||
|
||||
// Interaktivität hinzufügen
|
||||
group
|
||||
.on('mouseover', (event, d) => this.nodeMouseover(event, d))
|
||||
.on('mouseout', (event, d) => this.nodeMouseout(event, d))
|
||||
.on('click', (event, d) => this.nodeClicked(event, d));
|
||||
|
||||
return group;
|
||||
},
|
||||
update => {
|
||||
// Knoten aktualisieren
|
||||
update.select('.node')
|
||||
.attr('class', d => `node ${this.isNodeBookmarked(d.id) ? 'bookmarked' : ''}`)
|
||||
.attr('fill', d => d.color || this.generateColorFromString(d.name))
|
||||
.attr('stroke', d => this.isNodeBookmarked(d.id) ? '#FFD700' : '#ffffff50')
|
||||
.attr('stroke-width', d => this.isNodeBookmarked(d.id) ? 3 : 2);
|
||||
|
||||
// Text aktualisieren
|
||||
update.select('.node-label')
|
||||
.text(d => d.name.length > 12 ? d.name.slice(0, 10) + '...' : d.name);
|
||||
|
||||
return update;
|
||||
},
|
||||
exit => exit.remove()
|
||||
);
|
||||
|
||||
// Einzelne Elemente für direkten Zugriff speichern
|
||||
this.nodeElements = this.g.selectAll('.node');
|
||||
this.textElements = this.g.selectAll('.node-label');
|
||||
|
||||
// Performance-Optimierung: Weniger Simulationsschritte für schnellere Stabilisierung
|
||||
this.simulation
|
||||
.nodes(this.nodes)
|
||||
.on('tick', () => this.ticked())
|
||||
.alpha(0.3) // Reduzierter Wert für schnellere Stabilisierung
|
||||
.alphaDecay(0.05); // Erhöhter Wert für schnellere Stabilisierung
|
||||
|
||||
this.simulation.force('link')
|
||||
.links(this.links);
|
||||
|
||||
// Simulation neu starten
|
||||
this.simulation.restart();
|
||||
|
||||
// Update connection counts
|
||||
this.updateConnectionCounts();
|
||||
}
|
||||
|
||||
ticked() {
|
||||
// Linienpositionen aktualisieren
|
||||
this.linkElements
|
||||
.attr('x1', d => d.source.x)
|
||||
.attr('y1', d => d.source.y)
|
||||
.attr('x2', d => d.target.x)
|
||||
.attr('y2', d => d.target.y);
|
||||
|
||||
// Knotenpositionen aktualisieren
|
||||
this.g.selectAll('.node-group')
|
||||
.attr('transform', d => `translate(${d.x}, ${d.y})`);
|
||||
}
|
||||
|
||||
dragStarted(event, d) {
|
||||
if (!event.active) this.simulation.alphaTarget(0.3).restart();
|
||||
d.fx = d.x;
|
||||
d.fy = d.y;
|
||||
}
|
||||
|
||||
dragged(event, d) {
|
||||
d.fx = event.x;
|
||||
d.fy = event.y;
|
||||
}
|
||||
|
||||
dragEnded(event, d) {
|
||||
if (!event.active) this.simulation.alphaTarget(0);
|
||||
d.fx = null;
|
||||
d.fy = null;
|
||||
}
|
||||
|
||||
nodeMouseover(event, d) {
|
||||
this.mouseoverNode = d;
|
||||
|
||||
// Tooltip anzeigen
|
||||
if (this.tooltipEnabled) {
|
||||
const isBookmarked = this.isNodeBookmarked(d.id);
|
||||
const tooltipContent = `
|
||||
<div class="p-2">
|
||||
<strong>${d.name}</strong>
|
||||
${d.description ? `<p class="text-sm text-gray-200 mt-1">${d.description}</p>` : ''}
|
||||
<div class="text-xs text-gray-300 mt-1">
|
||||
Gedanken: ${d.thought_count}
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<button id="bookmark-button" class="px-2 py-1 text-xs rounded bg-gray-700 hover:bg-gray-600 text-white"
|
||||
data-nodeid="${d.id}">
|
||||
${isBookmarked ? '<i class="fas fa-bookmark mr-1"></i> Gemerkt' : '<i class="far fa-bookmark mr-1"></i> Merken'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.tooltipDiv
|
||||
.html(tooltipContent)
|
||||
.style('left', (event.pageX + 10) + 'px')
|
||||
.style('top', (event.pageY - 10) + 'px')
|
||||
.transition()
|
||||
.duration(200)
|
||||
.style('opacity', 1);
|
||||
|
||||
// Event-Listener für den Bookmark-Button hinzufügen
|
||||
document.getElementById('bookmark-button').addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const nodeId = e.currentTarget.getAttribute('data-nodeid');
|
||||
const isNowBookmarked = this.toggleBookmark(nodeId);
|
||||
|
||||
// Button-Text aktualisieren
|
||||
if (isNowBookmarked) {
|
||||
e.currentTarget.innerHTML = '<i class="fas fa-bookmark mr-1"></i> Gemerkt';
|
||||
} else {
|
||||
e.currentTarget.innerHTML = '<i class="far fa-bookmark mr-1"></i> Merken';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Knoten visuell hervorheben
|
||||
d3.select(event.currentTarget).select('circle')
|
||||
.transition()
|
||||
.duration(200)
|
||||
.attr('r', this.nodeRadius * 1.2)
|
||||
.attr('stroke', this.isNodeBookmarked(d.id) ? '#FFD700' : '#ffffff');
|
||||
}
|
||||
|
||||
nodeMouseout(event, d) {
|
||||
this.mouseoverNode = null;
|
||||
|
||||
// Tooltip ausblenden
|
||||
if (this.tooltipEnabled) {
|
||||
this.tooltipDiv
|
||||
.transition()
|
||||
.duration(200)
|
||||
.style('opacity', 0);
|
||||
}
|
||||
|
||||
// Knoten-Stil zurücksetzen, wenn nicht ausgewählt
|
||||
const nodeElement = d3.select(event.currentTarget).select('circle');
|
||||
if (d !== this.selectedNode) {
|
||||
const isBookmarked = this.isNodeBookmarked(d.id);
|
||||
nodeElement
|
||||
.transition()
|
||||
.duration(200)
|
||||
.attr('r', this.nodeRadius)
|
||||
.attr('stroke', isBookmarked ? '#FFD700' : '#ffffff50')
|
||||
.attr('stroke-width', isBookmarked ? 3 : 2);
|
||||
}
|
||||
}
|
||||
|
||||
nodeClicked(event, d) {
|
||||
// Frühere Auswahl zurücksetzen
|
||||
if (this.selectedNode && this.selectedNode !== d) {
|
||||
this.g.selectAll('.node')
|
||||
.filter(n => n === this.selectedNode)
|
||||
.transition()
|
||||
.duration(200)
|
||||
.attr('r', this.nodeRadius)
|
||||
.attr('stroke', '#ffffff50');
|
||||
}
|
||||
|
||||
// Neue Auswahl hervorheben
|
||||
if (this.selectedNode !== d) {
|
||||
this.selectedNode = d;
|
||||
d3.select(event.currentTarget).select('circle')
|
||||
.transition()
|
||||
.duration(200)
|
||||
.attr('r', this.selectedNodeRadius)
|
||||
.attr('stroke', '#ffffff');
|
||||
}
|
||||
|
||||
// Callback mit Node-Daten aufrufen
|
||||
this.onNodeClick(d);
|
||||
}
|
||||
|
||||
showError(message) {
|
||||
this.container.html(`
|
||||
<div class="w-full text-center p-6">
|
||||
<div class="mb-4 text-red-500">
|
||||
<i class="fas fa-exclamation-triangle text-4xl"></i>
|
||||
</div>
|
||||
<p class="text-lg text-gray-200">${message}</p>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
// Fokussiert die Ansicht auf einen bestimmten Knoten
|
||||
focusNode(nodeId) {
|
||||
const node = this.nodes.find(n => n.id === nodeId);
|
||||
if (!node) return;
|
||||
|
||||
// Simuliere einen Klick auf den Knoten
|
||||
const nodeElement = this.g.selectAll('.node-group')
|
||||
.filter(d => d.id === nodeId);
|
||||
|
||||
nodeElement.dispatch('click');
|
||||
|
||||
// Zentriere den Knoten in der Ansicht
|
||||
const transform = d3.zoomIdentity
|
||||
.translate(this.width / 2, this.height / 2)
|
||||
.scale(1.2)
|
||||
.translate(-node.x, -node.y);
|
||||
|
||||
this.svg.transition()
|
||||
.duration(750)
|
||||
.call(
|
||||
d3.zoom().transform,
|
||||
transform
|
||||
);
|
||||
}
|
||||
|
||||
// Filtert die Mindmap basierend auf einem Suchbegriff
|
||||
filterBySearchTerm(searchTerm) {
|
||||
if (!searchTerm || searchTerm.trim() === '') {
|
||||
// Alle Knoten anzeigen
|
||||
this.g.selectAll('.node-group')
|
||||
.style('opacity', 1)
|
||||
.style('pointer-events', 'all');
|
||||
|
||||
this.g.selectAll('.link')
|
||||
.style('opacity', 1);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const searchLower = searchTerm.toLowerCase();
|
||||
const matchingNodes = this.nodes.filter(node =>
|
||||
node.name.toLowerCase().includes(searchLower) ||
|
||||
(node.description && node.description.toLowerCase().includes(searchLower))
|
||||
);
|
||||
|
||||
const matchingNodeIds = new Set(matchingNodes.map(n => n.id));
|
||||
|
||||
// Passende Knoten hervorheben, andere ausblenden
|
||||
this.g.selectAll('.node-group')
|
||||
.style('opacity', d => matchingNodeIds.has(d.id) ? 1 : 0.2)
|
||||
.style('pointer-events', d => matchingNodeIds.has(d.id) ? 'all' : 'none');
|
||||
|
||||
// Verbindungen zwischen passenden Knoten hervorheben
|
||||
this.g.selectAll('.link')
|
||||
.style('opacity', d =>
|
||||
matchingNodeIds.has(d.source.id) && matchingNodeIds.has(d.target.id) ? 1 : 0.1
|
||||
);
|
||||
|
||||
// Auf den ersten passenden Knoten fokussieren, wenn vorhanden
|
||||
if (matchingNodes.length > 0) {
|
||||
this.focusNode(matchingNodes[0].id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the thought_count property for each node based on existing connections
|
||||
*/
|
||||
updateConnectionCounts() {
|
||||
// Reset all counts first
|
||||
this.nodes.forEach(node => {
|
||||
// Initialize thought_count if it doesn't exist
|
||||
if (typeof node.thought_count !== 'number') {
|
||||
node.thought_count = 0;
|
||||
}
|
||||
|
||||
// Count connections for this node
|
||||
const connectedNodes = this.getConnectedNodes(node);
|
||||
node.thought_count = connectedNodes.length;
|
||||
});
|
||||
|
||||
// Update UI to show counts
|
||||
this.updateNodeLabels();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the visual representation of node labels to include connection counts
|
||||
*/
|
||||
updateNodeLabels() {
|
||||
if (!this.textElements) return;
|
||||
|
||||
this.textElements.text(d => {
|
||||
if (d.thought_count > 0) {
|
||||
return `${d.name} (${d.thought_count})`;
|
||||
}
|
||||
return d.name;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new connection between nodes and updates the counts
|
||||
*/
|
||||
addConnection(sourceNode, targetNode) {
|
||||
if (!sourceNode || !targetNode) return false;
|
||||
|
||||
// Check if connection already exists
|
||||
if (this.isConnected(sourceNode, targetNode)) return false;
|
||||
|
||||
// Add new connection
|
||||
this.links.push({
|
||||
source: sourceNode.id,
|
||||
target: targetNode.id
|
||||
});
|
||||
|
||||
// Update counts
|
||||
this.updateConnectionCounts();
|
||||
|
||||
// Update visualization
|
||||
this.updateVisualization();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Lädt gemerkete Knoten aus dem LocalStorage
|
||||
loadBookmarkedNodes() {
|
||||
try {
|
||||
const bookmarked = localStorage.getItem('bookmarkedNodes');
|
||||
return bookmarked ? JSON.parse(bookmarked) : [];
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der gemerkten Knoten:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Speichert gemerkete Knoten im LocalStorage
|
||||
saveBookmarkedNodes() {
|
||||
try {
|
||||
localStorage.setItem('bookmarkedNodes', JSON.stringify(this.bookmarkedNodes));
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Speichern der gemerkten Knoten:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Prüft, ob ein Knoten gemerkt ist
|
||||
isNodeBookmarked(nodeId) {
|
||||
return this.bookmarkedNodes.includes(nodeId);
|
||||
}
|
||||
|
||||
// Merkt einen Knoten oder hebt die Markierung auf
|
||||
toggleBookmark(nodeId) {
|
||||
const index = this.bookmarkedNodes.indexOf(nodeId);
|
||||
if (index === -1) {
|
||||
// Node hinzufügen
|
||||
this.bookmarkedNodes.push(nodeId);
|
||||
this.updateNodeAppearance(nodeId, true);
|
||||
} else {
|
||||
// Node entfernen
|
||||
this.bookmarkedNodes.splice(index, 1);
|
||||
this.updateNodeAppearance(nodeId, false);
|
||||
}
|
||||
|
||||
// Änderungen speichern
|
||||
this.saveBookmarkedNodes();
|
||||
|
||||
// Event auslösen für andere Komponenten
|
||||
const event = new CustomEvent('nodeBookmarkToggled', {
|
||||
detail: {
|
||||
nodeId: nodeId,
|
||||
isBookmarked: index === -1
|
||||
}
|
||||
});
|
||||
document.dispatchEvent(event);
|
||||
|
||||
return index === -1; // true wenn jetzt gemerkt, false wenn Markierung aufgehoben
|
||||
}
|
||||
|
||||
// Aktualisiert das Aussehen eines Knotens basierend auf Bookmark-Status
|
||||
updateNodeAppearance(nodeId, isBookmarked) {
|
||||
this.g.selectAll('.node-group')
|
||||
.filter(d => d.id === nodeId)
|
||||
.select('.node')
|
||||
.classed('bookmarked', isBookmarked)
|
||||
.attr('stroke', isBookmarked ? '#FFD700' : '#ffffff50')
|
||||
.attr('stroke-width', isBookmarked ? 3 : 2);
|
||||
}
|
||||
|
||||
// Aktualisiert das Aussehen aller gemerkten Knoten
|
||||
updateAllBookmarkedNodes() {
|
||||
this.g.selectAll('.node-group')
|
||||
.each((d) => {
|
||||
const isBookmarked = this.isNodeBookmarked(d.id);
|
||||
this.updateNodeAppearance(d.id, isBookmarked);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Exportiere die Klasse für die Verwendung in anderen Modulen
|
||||
window.MindMapVisualization = MindMapVisualization;
|
||||
1133
static/js/social.js
1133
static/js/social.js
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
1834
static/mindmap.js
Normal file
1834
static/mindmap.js
Normal file
File diff suppressed because it is too large
Load Diff
88
static/network-animation.js
Normal file
88
static/network-animation.js
Normal file
@@ -0,0 +1,88 @@
|
||||
// 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);
|
||||
});
|
||||
232
static/network-background.js
Normal file
232
static/network-background.js
Normal file
@@ -0,0 +1,232 @@
|
||||
// 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);
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,415 +0,0 @@
|
||||
/**
|
||||
* Vereinfachter Neuronales Netzwerk Hintergrund
|
||||
* Verwendet Canvas 2D anstelle von WebGL für bessere Leistung
|
||||
*/
|
||||
|
||||
class NeuralNetworkBackground {
|
||||
constructor() {
|
||||
// Canvas einrichten
|
||||
this.canvas = document.createElement('canvas');
|
||||
this.canvas.id = 'neural-network-background';
|
||||
this.canvas.style.position = 'fixed';
|
||||
this.canvas.style.top = '0';
|
||||
this.canvas.style.left = '0';
|
||||
this.canvas.style.width = '100%';
|
||||
this.canvas.style.height = '100%';
|
||||
this.canvas.style.zIndex = '-10';
|
||||
this.canvas.style.pointerEvents = 'none';
|
||||
this.canvas.style.opacity = '1';
|
||||
this.canvas.style.transition = 'opacity 3.5s ease-in-out';
|
||||
|
||||
// Falls Canvas bereits existiert, entfernen
|
||||
const existingCanvas = document.getElementById('neural-network-background');
|
||||
if (existingCanvas) {
|
||||
existingCanvas.remove();
|
||||
}
|
||||
|
||||
// An body anhängen als erstes Kind
|
||||
if (document.body.firstChild) {
|
||||
document.body.insertBefore(this.canvas, document.body.firstChild);
|
||||
} else {
|
||||
document.body.appendChild(this.canvas);
|
||||
}
|
||||
|
||||
// 2D Context
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
|
||||
// Eigenschaften
|
||||
this.nodes = [];
|
||||
this.connections = [];
|
||||
this.activeConnections = new Set();
|
||||
this.animationFrameId = null;
|
||||
this.isDestroying = false;
|
||||
|
||||
// Farben für Dark/Light Mode
|
||||
this.colors = {
|
||||
dark: {
|
||||
background: '#040215',
|
||||
nodeColor: '#6a5498',
|
||||
nodePulse: '#9c7fe0',
|
||||
connectionColor: '#4a3870',
|
||||
flowColor: '#b47fea'
|
||||
},
|
||||
light: {
|
||||
background: '#f8f9fc',
|
||||
nodeColor: '#8c6db5',
|
||||
nodePulse: '#b094dd',
|
||||
connectionColor: '#9882bd',
|
||||
flowColor: '#7d5bb5'
|
||||
}
|
||||
};
|
||||
|
||||
// Aktuelle Farbpalette basierend auf Theme
|
||||
this.currentColors = document.documentElement.classList.contains('dark')
|
||||
? this.colors.dark
|
||||
: this.colors.light;
|
||||
|
||||
// Konfiguration
|
||||
this.config = {
|
||||
nodeCount: 80, // Anzahl der Knoten
|
||||
nodeSize: 2.5, // Größe der Knoten
|
||||
connectionDistance: 150, // Maximale Verbindungsdistanz
|
||||
connectionOpacity: 0.5, // Erhöht von 0.3 auf 0.5 - Deckkraft der ständigen Verbindungen
|
||||
animationSpeed: 0.15, // Geschwindigkeit der Animation
|
||||
flowDensity: 2, // Anzahl aktiver Verbindungen
|
||||
maxFlowsPerNode: 2, // Maximale Anzahl aktiver Verbindungen pro Knoten
|
||||
flowDuration: [2000, 5000], // Min/Max Dauer des Flows in ms
|
||||
nodePulseFrequency: 0.01 // Wie oft Knoten pulsieren
|
||||
};
|
||||
|
||||
// Initialisieren
|
||||
this.init();
|
||||
|
||||
// Event-Listener
|
||||
window.addEventListener('resize', this.resizeCanvas.bind(this));
|
||||
|
||||
console.log('Vereinfachter Neural Network Background initialized');
|
||||
}
|
||||
|
||||
init() {
|
||||
this.resizeCanvas();
|
||||
this.createNodes();
|
||||
this.createConnections();
|
||||
this.startAnimation();
|
||||
}
|
||||
|
||||
resizeCanvas() {
|
||||
const pixelRatio = window.devicePixelRatio || 1;
|
||||
const width = window.innerWidth;
|
||||
const height = window.innerHeight;
|
||||
|
||||
this.canvas.style.width = width + 'px';
|
||||
this.canvas.style.height = height + 'px';
|
||||
this.canvas.width = width * pixelRatio;
|
||||
this.canvas.height = height * pixelRatio;
|
||||
|
||||
if (this.ctx) {
|
||||
this.ctx.scale(pixelRatio, pixelRatio);
|
||||
}
|
||||
|
||||
// Neuberechnung der Knotenpositionen nach Größenänderung
|
||||
if (this.nodes.length) {
|
||||
this.createNodes();
|
||||
this.createConnections();
|
||||
}
|
||||
}
|
||||
|
||||
createNodes() {
|
||||
this.nodes = [];
|
||||
const width = this.canvas.width / (window.devicePixelRatio || 1);
|
||||
const height = this.canvas.height / (window.devicePixelRatio || 1);
|
||||
|
||||
// Cluster-Zentren für realistisches neuronales Netzwerk
|
||||
const clusterCount = Math.floor(6 + Math.random() * 4);
|
||||
const clusters = [];
|
||||
|
||||
for (let i = 0; i < clusterCount; i++) {
|
||||
clusters.push({
|
||||
x: Math.random() * width,
|
||||
y: Math.random() * height,
|
||||
radius: 100 + Math.random() * 150
|
||||
});
|
||||
}
|
||||
|
||||
// Knoten erstellen
|
||||
for (let i = 0; i < this.config.nodeCount; i++) {
|
||||
// Wähle zufällig ein Cluster
|
||||
const cluster = clusters[Math.floor(Math.random() * clusters.length)];
|
||||
|
||||
// Erstelle einen Knoten innerhalb des Clusters mit zufälligem Offset
|
||||
const angle = Math.random() * Math.PI * 2;
|
||||
const distance = Math.random() * cluster.radius;
|
||||
|
||||
const node = {
|
||||
id: i,
|
||||
x: cluster.x + Math.cos(angle) * distance,
|
||||
y: cluster.y + Math.sin(angle) * distance,
|
||||
size: this.config.nodeSize * (0.8 + Math.random() * 0.4),
|
||||
speed: {
|
||||
x: (Math.random() - 0.5) * 0.2,
|
||||
y: (Math.random() - 0.5) * 0.2
|
||||
},
|
||||
lastPulse: 0,
|
||||
pulseInterval: 5000 + Math.random() * 10000, // Zufälliges Pulsieren
|
||||
connections: []
|
||||
};
|
||||
|
||||
this.nodes.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
createConnections() {
|
||||
this.connections = [];
|
||||
|
||||
// Verbindungen zwischen Knoten erstellen
|
||||
for (let i = 0; i < this.nodes.length; i++) {
|
||||
const nodeA = this.nodes[i];
|
||||
|
||||
for (let j = i + 1; j < this.nodes.length; j++) {
|
||||
const nodeB = this.nodes[j];
|
||||
|
||||
const dx = nodeA.x - nodeB.x;
|
||||
const dy = nodeA.y - nodeB.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance < this.config.connectionDistance) {
|
||||
const connection = {
|
||||
id: `${i}-${j}`,
|
||||
from: i,
|
||||
to: j,
|
||||
distance: distance,
|
||||
opacity: Math.max(0.05, 1 - (distance / this.config.connectionDistance)),
|
||||
active: false,
|
||||
flowProgress: 0,
|
||||
flowDuration: 0,
|
||||
flowStart: 0
|
||||
};
|
||||
|
||||
this.connections.push(connection);
|
||||
nodeA.connections.push(connection);
|
||||
nodeB.connections.push(connection);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
startAnimation() {
|
||||
this.animate();
|
||||
}
|
||||
|
||||
animate() {
|
||||
this.animationFrameId = requestAnimationFrame(this.animate.bind(this));
|
||||
|
||||
const now = Date.now();
|
||||
this.updateNodes(now);
|
||||
this.updateConnections(now);
|
||||
this.render(now);
|
||||
}
|
||||
|
||||
updateNodes(now) {
|
||||
const width = this.canvas.width / (window.devicePixelRatio || 1);
|
||||
const height = this.canvas.height / (window.devicePixelRatio || 1);
|
||||
|
||||
// Knoten bewegen
|
||||
for (let i = 0; i < this.nodes.length; i++) {
|
||||
const node = this.nodes[i];
|
||||
|
||||
node.x += node.speed.x;
|
||||
node.y += node.speed.y;
|
||||
|
||||
// Begrenzung am Rand
|
||||
if (node.x < 0 || node.x > width) {
|
||||
node.speed.x *= -1;
|
||||
}
|
||||
|
||||
if (node.y < 0 || node.y > height) {
|
||||
node.speed.y *= -1;
|
||||
}
|
||||
|
||||
// Zufällig Richtung ändern
|
||||
if (Math.random() < 0.01) {
|
||||
node.speed.x = (Math.random() - 0.5) * 0.2;
|
||||
node.speed.y = (Math.random() - 0.5) * 0.2;
|
||||
}
|
||||
|
||||
// Zufälliges Pulsieren
|
||||
if (Math.random() < this.config.nodePulseFrequency && now - node.lastPulse > node.pulseInterval) {
|
||||
node.lastPulse = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateConnections(now) {
|
||||
// Update aktive Verbindungen
|
||||
for (const connectionId of this.activeConnections) {
|
||||
const connection = this.connections.find(c => c.id === connectionId);
|
||||
if (!connection) continue;
|
||||
|
||||
// Aktualisiere den Flow-Fortschritt
|
||||
const elapsed = now - connection.flowStart;
|
||||
const progress = elapsed / connection.flowDuration;
|
||||
|
||||
if (progress >= 1) {
|
||||
// Flow beenden
|
||||
connection.active = false;
|
||||
this.activeConnections.delete(connectionId);
|
||||
} else {
|
||||
connection.flowProgress = progress;
|
||||
}
|
||||
}
|
||||
|
||||
// Neue Flows starten, wenn unter dem Limit
|
||||
if (this.activeConnections.size < this.config.flowDensity) {
|
||||
// Wähle eine zufällige Verbindung
|
||||
const availableConnections = this.connections.filter(c => !c.active);
|
||||
|
||||
if (availableConnections.length > 0) {
|
||||
const randomIndex = Math.floor(Math.random() * availableConnections.length);
|
||||
const connection = availableConnections[randomIndex];
|
||||
|
||||
// Aktiviere die Verbindung
|
||||
connection.active = true;
|
||||
connection.flowProgress = 0;
|
||||
connection.flowStart = now;
|
||||
connection.flowDuration = this.config.flowDuration[0] +
|
||||
Math.random() * (this.config.flowDuration[1] - this.config.flowDuration[0]);
|
||||
|
||||
this.activeConnections.add(connection.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render(now) {
|
||||
// Aktualisiere Farben basierend auf aktuellem Theme
|
||||
this.currentColors = document.documentElement.classList.contains('dark')
|
||||
? this.colors.dark
|
||||
: this.colors.light;
|
||||
const colors = this.currentColors;
|
||||
const width = this.canvas.width / (window.devicePixelRatio || 1);
|
||||
const height = this.canvas.height / (window.devicePixelRatio || 1);
|
||||
|
||||
// Hintergrund löschen
|
||||
this.ctx.fillStyle = colors.background;
|
||||
this.ctx.fillRect(0, 0, width, height);
|
||||
|
||||
// Verbindungen zeichnen (statisch)
|
||||
this.ctx.strokeStyle = colors.connectionColor;
|
||||
this.ctx.lineWidth = 1.2;
|
||||
|
||||
for (const connection of this.connections) {
|
||||
const fromNode = this.nodes[connection.from];
|
||||
const toNode = this.nodes[connection.to];
|
||||
|
||||
this.ctx.globalAlpha = connection.opacity * 0.5;
|
||||
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(fromNode.x, fromNode.y);
|
||||
this.ctx.lineTo(toNode.x, toNode.y);
|
||||
this.ctx.stroke();
|
||||
}
|
||||
|
||||
// Aktive Verbindungen zeichnen (Flows)
|
||||
this.ctx.strokeStyle = colors.flowColor;
|
||||
this.ctx.lineWidth = 2.5;
|
||||
|
||||
for (const connectionId of this.activeConnections) {
|
||||
const connection = this.connections.find(c => c.id === connectionId);
|
||||
if (!connection) continue;
|
||||
|
||||
const fromNode = this.nodes[connection.from];
|
||||
const toNode = this.nodes[connection.to];
|
||||
|
||||
// Glühen-Effekt
|
||||
this.ctx.globalAlpha = Math.sin(connection.flowProgress * Math.PI) * 0.8;
|
||||
|
||||
// Linie zeichnen
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(fromNode.x, fromNode.y);
|
||||
this.ctx.lineTo(toNode.x, toNode.y);
|
||||
this.ctx.stroke();
|
||||
|
||||
// Fließendes Partikel
|
||||
const progress = connection.flowProgress;
|
||||
const x = fromNode.x + (toNode.x - fromNode.x) * progress;
|
||||
const y = fromNode.y + (toNode.y - fromNode.y) * progress;
|
||||
|
||||
this.ctx.globalAlpha = 0.9;
|
||||
this.ctx.fillStyle = colors.flowColor;
|
||||
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(x, y, 2, 0, Math.PI * 2);
|
||||
this.ctx.fill();
|
||||
}
|
||||
|
||||
// Knoten zeichnen
|
||||
for (const node of this.nodes) {
|
||||
// Pulsierende Knoten
|
||||
const timeSinceLastPulse = now - node.lastPulse;
|
||||
const isPulsing = timeSinceLastPulse < 800;
|
||||
const pulseProgress = isPulsing ? timeSinceLastPulse / 800 : 0;
|
||||
|
||||
// Knoten selbst
|
||||
this.ctx.globalAlpha = 1;
|
||||
this.ctx.fillStyle = isPulsing
|
||||
? colors.nodePulse
|
||||
: colors.nodeColor;
|
||||
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(node.x, node.y, node.size + (isPulsing ? 1 * Math.sin(pulseProgress * Math.PI) : 0), 0, Math.PI * 2);
|
||||
this.ctx.fill();
|
||||
|
||||
// Wenn pulsierend, füge einen Glow-Effekt hinzu
|
||||
if (isPulsing) {
|
||||
this.ctx.globalAlpha = 0.5 * (1 - pulseProgress);
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(node.x, node.y, node.size + 5 * pulseProgress, 0, Math.PI * 2);
|
||||
this.ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
this.ctx.globalAlpha = 1;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.isDestroying) return;
|
||||
this.isDestroying = true;
|
||||
|
||||
// Animation stoppen
|
||||
if (this.animationFrameId) {
|
||||
cancelAnimationFrame(this.animationFrameId);
|
||||
}
|
||||
|
||||
// Canvas ausblenden
|
||||
this.canvas.style.opacity = '0';
|
||||
|
||||
// Nach Übergang entfernen
|
||||
setTimeout(() => {
|
||||
if (this.canvas && this.canvas.parentNode) {
|
||||
this.canvas.parentNode.removeChild(this.canvas);
|
||||
}
|
||||
}, 3500);
|
||||
}
|
||||
|
||||
hexToRgb(hex) {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
return result ? {
|
||||
r: parseInt(result[1], 16),
|
||||
g: parseInt(result[2], 16),
|
||||
b: parseInt(result[3], 16)
|
||||
} : null;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialisiert den Hintergrund, sobald die Seite geladen ist
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
window.neuralBackground = new NeuralNetworkBackground();
|
||||
|
||||
// Theme-Wechsel-Event-Listener
|
||||
document.addEventListener('theme-changed', () => {
|
||||
if (window.neuralBackground) {
|
||||
window.neuralBackground.currentColors = document.documentElement.classList.contains('dark')
|
||||
? window.neuralBackground.colors.dark
|
||||
: window.neuralBackground.colors.light;
|
||||
}
|
||||
});
|
||||
});
|
||||
508
static/style.css
508
static/style.css
@@ -1,508 +0,0 @@
|
||||
/* 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
100
tailwind.config.js
Normal file
100
tailwind.config.js
Normal file
@@ -0,0 +1,100 @@
|
||||
/** @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
|
||||
],
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Datenbank aktualisieren{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4 py-10">
|
||||
<div class="bg-gray-800 bg-opacity-70 rounded-lg p-6 mb-6">
|
||||
<h1 class="text-2xl font-bold text-purple-400 mb-4">Datenbank aktualisieren</h1>
|
||||
|
||||
{% if message %}
|
||||
<div class="mb-6 p-4 rounded-lg {{ 'bg-green-800 bg-opacity-50' if success else 'bg-red-800 bg-opacity-50' }}">
|
||||
<p class="text-white">{{ message }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="mb-6">
|
||||
<p class="text-gray-300 mb-4">
|
||||
Diese Funktion aktualisiert die Datenbankstruktur, um mit dem aktuellen Datenmodell kompatibel zu sein.
|
||||
Dabei werden folgende Änderungen vorgenommen:
|
||||
</p>
|
||||
|
||||
<ul class="list-disc pl-6 text-gray-300 mb-6">
|
||||
<li>Hinzufügen von <code>bio</code>, <code>location</code>, <code>website</code>, <code>avatar</code> und <code>last_login</code> zur Benutzer-Tabelle</li>
|
||||
</ul>
|
||||
|
||||
<div class="bg-yellow-800 bg-opacity-30 p-4 rounded-lg mb-6">
|
||||
<p class="text-yellow-200">
|
||||
<i class="fas fa-exclamation-triangle mr-2"></i>
|
||||
<strong>Warnung:</strong> Bitte stelle sicher, dass du ein Backup der Datenbank erstellt hast, bevor du fortfährst.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ url_for('admin_update_database') }}">
|
||||
<div class="flex justify-between">
|
||||
<a href="{{ url_for('index') }}" class="px-4 py-2 bg-gray-700 text-white rounded-lg hover:bg-gray-600">
|
||||
Zurück zur Startseite
|
||||
</a>
|
||||
<button type="submit" class="px-4 py-2 bg-purple-700 text-white rounded-lg hover:bg-purple-600">
|
||||
Datenbank aktualisieren
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -6,18 +6,17 @@
|
||||
<title>Systades - {% block title %}{% endblock %}</title>
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" href="{{ url_for('static', filename='img/neuron-favicon.svg') }}" type="image/svg+xml">
|
||||
<link rel="icon" href="{{ url_for('static', filename='img/favicon.svg') }}" type="image/svg+xml">
|
||||
<link rel="icon" href="{{ url_for('static', filename='img/favicon.ico') }}" sizes="any">
|
||||
|
||||
<!-- Meta Tags -->
|
||||
<meta name="description" content="Eine interaktive Plattform zum Visualisieren, Erforschen und Teilen von Wissen">
|
||||
<meta name="keywords" content="systades, wissen, visualisierung, lernen, gedanken, theorie">
|
||||
<meta name="author" content="Systades-Team">
|
||||
|
||||
<!-- Tailwind CSS - CDN für Entwicklung und Produktion (laut Vorgabe) -->
|
||||
<!-- Tailwind CSS über CDN -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<!-- Alternative lokale Version, falls die CDN-Version blockiert wird -->
|
||||
<script>
|
||||
tailwind = window.tailwind || {};
|
||||
tailwind.config = {
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
@@ -58,35 +57,23 @@
|
||||
800: '#0e1220',
|
||||
900: '#0a0e19'
|
||||
}
|
||||
},
|
||||
keyframes: {
|
||||
float: {
|
||||
'0%, 100%': { transform: 'translateY(0)' },
|
||||
'50%': { transform: 'translateY(-5px)' }
|
||||
},
|
||||
'bounce-slow': {
|
||||
'0%, 100%': { transform: 'translateY(0)' },
|
||||
'50%': { transform: 'translateY(-8px)' }
|
||||
}
|
||||
},
|
||||
animation: {
|
||||
float: 'float 3s ease-in-out infinite',
|
||||
'bounce-slow': 'bounce-slow 2s ease-in-out infinite'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Local Font Files -->
|
||||
<link href="{{ url_for('static', filename='fonts/inter.css') }}" rel="stylesheet">
|
||||
<link href="{{ url_for('static', filename='fonts/jetbrains-mono.css') }}" rel="stylesheet">
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Font Awesome vom CDN -->
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" rel="stylesheet">
|
||||
<!-- Icons -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
|
||||
<!-- Assistent CSS -->
|
||||
<link href="{{ url_for('static', filename='css/assistant.css') }}" rel="stylesheet">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/src/cybernetwork-bg.css') }}">
|
||||
|
||||
<!-- Basis-Stylesheet -->
|
||||
<link href="{{ url_for('static', filename='style.css') }}" rel="stylesheet">
|
||||
@@ -94,297 +81,101 @@
|
||||
<!-- Base-Styles ausgelagert in eigene Datei -->
|
||||
<link href="{{ url_for('static', filename='css/base-styles.css') }}" rel="stylesheet">
|
||||
|
||||
<!-- Alpine.js - CDN Version -->
|
||||
<!-- Alpine.js -->
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.12.3/dist/cdn.min.js"></script>
|
||||
|
||||
<!-- Neural Network Background CSS -->
|
||||
<link href="{{ url_for('static', filename='css/neural-network-background.css') }}" rel="stylesheet">
|
||||
|
||||
<!-- Mindmap CSS -->
|
||||
<link href="{{ url_for('static', filename='css/mindmap.css', v='1.0.1') }}" rel="stylesheet">
|
||||
|
||||
<!-- D3.js für Visualisierungen -->
|
||||
<script src="https://d3js.org/d3.v7.min.js"></script>
|
||||
<!-- Network Background Script -->
|
||||
<script src="{{ url_for('static', filename='network-background.js') }}"></script>
|
||||
|
||||
<!-- Marked.js für Markdown-Parsing -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
|
||||
<!-- ChatGPT Assistant -->
|
||||
<script src="{{ url_for('static', filename='js/modules/chatgpt-assistant.js') }}"></script>
|
||||
|
||||
<!-- Neural Network Background Script -->
|
||||
<script src="{{ url_for('static', filename='neural-network-background.js') }}"></script>
|
||||
|
||||
<!-- Hauptmodul laden (als traditionelles Skript) -->
|
||||
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
|
||||
<!-- Hauptmodul laden (als ES6 Modul) -->
|
||||
<script type="module">
|
||||
import MindMap from "{{ url_for('static', filename='js/main.js') }}";
|
||||
// Alpine.js-Integration
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.data('layout', () => ({
|
||||
darkMode: false,
|
||||
mobileMenuOpen: false,
|
||||
userMenuOpen: false,
|
||||
showSettingsModal: false,
|
||||
|
||||
init() {
|
||||
this.fetchDarkModeFromSession();
|
||||
},
|
||||
|
||||
fetchDarkModeFromSession() {
|
||||
// Lade den Dark Mode-Status vom Server
|
||||
fetch('/get_dark_mode')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
this.darkMode = data.darkMode === 'true';
|
||||
document.querySelector('html').classList.toggle('dark', this.darkMode);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Fehler beim Laden der Dark Mode-Einstellung:', error);
|
||||
});
|
||||
},
|
||||
|
||||
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 -->
|
||||
{% block extra_css %}{% endblock %}
|
||||
|
||||
<!-- Custom dark/light mode styles -->
|
||||
<!-- ► ► Farb‑Token strikt getrennt ◄ ◄ -->
|
||||
<style>
|
||||
/* Light‑Mode */
|
||||
:root {
|
||||
--bg-primary:#f8fafc;
|
||||
--bg-secondary:#f1f5f9;
|
||||
--text-primary:#232837;
|
||||
--text-secondary:#475569;
|
||||
--accent-primary:#7c3aed;
|
||||
--accent-secondary:#8b5cf6;
|
||||
--glow-effect:0 0 8px rgba(139,92,246,.08);
|
||||
background-image: linear-gradient(to bottom right, rgba(248, 250, 252, 0.8), rgba(241, 245, 249, 0.8));
|
||||
background-attachment: fixed;
|
||||
}
|
||||
/* Dark‑Mode */
|
||||
.dark {
|
||||
--bg-primary:#181c24;
|
||||
--bg-secondary:#232837;
|
||||
--text-primary:#f9fafb;
|
||||
--text-secondary:#e5e7eb;
|
||||
--accent-primary:#6d28d9;
|
||||
--accent-secondary:#8b5cf6;
|
||||
--glow-effect:0 0 8px rgba(124,58,237,.15);
|
||||
}
|
||||
|
||||
body {
|
||||
@apply min-h-screen bg-[color:var(--bg-primary)] text-[color:var(--text-primary)];
|
||||
transition: background-color 0.5s ease-in-out, color 0.3s ease-in-out, background-image 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
/* Utilities */
|
||||
.mystical-glow { text-shadow: var(--glow-effect); }
|
||||
.gradient-text {
|
||||
background:linear-gradient(135deg,var(--accent-primary),var(--accent-secondary));
|
||||
-webkit-background-clip:text; background-clip:text; color:transparent; text-shadow:none;
|
||||
}
|
||||
.glass-morphism { backdrop-filter: blur(10px); }
|
||||
.glass-navbar { @apply glass-morphism border backdrop-blur-xl; }
|
||||
.light .glass-navbar { background-color:rgba(255,255,255,.8); border-color:rgba(0,0,0,.05); }
|
||||
.dark .glass-navbar { background-color:rgba(10,14,25,.8); border-color:rgba(255,255,255,.05); }
|
||||
|
||||
/* Light-Mode spezifische Stile */
|
||||
body:not(.dark) {
|
||||
background-color: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.nav-link-light {
|
||||
color: var(--text-secondary);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.nav-link-light:hover {
|
||||
color: var(--text-primary);
|
||||
background-color: rgba(126, 34, 206, 0.1);
|
||||
}
|
||||
|
||||
.nav-link-light-active {
|
||||
color: var(--accent-primary);
|
||||
background-color: rgba(126, 34, 206, 0.15);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Kartendesign im Light-Mode */
|
||||
body:not(.dark) .card {
|
||||
background-color: white;
|
||||
border: 1px solid #e5e7eb;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
body:not(.dark) .card:hover {
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--light-primary-hover);
|
||||
}
|
||||
|
||||
/* Light Mode Buttons */
|
||||
body:not(.dark) .btn,
|
||||
body:not(.dark) button:not(.toggle) {
|
||||
background: linear-gradient(135deg, #7c3aed, #6d28d9);
|
||||
color: white !important;
|
||||
border: none;
|
||||
box-shadow: 0 2px 4px rgba(124, 58, 237, 0.25);
|
||||
border-radius: 8px;
|
||||
padding: 0.625rem 1.25rem;
|
||||
transition: all 0.2s ease;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
body:not(.dark) .btn:hover,
|
||||
body:not(.dark) button:not(.toggle):hover {
|
||||
background: linear-gradient(135deg, #8b5cf6, #7c3aed);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(124, 58, 237, 0.3);
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
/* KI-Chat Button im Light-Mode */
|
||||
body:not(.dark) [onclick*="MindMap.assistant.toggleAssistant"] {
|
||||
background: linear-gradient(135deg, #7c3aed, #4f46e5);
|
||||
color: white !important;
|
||||
font-weight: 500;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
body:not(.dark) [onclick*="MindMap.assistant.toggleAssistant"]:hover {
|
||||
background: linear-gradient(135deg, #8b5cf6, #6366f1);
|
||||
box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3);
|
||||
}
|
||||
|
||||
/* Style improvements for the theme toggle button */
|
||||
.theme-toggle {
|
||||
position: relative;
|
||||
width: 48px;
|
||||
height: 24px;
|
||||
border-radius: 24px;
|
||||
padding: 2px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body.dark .theme-toggle {
|
||||
background: linear-gradient(to right, #7c3aed, #3b82f6);
|
||||
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.3), 0 0 10px rgba(124, 58, 237, 0.3);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
body:not(.dark) .theme-toggle {
|
||||
background: linear-gradient(to right, #8b5cf6, #60a5fa);
|
||||
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1), 0 0 10px rgba(124, 58, 237, 0.15);
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.theme-toggle::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
top: 2px;
|
||||
transition: all 0.3s ease;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
body.dark .theme-toggle::after {
|
||||
background: #f1f5f9 url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%237c3aed' width='14' height='14'%3E%3Cpath d='M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z'%3E%3C/path%3E%3C/svg%3E") no-repeat center center;
|
||||
transform: translateX(24px);
|
||||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
body:not(.dark) .theme-toggle::after {
|
||||
background: #fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23f59e0b' width='14' height='14'%3E%3Cpath d='M12 2.25a.75.75 0 01.75.75v2.25a.75.75 0 01-1.5 0V3a.75.75 0 01.75-.75zM7.5 12a4.5 4.5 0 119 0 4.5 4.5 0 01-9 0zM18.894 6.166a.75.75 0 00-1.06-1.06l-1.591 1.59a.75.75 0 101.06 1.061l1.591-1.59zM21.75 12a.75.75 0 01-.75.75h-2.25a.75.75 0 010-1.5H21a.75.75 0 01.75.75zM17.834 18.894a.75.75 0 001.06-1.06l-1.59-1.591a.75.75 0 10-1.061 1.06l1.59 1.591zM12 18a.75.75 0 01.75.75V21a.75.75 0 01-1.5 0v-2.25A.75.75 0 0112 18zM7.758 17.303a.75.75 0 00-1.061-1.06l-1.591 1.59a.75.75 0 001.06 1.061l1.591-1.59zM6 12a.75.75 0 01-.75.75H3a.75.75 0 010-1.5h2.25A.75.75 0 016 12zM6.697 7.757a.75.75 0 001.06-1.06l-1.59-1.591a.75.75 0 00-1.061 1.06l1.59 1.591z'%3E%3C/path%3E%3C/svg%3E") no-repeat center center;
|
||||
transform: translateX(2px);
|
||||
box-shadow: 0 0 8px rgba(124, 58, 237, 0.2);
|
||||
}
|
||||
|
||||
.theme-toggle:hover::after {
|
||||
box-shadow: 0 0 12px rgba(124, 58, 237, 0.4);
|
||||
}
|
||||
|
||||
/* Fixes for light mode button text colors */
|
||||
body:not(.dark) .btn-primary {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
/* Fix for KI-Chat container */
|
||||
#chatgpt-assistant {
|
||||
position: fixed;
|
||||
bottom: 1.5rem;
|
||||
right: 1.5rem;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.chat-assistant {
|
||||
max-height: 80vh !important;
|
||||
}
|
||||
|
||||
.chat-assistant .chat-messages {
|
||||
max-height: calc(80vh - 160px) !important;
|
||||
}
|
||||
</style>
|
||||
<!-- Cybertechnisches Netzwerk-Hintergrund -->
|
||||
<script type="module" src="{{ url_for('static', filename='js/modules/cyber-network-init.js') }}"></script>
|
||||
</head>
|
||||
<body data-page="{{ request.endpoint }}" class="relative overflow-x-hidden dark bg-gray-900 text-white" x-data="{
|
||||
darkMode: true,
|
||||
mobileMenuOpen: false,
|
||||
userMenuOpen: false,
|
||||
showSettingsModal: false,
|
||||
<body data-page="{{ request.endpoint }}" class="relative overflow-x-hidden">
|
||||
<!-- Cybertechnisches Netzwerk-Hintergrund Container (wird via JavaScript befüllt) -->
|
||||
<div id="cyber-background-container" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: -1; pointer-events: none; overflow: hidden;"></div>
|
||||
|
||||
init() {
|
||||
this.initDarkMode();
|
||||
},
|
||||
<!-- Globaler Hintergrund -->
|
||||
<div class="full-page-bg"></div>
|
||||
<!-- Statischer Fallback-Hintergrund (wird nur angezeigt, wenn JavaScript deaktiviert ist) -->
|
||||
<div class="fixed inset-0 z-[-9] bg-cover bg-center opacity-50"></div>
|
||||
|
||||
initDarkMode() {
|
||||
// Lade zuerst den Wert aus dem localStorage (client-seitig)
|
||||
const storedMode = localStorage.getItem('colorMode');
|
||||
if (storedMode) {
|
||||
this.darkMode = storedMode === 'dark';
|
||||
}
|
||||
|
||||
// Dann hole die Server-Einstellung, die Vorrang hat
|
||||
this.fetchDarkModeFromSession();
|
||||
},
|
||||
|
||||
fetchDarkModeFromSession() {
|
||||
fetch('/api/get_dark_mode')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
this.darkMode = data.darkMode === 'true';
|
||||
this.applyDarkMode();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Fehler beim Laden der Dark Mode-Einstellung:', error);
|
||||
});
|
||||
},
|
||||
|
||||
applyDarkMode() {
|
||||
document.querySelector('html').classList.toggle('dark', this.darkMode);
|
||||
document.querySelector('body').classList.toggle('dark', this.darkMode);
|
||||
localStorage.setItem('colorMode', this.darkMode ? 'dark' : 'light');
|
||||
},
|
||||
|
||||
toggleDarkMode() {
|
||||
this.darkMode = !this.darkMode;
|
||||
this.applyDarkMode();
|
||||
|
||||
// Server über Änderung informieren
|
||||
fetch('/api/set_dark_mode', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ darkMode: this.darkMode })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
// Event auslösen für andere Komponenten
|
||||
document.dispatchEvent(new CustomEvent('darkModeToggled', {
|
||||
detail: { isDark: this.darkMode }
|
||||
}));
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Fehler beim Speichern der Dark Mode-Einstellung:', error);
|
||||
});
|
||||
}
|
||||
}">
|
||||
<!-- App-Container -->
|
||||
<div id="app-container" class="flex flex-col min-h-screen">
|
||||
<div id="app-container" class="flex flex-col min-h-screen" x-data="layout">
|
||||
<!-- Hauptnavigation -->
|
||||
<nav class="sticky top-0 left-0 right-0 z-50 transition-all duration-300 py-4 px-5 border-b glass-morphism"
|
||||
x-bind:class="darkMode ? 'glass-navbar-dark border-white/10' : 'glass-navbar-light border-gray-200/50'">
|
||||
<div class="container mx-auto flex justify-between items-center">
|
||||
<!-- Logo -->
|
||||
<a href="{{ url_for('index') }}" class="flex items-center group">
|
||||
<img src="{{ url_for('static', filename='img/neuron-logo.svg') }}" alt="Systades Logo" class="w-8 h-8 mr-2 transform transition-transform group-hover:scale-110">
|
||||
<span class="text-2xl font-bold gradient-text transform transition-transform group-hover:scale-105">Systades</span>
|
||||
</a>
|
||||
|
||||
@@ -404,22 +195,6 @@
|
||||
: '{{ 'nav-link-light-active' if request.endpoint == 'mindmap' else 'nav-link-light' }}'">
|
||||
<i class="fa-solid fa-diagram-project mr-2"></i>Mindmap
|
||||
</a>
|
||||
{% if current_user.is_authenticated %}
|
||||
<a href="{{ url_for('social_feed') }}"
|
||||
class="nav-link flex items-center"
|
||||
x-bind:class="darkMode
|
||||
? '{{ 'nav-link-active' if request.endpoint == 'social_feed' else '' }}'
|
||||
: '{{ 'nav-link-light-active' if request.endpoint == 'social_feed' else 'nav-link-light' }}'">
|
||||
<i class="fa-solid fa-home mr-2"></i>Feed
|
||||
</a>
|
||||
<a href="{{ url_for('discover') }}"
|
||||
class="nav-link flex items-center"
|
||||
x-bind:class="darkMode
|
||||
? '{{ 'nav-link-active' if request.endpoint == 'discover' else '' }}'
|
||||
: '{{ 'nav-link-light-active' if request.endpoint == 'discover' else 'nav-link-light' }}'">
|
||||
<i class="fa-solid fa-compass mr-2"></i>Entdecken
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{{ url_for('search_thoughts_page') }}"
|
||||
class="nav-link flex items-center"
|
||||
x-bind:class="darkMode
|
||||
@@ -431,8 +206,8 @@
|
||||
<button onclick="window.MindMap && window.MindMap.assistant && window.MindMap.assistant.toggleAssistant(true)"
|
||||
class="nav-link flex items-center"
|
||||
x-bind:class="darkMode
|
||||
? 'bg-gradient-to-r from-purple-900/90 to-indigo-800/90 text-white font-medium px-4 py-2 rounded-xl hover:shadow-lg hover:shadow-purple-800/30 transition-all duration-300'
|
||||
: 'bg-gradient-to-r from-purple-600 to-indigo-500 text-white font-medium px-4 py-2 rounded-xl hover:shadow-md transition-all duration-300'">
|
||||
? 'bg-gradient-to-r from-purple-600/80 to-blue-500/80 text-white font-medium px-4 py-2 rounded-xl hover:shadow-lg transition-all duration-300 hover:-translate-y-0.5'
|
||||
: 'bg-gradient-to-r from-purple-500/20 to-blue-400/20 text-gray-800 font-medium px-4 py-2 rounded-xl hover:shadow-md transition-all duration-300 hover:-translate-y-0.5'">
|
||||
<i class="fa-solid fa-robot mr-2"></i>KI-Chat
|
||||
</button>
|
||||
{% if current_user.is_authenticated %}
|
||||
@@ -448,14 +223,25 @@
|
||||
|
||||
<!-- Rechte Seite -->
|
||||
<div class="flex items-center space-x-4">
|
||||
<!-- Dark/Light Mode Schalter -->
|
||||
<button
|
||||
@click="toggleDarkMode()"
|
||||
class="theme-toggle relative w-12 h-6 rounded-full transition-all duration-300 flex items-center overflow-hidden"
|
||||
aria-label="Dark Mode umschalten"
|
||||
>
|
||||
<span class="sr-only" x-text="darkMode ? 'Zum Light Mode wechseln' : 'Zum Dark Mode wechseln'"></span>
|
||||
</button>
|
||||
<!-- Dark Mode Toggle Switch -->
|
||||
<div class="flex items-center cursor-pointer" @click="toggleDarkMode">
|
||||
<div class="relative w-12 h-6">
|
||||
<input type="checkbox" id="darkModeToggle" class="sr-only" x-model="darkMode">
|
||||
<div class="block w-12 h-6 rounded-full transition-colors duration-300"
|
||||
x-bind:class="darkMode ? 'bg-blue-400/50' : 'bg-gray-400/50'"></div>
|
||||
<div class="dot absolute left-1 top-1 w-4 h-4 rounded-full transition-transform duration-300 shadow-md"
|
||||
x-bind:class="darkMode ? 'bg-blue-500 transform translate-x-6' : 'bg-white'"></div>
|
||||
</div>
|
||||
<div class="ml-3 hidden sm:block"
|
||||
x-bind:class="darkMode ? 'text-white/90' : 'text-gray-700'">
|
||||
<span x-text="darkMode ? 'Dunkel' : 'Hell'"></span>
|
||||
</div>
|
||||
<div class="ml-2 sm:hidden"
|
||||
x-bind:class="darkMode ? 'text-white/90' : 'text-gray-700'">
|
||||
<i class="fa-solid" :class="darkMode ? 'fa-sun' : 'fa-moon'"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Profil-Link oder Login -->
|
||||
{% if current_user.is_authenticated %}
|
||||
<div class="relative" x-data="{ open: false }">
|
||||
@@ -469,21 +255,12 @@
|
||||
{% if current_user.avatar %}
|
||||
<img src="{{ current_user.avatar }}" alt="{{ current_user.username }}" class="w-full h-full object-cover">
|
||||
{% else %}
|
||||
<svg width="100%" height="100%" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="100" cy="100" r="98" fill="url(#user-gradient)" stroke="#7C3AED" stroke-width="4"/>
|
||||
<circle cx="100" cy="80" r="36" fill="white"/>
|
||||
<path d="M100 140C77.9086 140 60 157.909 60 180H140C140 157.909 122.091 140 100 140Z" fill="white"/>
|
||||
<defs>
|
||||
<linearGradient id="user-gradient" x1="0" y1="0" x2="200" y2="200" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#8B5CF6"/>
|
||||
<stop offset="1" stop-color="#3B82F6"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
{{ current_user.username[0].upper() }}
|
||||
{% endif %}
|
||||
</div>
|
||||
<span class="hidden md:block">{{ current_user.username }}</span>
|
||||
<i class="fas fa-chevron-down text-xs opacity-60 ml-1.5"></i>
|
||||
<span class="text-sm hidden lg:block">{{ current_user.username }}</span>
|
||||
<i class="fa-solid fa-chevron-down text-xs hidden lg:block transition-transform duration-200"
|
||||
x-bind:class="open ? 'transform rotate-180' : ''"></i>
|
||||
</button>
|
||||
|
||||
<!-- Dropdown-Menü -->
|
||||
@@ -531,22 +308,13 @@
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="flex items-center space-x-2">
|
||||
<a href="{{ url_for('login') }}"
|
||||
class="py-2 px-4 rounded-lg transition-all duration-300"
|
||||
x-bind:class="darkMode
|
||||
? 'text-white/90 hover:bg-dark-700/80'
|
||||
: 'text-gray-700 hover:bg-gray-100/80'">
|
||||
<i class="fa-solid fa-sign-in-alt mr-2"></i>Login
|
||||
</a>
|
||||
<a href="{{ url_for('register') }}"
|
||||
class="py-2 px-4 rounded-lg transition-all duration-300 font-medium"
|
||||
x-bind:class="darkMode
|
||||
? 'bg-purple-800/80 text-white hover:bg-purple-700/80'
|
||||
: 'bg-purple-600/20 text-gray-700 hover:bg-purple-600/30'">
|
||||
Registrieren
|
||||
</a>
|
||||
</div>
|
||||
<a href="{{ url_for('login') }}"
|
||||
class="flex items-center px-4 py-2.5 rounded-xl font-medium transition-all duration-300"
|
||||
x-bind:class="darkMode
|
||||
? 'bg-gray-800/80 text-white hover:bg-gray-700/80 shadow-md hover:shadow-lg hover:-translate-y-0.5'
|
||||
: 'bg-gray-200/80 text-gray-800 hover:bg-gray-300/80 shadow-sm hover:shadow-md hover:-translate-y-0.5'">
|
||||
<i class="fa-solid fa-user mr-2"></i>Mein Konto
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<!-- Mobilmenü-Button -->
|
||||
@@ -563,7 +331,6 @@
|
||||
|
||||
<!-- Mobile Menü -->
|
||||
<div x-show="mobileMenuOpen"
|
||||
x-cloak
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 -translate-y-4"
|
||||
x-transition:enter-end="opacity-100 translate-y-0"
|
||||
@@ -589,22 +356,6 @@
|
||||
: '{{ 'bg-purple-500/10 text-gray-900' if request.endpoint == 'mindmap' else 'text-gray-700 hover:bg-gray-100 hover:text-gray-900' }}'">
|
||||
<i class="fa-solid fa-diagram-project w-5 mr-3"></i>Mindmap
|
||||
</a>
|
||||
{% if current_user.is_authenticated %}
|
||||
<a href="{{ url_for('social_feed') }}"
|
||||
class="block py-3.5 px-4 rounded-xl transition-all duration-200 flex items-center"
|
||||
x-bind:class="darkMode
|
||||
? '{{ 'bg-purple-500/20 text-white' if request.endpoint == 'social_feed' else 'text-white/80 hover:bg-gray-800/80 hover:text-white' }}'
|
||||
: '{{ 'bg-purple-500/10 text-gray-900' if request.endpoint == 'social_feed' else 'text-gray-700 hover:bg-gray-100 hover:text-gray-900' }}'">
|
||||
<i class="fa-solid fa-home w-5 mr-3"></i>Feed
|
||||
</a>
|
||||
<a href="{{ url_for('discover') }}"
|
||||
class="block py-3.5 px-4 rounded-xl transition-all duration-200 flex items-center"
|
||||
x-bind:class="darkMode
|
||||
? '{{ 'bg-purple-500/20 text-white' if request.endpoint == 'discover' else 'text-white/80 hover:bg-gray-800/80 hover:text-white' }}'
|
||||
: '{{ 'bg-purple-500/10 text-gray-900' if request.endpoint == 'discover' else 'text-gray-700 hover:bg-gray-100 hover:text-gray-900' }}'">
|
||||
<i class="fa-solid fa-compass w-5 mr-3"></i>Entdecken
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{{ url_for('search_thoughts_page') }}"
|
||||
class="block py-3.5 px-4 rounded-xl transition-all duration-200 flex items-center"
|
||||
x-bind:class="darkMode
|
||||
@@ -617,7 +368,7 @@
|
||||
class="block w-full text-left py-3.5 px-4 rounded-xl transition-all duration-200 flex items-center"
|
||||
x-bind:class="darkMode
|
||||
? 'bg-gradient-to-r from-purple-600/30 to-blue-500/30 text-white hover:from-purple-600/40 hover:to-blue-500/40'
|
||||
: 'bg-gradient-to-r from-purple-600 to-blue-500 text-white hover:from-purple-600/90 hover:to-blue-500/90'">
|
||||
: 'bg-gradient-to-r from-purple-500/10 to-blue-400/10 text-gray-900 hover:from-purple-500/20 hover:to-blue-400/20'">
|
||||
<i class="fa-solid fa-robot w-5 mr-3"></i>KI-Chat
|
||||
</button>
|
||||
{% if current_user.is_authenticated %}
|
||||
@@ -683,10 +434,6 @@
|
||||
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
||||
Mindmap
|
||||
</a>
|
||||
<a href="{{ url_for('search_thoughts_page') }}" class="text-sm transition-all duration-200"
|
||||
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
||||
Suche
|
||||
</a>
|
||||
{% if current_user.is_authenticated %}
|
||||
<a href="{{ url_for('profile') }}" class="text-sm transition-all duration-200"
|
||||
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
||||
@@ -715,10 +462,6 @@
|
||||
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
||||
Impressum
|
||||
</a>
|
||||
<a href="{{ url_for('ueber_uns') }}" class="text-sm transition-all duration-200"
|
||||
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
||||
Über uns
|
||||
</a>
|
||||
<a href="{{ url_for('datenschutz') }}" class="text-sm transition-all duration-200"
|
||||
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
||||
Datenschutz
|
||||
@@ -766,286 +509,27 @@
|
||||
|
||||
<!-- Hilfsscripts -->
|
||||
{% block scripts %}{% endblock %}
|
||||
{% block extra_js %}{% endblock %}
|
||||
|
||||
<!-- ChatGPT Initialisierung -->
|
||||
<script>
|
||||
// Prüfe, ob ChatGPTAssistant bereits existiert
|
||||
if (typeof ChatGPTAssistant === 'undefined') {
|
||||
class ChatGPTAssistant {
|
||||
constructor() {
|
||||
this.chatContainer = null;
|
||||
this.messages = [];
|
||||
this.isOpen = false;
|
||||
}
|
||||
<!-- KI-Chat Initialisierung -->
|
||||
<script type="module">
|
||||
// Importiere und initialisiere den ChatGPT-Assistenten direkt, um sicherzustellen,
|
||||
// dass er auf jeder Seite verfügbar ist, selbst wenn MindMap nicht geladen ist
|
||||
import ChatGPTAssistant from "{{ url_for('static', filename='js/modules/chatgpt-assistant.js') }}";
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Prüfen, ob der Assistent bereits durch MindMap initialisiert wurde
|
||||
if (!window.MindMap || !window.MindMap.assistant) {
|
||||
console.log('KI-Assistent wird direkt initialisiert...');
|
||||
const assistant = new ChatGPTAssistant();
|
||||
assistant.init();
|
||||
|
||||
init() {
|
||||
// Chat-Container erstellen, falls noch nicht vorhanden
|
||||
if (!document.getElementById('chat-assistant-container')) {
|
||||
this.createChatInterface();
|
||||
}
|
||||
|
||||
// Event-Listener für Chat-Button
|
||||
const chatButton = document.getElementById('chat-assistant-button');
|
||||
if (chatButton) {
|
||||
chatButton.addEventListener('click', () => this.toggleChat());
|
||||
}
|
||||
|
||||
// Event-Listener für Senden-Button
|
||||
const sendButton = document.getElementById('chat-send-button');
|
||||
if (sendButton) {
|
||||
sendButton.addEventListener('click', () => this.sendMessage());
|
||||
}
|
||||
|
||||
// Event-Listener für Eingabefeld (Enter-Taste)
|
||||
const inputField = document.getElementById('chat-input');
|
||||
if (inputField) {
|
||||
inputField.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
this.sendMessage();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log('KI-Assistent erfolgreich initialisiert');
|
||||
}
|
||||
|
||||
createChatInterface() {
|
||||
// Chat-Button erstellen
|
||||
const chatButton = document.createElement('button');
|
||||
chatButton.id = 'chat-assistant-button';
|
||||
chatButton.className = 'fixed bottom-6 right-6 bg-primary-600 text-white rounded-full p-4 shadow-lg z-50 hover:bg-primary-700 transition-all';
|
||||
chatButton.innerHTML = '<i class="fas fa-robot text-xl"></i>';
|
||||
document.body.appendChild(chatButton);
|
||||
|
||||
// Chat-Container erstellen
|
||||
const chatContainer = document.createElement('div');
|
||||
chatContainer.id = 'chat-assistant-container';
|
||||
chatContainer.className = 'fixed bottom-24 right-6 w-80 md:w-96 bg-white dark:bg-gray-800 rounded-xl shadow-xl z-50 flex flex-col transition-all duration-300 transform scale-0 origin-bottom-right';
|
||||
chatContainer.style.height = '500px';
|
||||
chatContainer.style.maxHeight = '70vh';
|
||||
|
||||
// Chat-Header
|
||||
chatContainer.innerHTML = `
|
||||
<div class="p-4 border-b dark:border-gray-700 flex justify-between items-center">
|
||||
<h3 class="font-bold text-gray-800 dark:text-white">Systades Assistent</h3>
|
||||
<button id="chat-close-button" class="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div id="chat-messages" class="flex-1 overflow-y-auto p-4 space-y-4"></div>
|
||||
<div class="p-4 border-t dark:border-gray-700">
|
||||
<div class="flex space-x-2">
|
||||
<input id="chat-input" type="text" placeholder="Frage stellen..." class="flex-1 px-4 py-2 rounded-lg border dark:border-gray-700 dark:bg-gray-700 dark:text-white focus:outline-none focus:ring-2 focus:ring-primary-500">
|
||||
<button id="chat-send-button" class="bg-primary-600 text-white px-4 py-2 rounded-lg hover:bg-primary-700 transition-all">
|
||||
<i class="fas fa-paper-plane"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(chatContainer);
|
||||
this.chatContainer = chatContainer;
|
||||
|
||||
// Event-Listener für Schließen-Button
|
||||
const closeButton = document.getElementById('chat-close-button');
|
||||
if (closeButton) {
|
||||
closeButton.addEventListener('click', () => this.toggleChat());
|
||||
}
|
||||
}
|
||||
|
||||
toggleChat() {
|
||||
this.isOpen = !this.isOpen;
|
||||
if (this.isOpen) {
|
||||
this.chatContainer.classList.remove('scale-0');
|
||||
this.chatContainer.classList.add('scale-100');
|
||||
} else {
|
||||
this.chatContainer.classList.remove('scale-100');
|
||||
this.chatContainer.classList.add('scale-0');
|
||||
}
|
||||
}
|
||||
|
||||
async sendMessage() {
|
||||
const inputField = document.getElementById('chat-input');
|
||||
const messageText = inputField.value.trim();
|
||||
|
||||
if (!messageText) return;
|
||||
|
||||
// Benutzer-Nachricht anzeigen
|
||||
this.addMessage('user', messageText);
|
||||
inputField.value = '';
|
||||
|
||||
// Lade-Indikator anzeigen
|
||||
this.addMessage('assistant', '...', 'loading-message');
|
||||
|
||||
try {
|
||||
// API-Anfrage senden
|
||||
const response = await fetch('/api/assistant', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
messages: this.messages.map(msg => ({
|
||||
role: msg.role,
|
||||
content: msg.content
|
||||
}))
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Lade-Nachricht entfernen
|
||||
const loadingMessage = document.getElementById('loading-message');
|
||||
if (loadingMessage) {
|
||||
loadingMessage.remove();
|
||||
}
|
||||
|
||||
if (data.error) {
|
||||
this.addMessage('assistant', 'Entschuldigung, es ist ein Fehler aufgetreten: ' + data.error);
|
||||
} else {
|
||||
this.addMessage('assistant', data.response);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler bei der API-Anfrage:', error);
|
||||
|
||||
// Lade-Nachricht entfernen
|
||||
const loadingMessage = document.getElementById('loading-message');
|
||||
if (loadingMessage) {
|
||||
loadingMessage.remove();
|
||||
}
|
||||
|
||||
this.addMessage('assistant', 'Entschuldigung, es ist ein Fehler aufgetreten. Bitte versuche es später erneut.');
|
||||
}
|
||||
}
|
||||
|
||||
addMessage(role, content, id = null) {
|
||||
const messagesContainer = document.getElementById('chat-messages');
|
||||
|
||||
// Nachricht zum Array hinzufügen (außer Lade-Nachrichten)
|
||||
if (id !== 'loading-message') {
|
||||
this.messages.push({ role, content });
|
||||
}
|
||||
|
||||
// Nachricht zum DOM hinzufügen
|
||||
const messageElement = document.createElement('div');
|
||||
messageElement.className = `p-3 rounded-lg ${role === 'user' ? 'bg-primary-100 dark:bg-primary-900/30 ml-6' : 'bg-gray-100 dark:bg-gray-700 mr-6'}`;
|
||||
if (id) {
|
||||
messageElement.id = id;
|
||||
}
|
||||
|
||||
messageElement.innerHTML = `
|
||||
<div class="flex items-start">
|
||||
<div class="w-8 h-8 rounded-full flex items-center justify-center ${role === 'user' ? 'bg-primary-600' : 'bg-gray-600'} text-white mr-2">
|
||||
<i class="fas ${role === 'user' ? 'fa-user' : 'fa-robot'} text-xs"></i>
|
||||
</div>
|
||||
<div class="flex-1 text-sm ${role === 'user' ? 'text-gray-800 dark:text-gray-200' : 'text-gray-700 dark:text-gray-300'}">
|
||||
${content}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
messagesContainer.appendChild(messageElement);
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
// Speichere in window.MindMap, falls es existiert, oder erstelle es
|
||||
if (!window.MindMap) {
|
||||
window.MindMap = {};
|
||||
}
|
||||
window.MindMap.assistant = assistant;
|
||||
}
|
||||
|
||||
// Initialisiere den ChatGPT-Assistenten direkt
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Prüfen, ob der Assistent bereits durch MindMap initialisiert wurde
|
||||
if (!window.MindMap || !window.MindMap.assistant) {
|
||||
console.log('KI-Assistent wird direkt initialisiert...');
|
||||
const assistant = new ChatGPTAssistant();
|
||||
assistant.init();
|
||||
|
||||
// Speichere in window.MindMap, falls es existiert, oder erstelle es
|
||||
if (!window.MindMap) {
|
||||
window.MindMap = {};
|
||||
}
|
||||
window.MindMap.assistant = assistant;
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Dark/Light-Mode vereinheitlicht -->
|
||||
<script>
|
||||
// Globaler Zugriff für externe Skripte
|
||||
window.MindMap = window.MindMap || {};
|
||||
|
||||
// Funktion zum Anwenden des Dark Mode, strikt getrennt
|
||||
function applyDarkModeClasses(isDarkMode) {
|
||||
if (isDarkMode) {
|
||||
document.documentElement.classList.add('dark');
|
||||
document.body.classList.add('dark');
|
||||
localStorage.setItem('colorMode', 'dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
document.body.classList.remove('dark');
|
||||
localStorage.setItem('colorMode', 'light');
|
||||
}
|
||||
|
||||
// Alpine.js darkMode-Variable aktualisieren, falls zutreffend
|
||||
const appEl = document.querySelector('body');
|
||||
if (appEl && appEl.__x) {
|
||||
appEl.__x.$data.darkMode = isDarkMode;
|
||||
}
|
||||
|
||||
// Event für andere Komponenten auslösen
|
||||
document.dispatchEvent(new CustomEvent('darkModeToggled', {
|
||||
detail: { isDark: isDarkMode }
|
||||
}));
|
||||
}
|
||||
|
||||
window.MindMap.toggleDarkMode = function() {
|
||||
const isDark = document.documentElement.classList.contains('dark');
|
||||
const newIsDark = !isDark;
|
||||
|
||||
// DOM aktualisieren
|
||||
applyDarkModeClasses(newIsDark);
|
||||
|
||||
// Server aktualisieren
|
||||
fetch('/api/set_dark_mode', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ darkMode: newIsDark })
|
||||
})
|
||||
.catch(console.error);
|
||||
};
|
||||
|
||||
// Initialisierung beim Laden
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Reihenfolge der Prüfungen: Serverseitige Einstellung > Lokale Einstellung > Browser-Präferenz
|
||||
|
||||
// 1. Zuerst lokale Einstellung prüfen
|
||||
const storedMode = localStorage.getItem('colorMode');
|
||||
if (storedMode) {
|
||||
applyDarkModeClasses(storedMode === 'dark');
|
||||
} else {
|
||||
// 2. Fallback auf Browser-Präferenz
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
applyDarkModeClasses(prefersDark);
|
||||
}
|
||||
|
||||
// 3. Serverseitige Einstellung abrufen und anwenden
|
||||
fetch('/api/get_dark_mode')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
const serverDarkMode = data.darkMode === true || data.darkMode === 'true';
|
||||
applyDarkModeClasses(serverDarkMode);
|
||||
}
|
||||
})
|
||||
.catch(error => console.error('Fehler beim Abrufen des Dark Mode Status:', error));
|
||||
|
||||
// Listener für Änderungen der Browser-Präferenz
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
|
||||
if (localStorage.getItem('colorMode') === null) {
|
||||
applyDarkModeClasses(e.matches);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,192 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}{{ category.title }} - Forum{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.thread-item {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.thread-item:hover {
|
||||
transform: translateX(2px);
|
||||
}
|
||||
.thread-pinned {
|
||||
border-left-width: 4px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<!-- Breadcrumb Navigation -->
|
||||
<div class="mb-6 flex items-center text-sm">
|
||||
<a href="{{ url_for('community') }}" class="opacity-75 hover:opacity-100 transition-opacity">
|
||||
<i class="fas fa-home mr-1"></i> Forum
|
||||
</a>
|
||||
<span class="mx-2 opacity-50">/</span>
|
||||
<span class="font-medium">{{ category.title }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Kategorie-Header -->
|
||||
<div class="mb-8 flex flex-wrap items-center justify-between gap-4">
|
||||
<div class="flex items-center">
|
||||
<!-- Kategorie-Icon -->
|
||||
<div class="w-12 h-12 rounded-xl mr-4 flex items-center justify-center text-white"
|
||||
style="background-color: {{ node.color_code or '#6d28d9' }}">
|
||||
<i class="fas {{ node.icon or 'fa-folder' }} text-2xl"></i>
|
||||
</div>
|
||||
|
||||
<!-- Kategorie-Info -->
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold">{{ category.title }}</h1>
|
||||
<p class="opacity-75">{{ category.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Neues Thema erstellen -->
|
||||
<a href="{{ url_for('new_post', category_id=category.id) }}"
|
||||
class="px-5 py-2.5 rounded-lg transition-all duration-300 flex items-center"
|
||||
x-bind:class="darkMode
|
||||
? 'bg-indigo-700 hover:bg-indigo-600 text-white'
|
||||
: 'bg-indigo-500 hover:bg-indigo-600 text-white'">
|
||||
<i class="fas fa-plus-circle mr-2"></i>
|
||||
Neues Thema
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Threads anzeigen -->
|
||||
<div class="mb-8 rounded-xl overflow-hidden"
|
||||
x-bind:class="darkMode ? 'bg-gray-800/60 border border-white/10' : 'bg-white border border-gray-200'">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="p-4 border-b" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
|
||||
<div class="grid grid-cols-12 gap-4">
|
||||
<div class="col-span-7 font-medium">Thema</div>
|
||||
<div class="col-span-1 text-center font-medium hidden md:block">Antworten</div>
|
||||
<div class="col-span-2 text-center font-medium hidden md:block">Autor</div>
|
||||
<div class="col-span-2 text-center font-medium hidden md:block">Letzte Antwort</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Thread-Liste -->
|
||||
{% if threads_data %}
|
||||
{% for thread_data in threads_data %}
|
||||
{% set thread = thread_data.thread %}
|
||||
<div class="thread-item p-4 border-b last:border-b-0 {{ 'thread-pinned' if thread.is_pinned }}"
|
||||
x-bind:class="darkMode
|
||||
? 'border-white/10 hover:bg-gray-700/50 {{ 'border-l-yellow-500' if thread.is_pinned }}'
|
||||
: 'border-gray-200 hover:bg-gray-50 {{ 'border-l-yellow-500' if thread.is_pinned }}'">
|
||||
<a href="{{ url_for('forum_post', post_id=thread.id) }}" class="block">
|
||||
<div class="grid grid-cols-12 gap-4">
|
||||
<!-- Thema -->
|
||||
<div class="col-span-12 md:col-span-7">
|
||||
<div class="flex items-start">
|
||||
<!-- Status-Icons -->
|
||||
<div class="flex flex-col items-center mr-3 pt-1">
|
||||
{% if thread.is_pinned %}
|
||||
<i class="fas fa-thumbtack text-yellow-500" title="Angepinnt"></i>
|
||||
{% endif %}
|
||||
{% if thread.is_locked %}
|
||||
<i class="fas fa-lock text-red-500 mt-1" title="Gesperrt"></i>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Themen-Info -->
|
||||
<div>
|
||||
<h3 class="font-medium leading-snug mb-1 {% if thread.is_locked %}opacity-70{% endif %}">
|
||||
{{ thread.title }}
|
||||
</h3>
|
||||
<div class="flex items-center text-xs opacity-70 mt-1">
|
||||
<span><i class="fas fa-eye mr-1"></i> {{ thread.view_count }}</span>
|
||||
<span class="mx-2 block md:hidden">•</span>
|
||||
<span class="block md:hidden"><i class="fas fa-reply mr-1"></i> {{ thread_data.reply_count }}</span>
|
||||
<span class="mx-2">•</span>
|
||||
<span><i class="fas fa-clock mr-1"></i> {{ thread.created_at.strftime('%d.%m.%Y, %H:%M') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Antworten -->
|
||||
<div class="col-span-1 text-center hidden md:flex items-center justify-center">
|
||||
<span class="px-2.5 py-1 rounded-full text-sm font-medium"
|
||||
x-bind:class="darkMode
|
||||
? 'bg-indigo-900/40 text-indigo-300'
|
||||
: 'bg-indigo-100 text-indigo-800'">
|
||||
{{ thread_data.reply_count }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Autor -->
|
||||
<div class="col-span-2 text-center hidden md:flex items-center justify-center">
|
||||
<div class="flex items-center">
|
||||
<div class="w-7 h-7 rounded-full flex items-center justify-center text-white text-xs font-medium overflow-hidden mr-2"
|
||||
style="background: linear-gradient(135deg, #8b5cf6, #6366f1);">
|
||||
{% if thread.author.avatar %}
|
||||
<img src="{{ thread.author.avatar }}" alt="{{ thread.author.username }}" class="w-full h-full object-cover">
|
||||
{% else %}
|
||||
{{ thread.author.username[0].upper() }}
|
||||
{% endif %}
|
||||
</div>
|
||||
<span class="text-sm truncate max-w-[80px]">{{ thread.author.username }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Letzte Antwort -->
|
||||
<div class="col-span-2 text-center hidden md:block text-sm">
|
||||
{% if thread_data.latest_reply %}
|
||||
<div>{{ thread_data.latest_reply.created_at.strftime('%d.%m.%Y') }}</div>
|
||||
<div class="opacity-75 text-xs">{{ thread_data.latest_reply.created_at.strftime('%H:%M') }} Uhr</div>
|
||||
{% else %}
|
||||
<span class="opacity-60">Keine Antworten</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="p-8 text-center">
|
||||
<div class="text-3xl mb-3 opacity-30"><i class="fas fa-comments"></i></div>
|
||||
<h3 class="text-xl font-semibold mb-2">Keine Themen vorhanden</h3>
|
||||
<p class="opacity-75 mb-4">In dieser Kategorie wurden noch keine Themen erstellt.</p>
|
||||
<a href="{{ url_for('new_post', category_id=category.id) }}"
|
||||
class="inline-block px-5 py-2.5 rounded-lg transition-all duration-300"
|
||||
x-bind:class="darkMode
|
||||
? 'bg-indigo-700 hover:bg-indigo-600 text-white'
|
||||
: 'bg-indigo-500 hover:bg-indigo-600 text-white'">
|
||||
<i class="fas fa-plus-circle mr-2"></i>
|
||||
Erstes Thema erstellen
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Link zur Mindmap -->
|
||||
<div class="rounded-xl p-5 mb-4 flex items-center"
|
||||
x-bind:class="darkMode ? 'bg-purple-900/20 border border-purple-800/30' : 'bg-purple-50 border border-purple-100'">
|
||||
<div class="text-3xl mr-4 opacity-80">
|
||||
<i class="fas fa-diagram-project" style="color: {{ node.color_code }}"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-medium mb-1">Mindmap-Knotenpunkt: {{ node.name }}</h3>
|
||||
<p class="text-sm opacity-75">In der Mindmap findest du weitere Informationen zu diesem Themenbereich.</p>
|
||||
</div>
|
||||
<div class="ml-auto">
|
||||
<a href="{{ url_for('mindmap') }}"
|
||||
class="px-4 py-2 rounded-lg inline-block text-sm transition-all"
|
||||
x-bind:class="darkMode
|
||||
? 'bg-purple-800/60 hover:bg-purple-700/60 text-white'
|
||||
: 'bg-white hover:bg-purple-100 text-purple-800 border border-purple-200'">
|
||||
Zur Mindmap
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// Hier können bei Bedarf kategoriespezifische Scripts eingefügt werden
|
||||
</script>
|
||||
{% endblock %}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user